Compare commits

...

38 commits

Author SHA1 Message Date
73dac81f2d
improve authorization 2024-09-29 20:53:07 +03:00
vulet
296cdc75a6 chore(package): bump version 2023-09-04 18:23:41 +08:00
vulet
4cb8d1da7e chore(help): add crossblog 2023-09-04 18:23:41 +08:00
vulet
17c7f819ac feat(fediverse): add twitter crossblog [kakashi]
chore(config): update maintained nitter instances
2023-09-04 18:23:37 +08:00
vulet
12c422c324 fix(timeline): some missing files 2023-09-04 17:43:52 +08:00
vulet
8e2ce18f26 feat(fediverse): media posting for e2ee rooms [kakashi]
feat(fediverse): direct messaging and follower-only posting [kakashi]
feat(config): emoji customization available in config [kakashi]
fix(cmd): unroll by text [kakashi]
refactor(reacts): timeline minimum +4 events => timeline minimum +1 events [kakashi]
chore(deps): upgrade matrix-js-sdk, olm, qs
chore(package): bump version
2023-08-23 15:25:48 +08:00
vulet
5924009154 feat(MSC3440): implement threads for feed and notifications.
fix(sendHtmlNotice): possibly MSC1767 related.
2022-04-26 03:09:55 +00:00
vulet
3122361c6c chore(deps): upgrade matrix-js-sdk.
fix(e2ee): getContent() only summons m.relates_to for reactions.
2022-04-25 06:53:50 +00:00
vulet
3b16a0495c feat(cmd): stop flood/notify.
fix(cmd): bad return.
chore(deps): upgrade all.
2022-01-31 18:06:01 +08:00
vulet
35cec7751f
chore(deps): bump axios from 0.21.1 to 0.21.2
chore(deps): bump axios from 0.21.1 to 0.21.2
2021-09-15 14:36:06 +08:00
dependabot[bot]
9067ae600b
chore(deps): bump axios from 0.21.1 to 0.21.2
Bumps [axios](https://github.com/axios/axios) from 0.21.1 to 0.21.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/master/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.21.1...v0.21.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-15 06:35:31 +00:00
vulet
25e49ffc78
chore(deps): bump matrix-js-sdk from 12.2.0 to 12.4.1
https://github.com/vulet/ligh7hau5/issues/11

https://matrix.org/blog/2021/09/13/vulnerability-disclosure-key-sharing

Special thanks to @otrapersona!
2021-09-15 14:35:02 +08:00
dependabot[bot]
ccdcf69bea
chore(deps): bump matrix-js-sdk from 12.2.0 to 12.4.1
Bumps [matrix-js-sdk](https://github.com/matrix-org/matrix-js-sdk) from 12.2.0 to 12.4.1.
- [Release notes](https://github.com/matrix-org/matrix-js-sdk/releases)
- [Changelog](https://github.com/matrix-org/matrix-js-sdk/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/matrix-org/matrix-js-sdk/compare/v12.2.0...v12.4.1)

---
updated-dependencies:
- dependency-name: matrix-js-sdk
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-14 20:25:01 +00:00
vulet
dded657009 chore(package): bump version. 2021-08-09 17:35:10 +08:00
vulet
06e0ce26a9 fix(archive): The latest version of the matrix-js-sdk dependency requires a Node version of at least: 12.9. Node 12 introduced TLSv1.3 as default, which axios then uses. This is causing the archive command to break, so the particular command will continue forward with TLSv1.2 for now. 2021-08-09 17:15:38 +08:00
vulet
6273452876 fix(e2ee): command-by-reaction/emote and reply by </mx-reply>.
chore(deps): upgrade matrix-js-sdk, and refactor for it. bump version.
2021-08-06 20:13:36 +08:00
vulet
44e4138b80 chore(all): bump version 2021-07-23 16:37:14 +08:00
vulet
be3e68f221 fix(nitter): missing threading.
fix(proxy): add into registrar.
2021-07-23 16:29:45 +08:00
vulet
b7e73fc533 refactor(proxy): w/ cycling, rm fallback.
refactor(config): adjust for cycling.
2021-07-23 14:02:32 +08:00
vulet
2f58d6bb84 chore(deps): update OLM dist. upgrade all. 2021-06-15 10:44:32 +08:00
vulet
472552c33b chore(cmd): add proxy shorthand 2021-06-07 15:44:21 +08:00
vulet
54631f5c2a refactor(invidious/nitter): add instance fallback. chore(readme): update instances. 2021-06-07 15:44:13 +08:00
dependabot[bot]
c7e3f26f60
chore(deps): bump ws from 7.4.3 to 7.4.6
Bumps [ws](https://github.com/websockets/ws) from 7.4.3 to 7.4.6.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/7.4.3...7.4.6)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-30 22:30:38 +00:00
vulet
a371b7c501 fix(e2ee): self-react after decryption promise 2021-04-25 16:04:01 +08:00
vulet
39a88b2722 fix(notifications/timeline): low lengths. chore(config): add invidious.fdn.fr. 2021-04-25 15:53:23 +08:00
vulet
340fed6346 fix(feed): attached content on mentions 2021-03-01 14:06:28 +08:00
vulet
bfde4265c6 chore(all): bump version 1.2.2 2021-02-26 13:56:40 +08:00
vulet
50e9f808da fix(fediverse): move mentions array to string. allow events for error handling. 2021-02-26 13:54:30 +08:00
vulet
dddb8ad014 chore(deps): bump versions 2021-02-26 12:32:16 +08:00
vulet
1e8577f865 feat(fediverse): automatically attach mentions to reply. refactor(mentions): clean-up. 2021-02-26 12:21:41 +08:00
vulet
f37e2471c0 fix(10grans): forecast regression 2021-02-25 16:43:08 +08:00
vulet
7a9bac2bf4 fix(10grans): don't allow rain on tipbot 2021-02-25 11:31:55 +08:00
vulet
e916778c49 refactor(10grans): adjust forecast 2021-02-23 11:07:00 +08:00
vulet
14006d9209 fix(feed): don't throw on unknown notification types. 2021-02-23 11:06:23 +08:00
vulet
58ef31356a feat(10grans): add make-it-rain 2021-02-22 17:49:26 +08:00
vulet
fcadc5addc refactor(e2ee): session management in config 2021-02-21 19:07:56 +08:00
vulet
9da2d13dbf fix(feed): use domestic homeserver 2021-02-21 14:10:32 +08:00
vulet
a9073b0b9d fix(reactions): match redaction codepoint. && fix/styling(feed): bad pathing, cleanup. 2021-02-19 14:09:40 +08:00
33 changed files with 2008 additions and 866 deletions

85
auth.js
View file

@ -1,12 +1,10 @@
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,
sessionStore: new sdk.WebStorageSessionStore(localStorage),
cryptoStore: new LocalStorageCryptoStore(localStorage),
});
matrixClient.initCrypto()
@ -18,6 +16,7 @@ const matrixTokenLogin = async () => {
+ '====================================================',
);
}
matrixClient.setGlobalErrorOnUnknownDevices(config.matrix.manualVerify);
matrixClient.startClient();
});
};
@ -25,7 +24,7 @@ const matrixTokenLogin = async () => {
module.exports.matrixTokenLogin = matrixTokenLogin;
module.exports.getMatrixToken = async () => {
matrixClient = sdk.createClient(config.matrix.domain);
matrixClient = sdk.createClient({ baseUrl: config.matrix.domain });
matrixClient.loginWithPassword(config.matrix.user, config.matrix.password)
.then((response) => {
matrix.auth = {
@ -40,31 +39,75 @@ module.exports.getMatrixToken = async () => {
});
};
module.exports.registerFediverseApp = async () => {
axios.post(`${config.fediverse.domain}/api/v1/apps`,
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) => {
axios.post(`${config.fediverse.domain}/oauth/token`,
{
username: config.fediverse.username,
password: config.fediverse.password,
client_id: response.data.client_id,
client_secret: response.data.client_secret,
scope: 'read write follow push',
grant_type: 'password',
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
})
.then((tokens) => {
localStorage.setItem('fediverse_auth', JSON.stringify(tokens.data, null, 2));
})
.catch((e) => {
console.log(e);
});
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,5 +1,6 @@
const { JSDOM } = require('jsdom');
const qs = require('qs');
const https = require('https');
const sleep = ms => new Promise(r => setTimeout(r, ms));
@ -35,6 +36,7 @@ const arc3Str = str => `<em>Timed out <code>${str}</code></em>`;
const run = async (roomId, userInput, rearchive) => {
const instance = axios.create({
baseURL: `https://${config.archive.domain}`,
httpsAgent: https.Agent({ maxVersion: "TLSv1.2"}),
headers: headers(config.archive),
transformResponse: [],
timeout: 10 * 1000
@ -42,7 +44,7 @@ const run = async (roomId, userInput, rearchive) => {
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));

9
commands/expand.js Normal file
View file

@ -0,0 +1,9 @@
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

@ -0,0 +1,12 @@
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,15 +0,0 @@
exports.runQuery = function (roomId, event) {
axios({
method: 'POST',
url: `${config.fediverse.domain}/api/v1/statuses`,
headers: { Authorization: `Bearer ${fediverse.auth.access_token}` },
data: { status: '@10grans@fedi.cc beg' },
})
.then(() => {
matrix.utils.addReact(event, '✅');
})
.catch((e) => {
matrix.utils.addReact(event, '❌');
matrix.utils.sendError(event, roomId, e);
});
};

View file

@ -1,8 +1,8 @@
exports.runQuery = function (roomId, event, userInput) {
axios({
method: 'POST',
url: `${config.fediverse.domain}/api/v1/statuses/${userInput}/unfavourite`,
headers: { Authorization: `Bearer ${fediverse.auth.access_token}` },
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, '✅');

View file

@ -1,8 +1,8 @@
exports.runQuery = function (roomId, event, userInput) {
axios({
method: 'POST',
url: `${config.fediverse.domain}/api/v1/statuses/${userInput}/favourite`,
headers: { Authorization: `Bearer ${fediverse.auth.access_token}` },
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, '✅');

View file

@ -1,8 +1,8 @@
exports.runQuery = function (roomId, event, userInput) {
axios({
method: 'POST',
url: `${config.fediverse.domain}/api/v1/statuses/${userInput}/reblog`,
headers: { Authorization: `Bearer ${fediverse.auth.access_token}` },
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, '✅');

View file

@ -1,23 +1,36 @@
exports.runQuery = function (roomId) {
setInterval(() => {
let intervalId = null;
exports.runQuery = function (roomId, disable) {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
if (disable) return;
intervalId = setInterval(() => {
axios({
method: 'GET',
url: `${config.fediverse.domain}/api/v1/timelines/home`,
headers: { Authorization: `Bearer ${fediverse.auth.access_token}` },
url: `https://${fediverse.auth[event.getSender()].domain}/api/v1/timelines/home`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
})
.then((res) => {
let past = JSON.parse(localStorage.getItem('timeline'));
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) {
if (events[i].created_at < past.slice(18, 19)[0].created_at) return;
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);
}
}
localStorage.setItem('timeline', JSON.stringify(events, null, 2));
timeline[event.getSender()] = events
localStorage.setItem('timeline', JSON.stringify(timeline, null, 2));
})
.catch((e) => {
matrix.utils.sendError(null, roomId, e);

View file

@ -5,8 +5,8 @@ exports.runQuery = async function (roomId, event, userInput) {
const suggest = [];
axios({
method: 'GET',
url: `${config.fediverse.domain}/api/v2/search?q=${userInput}&type=accounts`,
headers: { Authorization: `Bearer ${fediverse.auth.access_token}` },
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;

View file

@ -1,20 +0,0 @@
exports.runQuery = function (roomId, event, userInput) {
axios({
method: 'POST',
url: `${config.fediverse.domain}/api/v1/statuses`,
headers: { Authorization: `Bearer ${fediverse.auth.access_token}` },
data: {
status: `@mordekai ${userInput}`,
content_type: 'text/markdown',
visibility: 'unlisted',
expires_in: '7200',
},
})
.then(() => {
matrix.utils.addReact(event, '✅');
})
.catch((e) => {
matrix.utils.addReact(event, '❌');
matrix.utils.sendError(event, roomId, e);
});
};

View file

@ -1,23 +1,36 @@
exports.runQuery = function (roomId) {
setInterval(() => {
let intervalId = null;
exports.runQuery = function (roomId, disable) {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
if (disable) return;
intervalId = setInterval(() => {
axios({
method: 'GET',
url: `${config.fediverse.domain}/api/v1/notifications`,
headers: { Authorization: `Bearer ${fediverse.auth.access_token}` },
url: `https://${fediverse.auth[event.getSender()].domain}/api/v1/notifications`,
headers: { Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` },
})
.then((res) => {
let past = JSON.parse(localStorage.getItem('notifications'));
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) {
if (events[i].created_at < past.slice(18, 19)[0].created_at) return;
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);
}
}
localStorage.setItem('notifications', JSON.stringify(events, null, 2));
notifications[event.getSender()] = events
localStorage.setItem('notifications', JSON.stringify(notifications, null, 2));
})
.catch((e) => {
matrix.utils.sendError(null, roomId, e);

View file

@ -1,8 +1,8 @@
exports.runQuery = function (roomId, event, userInput) {
axios({
method: 'POST',
url: `${config.fediverse.domain}/api/v1/statuses/${userInput}/pin`,
headers: { Authorization: `Bearer ${fediverse.auth.access_token}` },
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, '✅');

View file

@ -1,9 +1,61 @@
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 {
@ -14,15 +66,14 @@ const getFilename = (header) => {
}
};
const mediaDownload = async (url, { whitelist, blacklist }) => {
const media = await axios({ method: 'GET', url, responseType: 'arraybuffer' });
if (media.statusText !== 'OK' || blacklist.includes(media.headers['content-type'])) throw media;
if (whitelist.length && !whitelist.includes(media.headers['content-type'])) throw media;
return {
data: media.data,
filename: getFilename(media.headers['content-disposition']),
mimetype: media.headers['content-type'],
};
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 }) => {
@ -33,27 +84,29 @@ const mediaUpload = async ({ domain }, { data, filename, mimetype }) => {
});
const upload = await axios({
method: 'POST',
url: `${domain}/api/v1/media`,
headers: form.getHeaders({ Authorization: `Bearer ${fediverse.auth.access_token}` }),
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, content, replyId, mediaURL, subject) => {
const run = async (roomId, event, content, replyId, mediaURL, subject, visibility) => {
let mediaId = null;
if (mediaURL) {
const media = await mediaDownload(mediaURL, config.fediverse.mimetypes);
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: `${config.fediverse.domain}/api/v1/statuses`,
headers: { Authorization: `Bearer ${fediverse.auth.access_token}`, 'Content-Type': 'application/x-www-form-urlencoded' },
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,
@ -62,29 +115,18 @@ const run = async (roomId, content, replyId, mediaURL, subject) => {
return fediverse.utils.sendEventWithMeta(roomId, `<a href="${response.data.url}">${response.data.id}</a>`, `redact ${response.data.id}`);
};
exports.runQuery = async (roomId, userInput, { isReply, hasMedia, hasSubject }) => {
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[0];
chunks.shift();
}
if (hasMedia) {
let url = new URL(chunks[0]);
chunks.shift();
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:') throw '';
if (!config.matrix.domains.includes(url.hostname)) throw '';
if (!/^\/_matrix\/media\/r0\/download\/[^/]+\/[^/]+\/?$/.test(url.pathname)) throw '';
mediaURL = url.toString();
}
return await run(roomId, chunks.join(' '), replyId, mediaURL, subject);
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(() => {});
}
};

26
commands/fediverse/react.js vendored Normal file
View file

@ -0,0 +1,26 @@
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,8 +1,8 @@
exports.runQuery = function (roomId, event, userInput) {
axios({
method: 'DELETE',
url: `${config.fediverse.domain}/api/v1/statuses/${userInput}`,
headers: { Authorization: `Bearer ${fediverse.auth.access_token}` },
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, '✅');

View file

@ -1,8 +1,8 @@
exports.runQuery = function (roomId, event, userInput) {
axios({
method: 'GET',
url: `${config.fediverse.domain}/api/v1/statuses/${userInput}`,
headers: { Authorization: `Bearer ${fediverse.auth.access_token}` },
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';

View file

@ -1,15 +0,0 @@
exports.runQuery = function (roomId, address, flaggedInput, event) {
axios({
method: 'POST',
url: `${config.fediverse.domain}/api/v1/statuses`,
headers: { Authorization: `Bearer ${fediverse.auth.access_token}` },
data: { status: `@10grans@fedi.cc tip ${flaggedInput} to ${address}` },
})
.then(() => {
matrix.utils.addReact(event, '✅');
})
.catch((e) => {
matrix.utils.addReact(event, '❌');
matrix.utils.sendError(event, roomId, e);
});
};

View file

@ -5,8 +5,8 @@ exports.runQuery = async function (roomId, event, userInput) {
const suggest = [];
axios({
method: 'GET',
url: `${config.fediverse.domain}/api/v2/search?q=${userInput}&type=accounts`,
headers: { Authorization: `Bearer ${fediverse.auth.access_token}` },
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;

View file

@ -1,8 +1,8 @@
exports.runQuery = function (roomId, event, userInput) {
axios({
method: 'POST',
url: `${config.fediverse.domain}/api/v1/statuses/${userInput}/unpin`,
headers: { Authorization: `Bearer ${fediverse.auth.access_token}` },
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, '✅');

View file

@ -1,8 +1,8 @@
exports.runQuery = function (roomId, event, userInput) {
axios({
method: 'POST',
url: `${config.fediverse.domain}/api/v1/statuses/${userInput}/unreblog`,
headers: { Authorization: `Bearer ${fediverse.auth.access_token}` },
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, '✅');

View file

@ -0,0 +1,28 @@
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

@ -8,7 +8,22 @@ const sendEventWithMeta = async (roomId, content, meta) => {
});
};
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);
@ -19,52 +34,70 @@ const hasAttachment = (res) => {
const notifyFormatter = (res, roomId) => {
userDetails = `<b><a href="${config.fediverse.domain}/${res.account.id}">
${res.account.acct}</a></b>`;
${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.</b></font>
<br><blockquote><i>${res.account.note}</i></blockquote>`;
<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="${res.status.uri}">favorited</a>
your post:</b></font>
<br><blockquote><i><b>${res.status.content}</i></b></blockquote>`;
<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="${res.status.uri}">mentioned</a>
you:</b></font><br><blockquote><i><b>${res.status.content}
<br>(id: ${res.status.id}) ${registrar.post.visibilityEmoji(res.status.visibility)}</i></b>
</blockquote>`;
<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="${res.status.uri}">repeated</a>
your post:</b></font><br>
<blockquote><i><b>${res.status.content}</i></b></blockquote>`;
<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:
throw 'Unknown notification type.';
return console.log('Unknown notification type.');
}
};
const isOriginal = (res, roomId) => {
const isOriginal = (res, roomId, event) => {
if (res.data) res = res.data;
userDetails = `<b><a href="${config.fediverse.domain}/${res.account.id}">
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}`;
@ -73,18 +106,19 @@ const isOriginal = (res, roomId) => {
${hasAttachment(res)}
<br>(id: ${res.id}) ${registrar.post.visibilityEmoji(res.visibility)}
</blockquote>`;
sendEventWithMeta(roomId, content, meta);
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.account.id}">
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 <a href="${config.fediverse.domain}/${res.reblog.id}">repeated</a>
${res.reblog.account.acct}'s post:</b></font>
<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)}
@ -94,11 +128,13 @@ const isReblog = (res, roomId) => {
module.exports.sendEventWithMeta = sendEventWithMeta;
module.exports.formatter = (res, roomId) => {
module.exports.thread = thread;
module.exports.formatter = (res, roomId, event) => {
const filtered = (res.label === 'notifications')
? notifyFormatter(res, roomId)
: (res.reblog == null)
? isOriginal(res, roomId)
? isOriginal(res, roomId, event)
: isReblog(res, roomId);
return filtered;
};
@ -106,8 +142,8 @@ module.exports.formatter = (res, roomId) => {
module.exports.follow = (roomId, account, event, original) => {
axios({
method: 'POST',
url: `${config.fediverse.domain}/api/v1/accounts/${account[0].id}/follow`,
headers: { Authorization: `Bearer ${fediverse.auth.access_token}` },
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, '✅');
@ -122,8 +158,8 @@ module.exports.follow = (roomId, account, event, original) => {
module.exports.unfollow = (roomId, account, event, original) => {
axios({
method: 'POST',
url: `${config.fediverse.domain}/api/v1/accounts/${account[0].id}/unfollow`,
headers: { Authorization: `Bearer ${fediverse.auth.access_token}` },
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, '✅');
@ -134,3 +170,24 @@ module.exports.unfollow = (roomId, account, event, original) => {
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;
};

View file

@ -1,21 +1,24 @@
exports.runQuery = function (roomId) {
matrixClient.sendHtmlNotice(roomId,
'',
' ',
'<blockquote><b>fediverse commands<br>'
+ '+post [your message] : post<br>'
+ '+direct [@recipient] [message] : direct message<br>'
+ '+private [message] : follower-only message<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>'
+ '+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<br>'
+ '+beg : beg for 10grans<br>'
+ '+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>'
@ -24,6 +27,7 @@ exports.runQuery = function (roomId) {
+ `<blockquote><b>ligh7hau5 version ${require('../package.json').version}</b><br>`
+ '<b>--- <i>Contributors🐱</i> ---</b><br>'
+ '<b>CRYPTOMOONERS</b><br>'
+ '<b>doesnm</b><br>'
+ '<b><i>docs by LINT</i></b></blockquote>'
);
};

View file

@ -1,13 +1,10 @@
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,
@ -18,8 +15,8 @@ const invidious = async (instance, url) => {
};
};
const card = (video, base, path) =>
`<a href="${base}/${path}"><b>${video.name}</a></b><blockquote><b><i>` +
const card = (video, path) =>
`<a href="https://${video.url}/${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}` : ``)+
@ -29,21 +26,28 @@ const card = (video, base, path) =>
`<br />(${video.date})</b> <br />
</blockquote>`;
const run = async (roomId, userInput) => {
const instance = axios.create({
baseURL: `https://${config.invidious.domain}/api/v1/videos/`,
headers: headers(config.invidious),
const getInstance = (domain, config) =>
axios.create({
baseURL: `https://${domain}/api/v1/videos`,
headers: {
Host: `${domain}`,
'User-Agent': `${config.userAgent}`,
},
transformResponse: [],
timeout: 10 * 1000,
});
const video = await invidious(instance, userInput);
return await matrixClient.sendHtmlNotice(roomId, '', card(video, `https://${config.invidious.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) => {
try {
const url = new URL(userInput);
if (!config.invidious.domains.includes(url.hostname)) throw '';
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 '';

View file

@ -1,10 +1,5 @@
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;
@ -15,7 +10,9 @@ const nitter = async (instance, url) => {
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,
@ -30,6 +27,10 @@ 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(),
@ -38,35 +39,49 @@ const nitter = async (instance, url) => {
};
};
const card = (tweet, base, check, path) =>
`<a href="${base}/${tweet.handle.replace(/^@/, '')}"><b>${tweet.name}</b></a> ` +
const card = (tweet, check, path) =>
`<a href="${tweet.url}/${tweet.handle.replace(/^@/, '')}"><b>${tweet.name}</b></a> ` +
(tweet.check ? `${check} ` : '') +
`<a href="${base}${path}"><b>${tweet.date}</b></a> ` +
`<a href="${tweet.url}${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="${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>` : '');
(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 run = async (roomId, userInput) => {
const instance = axios.create({
baseURL: `https://${config.nitter.domain}`,
headers: headers(config.nitter),
const getInstance = (domain, config) =>
axios.create({
baseURL: `https://${domain}`,
headers: {
Host: `${domain}`,
'User-Agent': `${config.userAgent}`,
},
transformResponse: [],
timeout: 10 * 1000,
});
const tweet = await nitter(instance, userInput);
return await matrixClient.sendHtmlNotice(roomId, '', card(tweet, `https://${config.nitter.domain}`, config.nitter.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) => {
exports.runQuery = async (roomId, event, userInput, fedi) => {
try {
const url = new URL(userInput);
if (!config.nitter.domains.includes(url.hostname)) throw '';
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);
return await run(roomId, url.pathname, fedi);
} catch (e) {
return matrixClient.sendHtmlNotice(roomId, 'Sad!', '<strong>Sad!</strong>').catch(() => {});
}

View file

@ -1,34 +0,0 @@
module.exports = {
matrix: {
domain: 'https://your_homeserver.com',
user: '@your_user:your_homeserver.com',
password: 'your_password',
domains: [ 'your_homeserver.com' ]
},
fediverse: {
domain: 'https://your_federation.com',
username: '',
password: '',
client_name: 'ligh7hau5',
subject: '',
mimetypes: {
whitelist: [],
blacklist: []
}
},
archive: {
domain: 'archive.is',
userAgent: 'Mozilla/4.0 (compatible; Beep Boop)'
},
nitter: {
domain: 'nitter.fdn.fr',
userAgent: 'Mozilla/4.0 (compatible; Beep Boop)',
domains: [ 'nitter.net', 'www.nitter.net', 'twitter.com', 'www.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', 'youtu.be' ]
}
};

View file

@ -1,7 +1,7 @@
global.registrar = require('./registrar.js');
matrix.auth.access_token ? auth.matrixTokenLogin() : auth.getMatrixToken();
if (!fediverse.auth.access_token && config.fediverse.username) auth.registerFediverseApp();
//if (!fediverse.auth.access_token && config.fediverse.username) auth.registerFediverseApp();
matrixClient.on('RoomMember.membership', (event, member) => {
if (member.membership === 'invite' && member.userId === matrixClient.credentials.userId) {
@ -18,8 +18,9 @@ matrixClient.on('RoomMember.membership', (event, member) => {
});
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.getContent()['m.relates_to']) return;
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);
@ -27,12 +28,13 @@ matrixClient.on('event', async (event) => {
matrixClient.on('Room.timeline', async (event, member, toStartOfTimeline) => {
if (toStartOfTimeline) return;
if (event.isEncrypted()) await event._decryptionPromise;
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();

1027
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,10 @@
{
"name": "ligh7hau5",
"version": "1.2.0",
"version": "3.0.1",
"description": "A Matrix to Fediverse client",
"engines": {
"node": ">=18.0.0"
},
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
@ -18,13 +21,13 @@
},
"homepage": "https://github.com/vulet/lighthau5#readme",
"dependencies": {
"axios": "^0.21.1",
"form-data": "^3.0.0",
"jsdom": "^16.4.0",
"matrix-js-sdk": "^9.5.1",
"node-localstorage": "^2.1.6",
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
"qs": "^6.9.6"
"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"
},
"devDependencies": {}
}

View file

@ -3,16 +3,16 @@ 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', '[]');
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 = {
@ -27,23 +27,25 @@ global.matrix = {
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'),
beg: require('./commands/fediverse/beg.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'),
help: require('./commands/help.js'),
mordy: require('./commands/fediverse/mordy.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'),
tip: require('./commands/fediverse/tip.js'),
unfollow: require('./commands/fediverse/unfollow.js'),
unpin: require('./commands/fediverse/unpin.js'),
unreblog: require('./commands/fediverse/unreblog.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")
};

140
utils.js
View file

@ -1,9 +1,13 @@
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);
' ', error);
};
const addReact = async (event, key) => {
@ -23,6 +27,7 @@ const eventHandler = (args, roomId, command, event) => {
const address = args.slice(0, 1).join(' ').replace(/"/g, '');
args = [];
let visibility = null;
switch (command) {
case 'config':
@ -30,8 +35,16 @@ const eventHandler = (args, roomId, command, event) => {
case 'help': case 'flood': case 'notify':
args.push(roomId);
break;
case 'tip':
args.push(roomId, address, flaggedInput);
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'));
@ -39,35 +52,52 @@ const eventHandler = (args, roomId, command, event) => {
break;
case 'post': case 'reply': case 'media': case 'mediareply':
case 'random': case 'randomreply': case 'randommedia': case 'randommediareply':
args.push(roomId, userInput, {
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 'proxy':
try {
const url = new URL(userInput);
command = config.invidious.domains.includes(url.hostname)
? 'invidious'
: config.nitter.domains.includes(url.hostname)
? 'nitter'
: 'proxy';
} catch (e) { sendError(event, roomId, e); }
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}`,
@ -86,30 +116,55 @@ module.exports.editNoticeHTML = (roomId, event, html, plain) => matrixClient.sen
});
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'];
if (!reaction) return;
const metaEvent = await matrixClient.fetchRoomEvent(roomId, reaction.event_id);
if (!metaEvent.content.meta || metaEvent.sender !== config.matrix.user) return;
const args = metaEvent.content.meta.split(' ');
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();
if (reaction.key === '🔃') command = 'copy';
if (reaction.key === '👏') command = 'clap';
if (reaction.key === '🗑') command = 'redact';
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;
const reply = event.getContent()['m.relates_to']['m.in_reply_to'];
if (!reply) return;
const metaEvent = await matrixClient.fetchRoomEvent(roomId, reply.event_id);
if (!metaEvent.content.meta || metaEvent.sender !== config.matrix.user) return;
const args = metaEvent.content.meta.split(' ');
args.push(event.event.content.formatted_body.trim().split('</mx-reply>')[1]);
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();
@ -118,14 +173,39 @@ module.exports.handleReply = async (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();
if (!meta) return;
const type = meta.split(' ')[0];
if (type === 'redact' || type === 'unreblog') addReact(event, '🗑️');
if (type === 'status' || type === 'reblog' || type === 'mention') {
addReact(event, '🔃');
addReact(event, '👏');
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');
};

1012
yarn.lock

File diff suppressed because it is too large Load diff