Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Matrix bridge #46

Merged
merged 19 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
/config
/config.ts
/docker-compose.yml
/db
8 changes: 5 additions & 3 deletions packages/bolt-matrix/deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ export {
AppServiceRegistration,
Bridge,
Intent,
MatrixUser,
Request,
type ClientEncryptionSession,
type WeakEvent
} from 'npm:[email protected]';
export {
Bolt,
BoltPlugin,
type BoltBridgeMessageArgs,
type BoltMessage
bolt_plugin,
type bridge_platform,
type message
} from '../bolt/mod.ts';
export { Buffer } from 'node:buffer';
61 changes: 38 additions & 23 deletions packages/bolt-matrix/events.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { BoltMessage, Intent, Request, WeakEvent } from './deps.ts';
import MatrixPlugin from './mod.ts';
import { message, Intent, Request, WeakEvent } from './deps.ts';
import { matrix_plugin } from './mod.ts';

export async function onEvent(this: MatrixPlugin, request: Request<WeakEvent>) {
export async function onEvent(
this: matrix_plugin,
request: Request<WeakEvent>
) {
const event = request.getData();
const bot = this.bot.getBot();
const intent = this.bot.getIntent();
Expand All @@ -16,52 +19,64 @@ export async function onEvent(this: MatrixPlugin, request: Request<WeakEvent>) {
this.emit('debug', `Failed to join room ${event.room_id}: ${e}`);
}
}
if (event.type === 'm.room.message' && !event['m.new_content']) {
this.emit('messageCreate', await messageToCore(event, intent));
if (event.type === 'm.room.message' && !event.content['m.new_content']) {
this.emit(
'create_message',
await messageToCore(event, intent, this.config.homeserverUrl)
);
}
if (event.type === 'm.room.message' && event['m.new_content']) {
this.emit('messageUpdate', await messageToCore(event, intent));
if (event.type === 'm.room.message' && event.content['m.new_content']) {
this.emit(
'edit_message',
await messageToCore(event, intent, this.config.homeserverUrl)
);
}
if (event.type === 'm.room.redaction') {
this.emit('messageDelete', {
this.emit('delete_message', {
id: event.redacts as string,
platform: { name: 'bolt-matrix', message: event },
channel: event.room_id,
timestamp: event.origin_server_ts
timestamp: Temporal.Instant.fromEpochMilliseconds(event.origin_server_ts)
});
}
}

export async function messageToCore(
event: WeakEvent,
intent: Intent
): Promise<BoltMessage<WeakEvent>> {
intent: Intent,
homeserverUrl: string
): Promise<message<WeakEvent>> {
const sender = await intent.getProfileInfo(event.sender);
return {
author: {
username: sender.displayname || event.sender,
rawname: event.sender,
id: event.sender,
profile: sender.avatar_url
profile: `${sender.avatar_url?.replace(
'mxc://',
`${homeserverUrl}/_matrix/media/v3/thumbnail/`
)}?width=96&height=96&method=scale`
},
channel: event.room_id,
id: event.event_id,
timestamp: event.origin_server_ts,
content: event.content.body as string,
reply: async (msg: BoltMessage<unknown>) => {
id:
event.content['m.relates_to']?.rel_type == 'm.replace'
? event.content['m.relates_to'].event_id
: event.event_id,
timestamp: Temporal.Instant.fromEpochMilliseconds(event.origin_server_ts),
content: (event.content['m.new_content']?.body ||
event.content.body) as string,
reply: async (msg: message<unknown>) => {
await intent.sendMessage(event.room_id, coreToMessage(msg));
},
platform: { name: 'bolt-matrix', message: event }
};
}

export function coreToMessage(msg: BoltMessage<unknown>) {
export function coreToMessage(msg: message<unknown>) {
return {
content: {
body: msg.content
? msg.content
: "*this bridge doesn't support anything except text at the moment*",
msgtype: 'm.text'
}
body: msg.content
? msg.content
: "*this bridge doesn't support anything except text at the moment*",
msgtype: 'm.text'
};
}
188 changes: 98 additions & 90 deletions packages/bolt-matrix/mod.ts
Original file line number Diff line number Diff line change
@@ -1,131 +1,139 @@
import {
AppServiceRegistration,
Bolt,
BoltBridgeMessageArgs,
BoltMessage,
BoltPlugin,
Bridge,
ClientEncryptionSession,
existsSync
Buffer,
MatrixUser,
existsSync,
bolt_plugin,
message,
bridge_platform
} from './deps.ts';
import { coreToMessage, onEvent } from './events.ts';

type MatrixConfig = {
accessToken: string;
appserviceUrl: string;
williamhorning marked this conversation as resolved.
Show resolved Hide resolved
homeserverUrl: string;
domain: string;
port?: number;
reg_path: string;
};

export default class MatrixPlugin extends BoltPlugin {
export class matrix_plugin extends bolt_plugin<MatrixConfig> {
bot: Bridge;
config: MatrixConfig;
name = 'bolt-revolt';
version = '0.5.4';
bolt?: Bolt;
constructor(config: MatrixConfig) {
super();
this.config = config;
name = 'bolt-matrix';
version = '0.5.6';
support = ['0.5.5'];

constructor(bolt: Bolt, config: MatrixConfig) {
super(bolt, config);
this.bot = new Bridge({
homeserverUrl: this.config.homeserverUrl,
domain: this.config.domain,
registration: this.config.reg_path,
bridgeEncryption: {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason to remove all the encryption code? I'd prefer it if bolt-matrix supported encrypted rooms but if there's a reason I guess that's fine

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because it doesn't launch on my end with it, i might revisit it in the future, but almost all matrix-appservice-${platform} don't have encryption

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah okay, i guess we could go back to that in the future then

homeserverUrl: config.homeserverUrl,
store: {
getStoredSession: async (userId: string) => {
return JSON.parse(
(await this.bolt?.redis?.get(`mtx-session-${userId}`)) || 'null'
);
},
setStoredSession: async (session: ClientEncryptionSession) => {
await this.bolt?.redis?.set(
`mtx-session-${session.userId}`,
JSON.stringify(session)
);
},
async updateSyncToken() {}
}
},
controller: {
onEvent: onEvent.bind(this)
},
roomStore: './db/roomStore.db',
userStore: './db/userStore.db',
userActivityStore: './db/userActivityStore.db'
});
}
async start(bolt: Bolt) {
this.bolt = bolt;
if (!existsSync(this.config.reg_path)) {
const reg = new AppServiceRegistration(this.config.homeserverUrl);
const reg = new AppServiceRegistration(this.config.appserviceUrl);
reg.setAppServiceToken(AppServiceRegistration.generateToken());
reg.setHomeserverToken(AppServiceRegistration.generateToken());
reg.setId(
'b4d15f02f7e406db25563c1a74ac78863dc4fbcc5595db8d835f6ee6ffef1448'
);
reg.setId(AppServiceRegistration.generateToken());
austinhuang0131 marked this conversation as resolved.
Show resolved Hide resolved
reg.setProtocols(['bolt']);
reg.setRateLimited(false);
reg.setSenderLocalpart('boltbot');
reg.addRegexPattern('users', '@bolt_*', true);
reg.setSenderLocalpart('bot.bolt');
reg.addRegexPattern('users', `@bolt-.+_.+:${this.config.domain}`, true);
reg.outputAsYaml(this.config.reg_path);
}
await this.bot.run(this.config.port || 8081);
this.bot.run(this.config.port || 8081);
}
bridgeSupport = { text: true };

// deno-lint-ignore require-await
async createSenddata(channelId: string) {
async create_bridge(channelId: string) {
return channelId;
}
async bridgeMessage(data: BoltBridgeMessageArgs) {
const intent = this.bot.getIntent(
`${data.data.platform.name}_${
'author' in data.data ? data.data.author.id : 'deletion'
}`
);
const room = data.data.bridgePlatform.senddata as string;
switch (data.type) {
case 'create':
case 'update': {
const message = coreToMessage(
data.data as unknown as BoltMessage<unknown>
);
let editinfo = {};
if (data.type === 'update') {
editinfo = {
'm.new_content': message,
'm.relates_to': {
rel_type: 'm.replace',
event_id: data.data.id
}
};

is_bridged(_msg: message<unknown>) {
// TODO: implement this
return true;
}

async create_message(
msg: message<unknown>,
platform: bridge_platform,
edit = false
) {
const room = platform.senddata as string;
const name = `@${platform.plugin}_${msg.author.id}:${this.config.domain}`;
const intent = this.bot.getIntent(name);
// check for profile
await intent.ensureProfile(msg.author.username);
const store = this.bot.getUserStore();
let storeUser = await store?.getMatrixUser(name);
if (!storeUser) {
storeUser = new MatrixUser(name);
}
if (storeUser?.get('avatar') != msg.author.profile) {
storeUser?.set('avatar', msg.author.profile);
const b = await (await fetch(msg.author.profile || '')).blob();
const newMxc = await intent.uploadContent(
Buffer.from(await b.arrayBuffer()),
{ type: b.type }
);
await intent.ensureProfile(msg.author.username, newMxc);
await store?.setMatrixUser(storeUser);
}
// now to our message
const message = coreToMessage(msg);
let editinfo = {};
if (edit) {
editinfo = {
'm.new_content': message,
'm.relates_to': {
rel_type: 'm.replace',
event_id: msg.id
}
const result = await intent.sendMessage(room, {
...message,
...editinfo
});
return {
channel: room,
id: result.event_id,
plugin: 'bolt-matrix',
senddata: room
};
}
case 'delete': {
await intent.sendEvent(room, 'm.room.redaction', {
content: {
reason: 'bridge message deletion'
},
redacts: data.data.id
});
return {
channel: room,
id: data.data.id,
plugin: 'bolt-matrix',
senddata: room
};
}
};
}
const result = await intent.sendMessage(room, {
...message,
...editinfo
});
return {
channel: room,
id: result.event_id,
plugin: 'bolt-matrix',
senddata: room
};
}

async edit_message(
msg: message<unknown>,
platform: bridge_platform & { id: string }
) {
return await this.create_message(msg, platform, true);
}

async delete_message(
_msg: message<unknown>,
platform: bridge_platform & { id: string }
) {
const room = platform.senddata as string;
const intent = this.bot.getIntent();
await intent.botSdkIntent.underlyingClient.redactEvent(
room,
platform.id,
'bridge message deletion'
);
return {
channel: room,
id: platform.id,
plugin: 'bolt-matrix',
senddata: room
};
}
}
1 change: 1 addition & 0 deletions packages/bolt/utils/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export type plugin_events = {
create_message: [message<unknown>];
create_command: [command_arguments];
create_nonbridged_message: [message<unknown>];
debug: [unknown];
edit_message: [message<unknown>];
delete_message: [deleted_message<unknown>];
ready: [];
Expand Down