2021-02-14 10:57:35 +03:00
|
|
|
const qs = require('qs');
|
2023-08-23 10:25:48 +03:00
|
|
|
const crypto = require('crypto');
|
2021-02-14 10:57:35 +03:00
|
|
|
const FormData = require('form-data');
|
|
|
|
|
|
|
|
const emojis = { public: '🌐', unlisted: '📝', private: '🔒️', direct: '✉️' };
|
|
|
|
exports.visibilityEmoji = (v) => emojis[v] || v;
|
|
|
|
|
2023-08-23 10:25:48 +03:00
|
|
|
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;
|
|
|
|
};
|
|
|
|
|
2021-02-14 10:57:35 +03:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-08-23 10:25:48 +03:00
|
|
|
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 };
|
2021-02-14 10:57:35 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
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',
|
2024-09-29 20:53:07 +03:00
|
|
|
url: `https://${fediverse.auth[event.getSender()].domain}/api/v1/media`,
|
|
|
|
headers: form.getHeaders({ Authorization: `Bearer ${fediverse.auth[event.getSender()].access_token}` }),
|
2021-02-14 10:57:35 +03:00
|
|
|
data: form,
|
|
|
|
});
|
|
|
|
if (upload.statusText !== 'OK') throw upload;
|
|
|
|
return upload.data.id;
|
|
|
|
};
|
|
|
|
|
2023-08-23 10:25:48 +03:00
|
|
|
const run = async (roomId, event, content, replyId, mediaURL, subject, visibility) => {
|
2021-02-14 10:57:35 +03:00
|
|
|
let mediaId = null;
|
|
|
|
if (mediaURL) {
|
2023-08-23 10:25:48 +03:00
|
|
|
const media = await getMedia(roomId, mediaURL);
|
2021-02-14 10:57:35 +03:00
|
|
|
mediaId = await mediaUpload(config.fediverse, media);
|
|
|
|
}
|
2021-02-26 08:54:30 +03:00
|
|
|
if (replyId) content = await fediverse.utils.getStatusMentions(replyId, event).then(m => m.concat(content).join(' '));
|
2021-02-14 10:57:35 +03:00
|
|
|
const response = await axios({
|
2019-07-04 00:27:39 +03:00
|
|
|
method: 'POST',
|
2024-09-29 20:53:07 +03:00
|
|
|
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' },
|
2021-02-14 10:57:35 +03:00
|
|
|
data: qs.stringify({
|
|
|
|
status: content,
|
|
|
|
content_type: 'text/markdown',
|
2023-08-23 10:25:48 +03:00
|
|
|
visibility: visibility || undefined,
|
2021-02-14 10:57:35 +03:00
|
|
|
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}`);
|
|
|
|
};
|
|
|
|
|
2023-08-23 10:25:48 +03:00
|
|
|
exports.runQuery = async (roomId, event, userInput, { isReply, hasMedia, hasSubject, visibility }) => {
|
2021-02-14 10:57:35 +03:00
|
|
|
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;
|
2023-08-23 10:25:48 +03:00
|
|
|
if (isReply) replyId = chunks.shift();
|
|
|
|
if (hasMedia) mediaURL = chunks.shift();
|
|
|
|
return await run(roomId, event, chunks.join(' '), replyId, mediaURL, subject, visibility);
|
2021-02-14 10:57:35 +03:00
|
|
|
} catch (e) {
|
2024-09-29 20:53:07 +03:00
|
|
|
console.error(e)
|
2021-02-14 10:57:35 +03:00
|
|
|
return matrixClient.sendHtmlNotice(roomId, 'Sad!', '<strong>Sad!</strong>').catch(() => {});
|
|
|
|
}
|
2019-07-04 00:27:39 +03:00
|
|
|
};
|