Skip to content

Commit

Permalink
feat(fediverse): media posting for e2ee rooms [kakashi]
Browse files Browse the repository at this point in the history
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
  • Loading branch information
vulet committed Aug 23, 2023
1 parent 5924009 commit 8e2ce18
Show file tree
Hide file tree
Showing 11 changed files with 339 additions and 482 deletions.
3 changes: 1 addition & 2 deletions auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ const matrixTokenLogin = async () => {
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()
Expand All @@ -26,7 +25,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 = {
Expand Down
94 changes: 67 additions & 27 deletions commands/fediverse/post.js
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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 }) => {
Expand All @@ -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(' '));
Expand All @@ -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,
Expand All @@ -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(() => {});
}
Expand Down
8 changes: 5 additions & 3 deletions commands/fediverse/unroll.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, '❌');
Expand Down
16 changes: 14 additions & 2 deletions commands/fediverse/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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,
},
})
};
Expand Down Expand Up @@ -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.');
}
Expand Down
2 changes: 2 additions & 0 deletions commands/help.js
Original file line number Diff line number Diff line change
Expand Up @@ -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>'
Expand Down
8 changes: 8 additions & 0 deletions config.example.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ module.exports = {
password: 'your_password',
domains: [ 'your_homeserver.com' ],
manualVerify: false,
reactions: {
copy: '🔁',
clap: '👏',
redact: '🗑️️',
rain: '🌧️',
unroll: '🔍️',
expand: '➕',
}
},
fediverse: {
domain: 'https://your_federation.com',
Expand Down
1 change: 1 addition & 0 deletions main.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ matrixClient.on('Room.timeline', async (event, member, toStartOfTimeline) => {
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();
Expand Down
11 changes: 7 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
{
"name": "ligh7hau5",
"version": "2.1.1",
"version": "3.0.0",
"description": "A Matrix to Fediverse client",
"engines": {
"node": ">=18.0.0"
},
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
Expand All @@ -21,10 +24,10 @@
"axios": "^0.25.0",
"form-data": "^4.0.0",
"jsdom": "^19.0.0",
"matrix-js-sdk": "^17.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.8.tgz",
"qs": "^6.10.3"
"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": {}
}
4 changes: 3 additions & 1 deletion registrar.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,7 @@ module.exports = {
unfollow: require('./commands/fediverse/unfollow.js'),
unpin: require('./commands/fediverse/unpin.js'),
unreblog: require('./commands/fediverse/unreblog.js'),
unroll: require('./commands/fediverse/unroll.js')
unroll: require('./commands/fediverse/unroll.js'),
react: require('./commands/fediverse/react.js'),
expand: require('./commands/expand.js')
};
Loading

0 comments on commit 8e2ce18

Please sign in to comment.