Compare commits

..

No commits in common. "master" and "0.3.0" have entirely different histories.

51 changed files with 2446 additions and 2655 deletions

5
.gitignore vendored
View file

@ -11,8 +11,9 @@ yarn-error.log*
# Ignore config
config.js
# Ignore localstorage
keys
# Ignore JSON
timeline.json
notification.json
# Runtime data
pids

View file

@ -1,47 +1,43 @@
# ligh7hau5
# Plemara
Plemara acts as a [Matrix](https://matrix.org/docs/spec/) bridge to the Fediverse. This application should allow you to do most actions on the Fediverse including livefeed, posting, subscribing, etc. via Matrix. Configuration for the app can be found in [config.js](https://github.com/vulet/plemara/blob/master/config.js). You will need to provide a Matrix username and password for the bridge to work, this can be done through an account made on @matrix.org, or your own homeserver. For the Fediverse side, you will need an access_token, this can be created through the CURL steps below. You would replace `fediverse.site` with where you would like to run the bridge from.
The ligh7hau5 project is used on the Matrix protocol to communicate with the Fediverse. It is also used to proxy popular media networks(Twitter, YouTube, etc) to alternative front ends(Nitter, Invidious, etc). This repository can be ran locally, as on a RPi, or on a VPS.
# Archive (+archive URL)
This command will send a given URL to archive.is, and return an archive.is URL. This can be beneficial in two ways. One, archive.is receives your traffic instead of the URL that you wish to archive. Two, you are creating a historical context of a given URL with a dated attribute. Additionally, if there are changes that have occurred on a page, since the time of last archive, you can also use the rearchive(+rearchive URL) command. If you wish to use a different archiver, this can be configured, see the config.example.js file.
# Social Media (+proxy URL)
This command is given a Twitter or YouTube post, and then returned a respective Nitter/Invidious URL. Additionally, some data is returned about what the URL is, such as: title, description, etc. Instances can also be configured like in the above, see the config.example.js file.
# Fediverse
The ligh7hau5 works as a lite client for the Fediverse. It was built to communicate with a Pleroma instance, but it most likely works on Mastodon as well. Assuming you already have a registered account in regards to the bot, just change the config.js file and fediverse_auth.json will fill out once the bot starts.
Commands for the Fediverse include:
`+flood : turn on timeline in channel`
`+notify : show notifications in channel`
`+post <your message> : post`
`+reply <post id> <message> : reply to message`
`+media <URL> <optional message> : post media`
`+redact <post id> : delete post`
`+follow <user id> : follow`
`+unfollow <user id> : unfollow`
`+copy <post id> : repeat/repost/retweet`
`+clap <post id> : favorite`
`+boo <post id> : unfavorite`
# Installation
First, set up your config.js file, you can see config.example.js as an example. The Matrix & Fediverse login information is then used to populate keys/matrix_auth and keys/fediverse_auth during your initial login. These tokens are then used on sequential logins.
1. `git clone https://github.com/vulet/ligh7hau5`
2. `cd ligh7hau5 && yarn install`
1. `git clone https://github.com/vulet/plemara`
2. `cd plemara && yarn install`
3. `node main.js`
# Generating an access_token
1. `curl -X POST -d "client_name=<NAME HERE>&redirect_uris=urn:ietf:wg:oauth:2.0:oob&scopes=write follow read&website=http://fediverse.site" https://fediverse.site/api/v1/apps`
Result:
```json
{"client_id":"result",
"client_secret":"result",
"id":"result",
"name":"result",
"redirect_uri":"urn:ietf:wg:oauth:2.0:oob",
"website":"http://fediverse.site",
"vapid_key":"vapid_key"}
```
2. `curl -X POST -d "client_id=sekret&client_secret=sekret&scope=write follow read&grant_type=password&username=sekret@email.com&password=sekret" https://fediverse.site/oauth/token`
Result:
```json
{"token_type":"Bearer",
"scope":"write read",
"me":"https://fediverse.site/users/<your username>",
"access_token":"result"}
```
The access_token from the above command is then stored in the [config.js](https://github.com/vulet/plemara/blob/master/config.js) file.
# Extra Features
- Nitter.net / Twitter
- Invidio.us / YouTube
- Archive.is
# Contributors
CryptoMooners

113
auth.js
View file

@ -1,113 +0,0 @@
const { LocalStorageCryptoStore } = require('matrix-js-sdk/lib/crypto/store/localStorage-crypto-store');
const matrixTokenLogin = async () => {
matrixClient = sdk.createClient({
baseUrl: config.matrix.domain,
accessToken: matrix.auth.access_token,
userId: matrix.auth.user_id,
deviceId: matrix.auth.device_id,
cryptoStore: new LocalStorageCryptoStore(localStorage),
});
matrixClient.initCrypto()
.then(() => {
if (!localStorage.getItem('crypto.device_data')) {
return console.log(
'====================================================\n'
+ 'New OLM Encryption Keys created, please restart ligh7hau5.\n'
+ '====================================================',
);
}
matrixClient.setGlobalErrorOnUnknownDevices(config.matrix.manualVerify);
matrixClient.startClient();
});
};
module.exports.matrixTokenLogin = matrixTokenLogin;
module.exports.getMatrixToken = async () => {
matrixClient = sdk.createClient({ baseUrl: config.matrix.domain });
matrixClient.loginWithPassword(config.matrix.user, config.matrix.password)
.then((response) => {
matrix.auth = {
user_id: response.user_id,
access_token: response.access_token,
device_id: response.device_id,
};
localStorage.setItem('matrix_auth', JSON.stringify(response, null, 2));
}).then(() => matrixTokenLogin())
.catch((e) => {
console.log(e);
});
};
const getFediverseLink = (domain,roomId) => {
let apps = {}
apps = JSON.parse(localStorage.getItem("apps"));
if(!apps[domain]){
axios.post(`https://${domain}/api/v1/apps`,
{
client_name: config.fediverse.client_name,
redirect_uris: 'urn:ietf:wg:oauth:2.0:oob',
scopes: 'read write follow push',
})
.then((response) => {
console.log(response.data)
if(!response.data.client_id || !response.data.client_secret) return false;
apps[domain] = {
client_id: response.data.client_id,
client_secret: response.data.client_secret
}
localStorage.setItem("apps",JSON.stringify(apps))
matrixClient.sendHtmlNotice(roomId,"Приложение зарегистрировано. Введите команду еще раз для создания ссылки")
// return getFediverseLink(domain)
}).catch((e) => {
console.log(e);
});
}else{
const app = apps[domain]
const uri = "urn:ietf:wg:oauth:2.0:oob".replace(/:/g,"%3A")
const scope = "read write follow push".replace(/ /g,"%20")
return `https://${domain}/oauth/authorize?client_id=${app.client_id}&response_type=code&redirect_uri=${uri}&scope=${scope}`
}
return "nothing"
};
const obtainAccessToken = (domain,code,event) => {
const apps = JSON.parse(localStorage.getItem("apps"));
console.log(domain,code)
const app = apps[domain];
axios.post(`https://${domain}/oauth/token`, {
client_id: app.client_id,
client_secret: app.client_secret,
redirect_uri: "urn:ietf:wg:oauth:2.0:oob",
grant_type: "authorization_code",
code: code,
scopes: "read write follow push"
}).then(response => {
if(!response.data.access_token) return console.log(response.data)
fediverse.auth[event.getSender()] = {
domain: domain,
access_token: response.data.access_token
}
localStorage.setItem("fediverse_auth", JSON.stringify(fediverse.auth))
getFediverseUserInfo(event)
}).catch(e => console.error(e))
}
const getFediverseUserInfo = (event) => {
const user = event.getSender()
axios({
method: "GET",
url: `https://${fediverse.auth[user].domain}/api/v1/accounts/verify_credentials`,
headers: {
Authorization: `Bearer ${fediverse.auth[user].access_token}`
}
}).then(response => {
if(response.data.username){
matrixClient.sendHtmlNotice(event.getRoomId(), `Успешный вход в аккаунт ${response.data.display_name || response.data.username} (@${response.data.username}@${fediverse.auth[user].domain})`)
}else{
console.log(response.data)
}
}).catch(e => console.error(e))
}
module.exports.getFediverseLink = getFediverseLink;
module.exports.obtainAccessToken = obtainAccessToken;

View file

@ -1,9 +1,27 @@
const { JSDOM } = require('jsdom');
const qs = require('qs');
const https = require('https');
const axios = require('axios');
const { JSDOM } = require('jsdom');
const registrar = require('../registrar.js');
const sleep = ms => new Promise(r => setTimeout(r, ms));
const editNoticeHTML = (client, roomId, event, html, plain) => client.sendMessage(roomId, {
body: ` * ${plain || html.replace(/<[^<]+?>/g, '')}`,
formatted_body: ` * ${html}`,
format: 'org.matrix.custom.html',
msgtype: 'm.notice',
'm.new_content': {
body: plain || html.replace(/<[^<]+?>/g, ''),
formatted_body: html,
format: 'org.matrix.custom.html',
msgtype: 'm.notice'
},
'm.relates_to': {
rel_type: 'm.replace',
event_id: event.event_id
}
});
const headers = ({ domain, userAgent }) => ({
'Host': `${domain}`,
'User-Agent': `${userAgent}`
@ -33,46 +51,47 @@ const arc1Str = str => `<em>Archiving page <code>${str}</code></em>`;
const arc2Str = (str, title, date) => `<em>Archived page <code><a href="https://${str}">${str}</code> [${date}]</em><br /><b>${title}</b>`;
const arc3Str = str => `<em>Timed out <code>${str}</code></em>`;
const run = async (roomId, userInput, rearchive) => {
const run = async (matrixClient, { roomId }, userInput, rearchive, registrar) => {
const config = registrar.config.archive;
const instance = axios.create({
baseURL: `https://${config.archive.domain}`,
httpsAgent: https.Agent({ maxVersion: "TLSv1.2"}),
headers: headers(config.archive),
baseURL: `https://${config.domain}`,
headers: headers(config),
transformResponse: [],
timeout: 10 * 1000
});
let reply = null;
try {
reply = await matrixClient.sendHtmlNotice(roomId, ' ', reqStr(userInput));
reply = await matrixClient.sendHtmlNotice(roomId, '', reqStr(userInput));
const { refresh, id, title, date } = await archive(instance, userInput, rearchive);
if (id)
return await matrix.utils.editNoticeHTML(roomId, reply, arc2Str(`${config.archive.domain}${id}`, title, date));
return await editNoticeHTML(matrixClient, roomId, reply, arc2Str(`${config.domain}${id}`, title, date));
if (refresh) {
const path = refresh.split(`https://${config.archive.domain}`);
const path = refresh.split(`https://${config.domain}`);
if (!path[1]) throw refresh;
await matrix.utils.editNoticeHTML(roomId, reply, arc1Str(refresh));
await editNoticeHTML(matrixClient, roomId, reply, arc1Str(refresh));
let tries = 30;
while (tries--) {
await sleep(10000);
const { title, date, id } = await archive(instance, userInput);
if (rearchive == false && title !== undefined)
return await matrix.utils.editNoticeHTML(roomId, reply, arc2Str(`${config.archive.domain}${id}`, title, date));
return await editNoticeHTML(matrixClient, roomId, reply, arc2Str(`${config.domain}${id}`, title, date));
const { request: { path: reqPath }, headers: { 'memento-datetime': rearchiveDate } } = await instance({ method: 'HEAD', url: path[1] })
.catch(e => ({ request: { path: path[1] } }));
if (rearchive == true && reqPath !== path[1])
return await matrix.utils.editNoticeHTML(roomId, reply, arc2Str(`${config.archive.domain}${reqPath}`, title, rearchiveDate));
return await editNoticeHTML(matrixClient, roomId, reply, arc2Str(`${config.domain}${reqPath}`, title, rearchiveDate));
}
return await matrix.utils.editNoticeHTML(roomId, reply, arc3Str(refresh));
return await editNoticeHTML(matrixClient, roomId, reply, arc3Str(refresh));
}
throw 'sad';
} catch (e) {
const sad = `<strong>Sad!</strong><br /><code>${`${e}`.replace(/<[^<]+?>/g, '').substr(0, 100)}</code>`;
if (reply)
matrix.utils.editNoticeHTML(roomId, reply, sad, 'sad').catch(() => {});
editNoticeHTML(matrixClient, roomId, reply, sad, 'sad').catch(() => {});
else
matrixClient.sendHtmlNotice(roomId, 'sad', sad).catch(() => {});
}
};
exports.runQuery = run;

21
commands/beg.js Normal file
View file

@ -0,0 +1,21 @@
const axios = require('axios');
exports.runQuery = function (matrixClient, room, registrar) {
axios({
method: 'POST',
url: `${registrar.config.fediverse.domain}/api/v1/statuses`,
headers: { Authorization: `Bearer ${registrar.config.fediverse.token}` },
data: { status: `@10grans@fedi.cc beg` },
}).then((response) => {
matrixClient.sendHtmlNotice(room.roomId,
'',
`<b>
<blockquote><i>You have begged for 10grans.<br>
(id: ${response.data.id}</a>)
</blockquote><br>`);
})
.catch((e) => {
matrixClient.sendHtmlNotice(room.roomId,
'', `${e}`);
});
};

18
commands/boo.js Normal file
View file

@ -0,0 +1,18 @@
const axios = require('axios');
exports.runQuery = function (matrixClient, room, userInput, registrar) {
axios({
method: 'POST',
url: `${registrar.config.fediverse.domain}/api/v1/statuses/${userInput}/unfavourite`,
headers: { Authorization: `Bearer ${registrar.config.fediverse.token}` },
}).then((response) => {
matrixClient.sendHtmlNotice(room.roomId,
'',
`You have boo'd: <a href="${response.data.url}">${response.data.account.acct}</a>
<blockquote>${response.data.content}`);
})
.catch((e) => {
matrixClient.sendHtmlNotice(room.roomId,
'', `${e}`);
});
};

18
commands/clap.js Normal file
View file

@ -0,0 +1,18 @@
const axios = require('axios');
exports.runQuery = function (matrixClient, room, userInput, registrar) {
axios({
method: 'POST',
url: `${registrar.config.fediverse.domain}/api/v1/statuses/${userInput}/favourite`,
headers: { Authorization: `Bearer ${registrar.config.fediverse.token}` },
}).then((response) => {
matrixClient.sendHtmlNotice(room.roomId,
'',
`You have clapped: <a href="${response.data.url}">${response.data.account.acct}</a>:
<blockquote>${response.data.content}`);
})
.catch((e) => {
matrixClient.sendHtmlNotice(room.roomId,
'', `${e}`);
});
};

18
commands/copy.js Normal file
View file

@ -0,0 +1,18 @@
const axios = require('axios');
exports.runQuery = function (matrixClient, room, userInput, registrar) {
axios({
method: 'POST',
url: `${registrar.config.fediverse.domain}/api/v1/statuses/${userInput}/reblog`,
headers: { Authorization: `Bearer ${registrar.config.fediverse.token}` },
}).then((response) => {
matrixClient.sendHtmlNotice(room.roomId,
'',
`You have repeated:
<blockquote>${response.data.content}`);
})
.catch((e) => {
matrixClient.sendHtmlNotice(room.roomId,
'', `${e}`);
});
};

View file

@ -1,9 +0,0 @@
exports.runQuery = async (roomId, event, userInput) => {
return matrix.utils.fetchEncryptedOrNot(roomId, { event_id: userInput })
.then(event => matrix.utils.expandReact(event))
.catch(e => {
matrixClient.sendHtmlNotice(roomId, 'Sad!', '<strong>Sad!</strong>')
})
.catch(() => {});
};

View file

@ -1,12 +0,0 @@
exports.runQuery = (roomId,event,userInput) => {
//matrixClient.sendHtmlNotice(roomId,"Проверка связи","Проверка связи");
const link = auth.getFediverseLink(userInput)
if(!link){
matrixClient.sendHtmlNotice(roomId,"Не удалось получить ссылку")
}else if(link == "nothing"){
}else{
authEvents.push(event.event_id)
matrixClient.sendHtmlNotice(roomId,`Перейдите по ссылке для входа в аккаунт. Для завершения ответьте на это сообщение кодом (еще не готово, пж не переходе по ссылке): ${link}`)
}
}

View file

@ -1,14 +0,0 @@
exports.runQuery = function (roomId, event, userInput) {
axios({
method: 'POST',
url: `https://${fediverse.auth[event.getSender()].domain}/api/v1/statuses/${userInput}/unfavourite`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
})
.then(() => {
matrix.utils.addReact(event, '✅');
})
.catch((e) => {
matrix.utils.addReact(event, '❌');
matrix.utils.sendError(event, roomId, e);
});
};

View file

@ -1,14 +0,0 @@
exports.runQuery = function (roomId, event, userInput) {
axios({
method: 'POST',
url: `https://${fediverse.auth[event.getSender()].domain}/api/v1/statuses/${userInput}/favourite`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
})
.then(() => {
matrix.utils.addReact(event, '✅');
})
.catch((e) => {
matrix.utils.addReact(event, '❌');
matrix.utils.sendError(event, roomId, e);
});
};

View file

@ -1,14 +0,0 @@
exports.runQuery = function (roomId, event, userInput) {
axios({
method: 'POST',
url: `https://${fediverse.auth[event.getSender()].domain}/api/v1/statuses/${userInput}/reblog`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
})
.then(() => {
matrix.utils.addReact(event, '✅');
})
.catch((e) => {
matrix.utils.addReact(event, '❌');
matrix.utils.sendError(event, roomId, e);
});
};

View file

@ -1,39 +0,0 @@
let intervalId = null;
exports.runQuery = function (roomId, disable) {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
if (disable) return;
intervalId = setInterval(() => {
axios({
method: 'GET',
url: `https://${fediverse.auth[event.getSender()].domain}/api/v1/timelines/home`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
})
.then((res) => {
let timeline = JSON.parse(localStorage.getItem('timeline'));
past = timeline[event.getSender()] || {}
if (past.length === 0) past = res.data;
const events = res.data;
const len = events.length;
for (let i = len - 1; i >= 0; i--) {
if (past.findIndex((x) => x.created_at === events[i].created_at) === -1) {
const lastStored = past.slice(past.length - 1, past.length);
if (events[i].created_at < lastStored[0].created_at) return;
events[i].label = 'status';
fediverse.utils.formatter(events[i], roomId);
}
}
timeline[event.getSender()] = events
localStorage.setItem('timeline', JSON.stringify(timeline, null, 2));
})
.catch((e) => {
matrix.utils.sendError(null, roomId, e);
});
}, 30000);
};

View file

@ -1,19 +0,0 @@
exports.runQuery = async function (roomId, event, userInput) {
const loadingString = `Searching for ${userInput}...`;
const original = await matrixClient.sendHtmlNotice(roomId, `${loadingString}`, `<code>${loadingString}</code>`);
const found = [];
const suggest = [];
axios({
method: 'GET',
url: `https://${fediverse.auth[event.getSender()].domain}/api/v2/search?q=${userInput}&type=accounts`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
}).then((findUserId) => {
const results = findUserId.data.accounts;
const len = results.length;
for (let i = 0; i < len; i++) results[i].acct !== userInput ? suggest.push(results[i].acct) : found.push(results[i]);
if (found.length > 0) return fediverse.utils.follow(roomId, found, event, original);
if (suggest.length > 0) msg = `<code>${userInput} was not found, suggesting:</code><blockquote>${suggest.join('<br>')}</blockquote>`;
if (suggest.length === 0) msg = `<code>No results found for: ${userInput}.</code>`;
return matrix.utils.editNoticeHTML(roomId, original, msg);
});
};

View file

@ -1,39 +0,0 @@
let intervalId = null;
exports.runQuery = function (roomId, disable) {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
if (disable) return;
intervalId = setInterval(() => {
axios({
method: 'GET',
url: `https://${fediverse.auth[event.getSender()].domain}/api/v1/notifications`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
})
.then((res) => {
let notifications = JSON.parse(localStorage.getItem('notifications'));
let past = notifications[event.getSender()] || {}
if (past.length === 0) past = res.data;
const events = res.data;
const len = events.length;
for (let i = len - 1; i >= 0; i--) {
if (past.findIndex((x) => x.created_at === events[i].created_at) === -1) {
const lastStored = past.slice(past.length - 1, past.length);
if (events[i].created_at < lastStored[0].created_at) return;
events[i].label = 'notifications';
fediverse.utils.formatter(events[i], roomId);
}
}
notifications[event.getSender()] = events
localStorage.setItem('notifications', JSON.stringify(notifications, null, 2));
})
.catch((e) => {
matrix.utils.sendError(null, roomId, e);
});
}, 30000);
};

View file

@ -1,14 +0,0 @@
exports.runQuery = function (roomId, event, userInput) {
axios({
method: 'POST',
url: `https://${fediverse.auth[event.getSender()].domain}/api/v1/statuses/${userInput}/pin`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
})
.then(() => {
matrix.utils.addReact(event, '✅');
})
.catch((e) => {
matrix.utils.addReact(event, '❌');
matrix.utils.sendError(event, roomId, e);
});
};

View file

@ -1,132 +0,0 @@
const qs = require('qs');
const crypto = require('crypto');
const FormData = require('form-data');
const emojis = { public: '🌐', unlisted: '📝', private: '🔒️', direct: '✉️' };
exports.visibilityEmoji = (v) => emojis[v] || v;
const mediaPathRegex = /^\/_matrix\/media\/r0\/download\/[^/]+\/[^/]+\/?$/;
const decryptMedia = (media, file) => {
const { v, key: { alg, ext, k, }, iv } = file;
if (v !== 'v2' || ext !== true || alg !== 'A256CTR')
throw new Error('Unsupported file encryption');
const key = Buffer.from(k, 'base64');
const _iv = Buffer.from(iv, 'base64');
const cipher = crypto.createDecipheriv('aes-256-ctr', key, _iv);
const data = Buffer.concat([ cipher.update(media.data), cipher.final() ]);
return Object.assign({}, media, { data });
};
const getMediaInfoFromEvent = async (roomId, event_id) => {
const event = await matrix.utils.fetchEncryptedOrNot(roomId, { event_id });
if (event.getType() !== 'm.room.message') throw new Error('Invalid type');
const content = event.getContent();
if (content.msgtype !== 'm.image') throw new Error('Invalid msgtype');
if (content.url) return { url: getMediaUrl(content.url) };
if (content.file) return {
url: getMediaUrl(content.file.url),
filename: content.body,
mimetype: content.info ? content.info.mimetype : null,
file: content.file
};
throw new Error('Invalid event');
};
const getMediaUrl = string => {
let url = new URL(string);
if (url.protocol === 'mxc:' && url.hostname && url.pathname)
url = new URL(`${config.matrix.domain}/_matrix/media/r0/download/${url.hostname}${url.pathname}`);
if (url.protocol !== 'https:' ||
!config.matrix.domains.includes(url.hostname) ||
!mediaPathRegex.test(url.pathname))
throw new Error('Invalid URL');
return url.toString();
};
const getMedia = async (roomId, string) => {
let opts = {};
if (string.startsWith('mxe://'))
opts = await getMediaInfoFromEvent(roomId, string.substring(6));
else
opts.url = getMediaUrl(string);
const media = await mediaDownload(opts);
return opts.file ? decryptMedia(media, opts.file) : media;
};
const getFilename = (header) => {
if (typeof header !== 'string') return null;
try {
const m = header.match(/inline; filename(?:=(.+)|\*=utf-8''(.+))/);
return !m ? null : m[2] && decodeURIComponent(m[2]) || m[1];
} catch (e) {
return null;
}
};
const mediaDownload = async (opts) => {
const { whitelist, blacklist } = config.fediverse.mimetypes;
const media = await axios({ method: 'GET', url: opts.url, responseType: 'arraybuffer' });
const filename = opts.filename || getFilename(media.headers['content-disposition']);
const mimetype = opts.mimetype || media.headers['content-type'];
if (media.statusText !== 'OK' || blacklist.includes(mimetype)) throw media;
if (whitelist.length && !whitelist.includes(mimetype)) throw media;
return { data: media.data, filename, mimetype };
};
const mediaUpload = async ({ domain }, { data, filename, mimetype }) => {
const form = new FormData();
form.append('file', data, {
filename: filename || 'upload',
contentType: mimetype,
});
const upload = await axios({
method: 'POST',
url: `https://${fediverse.auth[event.getSender()].domain}/api/v1/media`,
headers: form.getHeaders({ Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` }),
data: form,
});
if (upload.statusText !== 'OK') throw upload;
return upload.data.id;
};
const run = async (roomId, event, content, replyId, mediaURL, subject, visibility) => {
let mediaId = null;
if (mediaURL) {
const media = await getMedia(roomId, mediaURL);
mediaId = await mediaUpload(config.fediverse, media);
}
if (replyId) content = await fediverse.utils.getStatusMentions(replyId, event).then(m => m.concat(content).join(' '));
const response = await axios({
method: 'POST',
url: `https://${fediverse.auth[event.getSender()].domain}/api/v1/statuses`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}`, 'Content-Type': 'application/x-www-form-urlencoded' },
data: qs.stringify({
status: content,
content_type: 'text/markdown',
visibility: visibility || undefined,
media_ids: mediaURL && [mediaId] || undefined,
in_reply_to_id: replyId || undefined,
spoiler_text: subject || undefined,
}, { arrayFormat: 'brackets' }),
});
return fediverse.utils.sendEventWithMeta(roomId, `<a href="${response.data.url}">${response.data.id}</a>`, `redact ${response.data.id}`);
};
exports.runQuery = async (roomId, event, userInput, { isReply, hasMedia, hasSubject, visibility }) => {
try {
const chunks = userInput.trim().split(' ');
if (!chunks.length || chunks.length < !!isReply + !!hasMedia) throw '';
let replyId = null;
let mediaURL = null;
const subject = hasSubject ? config.fediverse.subject : null;
if (isReply) replyId = chunks.shift();
if (hasMedia) mediaURL = chunks.shift();
return await run(roomId, event, chunks.join(' '), replyId, mediaURL, subject, visibility);
} catch (e) {
console.error(e)
return matrixClient.sendHtmlNotice(roomId, 'Sad!', '<strong>Sad!</strong>').catch(() => {});
}
};

View file

@ -1,26 +0,0 @@
const run = async (roomId, event, id, emoji, remove) => {
axios({
method: remove ? 'DELETE' : 'PUT',
url: `https://${fediverse.auth[event.getSender()].domain}/api/v1/pleroma/statuses/${id}/reactions/${emoji}`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
})
.then(() => {
matrix.utils.addReact(event, '✅');
})
.catch((e) => {
matrix.utils.addReact(event, '❌');
matrix.utils.sendError(event, roomId, e);
});
};
exports.runQuery = async (roomId, event, userInput, remove) => {
try {
const chunks = userInput.trim().split(' ');
if (chunks.length !== 2) throw '';
const id = encodeURIComponent(chunks[0]);
const emoji = encodeURIComponent(chunks[1]);
return run(roomId, event, id, emoji, remove);
} catch (e) {
return matrixClient.sendHtmlNotice(roomId, 'Sad!', '<strong>Sad!</strong>').catch(() => {});
}
};

View file

@ -1,14 +0,0 @@
exports.runQuery = function (roomId, event, userInput) {
axios({
method: 'DELETE',
url: `https://${fediverse.auth[event.getSender()].domain}/api/v1/statuses/${userInput}`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
})
.then(() => {
matrix.utils.addReact(event, '✅');
})
.catch((e) => {
matrix.utils.addReact(event, '❌');
matrix.utils.sendError(event, roomId, e);
});
};

View file

@ -1,15 +0,0 @@
exports.runQuery = function (roomId, event, userInput) {
axios({
method: 'GET',
url: `https://${fediverse.auth[event.getSender()].domain}/api/v1/statuses/${userInput}`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
})
.then((response) => {
response.label = 'status';
fediverse.utils.formatter(response, roomId);
})
.catch((e) => {
matrix.utils.addReact(event, '❌');
matrix.utils.sendError(event, roomId, e);
});
};

View file

@ -1,19 +0,0 @@
exports.runQuery = async function (roomId, event, userInput) {
const loadingString = `Searching for ${userInput}...`;
const original = await matrixClient.sendHtmlNotice(roomId, `${loadingString}`, `<code>${loadingString}</code>`);
const found = [];
const suggest = [];
axios({
method: 'GET',
url: `https://${fediverse.auth[event.getSender()].domain}/api/v2/search?q=${userInput}&type=accounts`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
}).then((findUserId) => {
const results = findUserId.data.accounts;
const len = results.length;
for (let i = 0; i < len; i++) results[i].acct !== userInput ? suggest.push(results[i].acct) : found.push(results[i]);
if (found.length > 0) return fediverse.utils.unfollow(roomId, found, event, original);
if (suggest.length > 0) msg = `<code>${userInput} was not found, suggesting:</code><blockquote>${suggest.join('<br>')}</blockquote>`;
if (suggest.length === 0) msg = `<code>No results found for: ${userInput}.</code>`;
return matrix.utils.editNoticeHTML(roomId, original, msg);
});
};

View file

@ -1,14 +0,0 @@
exports.runQuery = function (roomId, event, userInput) {
axios({
method: 'POST',
url: `https://${fediverse.auth[event.getSender()].domain}/api/v1/statuses/${userInput}/unpin`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
})
.then(() => {
matrix.utils.addReact(event, '✅');
})
.catch((e) => {
matrix.utils.addReact(event, '❌');
matrix.utils.sendError(event, roomId, e);
});
};

View file

@ -1,14 +0,0 @@
exports.runQuery = function (roomId, event, userInput) {
axios({
method: 'POST',
url: `https://${fediverse.auth[event.getSender()].domain}/api/v1/statuses/${userInput}/unreblog`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
})
.then(() => {
matrix.utils.addReact(event, '✅');
})
.catch((e) => {
matrix.utils.addReact(event, '❌');
matrix.utils.sendError(event, roomId, e);
});
};

View file

@ -1,28 +0,0 @@
exports.runQuery = function (roomId, event, userInput) {
const instance = axios.create({
baseURL: 'https://' + fediverse.auth[event.getSender()].domain,
method: 'GET',
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
});
instance.get(`/api/v1/statuses/${userInput}/context`)
.then(async (response) => {
let story = [];
const rel = event.getContent()['m.relates_to'];
const eventId = rel && rel.event_id ? rel.event_id : event.getId();
const original = await instance.get(`/api/v1/statuses/${userInput}`);
const ancestors = response.data.ancestors;
const descendants = response.data.descendants;
story = [...story, ancestors, original.data, descendants];
const book = story.flat();
await fediverse.utils.thread(roomId, eventId, '<br><hr><h3>...Beginning thread...</h3><hr><br>');
for (const [i, entry] of book.entries()) {
entry.label = 'thread';
fediverse.utils.formatter(entry, roomId, eventId);
}
await fediverse.utils.thread(roomId, eventId, '<br><hr><h3>...Thread ended...</h3><hr><br>');
})
.catch((e) => {
matrix.utils.addReact(event, '❌');
matrix.utils.sendError(event, roomId, e);
});
};

View file

@ -1,193 +0,0 @@
const sendEventWithMeta = async (roomId, content, meta) => {
await matrixClient.sendEvent(roomId, 'm.room.message', {
body: content.replace(/<[^<]+?>/g, ''),
msgtype: 'm.notice',
formatted_body: content,
meta: meta,
format: 'org.matrix.custom.html',
});
};
const thread = async (roomId, eventId, content, meta) => {
await matrixClient.sendEvent(roomId, 'm.room.message', {
body: content.replace(/<[^<]+?>/g, ''),
msgtype: 'm.notice',
formatted_body: content,
meta: meta,
format: 'org.matrix.custom.html',
'm.relates_to': {
rel_type: 'm.thread',
event_id: eventId,
},
})
};
const hasAttachment = (res) => {
if (res.status) res = res.status;
if (!res.media_attachments) return '<br>';
return res.media_attachments.map((media) => {
const mediaURL = new URL(media.remote_url);
media.name = new URLSearchParams(mediaURL.search).get('name') || 'Unknown file name.';
return `File attachment: <a href="${media.remote_url}">${media.name}</a><br>`;
}).join('<br>');
};
const notifyFormatter = (res, roomId) => {
userDetails = `<b><a href="${config.fediverse.domain}/${res.account.id}">
${res.account.acct}</a>`;
switch (res.type) {
case 'follow':
fediverse.auth.me !== res.account.url ? res.meta = 'follow' : res.meta = 'redact';
meta = `${res.meta} ${res.account.id}`;
content = `${userDetails}
<font color="#03b381"><b>has followed you.</font>
<blockquote><i>${res.account.note}</i></blockquote>`;
sendEventWithMeta(roomId, content, meta);
break;
case 'favourite':
fediverse.auth.me !== res.account.url ? res.meta = 'favourite' : res.meta = 'redact';
meta = `${res.meta} ${res.status.id}`;
content = `${userDetails}
<font color="#03b381"><b>has <a href="${config.fediverse.domain}/notice/${res.status.id}">favorited</a>
your post:</font>
<blockquote><i>${res.status.content}</i><br>
${hasAttachment(res)}
<br>(id: ${res.status.id}) ${registrar.post.visibilityEmoji(res.status.visibility)}
</blockquote>`;
sendEventWithMeta(roomId, content, res.meta);
break;
case 'mention':
fediverse.auth.me !== res.account.url ? res.meta = 'mention' : res.meta = 'redact';
meta = `${res.meta} ${res.status.id}`;
content = `${userDetails}
<font color="#03b381"><b>has <a href="${config.fediverse.domain}/notice/${res.status.id}">mentioned</a>
you:</font><blockquote><i>${res.status.content}</i><br>
${hasAttachment(res)}
<br>(id: ${res.status.id}) ${registrar.post.visibilityEmoji(res.status.visibility)}
</blockquote>`;
sendEventWithMeta(roomId, content, meta);
break;
case 'reblog':
fediverse.auth.me !== res.account.url ? res.meta = 'reblog' : res.meta = 'redact';
meta = `${res.meta} ${res.status.id}`;
content = `${userDetails}
<font color="#03b381"><b>has <a href="${config.fediverse.domain}/notice/${res.status.id}">repeated</a>
your post:</font><blockquote><i>${res.status.content}</i><br>
${hasAttachment(res)}
<br>(id: ${res.status.id}) ${registrar.post.visibilityEmoji(res.status.visibility)}
</blockquote>`;
sendEventWithMeta(roomId, content, meta);
break;
case 'pleroma:emoji_reaction':
fediverse.auth.me !== res.account.url ? res.meta = 'react' : res.meta = 'redact';
meta = `${res.meta} ${res.status.id}`;
content = `${userDetails}
<font color="#03b381"><b>has <a href="${config.fediverse.domain}/notice/${res.status.id}">reacted</a> with
${ res.emoji_url ? `<a href="${res.emoji_url}">${res.emoji}</a>` : `<span>${res.emoji}</span>` }
to your post:</font><blockquote><i>${res.status.content}</i><br>
${hasAttachment(res)}
<br>(id: ${res.status.id}) ${registrar.post.visibilityEmoji(res.status.visibility)}
</blockquote>`;
sendEventWithMeta(roomId, content, meta);
break;
default:
return console.log('Unknown notification type.');
}
};
const isOriginal = (res, roomId, event) => {
if (res.data) res = res.data;
userDetails = `<b><a href="${config.fediverse.domain}/notice/${res.id}">
${res.account.acct}</a>`;
fediverse.auth.me !== res.account.url ? res.meta = 'status' : res.meta = 'redact';
meta = `${res.meta} ${res.id}`;
content = `${userDetails}
<blockquote><i>${res.content}</i><br>
${hasAttachment(res)}
<br>(id: ${res.id}) ${registrar.post.visibilityEmoji(res.visibility)}
</blockquote>`;
if (res.label == 'thread') thread(roomId, event, content, meta);
else sendEventWithMeta(roomId, content, meta);
};
const isReblog = (res, roomId) => {
if (res.data) res = res.data;
userDetails = `<b><a href="${config.fediverse.domain}/${res.id}">
${res.account.acct}</a>`;
fediverse.auth.me !== res.account.url ? res.meta = 'status' : res.meta = 'unreblog';
meta = `${res.meta} ${res.reblog.id}`;
content = `${userDetails}
<font color="#7886D7"><b>has repeated</a>
<a href="${config.fediverse.domain}/notice/${res.reblog.id}">${res.reblog.account.acct}</a>'s post:</font>
<blockquote><i>${res.content}</i><br>
${hasAttachment(res)}
<br>(id: ${res.reblog.id}) ${registrar.post.visibilityEmoji(res.visibility)}
</blockquote>`;
sendEventWithMeta(roomId, content, meta);
};
module.exports.sendEventWithMeta = sendEventWithMeta;
module.exports.thread = thread;
module.exports.formatter = (res, roomId, event) => {
const filtered = (res.label === 'notifications')
? notifyFormatter(res, roomId)
: (res.reblog == null)
? isOriginal(res, roomId, event)
: isReblog(res, roomId);
return filtered;
};
module.exports.follow = (roomId, account, event, original) => {
axios({
method: 'POST',
url: `https://${config.fediverse[event.getSender()].domain}/api/v1/accounts/${account[0].id}/follow`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
})
.then(() => {
matrix.utils.addReact(event, '✅');
matrix.utils.editNoticeHTML(roomId, original, `<code>Followed ${account[0].acct}.</code>`);
})
.catch((e) => {
matrix.utils.addReact(event, '❌');
matrix.utils.sendError(event, roomId, e);
});
};
module.exports.unfollow = (roomId, account, event, original) => {
axios({
method: 'POST',
url: `https://${fediverse.auth[event.getSender()].domain}/api/v1/accounts/${account[0].id}/unfollow`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
})
.then(() => {
matrix.utils.addReact(event, '✅');
matrix.utils.editNoticeHTML(roomId, original, `<code>Unfollowed ${account[0].acct}.</code>`);
})
.catch((e) => {
matrix.utils.addReact(event, '❌');
matrix.utils.sendError(event, roomId, e);
});
};
module.exports.getStatusMentions = (notice, event) => {
const users = axios({
method: 'GET',
url: `https://${fediverse.auth[event.getSender()].domain}/api/v1/statuses/${notice}`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
}).then((notice) => {
const users = [];
users.push('@' + notice.data.account.acct);
for(let i = 0; i < notice.data.mentions.length; i++) {
if(!config.fediverse.username.includes(notice.data.mentions[i].acct))
users.push('@' + notice.data.mentions[i].acct)
}
return users;
})
.catch((e) => {
matrix.utils.addReact(event, '❌');
matrix.utils.sendError(event, roomId, e);
});
return users;
};

41
commands/flood.js Normal file
View file

@ -0,0 +1,41 @@
const axios = require('axios');
const fs = require('fs');
exports.runQuery = function (matrixClient, room, registrar) {
setInterval(() => {
axios({
method: 'GET',
url: `${registrar.config.fediverse.domain}/api/v1/timelines/home`,
headers: { Authorization: `Bearer ${registrar.config.fediverse.token}` },
}).then((events) => {
const event = fs.readFileSync('timeline.json', 'utf8');
fs.writeFileSync('timeline.json', events.data[0].created_at, 'utf8');
if (event !== events.data[0].created_at) {
if (events.data[0].reblog === null) {
matrixClient.sendHtmlNotice(room.roomId,
'',
`<b><a href="${registrar.config.fediverse.domain}/notice/${events.data[0].id}">${events.data[0].account.acct}</a>
<blockquote><i>${events.data[0].content}<br>
${events.data[0].media_attachments.map(media =>
`<a href="${media.remote_url}">`+`${media.description}`+'</a>'
).join('<br>')}
(id: ${events.data[0].id}</a>)
</blockquote>`);
} else {
matrixClient.sendHtmlNotice(room.roomId,
'',
`<b><a href="${registrar.config.fediverse.domain}/${events.data[0].account.id}">
${events.data[0].account.acct}</a>
<font color="#7886D7">has <a href="${registrar.config.fediverse.domain}/notice/${events.data[0].id}">repeated</a>:
<blockquote><a href="${events.data[0].reblog.account.url}">${events.data[0].reblog.account.acct}</a></blockquote>
<blockquote>${events.data[0].content}<br>
${events.data[0].media_attachments.map(media =>
`<a href="${media.remote_url}">`+`Proxied image, no description available.`+'</a>'
).join('<br>')}
<br>(id: ${events.data[0].id})
</blockquote>`);
}
}
});
}, 8000);
};

20
commands/fren.js Normal file
View file

@ -0,0 +1,20 @@
const axios = require('axios');
exports.runQuery = function (matrixClient, room, userInput, registrar) {
axios.get(`${registrar.config.fediverse.domain}/api/v1/accounts/${userInput}`).then((findUID) => {
axios({
method: 'POST',
url: `${registrar.config.fediverse.domain}/api/v1/accounts/${findUID.data.id}/follow`,
headers: { Authorization: `Bearer ${registrar.config.fediverse.token}` },
})
.then((response) => {
matrixClient.sendHtmlNotice(room.roomId,
'',
`Subscribed:
<blockquote>${registrar.config.fediverse.domain}/${response.data.id}`);
});
}).catch((e) => {
matrixClient.sendHtmlNotice(room.roomId,
'', `${e}`);
});
};

View file

@ -1,33 +1,25 @@
exports.runQuery = function (roomId) {
matrixClient.sendHtmlNotice(roomId,
' ',
exports.runQuery = function (matrixClient, room) {
matrixClient.sendHtmlNotice(room.roomId,
'',
'<blockquote><b>fediverse commands<br>'
+ '+post [your message] : post<br>'
+ '+direct [@recipient] [message] : direct message<br>'
+ '+private [message] : follower-only message<br>'
+ '+plemara [your message] : post<br>'
+ '+redact [post id] : delete post<br>'
+ '+follow [user id] : follow<br>'
+ '+unfollow [user id] : unfollow<br>'
+ '+media [homeserver image URL or MXC] [optional message] : post media<br>'
+ '+fren [user id] : follow<br>'
+ '+unfren [user id] : unfollow<br>'
+ '+copy [post id] : repeat/repost/retweet<br>'
+ '+crossblog [status URL]: cross blog twitter post to fediverse post<br>'
+ '+reply [post id] [content] : reply to post<br>'
+ '+tip [@user@fedi.url] [amount] : tip 10grans'
+ '+beg : beg for 10grans'
+ '+clap [post id] : favorite<br>'
+ '+boo [post id] : unfavorite</blockquote>'
+ '<blockquote><b>channel commands<br>'
+ '+flood : turn on timeline in channel<br>'
+ '+notify : show notifications in channel<br>'
+ '+unflood : stop timeline in channel<br>'
+ '+unnotify : stop notifications in channel<br>'
+ '+archive [URL] : archive content<br>'
+ '+rearchive [URL] : re-archive content<br>'
+ '+nitter [status URL] : redirect twitter to nitter, also embed tweet<br>'
+ '+invidious [video URL] : redirect youtube to invidious, also embed description<br>'
+ '+proxy [twitter/youtube]: both +nitter and +invidious commands combined</b><br></blockquote>'
+ `<blockquote><b>ligh7hau5 version ${require('../package.json').version}</b><br>`
+ '<b>--- <i>Contributors🐱</i> ---</b><br>'
+ '+invidious [video URL] : redirect youtube to invidious, also embed description</b><br></blockquote>'
+ '<blockquote><b>--- <i>Contributors🐱</i> ---</b><br>'
+ '<b>CRYPTOMOONERS</b><br>'
+ '<b>doesnm</b><br>'
+ '<b><i>docs by LINT</i></b></blockquote>'
);
+ '<b><i>docs by LINT</i></b></blockquote>');
};

View file

@ -1,22 +1,27 @@
const axios = require('axios');
const headers = ({ domain, userAgent }) => ({
'Host': `${domain}`,
'User-Agent': `${userAgent}`
});
const invidious = async (instance, url) => {
const req = await instance({ method: 'GET', url });
if (req.statusText !== 'OK') throw req;
const { headers } = instance.defaults;
const video = JSON.parse(req.data);
return {
url: headers['Host'],
name: video.title,
date: video.publishedText,
description: video.descriptionHtml,
author: video.author,
views: video.viewCount,
likes: video.likeCount,
dislikes: video.dislikeCount,
dislikes: video.dislikeCount
};
};
const card = (video, path) =>
`<a href="https://${video.url}/${path}"><b>${video.name}</a></b><blockquote><b><i>` +
const card = (video, base, path) =>
`<a href="${base}/${path}"><b>${video.name}</a></b><blockquote><b><i>` +
((video.description.length > 300) ? `${video.description.substr(0, 300)}&hellip;` : ``)+
((video.description === '<p></p>') ? `No description.`: ``)+
((video.description.length < 300 && video.description !== '<p></p>') ? `${video.description}` : ``)+
@ -26,33 +31,26 @@ const card = (video, path) =>
`<br />(${video.date})</b> <br />
</blockquote>`;
const getInstance = (domain, config) =>
axios.create({
baseURL: `https://${domain}/api/v1/videos`,
headers: {
Host: `${domain}`,
'User-Agent': `${config.userAgent}`,
},
const run = async (matrixClient, { roomId }, userInput, registrar) => {
const config = registrar.config.invidious;
const instance = axios.create({
baseURL: `https://${config.domain}/api/v1/videos/`,
headers: headers(config),
transformResponse: [],
timeout: 10 * 1000,
timeout: 10 * 1000
});
const video = await invidious(instance, userInput);
return await matrixClient.sendHtmlNotice(roomId, '', card(video, `https://${config.domain}`, userInput));
}
const run = async (roomId, userInput) => {
const cfg = config.invidious;
const video = await matrix.utils.retryPromise(cfg.domains.redirect, domain => invidious(getInstance(domain, cfg), userInput));
return matrixClient.sendHtmlNotice(roomId, ' ', card(video, userInput));
};
exports.runQuery = async (roomId, event, userInput) => {
exports.runQuery = async (client, room, userInput, registrar) => {
try {
const url = new URL(userInput);
const { redirect, original } = config.invidious.domains;
if (!redirect.includes(url.hostname) && !original.includes(url.hostname)) throw '';
if (/^\/[\w-]{11}$/.test(url.pathname)) return await run(roomId, url.pathname.slice(1));
const params = new URLSearchParams(url.search).get('v');
if (!/^[\w-]{11}$/.test(params)) throw '';
return await run(roomId, params);
} catch (e) {
return matrixClient.sendHtmlNotice(roomId, 'Sad!', '<strong>Sad!</strong>').catch(() => {});
if(!registrar.config.invidious.domains.includes(url.hostname)) throw '';
const params = new URLSearchParams(url.search).get("v");
if(!/([a-z0-9_-]{11})?$/.test(params)) throw '';
return await run(client, room, params, registrar);
} catch(e) {
return client.sendHtmlNotice(room.roomId, 'Sad!', `<strong>Sad!</strong>`).catch(()=>{});
}
};

76
commands/media.js Normal file
View file

@ -0,0 +1,76 @@
const qs = require('qs');
const axios = require('axios');
const FormData = require('form-data');
const mediaDownload = async (url, types) => {
const media = await axios({ method: 'GET', url, responseType: 'arraybuffer' });
if (media.statusText !== 'OK' || !types.includes(media.headers['content-type'])) throw media;
return {
data: media.data,
//filename: //TODO,
mimetype: media.headers['content-type']
};
};
const mediaUpload = async ({ domain, token }, { data, mimetype }) => {
const form = new FormData();
form.append('file', data, {
filename: 'upload',
contentType: mimetype,
});
const upload = await axios({
method: 'POST',
url: `${domain}/api/v1/media`,
headers: form.getHeaders({ Authorization: `Bearer ${token}` }),
data: form,
});
if(upload.statusText !== 'OK') throw upload;
return upload.data.id;
};
const run = async (matrixClient, { roomId }, content, replyId, mediaURL, subject, registrar) => {
let mediaId = null;
const fediverse = registrar.config.fediverse;
if(mediaURL) {
const media = await mediaDownload(mediaURL, registrar.config.matrix.mimetypes);
mediaId = await mediaUpload(fediverse, media);
}
const response = await axios({
method: 'POST',
url: `${fediverse.domain}/api/v1/statuses`,
headers: { Authorization: `Bearer ${fediverse.token}`, 'Content-Type': 'application/x-www-form-urlencoded' },
data : qs.stringify({
status: content,
content_type: `text/markdown`,
media_ids: mediaURL && [ mediaId ] || undefined,
in_reply_to_id: replyId || undefined,
spoiler_text: subject || undefined
}, { arrayFormat: 'brackets' })
});
return matrixClient.sendHtmlNotice(roomId, '', `<a href="${response.data.url}">${response.data.id}</a>`);
}
exports.runQuery = async (client, room, userInput, registrar, { isReply, hasMedia, hasSubject }) => {
try {
const chunks = userInput.trim().split(' ');
if(!chunks.length || chunks.length < !!isReply + !!hasMedia) throw '';
let replyId = null;
let mediaURL = null;
const subject = hasSubject ? registrar.config.matrix.subject : null;
if(isReply) {
replyId = chunks[0];
chunks.shift();
}
if(hasMedia) {
const url = new URL(chunks[0]);
chunks.shift();
if(url.protocol !== 'https:') throw '';
if(!registrar.config.matrix.domains.includes(url.hostname)) throw '';
if(!/^\/_matrix\/media\/r0\/download\/[^/]+\/[^/]+\/?$/.test(url.pathname)) throw '';
mediaURL = url.toString();
}
return await run(client, room, chunks.join(' '), replyId, mediaURL, subject, registrar);
} catch(e) {
return client.sendHtmlNotice(room.roomId, 'Sad!', `<strong>Sad!</strong>`).catch(()=>{});
}
};

26
commands/mordy.js Normal file
View file

@ -0,0 +1,26 @@
const axios = require('axios');
exports.runQuery = function (matrixClient, room, userInput, registrar) {
axios({
method: 'POST',
url: `${registrar.config.fediverse.domain}/api/v1/statuses`,
headers: { Authorization: `Bearer ${registrar.config.fediverse.token}` },
data: {
status: `@mordekai ${userInput}`,
content_type: `text/markdown`,
visibility: 'unlisted',
expires_in: '7200'
},
}).then((response) => {
matrixClient.sendHtmlNotice(room.roomId,
'',
`<b>
<blockquote><i>${response.data.content}<br>
(id: ${response.data.id}</a>)
</blockquote><br>`);
})
.catch((e) => {
matrixClient.sendHtmlNotice(room.roomId,
'', `${e}`);
});
};

View file

@ -1,18 +1,22 @@
const { JSDOM } = require('jsdom');
const axios = require('axios');
const { JSDOM } = require("jsdom");
const headers = ({ domain, userAgent }) => ({
'Host': `${domain}`,
'User-Agent': `${userAgent}`
});
const nitter = async (instance, url) => {
const req = await instance({ method: 'GET', url });
if (req.statusText !== 'OK') throw req;
const dom = new JSDOM(req.data);
const { document } = dom.window;
const document = dom.window.document;
const tweet = document.querySelector('#m');
const stats = tweet.querySelectorAll('.tweet-body > .tweet-stats .icon-container');
const quote = tweet.querySelector('.tweet-body > .quote');
const isReply = tweet.querySelector('.tweet-body > .replying-to');
const replies = document.querySelectorAll('.main-thread > .before-tweet > .timeline-item');
const { defaults } = instance;
return {
url: defaults.baseURL,
text: tweet.querySelector('.tweet-body > .tweet-content').innerHTML,
date: tweet.querySelector('.tweet-body > .tweet-published').textContent,
name: tweet.querySelector('.tweet-body > div .fullname').textContent,
@ -27,62 +31,44 @@ const nitter = async (instance, url) => {
path: replies[replies.length - 1].querySelector('a.tweet-link').href,
text: replies[replies.length - 1].querySelector('.tweet-content').innerHTML,
} : null,
isThread: !isReply && replies.length > 0 ? replies[replies.length - 1].classList.contains('unavailable') ? 'unavailable' : {
path: replies[replies.length - 1].querySelector('a.tweet-link').href,
text: replies[replies.length - 1].querySelector('.tweet-content').innerHTML,
} : null,
stats: {
replies: stats[0].textContent.trim(),
retweets: stats[1].textContent.trim(),
favorites: stats[2].textContent.trim(),
},
favorites: stats[2].textContent.trim()
}
};
};
const card = (tweet, check, path) =>
`<a href="${tweet.url}/${tweet.handle.replace(/^@/, '')}"><b>${tweet.name}</b></a> ` +
(tweet.check ? `${check} ` : '') +
`<a href="${tweet.url}${path}"><b>${tweet.date}</b></a> ` +
const card = (tweet, base, check, path) =>
`<a href="${base}/${tweet.handle.replace(/^@/, '')}"><b>${tweet.name}</b></a> ` +
(tweet.check ? check : '') +
`<a href="${base}${path}"><b>${tweet.date}</b></a> ` +
`<span>🗨️ ${tweet.stats.replies}</span> ` +
`<span>🔁 ${tweet.stats.retweets}</span> ` +
`<span>❤️ ${tweet.stats.favorites}</span> ` +
`<br /><blockquote><b><i>${tweet.text.replace('\n', '<br />')}</i></b></blockquote>` +
(tweet.hasAttachments ? '<blockquote><b>This tweet has attached media.</b></blockquote>' : '') +
(tweet.isReply ? tweet.isReply === 'unavailable' ? '<blockquote>Replied Tweet is unavailable</blockquote>' : `<blockquote><b><a href="${tweet.url}${tweet.isReply.path}">Replied Tweet</a></b><br /><b><i>${tweet.isReply.text.replace('\n', '<br />')}</i></b></blockquote>` : '') +
(tweet.isThread ? tweet.isThread === 'unavailable' ? '<blockquote>Previous Tweet is unavailable</blockquote>' : `<blockquote><b><a href="${tweet.url}${tweet.isThread.path}">Previous Tweet</a></b><br /><b><i>${tweet.isThread.text.replace('\n', '<br />')}</i></b></blockquote>` : '') +
(tweet.quote ? `<blockquote><b><a href="${tweet.url}${tweet.quote.path}">Quoted Tweet</a></b><br /><b><i>${tweet.quote.text.replace('\n', '<br />')}</i></b></blockquote>` : '');
const getInstance = (domain, config) =>
axios.create({
baseURL: `https://${domain}`,
headers: {
Host: `${domain}`,
'User-Agent': `${config.userAgent}`,
},
(tweet.isReply ? tweet.isReply === 'unavailable' ? '<blockquote>Replied Tweet is unavailable</blockquote>' : `<blockquote><b><a href="${base}${tweet.isReply.path}">Replied Tweet</a></b><br /><b><i>${tweet.isReply.text.replace('\n', '<br />')}</i></b></blockquote>` : '') +
(tweet.quote ? `<blockquote><b><a href="${base}${tweet.quote.path}">Quoted Tweet</a></b><br /><b><i>${tweet.quote.text.replace('\n', '<br />')}</i></b></blockquote>` : '');
const run = async (matrixClient, { roomId }, userInput, registrar) => {
const config = registrar.config.nitter;
const instance = axios.create({
baseURL: `https://${config.domain}`,
headers: headers(config),
transformResponse: [],
timeout: 10 * 1000,
timeout: 10 * 1000
});
const tweet = await nitter(instance, userInput);
return await matrixClient.sendHtmlNotice(roomId, '', card(tweet, `https://${config.domain}`, config.check, userInput));
}
const run = async (roomId, userInput, fedi) => {
const cfg = config.nitter;
const tweet = await matrix.utils.retryPromise(cfg.domains.redirect, domain => nitter(getInstance(domain, cfg), userInput));
const tweetCard = card(tweet, cfg.check, userInput);
return fedi ? axios({
method: 'POST',
url: `${config.fediverse.domain}/api/v1/statuses`,
headers: { Authorization: `Bearer ${fediverse.auth.access_token}` },
data: { status: tweetCard, content_type: 'text/html' }
}) : matrixClient.sendHtmlNotice(roomId, ' ', tweetCard);
};
exports.runQuery = async (roomId, event, userInput, fedi) => {
exports.runQuery = async (client, room, userInput, registrar) => {
try {
const url = new URL(userInput);
const { redirect, original } = config.nitter.domains;
if (!redirect.includes(url.hostname) && !original.includes(url.hostname)) throw '';
if (!/^\/[^/]+\/status\/\d+\/?$/.test(url.pathname)) throw '';
return await run(roomId, url.pathname, fedi);
} catch (e) {
return matrixClient.sendHtmlNotice(roomId, 'Sad!', '<strong>Sad!</strong>').catch(() => {});
if(!registrar.config.nitter.domains.includes(url.hostname)) throw '';
if(!/^\/[^/]+\/status\/\d+\/?$/.test(url.pathname)) throw '';
return await run(client, room, url.pathname, registrar);
} catch(e) {
return client.sendHtmlNotice(room.roomId, 'Sad!', `<strong>Sad!</strong>`).catch(()=>{});
}
};

51
commands/notify.js Normal file
View file

@ -0,0 +1,51 @@
const axios = require('axios');
const fs = require('fs');
exports.runQuery = function (matrixClient, room, registrar) {
setInterval(() => {
axios({
method: 'GET',
url: `${registrar.config.fediverse.domain}/api/v1/notifications`,
headers: { Authorization: `Bearer ${registrar.config.fediverse.token}` },
}).then((notifications) => {
const event = fs.readFileSync('notification.json', 'utf8');
fs.writeFileSync('notification.json', notifications.data[0].created_at, 'utf8');
if (event !== notifications.data[0].created_at) {
if (notifications.data[0].type === 'follow') {
matrixClient.sendHtmlNotice(room.roomId,
'',
`<b><a href="${registrar.config.fediverse.domain}/${notifications.data[0].account.id}">
${notifications.data[0].account.acct}</a></b>
<font color="#03b381"><b>has followed you.</b></font>
<br><i>${notifications.data[0].account.note}</i>`);
} else if (notifications.data[0].type === 'favourite') {
matrixClient.sendHtmlNotice(room.roomId,
'',
`<b><a href="${registrar.config.fediverse.domain}/${notifications.data[0].account.id}">
${notifications.data[0].account.acct}</a></b>
<font color="#03b381"><b>has <a href="${notifications.data[0].status.uri}">favorited</a>
your post:</b></font>
<br><blockquote><i><b>${notifications.data[0].status.content}</i></b></blockquote>`);
} else if (notifications.data[0].type === 'mention') {
matrixClient.sendHtmlNotice(room.roomId,
'',
`<b><a href="${registrar.config.fediverse.domain}/${notifications.data[0].account.id}">
${notifications.data[0].account.acct}</a></b>
<font color="#03b381"><b>has <a href="${notifications.data[0].status.uri}">mentioned</a>
you:</b></font><br><blockquote><i><b>${notifications.data[0].status.content}
<br>(id: ${notifications.data[0].status.id})</i></b>
</blockquote>`);
} else if (notifications.data[0].type === 'reblog') {
matrixClient.sendHtmlNotice(room.roomId,
'',
`<b><a href="${registrar.config.fediverse.domain}/${notifications.data[0].account.id}">
${notifications.data[0].account.acct}</a></b>
<font color="#03b381"><b>has <a href="${notifications.data[0].status.uri}">repeated</a>
your post:</b></font><br>
<blockquote><i><b>${notifications.data[0].status.content}</i></b></blockquote>`);
}
}
});
}, 8000);
};

20
commands/pin.js Normal file
View file

@ -0,0 +1,20 @@
const axios = require('axios');
exports.runQuery = function (matrixClient, room, userInput, registrar) {
axios({
method: 'POST',
url: `${registrar.config.fediverse.domain}/api/v1/statuses/${userInput}/pin`,
headers: { Authorization: `Bearer ${registrar.config.fediverse.token}` },
}).then((response) => {
matrixClient.sendHtmlNotice(room.roomId,
'',
`Pinned:
<blockquote><i><a href="${registrar.config.fediverse.domain}/notice/${response.data.id}">
${response.data.content}</a></i>
</blockquote>`);
})
.catch((e) => {
matrixClient.sendHtmlNotice(room.roomId,
'', `${e}`);
});
};

21
commands/plemara.js Normal file
View file

@ -0,0 +1,21 @@
const axios = require('axios');
exports.runQuery = function (matrixClient, room, userInput, registrar) {
axios({
method: 'POST',
url: `${registrar.config.fediverse.domain}/api/v1/statuses`,
headers: { Authorization: `Bearer ${registrar.config.fediverse.token}` },
data: { status: userInput, content_type: `text/markdown` },
}).then((response) => {
matrixClient.sendHtmlNotice(room.roomId,
'',
`<b>
<blockquote><i>${response.data.content}<br>
(id: ${response.data.id}</a>)
</blockquote><br>`);
})
.catch((e) => {
matrixClient.sendHtmlNotice(room.roomId,
'', `${e}`);
});
};

17
commands/redact.js Normal file
View file

@ -0,0 +1,17 @@
const axios = require('axios');
exports.runQuery = function (matrixClient, room, userInput, registrar) {
axios({
method: 'DELETE',
url: `${registrar.config.fediverse.domain}/api/v1/statuses/${userInput}`,
headers: { Authorization: `Bearer ${registrar.config.fediverse.token}` },
}).then((response) => {
matrixClient.sendHtmlNotice(room.roomId,
'',
'<blockquote>Redacted.</blockquote');
})
.catch((e) => {
matrixClient.sendHtmlNotice(room.roomId,
'', `${e}`);
});
};

18
commands/reply.js Normal file
View file

@ -0,0 +1,18 @@
const axios = require('axios');
exports.runQuery = function (matrixClient, room, address, flaggedInput, registrar) {
axios({
method: 'POST',
url: `${registrar.config.fediverse.domain}/api/v1/statuses`,
headers: { Authorization: `Bearer ${registrar.config.fediverse.token}` },
data: { status: flaggedInput, in_reply_to_id: address, content_type: `text/markdown` },
}).then((response) => {
matrixClient.sendHtmlNotice(room.roomId,
'',
`${response.data.content} ${response.data.url}`);
})
.catch((e) => {
matrixClient.sendHtmlNotice(room.roomId,
'', `${e}`);
});
};

19
commands/status.js Normal file
View file

@ -0,0 +1,19 @@
const axios = require('axios');
exports.runQuery = function (matrixClient, room, userInput, registrar) {
axios({
method: 'GET',
url: `${registrar.config.fediverse.domain}/api/v1/statuses/${userInput}`,
headers: { Authorization: `Bearer ${registrar.config.fediverse.token}` },
}).then((response) => {
matrixClient.sendHtmlNotice(room.roomId,
'',
`<b><a href="${registrar.config.fediverse.domain}/notice/${response.data.id}">${response.data.account.acct}</a>
<blockquote><i>${response.data.content}<br>
${response.data.media_attachments.map(media =>
`<a href="${media.remote_url}"><b>${media.description}</b></a>`)
.join('<br>')}
(id: ${response.data.id}</a>)
</blockquote>`);
});
};

21
commands/tip.js Normal file
View file

@ -0,0 +1,21 @@
const axios = require('axios');
exports.runQuery = function (matrixClient, room, address, flaggedInput, registrar) {
axios({
method: 'POST',
url: `${registrar.config.fediverse.domain}/api/v1/statuses`,
headers: { Authorization: `Bearer ${registrar.config.fediverse.token}` },
data: { status: `@10grans@fedi.cc tip `+ flaggedInput + ` to `+address },
}).then((response) => {
matrixClient.sendHtmlNotice(room.roomId,
'',
`<b>
<blockquote><i>Tipping ${response.data.content}<br>
(id: ${response.data.id}</a>)
</blockquote><br>`);
})
.catch((e) => {
matrixClient.sendHtmlNotice(room.roomId,
'', `${e}`);
});
};

20
commands/unfren.js Normal file
View file

@ -0,0 +1,20 @@
const axios = require('axios');
exports.runQuery = function (matrixClient, room, userInput, registrar) {
axios.get(`${registrar.config.fediverse.domain}/api/v1/accounts/${userInput}`).then((findUID) => {
axios({
method: 'POST',
url: `${registrar.config.fediverse.domain}/api/v1/accounts/${findUID.data.id}/unfollow`,
headers: { Authorization: `Bearer ${registrar.config.fediverse.token}` },
})
.then((response) => {
matrixClient.sendHtmlNotice(room.roomId,
'',
`Unsubscribed:
<blockquote>${registrar.config.fediverse.domain}/${response.data.id}`);
});
}).catch((e) => {
matrixClient.sendHtmlNotice(room.roomId,
'', `${e}`);
});
};

20
commands/unpin.js Normal file
View file

@ -0,0 +1,20 @@
const axios = require('axios');
exports.runQuery = function (matrixClient, room, userInput, registrar) {
axios({
method: 'POST',
url: `${registrar.config.fediverse.domain}/api/v1/statuses/${userInput}/unpin`,
headers: { Authorization: `Bearer ${registrar.config.fediverse.token}` },
}).then((response) => {
matrixClient.sendHtmlNotice(room.roomId,
'',
`Unpinned:
<blockquote><i><a href="${registrar.config.fediverse.domain}/notice/${response.data.id}">
${response.data.content}</a></i>
</blockquote>`);
})
.catch((e) => {
matrixClient.sendHtmlNotice(room.roomId,
'', `${e}`);
});
};

29
config.example.js Normal file
View file

@ -0,0 +1,29 @@
module.exports = {
matrix: {
domain: 'https://your_homeserver.com',
user: 'your_user',
password: 'your_password',
domains: [ 'your_homeserver.com' ],
mimetypes: [ 'image/png', 'image/jpeg', 'video/webm', 'image/jpg', 'video/mp4', 'audio/mp3' ],
subject: ''
},
fediverse: {
domain: 'https://your_federation.com',
token: 'your_federation_token',
},
archive: {
domain: 'archive.is',
userAgent: 'Mozilla/4.0 (compatible; Beep Boop)'
},
nitter: {
domain: 'nitter.net',
userAgent: 'Mozilla/4.0 (compatible; Beep Boop)',
domains: [ 'nitter.net', 'www.nitter.net', 'twitter.com', 'wwww.twitter.com' ],
check: '(✓)'
},
invidious: {
domain: 'invidious.fdn.fr',
userAgent: 'Mozilla/4.0 (compatible; Beep Boop)',
domains: [ 'invidious.snopyta.org', 'invidious.xyz', 'youtube.com', 'www.youtube.com' ]
}
};

119
main.js
View file

@ -1,44 +1,85 @@
global.registrar = require('./registrar.js');
const sdk = require('matrix-js-sdk');
const axios = require('axios');
const registrar = require('./registrar.js');
matrix.auth.access_token ? auth.matrixTokenLogin() : auth.getMatrixToken();
//if (!fediverse.auth.access_token && config.fediverse.username) auth.registerFediverseApp();
const auth = {
type: 'm.login.password',
user: registrar.config.matrix.user,
password: registrar.config.matrix.password,
};
matrixClient.on('RoomMember.membership', (event, member) => {
if (member.membership === 'invite' && member.userId === matrixClient.credentials.userId) {
matrixClient.joinRoom(member.roomId).then(() => {
console.log('Auto-joined %s', member.roomId);
});
}
if (member.membership === 'leave' && member.userId === matrixClient.credentials.userId) {
matrixClient.forget(member.roomId).then(() => {
console.log('Kicked %s', member.roomId);
});
}
axios.post(`${registrar.config.matrix.domain}/_matrix/client/r0/login`, auth).then((response) => {
CreateClient(response.data.access_token, response.data.user_id);
}).catch((e) => {
console.log(e);
});
matrixClient.on('event', async (event) => {
if (event.isEncrypted()) await matrixClient.decryptEventIfNeeded(event, { emit: false, isRetry: false });
if (event.getSender() === matrixClient.credentials.userId) return matrix.utils.selfReact(event);
if (!event.event.content['m.relates_to']) return;
if (event.event.unsigned.age > 10000) return;
return event.getType() === 'm.room.message'
? matrix.utils.handleReply(event) : matrix.utils.handleReact(event);
});
let CreateClient = (token, user_id) => {
const matrixClient = sdk.createClient({
baseUrl: registrar.config.matrix.domain,
accessToken: token,
userId: user_id,
timelineSupport: true,
});
matrixClient.on('Room.timeline', async (event, member, toStartOfTimeline) => {
if (toStartOfTimeline) return;
if (event.isEncrypted()) await matrixClient.decryptEventIfNeeded(event, { emit: false, isRetry: false });
if (event.getType() !== 'm.room.message') return;
if (event.getSender() === matrixClient.credentials.userId) return;
if (event.event.unsigned.age > 10000) return;
roomId = event.event.room_id;
content = event.getContent().body;
if (!typeof content === 'string') return;
if (content.charAt(0) === '+') {
const args = content.slice(1).trim().split(/ +/g);
const command = args.shift().toLowerCase();
console.log(`Logs: ${event.event.sender} - ${content}`);
matrix.utils.eventHandler(args, roomId, command, event);
}
});
matrixClient.on('RoomMember.membership', (event, member) => {
if (member.membership === 'invite' && member.userId === matrixClient.credentials.userId) {
matrixClient.joinRoom(member.roomId).done(() => {
console.log('Auto-joined %s', member.roomId);
});
}
if (member.membership === 'leave' && member.userId === matrixClient.credentials.userId) {
matrixClient.forget(member.roomId).then(() => {
console.log('Kicked %s', member.roomId);
});
}
});
matrixClient.on('Room.timeline', (event, room, toStartOfTimeline) => {
if (toStartOfTimeline) return;
if (event.getType() !== 'm.room.message') return;
if (event.getSender() === matrixClient.credentials.userId) return;
if (event.event.unsigned.age > 10000) return;
if (event.event.content.body.charAt(0) === '+') {
console.log(`Logs: ${event.event.sender} - ${event.event.content.body}`);
let args = event.event.content.body.slice(1).trim().split(/ +/g);
let command = args.shift().toLowerCase();
const userInput = args.join(' ');
const flaggedInput = userInput.substr(userInput.indexOf(' ') + 1);
const address = args.slice(0, 1).join(' ').replace(/"/g, '');
args = [];
switch(command) {
case 'config':
return;
case 'help': case 'beg': case 'flood': case 'notify':
args.push(matrixClient, room, registrar);
break;
case 'tip':
args.push(matrixClient, room, address, flaggedInput, registrar);
break;
case 'archive': case 'rearchive':
args.push(matrixClient, room, userInput, !!~command.indexOf('re'), registrar);
command = 'archive';
break;
case 'plemara': case 'reply': case 'media': case 'mediareply':
case 'random': case 'randomreply': case 'randommedia': case 'randommediareply':
args.push(matrixClient, room, userInput, registrar, {
isReply: !!~command.indexOf('reply'),
hasMedia: !!~command.indexOf('media'),
hasSubject: !!~command.indexOf('random'),
});
command = 'media';
break;
default:
args.push(matrixClient, room, userInput, registrar);
}
registrar[command] && registrar[command].runQuery.apply(null, args);
}
});
matrixClient.startClient();
module.exports = matrixClient;
};

0
notification.json Normal file
View file

1027
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,7 @@
{
"name": "ligh7hau5",
"version": "3.0.1",
"name": "plemara",
"version": "0.3.0",
"description": "A Matrix to Fediverse client",
"engines": {
"node": ">=18.0.0"
},
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
@ -12,22 +9,24 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/vulet/ligh7hau5.git"
"url": "git+https://github.com/vulet/plemara.git"
},
"author": "vul",
"license": "AGPL-3.0-only",
"bugs": {
"url": "https://github.com/vulet/lighthau5/issues"
"url": "https://github.com/vulet/plemara/issues"
},
"homepage": "https://github.com/vulet/lighthau5#readme",
"homepage": "https://github.com/vulet/plemara#readme",
"dependencies": {
"axios": "^0.25.0",
"form-data": "^4.0.0",
"jsdom": "^19.0.0",
"matrix-js-sdk": "^27.2.0",
"node-localstorage": "^2.2.1",
"olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz",
"qs": "^6.11.2"
"axios": "^0.19.2",
"file-system": "^2.2.2",
"form-data": "^3.0.0",
"jsdom": "^16.2.2",
"matrix-js-sdk": "^2.4.6"
},
"devDependencies": {}
"devDependencies": {
"eslint": "^5.16.0",
"eslint-config-airbnb-base": "^13.1.0",
"eslint-plugin-import": "^2.17.3"
}
}

View file

@ -1,51 +1,24 @@
global.Olm = require('olm');
global.sdk = require('matrix-js-sdk');
global.axios = require('axios');
global.config = require('./config.js');
global.auth = require('./auth.js');
global.authEvents = [];
const { LocalStorage } = require('node-localstorage');
global.localStorage = new LocalStorage('./keys');
if (!localStorage.getItem('matrix_auth')) {
localStorage.clear();
localStorage.setItem('matrix_auth', '[]');
}
if (!localStorage.getItem('fediverse_auth')) localStorage.setItem('fediverse_auth', '{}');
if (!localStorage.getItem('timeline')) localStorage.setItem('timeline', '{}');
if (!localStorage.getItem('notifications')) localStorage.setItem('notifications', '{}');
global.fediverse = {
auth: JSON.parse(localStorage.getItem('fediverse_auth')),
utils: require('./commands/fediverse/utils.js'),
};
global.matrix = {
auth: JSON.parse(localStorage.getItem('matrix_auth')),
utils: require('./utils.js'),
};
module.exports = {
config: require('./config.js'),
archive: require('./commands/archive.js'),
proxy: require('./commands/proxy.js'),
invidious: require('./commands/invidious.js'),
nitter: require('./commands/nitter.js'),
boo: require('./commands/fediverse/boo.js'),
clap: require('./commands/fediverse/clap.js'),
copy: require('./commands/fediverse/copy.js'),
flood: require('./commands/fediverse/flood.js'),
follow: require('./commands/fediverse/follow.js'),
boo: require('./commands/boo.js'),
beg: require('./commands/beg.js'),
clap: require('./commands/clap.js'),
copy: require('./commands/copy.js'),
flood: require('./commands/flood.js'),
fren: require('./commands/fren.js'),
help: require('./commands/help.js'),
notify: require('./commands/fediverse/notify.js'),
pin: require('./commands/fediverse/pin.js'),
post: require('./commands/fediverse/post.js'),
redact: require('./commands/fediverse/redact.js'),
status: require('./commands/fediverse/status.js'),
unfollow: require('./commands/fediverse/unfollow.js'),
unpin: require('./commands/fediverse/unpin.js'),
unreblog: require('./commands/fediverse/unreblog.js'),
unroll: require('./commands/fediverse/unroll.js'),
react: require('./commands/fediverse/react.js'),
expand: require('./commands/expand.js'),
auth: require("./commands/fediverse/auth.js")
pin: require('./commands/pin.js'),
plemara: require('./commands/plemara.js'),
redact: require('./commands/redact.js'),
notify: require('./commands/notify.js'),
reply: require('./commands/reply.js'),
tip: require('./commands/tip.js'),
unfren: require('./commands/unfren.js'),
unpin: require('./commands/unpin.js'),
mordy: require('./commands/mordy.js'),
archive: require('./commands/archive.js'),
nitter: require('./commands/nitter.js'),
invidious: require('./commands/invidious.js'),
media: require('./commands/media.js'),
status: require('./commands/status.js')
};

0
timeline.json Normal file
View file

211
utils.js
View file

@ -1,211 +0,0 @@
const { MatrixEvent } = require('matrix-js-sdk/lib/models/event');
const url = require("url")
const isEmoji = string => true;
const sendError = async (event, roomId, e) => {
e.response ? error = `Error(${e.response.status}): ${e.response.data.error}`
: e.data ? error = `Error(${e.errcode}): ${e.data.error}`
: error = `Error: ${e.syscall}, ${e.code}`;
return matrixClient.sendHtmlNotice(roomId,
' ', error);
};
const addReact = async (event, key) => {
const roomId = event.event.room_id;
return matrixClient.sendEvent(event.event.room_id, 'm.reaction', {
'm.relates_to': {
rel_type: 'm.annotation',
event_id: event.getId(),
key,
},
}).catch((e) => sendError(null, roomId, e));
};
const eventHandler = (args, roomId, command, event) => {
const userInput = args.join(' ');
const flaggedInput = userInput.substr(userInput.indexOf(' ') + 1);
const address = args.slice(0, 1).join(' ').replace(/"/g, '');
args = [];
let visibility = null;
switch (command) {
case 'config':
return;
case 'help': case 'flood': case 'notify':
args.push(roomId);
break;
case 'unflood': case 'unnotify':
args.push(roomId, true);
command = command.substring(2);
break;
case 'unreact':
args.push(roomId, event, userInput, true);
command = 'react';
break;
case 'tip': case 'makeitrain':
args.push(roomId, event, address, flaggedInput);
break;
case 'archive': case 'rearchive':
args.push(roomId, userInput, !!~command.indexOf('re'));
command = 'archive';
break;
case 'post': case 'reply': case 'media': case 'mediareply':
case 'random': case 'randomreply': case 'randommedia': case 'randommediareply':
case 'direct': case 'directreply': case 'directmedia': case 'directmediareply':
case 'private': case 'privatereply': case 'privatemedia': case 'privatemediareply':
case 'unlisted': case 'unlistedreply': case 'unlistedmedia': case 'unlistedmediareply':
visibility = command.match(/^(direct|private|unlisted)/);
args.push(roomId, event, userInput, {
isReply: !!~command.indexOf('reply'),
hasMedia: !!~command.indexOf('media'),
hasSubject: !!~command.indexOf('random'),
visibility: visibility ? visibility[1] : null
});
command = 'post';
break;
case 'crossblog':
args.push(roomId, event, userInput, true);
command = 'nitter'
break;
// fallthrough
default:
args.push(roomId, event, userInput);
}
if(["boo","clap","copy","flood","follow","notify","pin","unpin","post","react","redact","status","unfollow","unreblog","unroll"].includes(command) && !fediverse.auth[event.getSender()]) return matrixClient.sendHtmlNotice(roomId, ' ',`${event.getSender()}, для использования команды ${command} нужно привязать аккаунт Fediverse. Используйте для этого команду +auth <имя сервера>`)
registrar[command] && registrar[command].runQuery.apply(null, args);
};
/**
matrixClient.fetchRoomEvent() does not return an Event class
however, this class is necessary for decryption, so reinstate it.
afterwards, decrypt.
*/
const fetchEncryptedOrNot = async (roomId, event) => {
const fetchedEvent = await matrixClient.fetchRoomEvent(roomId, event.event_id)
const realEvent = new MatrixEvent(fetchedEvent);
if (realEvent.isEncrypted()) {
await matrixClient.decryptEventIfNeeded(realEvent, { emit: false, isRetry: false });
}
return realEvent;
}
module.exports.sendError = sendError;
module.exports.addReact = addReact;
module.exports.eventHandler = eventHandler;
module.exports.fetchEncryptedOrNot = fetchEncryptedOrNot
module.exports.editNoticeHTML = (roomId, event, html, plain) => matrixClient.sendMessage(roomId, {
body: ` * ${plain || html.replace(/<[^<]+?>/g, '')}`,
formatted_body: ` * ${html}`,
format: 'org.matrix.custom.html',
msgtype: 'm.notice',
'm.new_content': {
body: plain || html.replace(/<[^<]+?>/g, ''),
formatted_body: html,
format: 'org.matrix.custom.html',
msgtype: 'm.notice',
},
'm.relates_to': {
rel_type: 'm.replace',
event_id: event.event_id,
},
});
module.exports.handleReact = async (event) => {
const reactions = config.matrix.reactions;
const roomId = event.event.room_id;
if (!event.getContent()['m.relates_to']) return;
const reaction = event.getContent()['m.relates_to'];
const metaEvent = await fetchEncryptedOrNot(roomId, reaction);
if (!metaEvent.getContent().meta || metaEvent.event.sender !== config.matrix.user) return;
let args = metaEvent.getContent().meta.split(' ');
isMeta = ['status', 'reblog', 'mention', 'redact', 'unreblog'];
if (!isMeta.includes(args[0])) return;
let command = [];
args.shift().toLowerCase();
switch (reaction.key) {
case reactions.copy: command = 'copy'; break;
case reactions.clap: command = 'clap'; break;
case reactions.redact: command = 'redact'; break;
case reactions.rain: command = 'makeitrain'; break;
case reactions.unroll: command = 'unroll'; break;
case reactions.expand:
command = 'expand';
args = [ reaction.event_id ];
break;
default:
if (isEmoji(reaction.key)) {
command = 'react';
args.push(reaction.key);
}
break;
}
eventHandler(args, roomId, command, event);
};
module.exports.handleReply = async (event) => {
const roomId = event.event.room_id;
if(!event.event.content['m.relates_to']['m.in_reply_to']) return;
const reply = event.event.content['m.relates_to']['m.in_reply_to'];
const metaEvent = await fetchEncryptedOrNot(roomId, reply);
if(authEvents.includes(metaEvent.event_id)){
const domain = metaEvent.event.content.body.match(/https?:\/\/[^\s]+/);
if(domain && domain[0]){
domain[0] = url.parse(domain[0]).host
let code = event.getContent().body.split("\n");
code = code[code.length-1].trim()
auth.obtainAccessToken(domain[0],code,event)
authEvents = authEvents.filter(f => f != event.event_id)
}
}
if (!metaEvent.getContent().meta || metaEvent.event.sender !== config.matrix.user) return;
const args = metaEvent.getContent().meta.split(' ');
args.push(event.getContent().formatted_body.trim().split('</mx-reply>')[1]);
isMeta = ['status', 'reblog', 'mention', 'redact', 'unreblog'];
if (!isMeta.includes(args[0])) return;
args.shift().toLowerCase();
command = 'reply';
eventHandler(args, roomId, command, event);
};
module.exports.selfReact = async (event) => {
const reactions = config.matrix.reactions;
if (event.getType() !== 'm.room.message') return;
if (event.event.unsigned.age > 10000) return;
if (!event.getContent().meta) return;
const { meta } = event.getContent();
const type = meta.split(' ')[0];
if (type === 'redact' || type === 'unreblog')
addReact(event, reactions.redact);
if (type === 'status' || type === 'reblog' || type === 'mention')
addReact(event, reactions.expand);
};
module.exports.expandReact = async (event) => {
const reactions = config.matrix.reactions;
if (event.getSender() !== matrixClient.credentials.userId) return;
if (!event.getContent().meta) return;
const { meta } = event.getContent();
const type = meta.split(' ')[0];
if (type === 'status' || type === 'reblog' || type === 'mention') {
addReact(event, reactions.unroll);
addReact(event, reactions.copy);
addReact(event, reactions.clap);
if (config.fediverse.tipping)
addReact(event, reactions.rain);
}
};
module.exports.retryPromise = async (argList, promiseFn) => {
let err;
for(var arg of argList) {
try {
return await promiseFn(arg);
} catch(e) { err = e; }
}
throw err || new Error('retryPromise error');
};

2142
yarn.lock

File diff suppressed because it is too large Load diff