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
This commit is contained in:
parent
5924009154
commit
8e2ce18f26
11 changed files with 333 additions and 476 deletions
|
@ -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 }) => {
|
||||
|
@ -41,10 +92,10 @@ const mediaUpload = async ({ domain }, { data, filename, mimetype }) => {
|
|||
return upload.data.id;
|
||||
};
|
||||
|
||||
const run = async (roomId, event, 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(' '));
|
||||
|
@ -55,6 +106,7 @@ const run = async (roomId, event, content, replyId, mediaURL, subject) => {
|
|||
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,
|
||||
|
@ -63,28 +115,16 @@ const run = async (roomId, event, 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, event, 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, event, 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) {
|
||||
return matrixClient.sendHtmlNotice(roomId, 'Sad!', '<strong>Sad!</strong>').catch(() => {});
|
||||
}
|
||||
|
|
|
@ -7,17 +7,19 @@ exports.runQuery = function (roomId, event, userInput) {
|
|||
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, event, '<br><hr><h3>...Beginning thread...</h3><hr><br>');
|
||||
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, event)
|
||||
fediverse.utils.formatter(entry, roomId, eventId);
|
||||
}
|
||||
await fediverse.utils.thread(roomId, event, '<br><hr><h3>...Thread ended...</h3><hr><br>');
|
||||
await fediverse.utils.thread(roomId, eventId, '<br><hr><h3>...Thread ended...</h3><hr><br>');
|
||||
})
|
||||
.catch((e) => {
|
||||
matrix.utils.addReact(event, '❌');
|
||||
|
|
|
@ -8,7 +8,7 @@ const sendEventWithMeta = async (roomId, content, meta) => {
|
|||
});
|
||||
};
|
||||
|
||||
const thread = async (roomId, event, content, meta) => {
|
||||
const thread = async (roomId, eventId, content, meta) => {
|
||||
await matrixClient.sendEvent(roomId, 'm.room.message', {
|
||||
body: content.replace(/<[^<]+?>/g, ''),
|
||||
msgtype: 'm.notice',
|
||||
|
@ -17,7 +17,7 @@ const thread = async (roomId, event, content, meta) => {
|
|||
format: 'org.matrix.custom.html',
|
||||
'm.relates_to': {
|
||||
rel_type: 'm.thread',
|
||||
event_id: event['event']['content']['m.relates_to']['event_id'],
|
||||
event_id: eventId,
|
||||
},
|
||||
})
|
||||
};
|
||||
|
@ -78,6 +78,18 @@ const notifyFormatter = (res, roomId) => {
|
|||
</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.');
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ exports.runQuery = function (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>'
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue