From df1e8d06f3beec9ee29ae7d3e8b2dd37312a89bc Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Sat, 3 Feb 2024 23:21:01 +0000 Subject: [PATCH 01/16] fix(bolt-matrix): fix setup procedure * fix name * fix localpart reservation and request * add appserviceUrl option (a web server where Matrix homeserver should send messages to) * remove encryption --- packages/bolt-matrix/mod.ts | 35 ++++++++--------------------------- 1 file changed, 8 insertions(+), 27 deletions(-) diff --git a/packages/bolt-matrix/mod.ts b/packages/bolt-matrix/mod.ts index 7b3a13d..07e4b65 100644 --- a/packages/bolt-matrix/mod.ts +++ b/packages/bolt-matrix/mod.ts @@ -11,7 +11,7 @@ import { import { coreToMessage, onEvent } from './events.ts'; type MatrixConfig = { - accessToken: string; + appserviceUrl: string; homeserverUrl: string; domain: string; port?: number; @@ -21,7 +21,7 @@ type MatrixConfig = { export default class MatrixPlugin extends BoltPlugin { bot: Bridge; config: MatrixConfig; - name = 'bolt-revolt'; + name = 'bolt-matrix'; version = '0.5.4'; bolt?: Bolt; constructor(config: MatrixConfig) { @@ -31,23 +31,6 @@ export default class MatrixPlugin extends BoltPlugin { homeserverUrl: this.config.homeserverUrl, domain: this.config.domain, registration: this.config.reg_path, - bridgeEncryption: { - 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) }, @@ -59,16 +42,14 @@ export default class MatrixPlugin extends BoltPlugin { 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()); reg.setProtocols(['bolt']); reg.setRateLimited(false); - reg.setSenderLocalpart('boltbot'); - reg.addRegexPattern('users', '@bolt_*', true); + reg.setSenderLocalpart('bot.bolt'); + reg.addRegexPattern('users', `@bolt-(discord|revolt)_.+:${this.config.domain}`, true); reg.outputAsYaml(this.config.reg_path); } await this.bot.run(this.config.port || 8081); @@ -80,9 +61,9 @@ export default class MatrixPlugin extends BoltPlugin { } async bridgeMessage(data: BoltBridgeMessageArgs) { const intent = this.bot.getIntent( - `${data.data.platform.name}_${ + `@${data.data.platform.name}_${ 'author' in data.data ? data.data.author.id : 'deletion' - }` + }:${this.config.domain}` ); const room = data.data.bridgePlatform.senddata as string; switch (data.type) { From ca9a53c58f3aab059c7301d4e9436aeccf3810cc Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Sat, 3 Feb 2024 23:21:36 +0000 Subject: [PATCH 02/16] fix(bolt-matrix): convert mxc uri to thumbnail endpoint for avatar bridging --- packages/bolt-matrix/events.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/bolt-matrix/events.ts b/packages/bolt-matrix/events.ts index 94251e7..e1b2443 100644 --- a/packages/bolt-matrix/events.ts +++ b/packages/bolt-matrix/events.ts @@ -17,10 +17,10 @@ export async function onEvent(this: MatrixPlugin, request: Request) { } } if (event.type === 'm.room.message' && !event['m.new_content']) { - this.emit('messageCreate', await messageToCore(event, intent)); + this.emit('messageCreate', await messageToCore(event, intent, this.config.homeserverUrl)); } if (event.type === 'm.room.message' && event['m.new_content']) { - this.emit('messageUpdate', await messageToCore(event, intent)); + this.emit('messageUpdate', await messageToCore(event, intent, this.config.homeserverUrl)); } if (event.type === 'm.room.redaction') { this.emit('messageDelete', { @@ -34,7 +34,8 @@ export async function onEvent(this: MatrixPlugin, request: Request) { export async function messageToCore( event: WeakEvent, - intent: Intent + intent: Intent, + homeserverUrl: String ): Promise> { const sender = await intent.getProfileInfo(event.sender); return { @@ -42,7 +43,7 @@ export async function messageToCore( 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, @@ -57,11 +58,9 @@ export async function messageToCore( export function coreToMessage(msg: BoltMessage) { 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' }; } From 4f971cbf5842a3c3c2d74a031b9784ed83976fe6 Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Sun, 4 Feb 2024 16:32:34 +0000 Subject: [PATCH 03/16] refactor(bolt-matrix): homeserverUrl in avatar Co-authored-by: William Horning --- packages/bolt-matrix/events.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/bolt-matrix/events.ts b/packages/bolt-matrix/events.ts index e1b2443..35dfd71 100644 --- a/packages/bolt-matrix/events.ts +++ b/packages/bolt-matrix/events.ts @@ -17,10 +17,10 @@ export async function onEvent(this: MatrixPlugin, request: Request) { } } if (event.type === 'm.room.message' && !event['m.new_content']) { - this.emit('messageCreate', await messageToCore(event, intent, this.config.homeserverUrl)); + this.emit('messageCreate', await messageToCore(event, intent); } if (event.type === 'm.room.message' && event['m.new_content']) { - this.emit('messageUpdate', await messageToCore(event, intent, this.config.homeserverUrl)); + this.emit('messageUpdate', await messageToCore(event, intent)); } if (event.type === 'm.room.redaction') { this.emit('messageDelete', { @@ -35,7 +35,6 @@ export async function onEvent(this: MatrixPlugin, request: Request) { export async function messageToCore( event: WeakEvent, intent: Intent, - homeserverUrl: String ): Promise> { const sender = await intent.getProfileInfo(event.sender); return { @@ -43,7 +42,7 @@ export async function messageToCore( username: sender.displayname || event.sender, rawname: event.sender, id: event.sender, - profile: `${sender.avatar_url.replace("mxc://", `${homeserverUrl}/_matrix/media/v3/thumbnail/`)}?width=96&height=96&method=scale` + profile: `${sender.avatar_url.replace("mxc://", `${this.config.homeserverUrl}/_matrix/media/v3/thumbnail/`)}?width=96&height=96&method=scale` }, channel: event.room_id, id: event.event_id, From bf2d14206309128b8ec6df3f53ddd992758eacc5 Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Sun, 4 Feb 2024 16:37:51 +0000 Subject: [PATCH 04/16] fix(bolt-matrix): regex pattern for bridge puppets Co-authored-by: William Horning --- packages/bolt-matrix/mod.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bolt-matrix/mod.ts b/packages/bolt-matrix/mod.ts index 07e4b65..641fa7e 100644 --- a/packages/bolt-matrix/mod.ts +++ b/packages/bolt-matrix/mod.ts @@ -49,7 +49,7 @@ export default class MatrixPlugin extends BoltPlugin { reg.setProtocols(['bolt']); reg.setRateLimited(false); reg.setSenderLocalpart('bot.bolt'); - reg.addRegexPattern('users', `@bolt-(discord|revolt)_.+:${this.config.domain}`, true); + reg.addRegexPattern('users', `@bolt-.+_.+:${this.config.domain}`, true); reg.outputAsYaml(this.config.reg_path); } await this.bot.run(this.config.port || 8081); From 37becfcf0acbcd01c954a8fdceaac24178edf8a9 Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Sun, 4 Feb 2024 13:17:36 -0500 Subject: [PATCH 05/16] fix(bolt-matrix): syntax --- packages/bolt-matrix/events.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bolt-matrix/events.ts b/packages/bolt-matrix/events.ts index 35dfd71..40ea043 100644 --- a/packages/bolt-matrix/events.ts +++ b/packages/bolt-matrix/events.ts @@ -17,7 +17,7 @@ export async function onEvent(this: MatrixPlugin, request: Request) { } } if (event.type === 'm.room.message' && !event['m.new_content']) { - this.emit('messageCreate', await messageToCore(event, intent); + this.emit('messageCreate', await messageToCore(event, intent)); } if (event.type === 'm.room.message' && event['m.new_content']) { this.emit('messageUpdate', await messageToCore(event, intent)); From 548846676814ab4bd513b81f516aee497add186e Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Sun, 4 Feb 2024 13:38:02 -0500 Subject: [PATCH 06/16] Revert "refactor(bolt-matrix): homeserverUrl in avatar" This reverts commit 4f971cbf5842a3c3c2d74a031b9784ed83976fe6. simply put, it doesn't work --- packages/bolt-matrix/events.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/bolt-matrix/events.ts b/packages/bolt-matrix/events.ts index 40ea043..e1b2443 100644 --- a/packages/bolt-matrix/events.ts +++ b/packages/bolt-matrix/events.ts @@ -17,10 +17,10 @@ export async function onEvent(this: MatrixPlugin, request: Request) { } } if (event.type === 'm.room.message' && !event['m.new_content']) { - this.emit('messageCreate', await messageToCore(event, intent)); + this.emit('messageCreate', await messageToCore(event, intent, this.config.homeserverUrl)); } if (event.type === 'm.room.message' && event['m.new_content']) { - this.emit('messageUpdate', await messageToCore(event, intent)); + this.emit('messageUpdate', await messageToCore(event, intent, this.config.homeserverUrl)); } if (event.type === 'm.room.redaction') { this.emit('messageDelete', { @@ -35,6 +35,7 @@ export async function onEvent(this: MatrixPlugin, request: Request) { export async function messageToCore( event: WeakEvent, intent: Intent, + homeserverUrl: String ): Promise> { const sender = await intent.getProfileInfo(event.sender); return { @@ -42,7 +43,7 @@ export async function messageToCore( username: sender.displayname || event.sender, rawname: event.sender, id: event.sender, - profile: `${sender.avatar_url.replace("mxc://", `${this.config.homeserverUrl}/_matrix/media/v3/thumbnail/`)}?width=96&height=96&method=scale` + 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, From 85e094e87dc6a8c028b06e6efcb99b5f59d5d4ac Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Sun, 4 Feb 2024 13:52:58 -0500 Subject: [PATCH 07/16] chore(bolt-matrix): ignore db --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a5594c1..e83d93a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /config /config.ts /docker-compose.yml +db \ No newline at end of file From 0cff1f836bbedbe856ea90c2555c0ff3db1d945b Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Sun, 4 Feb 2024 14:54:59 -0500 Subject: [PATCH 08/16] fix(bolt-revolt): fix regression due to 040839ccf5bd6c21d57b48a758703bb33ff2e443 --- packages/bolt-revolt/mod.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bolt-revolt/mod.ts b/packages/bolt-revolt/mod.ts index ed4b2d6..5c843c1 100644 --- a/packages/bolt-revolt/mod.ts +++ b/packages/bolt-revolt/mod.ts @@ -61,8 +61,8 @@ export default class RevoltPlugin extends BoltPlugin { try { const msg = await coreToMessage({ ...dat, replyto }); const result = data.type === 'update' - ? (await channel.fetchMessage(dat.id)).edit(msg) // TODO - : channel.sendMessage(msg); + ? await (await channel.fetchMessage(dat.id)).edit(msg) // TODO + : await channel.sendMessage(msg); return { channel: dat.channel, id: 'id' in result ? result.id : result._id, From 3bf7ac65b4447a6b67f3ef6a4553ef686724ba02 Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Sun, 4 Feb 2024 15:46:28 -0500 Subject: [PATCH 09/16] fix: deletion logic --- packages/bolt/lib/bolt.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/bolt/lib/bolt.ts b/packages/bolt/lib/bolt.ts index d977cf1..a6e95ff 100644 --- a/packages/bolt/lib/bolt.ts +++ b/packages/bolt/lib/bolt.ts @@ -101,7 +101,6 @@ export class Bolt extends EventEmitter { bridgeBoltMessage(this, 'update', msg); }); this.on('messageDelete', async msg => { - if (await getBoltBridgedMessage(this, msg.id)) return; bridgeBoltMessage(this, 'delete', msg); }); this.on('threadCreate', thread => bridgeBoltThread(this, 'create', thread)); From 8d6dc72eadf1224464bfef9b8077c2ab5e484aa4 Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Sun, 4 Feb 2024 15:46:55 -0500 Subject: [PATCH 10/16] feat(bolt-matrix): deletion --- packages/bolt-matrix/mod.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/packages/bolt-matrix/mod.ts b/packages/bolt-matrix/mod.ts index 641fa7e..7be3a86 100644 --- a/packages/bolt-matrix/mod.ts +++ b/packages/bolt-matrix/mod.ts @@ -60,15 +60,13 @@ export default class MatrixPlugin extends BoltPlugin { return channelId; } async bridgeMessage(data: BoltBridgeMessageArgs) { - const intent = this.bot.getIntent( - `@${data.data.platform.name}_${ - 'author' in data.data ? data.data.author.id : 'deletion' - }:${this.config.domain}` - ); const room = data.data.bridgePlatform.senddata as string; switch (data.type) { case 'create': case 'update': { + const intent = this.bot.getIntent( + `@${data.data.platform.name}_${data.data.author.id}:${this.config.domain}` + ); const message = coreToMessage( data.data as unknown as BoltMessage ); @@ -94,15 +92,13 @@ export default class MatrixPlugin extends BoltPlugin { }; } case 'delete': { - await intent.sendEvent(room, 'm.room.redaction', { - content: { - reason: 'bridge message deletion' - }, - redacts: data.data.id - }); + const intent = this.bot.getIntent(); + await intent.botSdkIntent.underlyingClient.redactEvent( + room, data.data.id, 'bridge message deletion' + ); return { channel: room, - id: data.data.id, + id: data.data.id, plugin: 'bolt-matrix', senddata: room }; From acda0cd04e187ab931c86b718cec4cee5683f959 Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Sun, 4 Feb 2024 16:04:14 -0500 Subject: [PATCH 11/16] refactor: separate duplication check and original message check --- packages/bolt-revolt/mod.ts | 3 ++- packages/bolt/lib/bolt.ts | 4 ++-- packages/bolt/lib/bridge/bridge.ts | 6 +++--- packages/bolt/lib/bridge/utils.ts | 4 ++-- packages/bolt/lib/commands/commands.ts | 2 +- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/bolt-revolt/mod.ts b/packages/bolt-revolt/mod.ts index 5c843c1..16a0e4b 100644 --- a/packages/bolt-revolt/mod.ts +++ b/packages/bolt-revolt/mod.ts @@ -76,7 +76,8 @@ export default class RevoltPlugin extends BoltPlugin { } case 'delete': { const channel = await this.bot.channels.fetch(data.data.channel); - await channel.deleteMessages([data.data.id]); + const msg = await channel.fetchMessage(data.data.id); + await msg.delete(); return { ...data.data.bridgePlatform, id: data.data.id }; } } diff --git a/packages/bolt/lib/bolt.ts b/packages/bolt/lib/bolt.ts index a6e95ff..b32105e 100644 --- a/packages/bolt/lib/bolt.ts +++ b/packages/bolt/lib/bolt.ts @@ -93,11 +93,11 @@ export class Bolt extends EventEmitter { private registerPluginEvents() { // TODO: move all code below to bridge folder this.on('messageCreate', async msg => { - if (await getBoltBridgedMessage(this, msg.id)) return; + if (await getBoltBridgedMessage(this, true, msg.id)) return; bridgeBoltMessage(this, 'create', msg); }); this.on('messageUpdate', async msg => { - if (await getBoltBridgedMessage(this, msg.id)) return; + if (await getBoltBridgedMessage(this, true, msg.id)) return; bridgeBoltMessage(this, 'update', msg); }); this.on('messageDelete', async msg => { diff --git a/packages/bolt/lib/bridge/bridge.ts b/packages/bolt/lib/bridge/bridge.ts index 1caade8..5f8e080 100644 --- a/packages/bolt/lib/bridge/bridge.ts +++ b/packages/bolt/lib/bridge/bridge.ts @@ -21,7 +21,7 @@ export async function bridgeBoltMessage( const platforms: (BoltBridgePlatform | BoltBridgeSentPlatform)[] | false = type === 'create' ? bridge.platforms.filter(i => i.channel !== message.channel) - : await getBoltBridgedMessage(bolt, message.id); + : await getBoltBridgedMessage(bolt, false, message.id); if (!platforms || platforms.length < 1) return; @@ -46,7 +46,7 @@ export async function bridgeBoltMessage( : (platform as BoltBridgeSentPlatform).thread?.id : undefined; - const replyto = await getBoltBridgedMessage(bolt, message.replyto?.id); + const replyto = await getBoltBridgedMessage(bolt, false, message.replyto?.id); if (bridge.settings?.realnames && 'author' in message) { message.author.username = message.author.rawname; @@ -110,7 +110,7 @@ export async function bridgeBoltMessage( if (type !== 'delete') { for (const i of data) { // since this key is used to prevent echo, 15 sec expiry should be enough - await bolt.redis?.set(`message-${i.id}`, JSON.stringify(data), { + await bolt.redis?.set(`message-temp-${i.id}`, JSON.stringify(data), { ex: 15 }); } diff --git a/packages/bolt/lib/bridge/utils.ts b/packages/bolt/lib/bridge/utils.ts index cc4bfce..72055bc 100644 --- a/packages/bolt/lib/bridge/utils.ts +++ b/packages/bolt/lib/bridge/utils.ts @@ -1,8 +1,8 @@ import { Bolt } from '../bolt.ts'; import { BoltBridgeDocument, BoltBridgeSentPlatform } from './types.ts'; -export async function getBoltBridgedMessage(bolt: Bolt, id?: string) { - return JSON.parse((await bolt.redis?.get(`message-${id}`)) || 'false') as +export async function getBoltBridgedMessage(bolt: Bolt, bCheck: Boolean, id?: string) { + return JSON.parse((await bolt.redis?.get(`message${bCheck ? "-temp" : ""}-${id}`)) || 'false') as | BoltBridgeSentPlatform[] | false; } diff --git a/packages/bolt/lib/commands/commands.ts b/packages/bolt/lib/commands/commands.ts index 3f1b074..96df6c7 100644 --- a/packages/bolt/lib/commands/commands.ts +++ b/packages/bolt/lib/commands/commands.ts @@ -14,7 +14,7 @@ export class BoltCommands { this.bolt = bolt; this.registerCommands(...defaultcommands); bolt.on('messageCreate', async msg => { - if (await getBoltBridgedMessage(bolt, msg.id)) return; + if (await getBoltBridgedMessage(bolt, true, msg.id)) return; if (msg.content?.startsWith('!bolt')) { let [_, name, ...arg] = msg.content.split(' '); this.runCommand({ From 2ce69477b3bd16e32787ef63688db3abbda38edb Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Sun, 4 Feb 2024 16:32:36 -0500 Subject: [PATCH 12/16] fix(bolt-matrix): edit --- packages/bolt-matrix/events.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/bolt-matrix/events.ts b/packages/bolt-matrix/events.ts index e1b2443..8b46249 100644 --- a/packages/bolt-matrix/events.ts +++ b/packages/bolt-matrix/events.ts @@ -16,10 +16,10 @@ export async function onEvent(this: MatrixPlugin, request: Request) { this.emit('debug', `Failed to join room ${event.room_id}: ${e}`); } } - if (event.type === 'm.room.message' && !event['m.new_content']) { + if (event.type === 'm.room.message' && !event.content['m.new_content']) { this.emit('messageCreate', await messageToCore(event, intent, this.config.homeserverUrl)); } - if (event.type === 'm.room.message' && event['m.new_content']) { + if (event.type === 'm.room.message' && event.content['m.new_content']) { this.emit('messageUpdate', await messageToCore(event, intent, this.config.homeserverUrl)); } if (event.type === 'm.room.redaction') { @@ -46,9 +46,11 @@ export async function messageToCore( 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, + id: event.content['m.relates_to']?.rel_type == "m.replace" + ? event.content['m.relates_to'].event_id + : event.event_id, timestamp: event.origin_server_ts, - content: event.content.body as string, + content: (event.content['m.new_content']?.body || event.content.body) as string, reply: async (msg: BoltMessage) => { await intent.sendMessage(event.room_id, coreToMessage(msg)); }, From 12febf32c2dc86af8998c21bb28d06e7aacdd1ba Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Sun, 4 Feb 2024 16:53:24 -0500 Subject: [PATCH 13/16] fix(bolt-revolt): display name --- packages/bolt-revolt/messages.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bolt-revolt/messages.ts b/packages/bolt-revolt/messages.ts index 30c46b4..f4c30aa 100644 --- a/packages/bolt-revolt/messages.ts +++ b/packages/bolt-revolt/messages.ts @@ -58,7 +58,7 @@ export async function messageToCore( return { author: { username: - message.member?.nickname || + message.member?.displayName || message.author?.username || `${message.authorId || 'unknown user'} on revolt`, rawname: From aff713c69a260f3615bfdbaff3a8cf1560712c1e Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Sun, 4 Feb 2024 18:08:40 -0500 Subject: [PATCH 14/16] feat(bolt-matrix): avatar update --- packages/bolt-matrix/deps.ts | 2 ++ packages/bolt-matrix/mod.ts | 25 ++++++++++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/packages/bolt-matrix/deps.ts b/packages/bolt-matrix/deps.ts index dc688cb..68367ab 100644 --- a/packages/bolt-matrix/deps.ts +++ b/packages/bolt-matrix/deps.ts @@ -3,6 +3,7 @@ export { AppServiceRegistration, Bridge, Intent, + MatrixUser, Request, type ClientEncryptionSession, type WeakEvent @@ -13,3 +14,4 @@ export { type BoltBridgeMessageArgs, type BoltMessage } from '../bolt/mod.ts'; +export { Buffer } from "https://deno.land/std@0.177.0/node/buffer.ts"; diff --git a/packages/bolt-matrix/mod.ts b/packages/bolt-matrix/mod.ts index 7be3a86..6dddf0e 100644 --- a/packages/bolt-matrix/mod.ts +++ b/packages/bolt-matrix/mod.ts @@ -5,7 +5,9 @@ import { BoltMessage, BoltPlugin, Bridge, + Buffer, ClientEncryptionSession, + MatrixUser, existsSync } from './deps.ts'; import { coreToMessage, onEvent } from './events.ts'; @@ -64,9 +66,26 @@ export default class MatrixPlugin extends BoltPlugin { switch (data.type) { case 'create': case 'update': { - const intent = this.bot.getIntent( - `@${data.data.platform.name}_${data.data.author.id}:${this.config.domain}` - ); + const name = `@${data.data.platform.name}_${data.data.author.id}:${this.config.domain}`; + const intent = this.bot.getIntent(name); + // check for profile + await intent.ensureProfile(data.data.author.username); + const store = this.bot.getUserStore(); + let storeUser = await store?.getMatrixUser(name); + if (!storeUser) { + storeUser = new MatrixUser(name); + } + if (storeUser?.get("avatar") != data.data.author.profile) { + storeUser?.set("avatar", data.data.author.profile); + let b = await (await fetch(data.data.author.profile)).blob(); + const newMxc = await intent.uploadContent( + Buffer.from(await b.arrayBuffer()), + { type: b.type } + ); + await intent.ensureProfile(data.data.author.username, newMxc); + await store?.setMatrixUser(storeUser); + } + // now to our message const message = coreToMessage( data.data as unknown as BoltMessage ); From 5c71723c702ff53e1798bfc991d75991f85ee680 Mon Sep 17 00:00:00 2001 From: Austin Huang Date: Tue, 6 Feb 2024 01:48:27 +0000 Subject: [PATCH 15/16] refactor(bolt-matrix): buffer import Co-authored-by: William Horning --- packages/bolt-matrix/deps.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bolt-matrix/deps.ts b/packages/bolt-matrix/deps.ts index 68367ab..ba77d30 100644 --- a/packages/bolt-matrix/deps.ts +++ b/packages/bolt-matrix/deps.ts @@ -14,4 +14,4 @@ export { type BoltBridgeMessageArgs, type BoltMessage } from '../bolt/mod.ts'; -export { Buffer } from "https://deno.land/std@0.177.0/node/buffer.ts"; +export { Buffer } from "node:buffer"; From faad2738f6145fabe2afeb2798679f689ea47516 Mon Sep 17 00:00:00 2001 From: Jersey Date: Tue, 5 Mar 2024 02:30:03 -0500 Subject: [PATCH 16/16] Merge branch 'main' into pr/austinhuang0131/46 Signed-off-by: Jersey --- .gitignore | 10 +- README.md | 20 -- docker-compose.example.yml | 3 +- Dockerfile => dockerfile | 6 +- .../bolt-dash/docs/content/Developing/bolt.md | 94 -------- .../docs/content/Developing/index.md | 14 -- .../docs/content/Developing/plugins.md | 40 ---- .../docs/content/Hosting/configure.md | 71 ------ .../docs/content/Hosting/dashboard.md | 10 - .../docs/content/Hosting/database.md | 16 -- .../bolt-dash/docs/content/Hosting/deno.md | 37 --- .../bolt-dash/docs/content/Hosting/docker.md | 80 ------- .../bolt-dash/docs/content/Hosting/index.md | 21 -- .../bolt-dash/docs/content/Using/commands.md | 47 ---- .../bolt-dash/docs/content/Using/index.md | 18 -- .../bolt-dash/docs/content/Using/legal.md | 107 --------- packages/bolt-dash/docs/content/changelog.md | 115 ---------- packages/bolt-dash/docs/content/index.md | 9 - packages/bolt-dash/docs/content/security.md | 34 --- packages/bolt-dash/docs/mkdocs.yml | 10 - packages/bolt-dash/docs/requirements.txt | 1 - packages/bolt-discord/_deps.ts | 18 ++ packages/bolt-discord/commands.ts | 53 ++--- packages/bolt-discord/deps.ts | 26 --- packages/bolt-discord/events.ts | 44 ++++ packages/bolt-discord/handlers.ts | 93 -------- packages/bolt-discord/messages.ts | 116 +++++----- packages/bolt-discord/mod.ts | 211 +++++++----------- packages/bolt-guilded/_deps.ts | 21 ++ packages/bolt-guilded/deps.ts | 17 -- packages/bolt-guilded/legacybridging.ts | 88 ++++---- packages/bolt-guilded/messages.ts | 191 +++++++--------- packages/bolt-guilded/mod.ts | 179 +++++++-------- packages/bolt-matrix/deps.ts | 8 +- packages/bolt-matrix/events.ts | 48 ++-- packages/bolt-matrix/mod.ts | 168 +++++++------- packages/bolt-revolt/deps.ts | 9 +- packages/bolt-revolt/messages.ts | 157 +++++-------- packages/bolt-revolt/mod.ts | 121 +++++----- packages/bolt/README.md | 5 - packages/bolt/_deps.ts | 8 + packages/bolt/bolt.ts | 78 +++++++ packages/bolt/bridges/_command_functions.ts | 174 +++++++++++++++ packages/bolt/bridges/_commands.ts | 48 ++++ packages/bolt/bridges/_deps.ts | 10 + packages/bolt/bridges/mod.ts | 198 ++++++++++++++++ packages/bolt/bridges/types.ts | 15 ++ packages/bolt/cli.ts | 135 +++++++++++ packages/bolt/cli/deps.ts | 13 -- packages/bolt/cli/migrations.ts | 81 ------- packages/bolt/cli/mod.ts | 24 -- packages/bolt/cli/run.ts | 38 ---- packages/bolt/cmds/_default.ts | 45 ++++ packages/bolt/cmds/_deps.ts | 3 + packages/bolt/cmds/_testdata.ts | 25 +++ packages/bolt/cmds/_tests.ts | 36 +++ packages/bolt/cmds/mod.ts | 56 +++++ packages/bolt/cmds/types.ts | 25 +++ packages/bolt/lib/bolt.ts | 109 --------- packages/bolt/lib/bridge/bridge.ts | 190 ---------------- packages/bolt/lib/bridge/commandinternal.ts | 126 ----------- packages/bolt/lib/bridge/commands.ts | 61 ----- packages/bolt/lib/bridge/mod.ts | 4 - packages/bolt/lib/bridge/types.ts | 51 ----- packages/bolt/lib/bridge/utils.ts | 34 --- packages/bolt/lib/commands/commands.ts | 66 ------ packages/bolt/lib/commands/default.ts | 59 ----- packages/bolt/lib/commands/mod.ts | 2 - packages/bolt/lib/commands/types.ts | 29 --- packages/bolt/lib/deps.ts | 10 - packages/bolt/lib/mod.ts | 28 --- packages/bolt/lib/types.ts | 100 --------- packages/bolt/lib/utils.ts | 169 -------------- packages/bolt/migrations/_deps.ts | 1 + packages/bolt/migrations/_testdata.ts | 67 ++++++ packages/bolt/migrations/_tests.ts | 21 ++ packages/bolt/migrations/deps.ts | 1 - packages/bolt/migrations/fourbetatofive.ts | 20 +- packages/bolt/migrations/fourtofourbeta.ts | 18 +- packages/bolt/migrations/mod.ts | 61 +++-- packages/bolt/migrations/utils.ts | 24 -- packages/bolt/mod.ts | 27 ++- packages/bolt/utils/_deps.ts | 5 + packages/bolt/utils/_testdata.ts | 76 +++++++ packages/bolt/utils/_tests.ts | 65 ++++++ packages/bolt/utils/config.ts | 22 ++ packages/bolt/utils/errors.ts | 74 ++++++ packages/bolt/utils/messages.ts | 87 ++++++++ packages/bolt/utils/mod.ts | 4 + packages/bolt/utils/plugins.ts | 67 ++++++ readme.md | 17 ++ 91 files changed, 2139 insertions(+), 2904 deletions(-) delete mode 100644 README.md rename Dockerfile => dockerfile (60%) delete mode 100644 packages/bolt-dash/docs/content/Developing/bolt.md delete mode 100644 packages/bolt-dash/docs/content/Developing/index.md delete mode 100644 packages/bolt-dash/docs/content/Developing/plugins.md delete mode 100644 packages/bolt-dash/docs/content/Hosting/configure.md delete mode 100644 packages/bolt-dash/docs/content/Hosting/dashboard.md delete mode 100644 packages/bolt-dash/docs/content/Hosting/database.md delete mode 100644 packages/bolt-dash/docs/content/Hosting/deno.md delete mode 100644 packages/bolt-dash/docs/content/Hosting/docker.md delete mode 100644 packages/bolt-dash/docs/content/Hosting/index.md delete mode 100644 packages/bolt-dash/docs/content/Using/commands.md delete mode 100644 packages/bolt-dash/docs/content/Using/index.md delete mode 100644 packages/bolt-dash/docs/content/Using/legal.md delete mode 100644 packages/bolt-dash/docs/content/changelog.md delete mode 100644 packages/bolt-dash/docs/content/index.md delete mode 100644 packages/bolt-dash/docs/content/security.md delete mode 100644 packages/bolt-dash/docs/mkdocs.yml delete mode 100644 packages/bolt-dash/docs/requirements.txt create mode 100644 packages/bolt-discord/_deps.ts delete mode 100644 packages/bolt-discord/deps.ts create mode 100644 packages/bolt-discord/events.ts delete mode 100644 packages/bolt-discord/handlers.ts create mode 100644 packages/bolt-guilded/_deps.ts delete mode 100644 packages/bolt-guilded/deps.ts delete mode 100644 packages/bolt/README.md create mode 100644 packages/bolt/_deps.ts create mode 100644 packages/bolt/bolt.ts create mode 100644 packages/bolt/bridges/_command_functions.ts create mode 100644 packages/bolt/bridges/_commands.ts create mode 100644 packages/bolt/bridges/_deps.ts create mode 100644 packages/bolt/bridges/mod.ts create mode 100644 packages/bolt/bridges/types.ts create mode 100644 packages/bolt/cli.ts delete mode 100644 packages/bolt/cli/deps.ts delete mode 100644 packages/bolt/cli/migrations.ts delete mode 100644 packages/bolt/cli/mod.ts delete mode 100644 packages/bolt/cli/run.ts create mode 100644 packages/bolt/cmds/_default.ts create mode 100644 packages/bolt/cmds/_deps.ts create mode 100644 packages/bolt/cmds/_testdata.ts create mode 100644 packages/bolt/cmds/_tests.ts create mode 100644 packages/bolt/cmds/mod.ts create mode 100644 packages/bolt/cmds/types.ts delete mode 100644 packages/bolt/lib/bolt.ts delete mode 100644 packages/bolt/lib/bridge/bridge.ts delete mode 100644 packages/bolt/lib/bridge/commandinternal.ts delete mode 100644 packages/bolt/lib/bridge/commands.ts delete mode 100644 packages/bolt/lib/bridge/mod.ts delete mode 100644 packages/bolt/lib/bridge/types.ts delete mode 100644 packages/bolt/lib/bridge/utils.ts delete mode 100644 packages/bolt/lib/commands/commands.ts delete mode 100644 packages/bolt/lib/commands/default.ts delete mode 100644 packages/bolt/lib/commands/mod.ts delete mode 100644 packages/bolt/lib/commands/types.ts delete mode 100644 packages/bolt/lib/deps.ts delete mode 100644 packages/bolt/lib/mod.ts delete mode 100644 packages/bolt/lib/types.ts delete mode 100644 packages/bolt/lib/utils.ts create mode 100644 packages/bolt/migrations/_deps.ts create mode 100644 packages/bolt/migrations/_testdata.ts create mode 100644 packages/bolt/migrations/_tests.ts delete mode 100644 packages/bolt/migrations/deps.ts delete mode 100644 packages/bolt/migrations/utils.ts create mode 100644 packages/bolt/utils/_deps.ts create mode 100644 packages/bolt/utils/_testdata.ts create mode 100644 packages/bolt/utils/_tests.ts create mode 100644 packages/bolt/utils/config.ts create mode 100644 packages/bolt/utils/errors.ts create mode 100644 packages/bolt/utils/messages.ts create mode 100644 packages/bolt/utils/mod.ts create mode 100644 packages/bolt/utils/plugins.ts create mode 100644 readme.md diff --git a/.gitignore b/.gitignore index e83d93a..384492b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ -/.env -/config -/config.ts -/docker-compose.yml -db \ No newline at end of file +/.env +/config +/config.ts +/docker-compose.yml +/db diff --git a/README.md b/README.md deleted file mode 100644 index b20ea9c..0000000 --- a/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# Bolt 0.5 - -Bolt is a cross-platform chat bot that bridges communities that's written in -Typescript and powered by Deno. To learn more, see the -[docs](https://williamhorning.dev/bolt/docs). - -## Feature support matrix - -| | text | threads | forums | -| ------------ | ----- | ------- | ------ | -| bolt | ✓ | ✓ | X | -| bolt-discord | ✓ | ✓ | ✓ | -| bolt-guilded | ✓\* | X | X | -| bolt-matrix | ✓\*\* | X | X | -| bolt-revolt | ✓ | X | X | - -\* bolt-guilded's text support doesn't fully support all the stuff other -platforms do -\*\* bolt-matrix's implementation is barely functional and shouldn't be used, -mostly a POC diff --git a/docker-compose.example.yml b/docker-compose.example.yml index bd126be..5bf9f8a 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -1,9 +1,10 @@ version: '2' + services: bolt: build: . volumes: - - ./config/config:/app/data + - ./config/data:/app/data restart: always mongo: image: mongo:6-jammy diff --git a/Dockerfile b/dockerfile similarity index 60% rename from Dockerfile rename to dockerfile index 1b51d41..5f1a289 100644 --- a/Dockerfile +++ b/dockerfile @@ -1,12 +1,12 @@ -ARG DENO_VERSION=v1.39.4 +ARG DENO_VERSION=v1.40.4 FROM docker.io/lukechannings/deno:${DENO_VERSION} # add bolt to the image WORKDIR /app ADD ./packages/bolt /app -RUN deno install --allow-all -n bolt /app/mod.ts +RUN deno install -A --unstable-temporal -n bolt /app/cli.ts # set bolt as the entrypoint and use the run command by default ENTRYPOINT [ "bolt" ] -CMD [ "run", "--config", "data/config.ts"] +CMD [ "--run", "--config", "./data/config.ts"] diff --git a/packages/bolt-dash/docs/content/Developing/bolt.md b/packages/bolt-dash/docs/content/Developing/bolt.md deleted file mode 100644 index f9a0106..0000000 --- a/packages/bolt-dash/docs/content/Developing/bolt.md +++ /dev/null @@ -1,94 +0,0 @@ -# Developing Bolt - -Hey there! Thanks for being interested in developing Bolt! As a project, work is -mostly divided between a few different areas: - -- `bolt` itself, sometimes refered to as `core`. This is where stuff like the - Plugin API, bridge system, and CLI live. -- `bolt-dash`, the site you're currently on. -- First-party plugins like `bolt-discord`. These plugins are officially - maintained and kept up to date as examples for other plugin and for use in the - hosted version of Bolt. -- Contributing to upstream projects. As part of the work done in bolt or the - first-party plugins, we sometimes need to have upstream dependencies add - features that would also be useful to other people. - -Some stuff, such as consistent styling, naming commits, running linters, and -making sure tests pass are shared among these projects. Others might have -additional or different rules as documented below. - -## General guidelines - -- Format your code using Prettier. Having consistent style across files helps - keep the codebase nicer to navigate and the code easier to read. See - [Editor Integration](https://prettier.io/docs/en/editors.html) if you want to - use Prettier in your editor. We don't use Deno's `fmt` tool. -- Use Deno Lint to check for issues in your code. While manually checking your - code should help you find errors or other issues, a linter can help make sure - you catch these. See - [this page](https://deno.land/manual@v1.39.4/tools/formatter) for more - information. -- Follow the - [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) and the - [semver 2.0.0](https://semver.org/spec/v2.0.0.html) specs whenever reasonable -- Try to not introduce breaking changes- whether they're in the DB schema or - plugin API- unless they're necessary. - -## `bolt` - -Bolt itself is where the majority of platform-independent code lives. This -module is further divided into three distinct areas: - -- `cli`, the `bolt` command. This uses the `lib` and `migrations` libraries to - provide users an easy way to run bolt from a shell. -- `lib`, the public api. This is where stuff like bridges, command system, - Plugin API, and other programatic apis aside from `migrations` live. -- `migrations`, a library dealing with database migrations. - -### General guidelines - -- Prefix all exported types/classes/enums with `Bolt` -- Name all functions that are not on classes `Bolt` whenever - reasonable -- If your functions require more than two arguments, use an object with - destructuring. -- Document all changes in `changelog.md` - -### `cli` - -Generally, the CLI shouldn't need to have breaking changes made unless -funtionality from `lib` or `migrations`. This invites the question of "well, -what exactly is a breaking change in a CLI?" To that, there's not really much in -`cli` that can really be considered a breaking change except making an existing -use-case not work. This could be something as simple as renaming a command or -changing the output of something like `bolt --version` when other programs use -that. - -### `lib` - -`lib` is the part of Bolt where bridges, commands, and plugins are defined. -Breaking changes are any changes that would require change in any usage of the -exported items from `mod.ts`, whether those changes are requiring an object -instead of a string or removing a deprecated function. Try to keep breaking -changes to a minimum, especially if they impact the plugin system. If you do -make breaking changes to the plugin system, you should increment the -`boltversion` property. Ideally, we support two versions at a time-even if it -requires more code- to make upgrading versions easier. - -### `migrations` - -The `migrations` library exists to provide a programatic way to migrate data -passed to corresponding functions exported by it. There's not a lot of reason to -change stuff here so create an issue if you're considering something. - -## First-party plugins - -Follow the general guidelines on this page and those on the -[plugins](./plugins.md) page - -## Upstream projects - -Most of the time upstream projects should just work for us, but if we need a -feature that other people would find useful (such as Deno support), we should -contribute back to these projects. Follow the contributing guidelines of those -projects and any other documentation they might have. diff --git a/packages/bolt-dash/docs/content/Developing/index.md b/packages/bolt-dash/docs/content/Developing/index.md deleted file mode 100644 index 42f46f8..0000000 --- a/packages/bolt-dash/docs/content/Developing/index.md +++ /dev/null @@ -1,14 +0,0 @@ -# Developing Bolt and plugins - -Bolt is built on top of a flexible Typescript codebase which runs on Deno. See -[bolt.md](./bolt.md) for information about developing in the Bolt repo and -[plugins.md](./plugins.md) for information about developing plugins. - -Need support? Take a look at some of the pages listed in the sidebar or join one -of the supports servers: - -- [Discord](https://discord.gg/eGq7uhtJDx) -- [Guilded](https://www.guilded.gg/i/kamX0vek) -- [Revolt](https://app.revolt.chat/invite/tpGKXcqk) - -You can find a changelog [here](../changelog.md). diff --git a/packages/bolt-dash/docs/content/Developing/plugins.md b/packages/bolt-dash/docs/content/Developing/plugins.md deleted file mode 100644 index 8f870b6..0000000 --- a/packages/bolt-dash/docs/content/Developing/plugins.md +++ /dev/null @@ -1,40 +0,0 @@ -# Developing plugins - -Bolt Plugins allow you to extend the functionality of Bolt by supplying support -for another platform or by introducing new commands. To create a plugin, export -an implementation of the `BoltPlugin` class. - -## Example: - -```ts -import { - createBoltMessage, - BoltPlugin, - Bolt -} from 'https://williamhorning.dev/bolt/x/bolt/0.5.4/mod.ts'; - -export default class ExamplePlugin extends BoltPlugin { - name = 'example'; - version = '1.0.0'; - bridgeSupport = { - text: true, - threads: true, - forum: false, - voice: false - }; - commands = [ - { - name: 'ping', - description: 'pong', - execute({ timestamp }) { - return createBoltMessage({ - content: `Pong! 🏓 ${Date.now() - timestamp}ms` - }); - } - } - ]; - async start(bolt: Bolt) { - bolt.on('event', () => {}); - } -} -``` diff --git a/packages/bolt-dash/docs/content/Hosting/configure.md b/packages/bolt-dash/docs/content/Hosting/configure.md deleted file mode 100644 index 1bedb52..0000000 --- a/packages/bolt-dash/docs/content/Hosting/configure.md +++ /dev/null @@ -1,71 +0,0 @@ -# Configuring Bolt - -## `config.ts` - -Bolt looks for a `config.ts` file in either the `--config` option passed to the -run command or in the current directory, exiting if it can't find one or parse -it. The format of `config.ts` is similar to `vite.config.ts` as it also uses a -default export and allows you to use a helper function. - -## Example - -```ts -import { defineBoltConfig } from 'https://williamhorning.dev/bolt/x/bolt/0.5.4/mod.ts'; -import bolt_discord from 'https://williamhorning.dev/bolt/x/bolt-discord/0.5.4/mod.ts'; - -export default defineBoltConfig({ - plugins: [ - new bolt_discord({ - token: 'woah', - appId: 'example' - }) - ], - database: { - mongo: { - connection: 'mongodb://localhost:27017' - }, - redis: { - hostname: 'redis' - } - }, - http: { - apiURL: 'http://bolt.localhost:8080' - }, - prod: false -}); -``` - -## Options - -### prod - -Tells Bolt whether or not it should use the prod DB collection. - -### http.apiURL - -Tells Bolt where the API is exposed. Currently unused. - -### http.dashURL - -Tells Bolt where the dashboard/docs site is located. - -### http.errorURL - -A Discord-compatible webhook to send errors to. - -### database.mongo.connection - -A MongoDB connection URL or -[ConnectOptions](https://deno.land/x/mongo@v0.31.2/mod.ts?s=ConnectOptions) - -### database.mongo.database - -The MongoDB database to be used to. - -### database.redis - -[RedisConnectOptios](https://deno.land/x/redis@v0.29.2/mod.ts?s=RedisConnectOptions) - -### plugins - -An array of BoltPlugins diff --git a/packages/bolt-dash/docs/content/Hosting/dashboard.md b/packages/bolt-dash/docs/content/Hosting/dashboard.md deleted file mode 100644 index 0bfc6e6..0000000 --- a/packages/bolt-dash/docs/content/Hosting/dashboard.md +++ /dev/null @@ -1,10 +0,0 @@ -# Dashboard website - -Bolt has a dashboard / documentation site that you should configure for your own -instance. - -## Cloudflare Pages - -Cloudflare Pages is the way the official dashboard is hosted. To set it up, set -your root directory to `./packages/bolt-dash`, build command to `./mksite.sh` -and your output directory to `./output`. diff --git a/packages/bolt-dash/docs/content/Hosting/database.md b/packages/bolt-dash/docs/content/Hosting/database.md deleted file mode 100644 index 41a9a37..0000000 --- a/packages/bolt-dash/docs/content/Hosting/database.md +++ /dev/null @@ -1,16 +0,0 @@ -# Database - -Bolt uses MongoDB to store long-term information such as bridge information and -Redis to store shorter-term data such as information about where messages were -sent to allow for editing. - -## Setup - -To configure your database, look at the `database` key in -[`config.ts`](./configure.md) - -## Migrations - -Database migrations allow you to move user data from one version of Bolt to -another. Bolt ships with an interactive CLI tool that'll help you migrate data -from one version of Bolt to another. Run `bolt migration` to get started. diff --git a/packages/bolt-dash/docs/content/Hosting/deno.md b/packages/bolt-dash/docs/content/Hosting/deno.md deleted file mode 100644 index 5fd21d6..0000000 --- a/packages/bolt-dash/docs/content/Hosting/deno.md +++ /dev/null @@ -1,37 +0,0 @@ -# Deno - -Deno isn't the way you should run in production as you want to add an extra -layer between Bolt and the host. Deno Deploy also doesn't work with Bolt, so try -using [Docker](./docker.md) or a VM in a production environment. - -## Prerequisites - -- [Deno >=1.39.4](https://deno.land) -- [Git (when running from source)](https://git-scm.com) -- [MongoDB >=4](https://www.mongodb.com/docs/manual/installation/) -- [Redis (optional)](https://redis.io/docs/getting-started/installation/) - -## Getting started - -Clone the Bolt Github repository from -`https://github.com/williamhorning/bolt.git` and open a terminal in that folder. - -If you want to use a stable version, run the following: - -```sh -git switch 0.5.4 -``` - -Then, you will probably want to install the Bolt CLI using the following: - -```sh -deno install -A --name bolt ./packages/bolt/mod.ts -``` - -If you are developing locally though or if you want to be able to make -modifications, replace `bolt` with -`deno run -A path/to/bolt/packages/bolt/mod.ts ...` - -Then take some time to [configure your bolt instance](./configure.md) and -[run any necessary DB migrations](./database.md). After all of that, run -`bolt run` to start Bolt. diff --git a/packages/bolt-dash/docs/content/Hosting/docker.md b/packages/bolt-dash/docs/content/Hosting/docker.md deleted file mode 100644 index ebe6c0c..0000000 --- a/packages/bolt-dash/docs/content/Hosting/docker.md +++ /dev/null @@ -1,80 +0,0 @@ -# Docker - -Docker is the recommended way to run Bolt and is what we use for the hosted -version documented [here](../Using/index.md). - -## Prerequisites - -- [Docker >=20.10.21](https://docker.io) -- [Docker Compose >=2](https://docs.docker.com/compose/install/) - -## Getting started - -On your server, make a new folder and create a `docker-compose.yml` file similar -to the one below: - -```yaml -version: '2' -services: - bolt: - image: williamfromnj/bolt:0.5.4 - volumes: - - ./config:/app/data - restart: always - mongo: - image: mongo:6-jammy - ports: - - 27017:27017 - volumes: - - ./db:/data/db - restart: always - redis: - image: redis:6.2-alpine - ports: - - 6379:6379 - volumes: - - ./redis:/data - restart: always -``` - -You may want to change the paths data are stored in by modifying the volumes -entry under each of the services. Once you've setup that, you can -[configure your bolt instance](./configure.md). When using the above setup, data -is stored in the `./config` directory. You must set `database.mongo` to -`mongodb://mongo:27017` and `database.redis` to the following: - -```ts -{ - hostname: 'redis'; -} -``` - -Then, you should [run any necessary DB migrations](./database.md). Run -`docker compose up` to start Bolt. - -## Accessing the Bolt CLI - -Instead of running `bolt` to access the Bolt CLI, you should use the following: - -```sh -docker compose exec bolt ... -``` - -## Running from source - -To run Bolt from source, clone the Bolt Github repository from -`https://github.com/williamhorning/bolt.git`, move your `docker-compose.yml` -file to that folder, and change the image line under the Bolt service to the -following: - -```yml -services: - bolt: - build: . - # ... -``` - -## Forwarding ports used by plugins - -To forward ports required by plugins like `bolt-matrix`, add the necessary ports -to a ports key in your compose file. diff --git a/packages/bolt-dash/docs/content/Hosting/index.md b/packages/bolt-dash/docs/content/Hosting/index.md deleted file mode 100644 index 4fc5cb0..0000000 --- a/packages/bolt-dash/docs/content/Hosting/index.md +++ /dev/null @@ -1,21 +0,0 @@ -# Hosting Bolt - -Bolt is a chat bot that's pretty easy to run on your own server if you want to. -To get started, take a look at one of the following options: - -- [Docker (recommended)](./docker.md) -- [Deno](./deno.md) - -After you've set up an instance of Bolt, take a look at these pages: - -- [Configuring Bolt](./configure.md) -- [DB Migrations](./database.md) - -Need support? Take a look at some of the pages listed in the sidebar or join one -of the supports servers: - -- [Discord](https://discord.gg/eGq7uhtJDx) -- [Guilded](https://www.guilded.gg/i/kamX0vek) -- [Revolt](https://app.revolt.chat/invite/tpGKXcqk) - -You can find a changelog [here](../changelog.md). diff --git a/packages/bolt-dash/docs/content/Using/commands.md b/packages/bolt-dash/docs/content/Using/commands.md deleted file mode 100644 index 24f219e..0000000 --- a/packages/bolt-dash/docs/content/Using/commands.md +++ /dev/null @@ -1,47 +0,0 @@ -# Commands - -The prefix for all of these commands are `!bolt`. You may also be able to use -Discord slash commands or other platform-specific ways of running commands. If -there are square brackets around text that means that's an optional parameter -you can pass to that command. If there're surrounded by angle brackets that -means that parameter is required by that command. - -## informational commands - -### help - -Links to this documentation site - -### info - -Shows some information about the instance of Bolt you're interacting with - -### ping - -A simple ping command that shows how long it takes Bolt to reply to you. - -### site - -Links to the landing page for the Bolt website - -## bridge commands - -### bridge status - -Gets information about what bridge the current channel is in. - -### bridge join - -Joins the bridge with the name provided. - -When using Bolt 0.4.x, use `--bridge=` instead. - -### bridge reset [name] - -Resets the current bridge with the name provided or the current bridges name. - -This command isn't available in Bolt 0.4.x. - -### bridge leave - -Leaves the bridge the channel is currently in. diff --git a/packages/bolt-dash/docs/content/Using/index.md b/packages/bolt-dash/docs/content/Using/index.md deleted file mode 100644 index adc1824..0000000 --- a/packages/bolt-dash/docs/content/Using/index.md +++ /dev/null @@ -1,18 +0,0 @@ -# Using Bolt - -Bolt is a cross-platform chat bot that bridges communities. To get started with -the bot, invite it using one of the following links: - -- [Discord](https://discord.com/api/oauth2/authorize?client_id=946939274434080849&permissions=8&scope=bot) -- [Guilded](https://www.guilded.gg/b/9fc1c387-fda8-47cd-b5ec-2de50c03cd64) -- [Revolt](https://app.revolt.chat/bot/01G1Y9M6G254VWBF41W3N5DQY5) - -Need support? Take a look at some of the pages listed in the sidebar or join one -of the supports servers: - -- [Discord](https://discord.gg/HEysJsa4VZ) -- [Guilded](https://www.guilded.gg/i/240lgAJ2) -- [Revolt](https://app.revolt.chat/invite/N4XXTjYF) - -You can find a changelog [here](../changelog.md) and the legal documents for the -hosted version [here](./legal.md) diff --git a/packages/bolt-dash/docs/content/Using/legal.md b/packages/bolt-dash/docs/content/Using/legal.md deleted file mode 100644 index cb1683a..0000000 --- a/packages/bolt-dash/docs/content/Using/legal.md +++ /dev/null @@ -1,107 +0,0 @@ -## Introduction - -Bolt is a cross-platform chatbot operated by William Horning and Isaac McFadyen. -This document, containing the Privacy Policy and Terms of Service for Bolt, is a -legal agreement between you, William Horning, and Isaac McFadyen. - -## Definitions - -We / Us / Our - William Horning and Isaac McFadyen -You - the user of Bolt -Bolt - Hosted version of -[williamhorning/bolt](https://github.com/williamhirning/bolt) provided by us -Third-party Chat Platforms - Discord, Guilded, and Revolt - -## Terms of Service - -### Service Provider - -Bolt operates under the laws of the following jurisdictions and is solely -provided by us: - -- Harris Township, Centre County, Pennsylvania, United States of America -- Toronto, Ontario, Canada - -The laws of Toronto supersede the laws of any other jurisdiction for the -operation of Bolt. - -### Age Requirements - -You must be at least 13 years old, or the age of majority in your jurisdiction, -to use Bolt. If you are under 13, you are not permitted to use Bolt. - -### Warranty & Liability Disclaimer - -While we may provide support and try to fix bugs and keep Bolt operating, we do -not offer warranties with Bolt. - -> TO THE EXTENT ALLOWED BY APPLICABLE LAW, WE PROVIDE BOLT “AS IS” WITHOUT ANY -> EXPRESS OR IMPLIED WARRANTIES, INCLUDING THE IMPLIED WARRANTIES OF -> MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. FOR -> EXAMPLE, WE DON’T MAKE ANY WARRANTIES ABOUT THE CONTENT OR FEATURES OF THE -> SERVICES, INCLUDING THEIR ACCURACY, RELIABILITY, AVAILABILITY, OR ABILITY TO -> MEET YOUR NEEDS. - -Additionally, you agree not to hold us liable for any action taken from you -using Bolt. In addition to that, you agree not to hold us liable for any content -proxied through Bolt (messages, videos, images, et cetera). If you are in the -United States of America, you waive your right to file a class action lawsuit -against us. - -## Privacy Policy - -### Information we collect - -To provide Bolt, we need to collect information about you. The amount of -information we collect may include the following: - -- Information provided to us by Third-party Chat Platforms -- Information you provide to us -- Channel information -- Server/Guild information -- Message content and metadata -- User information -- User Activity -- Your IP address - -### Data storage - -Bolt stores data on an Oracle VM in Toronto, Canada. We do not provide third -parties access to this data, except Oracle, Inc., who may access this data to -support us in operating Bolt. We backup encrypted copies of our data to -Microsoft Onedrive. - -### Data protection practices - -We try to protect your information as best as we can. We do not share your -information with third parties unless needed to provide Bolt. If you allow us to -share your information with other third parties, we may share your information -with those third parties. If we learn that we have collected information about a -minor, we will delete that information immediately and to the extent possible. -If we are made aware of unauthorized sharing of your information, we will notify -you immediately and try to take action against those responsible. - -### Requesting your data - -If you want a copy of your data, your data deleted, or to ask questions about -the data used by Bolt, please email bolt-data@williamhorning.dev. - -## Government Agencies and Law Enforcement - -Government Agencies and Law enforcement agencies may request that we share -information with them, and we want to balance user privacy and government -requests. - -When we receive a request from a Government Agency to share data, we will -carefully review the request to ensure it follows the laws of all applicable -jurisdictions. Additionally, we will only provide the information necessary to -comply and only if it is not a privacy risk. - -If we believe that we can prevent or reduce the risk of harm by sharing -information with Government Agencies, we may share information with those -agencies. We still review these requests to make sure they comply with our -policies and applicable laws. - -If we get a request from a Government Agency regarding you, we will notify you -as soon as possible unless legally prohibited. We will not notify you if a -request is related to an emergency until after the emergency has passed. diff --git a/packages/bolt-dash/docs/content/changelog.md b/packages/bolt-dash/docs/content/changelog.md deleted file mode 100644 index 12e7ed6..0000000 --- a/packages/bolt-dash/docs/content/changelog.md +++ /dev/null @@ -1,115 +0,0 @@ -# Changelog - -## 0.5.4 - -- update docker example -- rename license file -- update docs and /x links -- update legal stuff -- move site stuff to williamhorning/williamhorning -- bump versions - -## 0.4.13 - -- add tool-versions file -- remove prettier config -- update license year and docs links -- switch over to bridgev1 -- clean up bridge code -- update command stuff -- backport platform code -- fix docker-compose file - -## 0.5.3 - -- bump dependency versions -- bump deno version -- fix bug in bolt-guilded -- fix some typing errors - -## 0.5.2 - -- update the bridging commands to match 0.4.x in style -- redo the command system -- implement role colors for revolt -- discord subcommands -- fix bridge creation -- refactor core -- bump dependency versions -- implement embed-bridge migration -- fix discord intents -- bump dependency versions -- bump deno version - -## 0.5.1 - -- bump dependency versions - -## 0.5.0 - -- that whole rewrite thing - -## 0.4.9 - -- backport some fixes from 0.5.0 - -## 0.4.8 - -- fix Revolt replies - -## 0.4.7 - -- actually fix DB issues - -## 0.4.6 - -- try fixing DB issues - -## 0.4.5 - -- fix DB handling that broke beta bridges... and caused discord to decide to - reset our token in prod, oops - -## 0.4.4 - -- fix `!bolt bridge status` command DB lookup -- fix beta bridge system edit support -- docs changes - -## 0.4.3 - -- actualy fix webhook issue - -## 0.4.2 - -- try new webhook creation method for guilded - -## 0.4.1 - -- fix bug - -## 0.4.0 - -- slash commands - -## 0.3.1 - -- Move bridge-specific stuff out of `/index.js` -- Rename `/bridge/legacyBridge.js` to `/bridge/legacyBridgeSend.js` -- Edit bridge documentation to emphasise the word `NOT` -- Fix Guilded username issue, hopefully -- Fix Guilded embed issue, hopefully -- Fix `/docker.compose.yml` - -## 0.3.0 - -- Reimplement the command handler (look at `/commands/index.js`) -- Implement `!bolt ping` command -- Implement docs -- Move most utilities out of other files into `/utils.js` -- Fix Revolt embed mapping -- Implement versioning - -## 0.2.0 and below - -Check out the commit log for this info, sorry diff --git a/packages/bolt-dash/docs/content/index.md b/packages/bolt-dash/docs/content/index.md deleted file mode 100644 index 19a71a6..0000000 --- a/packages/bolt-dash/docs/content/index.md +++ /dev/null @@ -1,9 +0,0 @@ -# Bolt - -Bolt is a cross-platform chat bot that bridges communities written in Typescript -and powered by Deno. This docs site contains documentation for the hosted -version of Bolt, the Bolt CLI, and other things related to Bolt. - -- [Using Bolt](./Using/index.md) -- [Hosting Bolt](./Hosting/index.md) -- [Developing Bolt](./Developing/index.md) diff --git a/packages/bolt-dash/docs/content/security.md b/packages/bolt-dash/docs/content/security.md deleted file mode 100644 index 5959514..0000000 --- a/packages/bolt-dash/docs/content/security.md +++ /dev/null @@ -1,34 +0,0 @@ -# Security Policy - -Thanks for taking the time to look into the security of Bolt! We prioritize -security and appriciate efforts to improve the security of Bolt. This document -outlines how to work with us and what to do if you find a security issue. - -## Supported Versions - -| version | supported | -| ------- | --------- | -| >= 0.5 | ✓ | -| 0.4.x | patches | -| <=0.3.1 | X | - -## How to report a vulnerability - -**_DO NOT, under any circumstances, report security issues on Github._** - -Please email any issues to -[security@williamhorning.dev](mailto:security@williamhorning.dev) and include as -much information as possible. We aim to solve all issues as quickly as possible -and are willing to help you publish writeups after the issue is fixed. - -### Things not to do: - -- exploiting any vulnerabilities that you find on instances of Bolt that are not - operated by yourself. -- publishing or otherwise revealing the issue until it has been patched -- attack physical security - -### Things we won't do: - -- take legal action of any kind against you because of your report -- reveal personal information to third-parties without your consent diff --git a/packages/bolt-dash/docs/mkdocs.yml b/packages/bolt-dash/docs/mkdocs.yml deleted file mode 100644 index ef57b7b..0000000 --- a/packages/bolt-dash/docs/mkdocs.yml +++ /dev/null @@ -1,10 +0,0 @@ -site_url: 'https://williamhorning.dev/bolt/docs' -repo_url: 'https://github.com/williamhorning/bolt' -edit_uri: 'https://github.com/williamhorning/bolt/blob/main/packages/bolt-dash/docs/docs' -site_description: 'Documentation for Bolt' -site_name: 'Bolt Docs' -theme: - name: readthedocs - locale: en -site_dir: './output' -docs_dir: './content' diff --git a/packages/bolt-dash/docs/requirements.txt b/packages/bolt-dash/docs/requirements.txt deleted file mode 100644 index 016bb16..0000000 --- a/packages/bolt-dash/docs/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -mkdocs diff --git a/packages/bolt-discord/_deps.ts b/packages/bolt-discord/_deps.ts new file mode 100644 index 0000000..3576edb --- /dev/null +++ b/packages/bolt-discord/_deps.ts @@ -0,0 +1,18 @@ +export { + API, + Client, + GatewayDispatchEvents as events, + type RESTPutAPIApplicationCommandsJSONBody as cmd_body, + type GatewayMessageUpdateDispatchData as update_data, + type RESTPostAPIWebhookWithTokenQuery as wh_query, + type RESTPostAPIWebhookWithTokenJSONBody as wh_token +} from 'npm:@discordjs/core@1.1.1'; +export { REST as rest, type RawFile } from 'npm:@discordjs/rest@2.2.0'; +export { WebSocketManager as socket } from 'npm:@discordjs/ws@1.0.2'; +export { + Bolt, + bolt_plugin, + type bridge_platform, + type deleted_message, + type message +} from '../bolt/mod.ts'; diff --git a/packages/bolt-discord/commands.ts b/packages/bolt-discord/commands.ts index fa3a488..32d2a8c 100644 --- a/packages/bolt-discord/commands.ts +++ b/packages/bolt-discord/commands.ts @@ -1,37 +1,43 @@ -import { Bolt, RESTPutAPIApplicationCommandsJSONBody } from './deps.ts'; -import DiscordPlugin from './mod.ts'; +import { API, Bolt, cmd_body } from './_deps.ts'; +import { discord_config } from './mod.ts'; -export async function registerCommands(discord: DiscordPlugin, bolt: Bolt) { - const data: RESTPutAPIApplicationCommandsJSONBody = [ - ...bolt.cmds.commands.values() - ].map(command => { +export async function register_commands( + config: discord_config, + api: API, + bolt: Bolt +) { + if (!config.slash_cmds) return; + + const data: cmd_body = [...bolt.cmds.values()].map(command => { const opts = []; - if (command.options?.hasArgument) { + if (command.options?.argument_name) { opts.push({ - name: 'options', + name: command.options.argument_name, description: 'option to pass to this command', - type: 3 + type: 3, + required: command.options.argument_required }); } if (command.options?.subcommands) { opts.push( ...command.options.subcommands.map(i => { - const cmd = { + return { name: i.name, description: i.description || i.name, type: 1, - options: [] as { type: number; name: string; description: string }[] + options: i.options?.argument_name + ? [ + { + name: i.options.argument_name, + description: i.options.argument_name, + type: 3, + required: i.options.argument_required || false + } + ] + : undefined }; - if (i.options?.hasArgument) { - cmd.options.push({ - name: 'options', - description: 'option to pass to this command', - type: 3 - }); - } - return cmd; }) ); } @@ -44,13 +50,8 @@ export async function registerCommands(discord: DiscordPlugin, bolt: Bolt) { }; }); - await discord.bot.api.applicationCommands.bulkOverwriteGlobalCommands( - discord.config.appId, - [] - ); - - await discord.bot.api.applicationCommands.bulkOverwriteGlobalCommands( - discord.config.appId, + await api.applicationCommands.bulkOverwriteGlobalCommands( + config.app_id, data ); } diff --git a/packages/bolt-discord/deps.ts b/packages/bolt-discord/deps.ts deleted file mode 100644 index a6b0f51..0000000 --- a/packages/bolt-discord/deps.ts +++ /dev/null @@ -1,26 +0,0 @@ -export { Buffer } from 'node:buffer'; -export { - API, - Client, - GatewayDispatchEvents, - GatewayIntentBits, - type APIApplicationCommandInteractionDataOption, - type GatewayMessageUpdateDispatchData, - type GatewayThreadDeleteDispatchData, - type GatewayThreadUpdateDispatchData, - type RESTPostAPIWebhookWithTokenJSONBody, - type RESTPostAPIWebhookWithTokenQuery, - type RESTPutAPIApplicationCommandsJSONBody, - type WithIntrinsicProps -} from 'npm:@discordjs/core@1.1.1'; -export { REST, type RawFile } from 'npm:@discordjs/rest@2.2.0'; -export { WebSocketManager } from 'npm:@discordjs/ws@1.0.2'; -export { - Bolt, - BoltPlugin, - type BoltBridgeMessage, - type BoltBridgeMessageArgs, - type BoltBridgeThreadArgs, - type BoltMessage, - type BoltThread -} from '../bolt/mod.ts'; diff --git a/packages/bolt-discord/events.ts b/packages/bolt-discord/events.ts new file mode 100644 index 0000000..0647569 --- /dev/null +++ b/packages/bolt-discord/events.ts @@ -0,0 +1,44 @@ +import { events } from './_deps.ts'; +import { id_to_temporal, tocore, todiscord } from './messages.ts'; +import { discord_plugin } from './mod.ts'; + +export function register_events(plugin: discord_plugin) { + plugin.bot.on(events.Ready, () => { + plugin.emit('ready'); + }); + plugin.bot.on(events.MessageCreate, async msg => { + plugin.emit('create_message', await tocore(msg.api, msg.data)); + }); + plugin.bot.on(events.MessageUpdate, async msg => { + plugin.emit('edit_message', await tocore(msg.api, msg.data)); + }); + plugin.bot.on(events.MessageDelete, async msg => { + plugin.emit('delete_message', await tocore(msg.api, msg.data)); + }); + plugin.bot.on(events.InteractionCreate, interaction => { + if (interaction.data.type !== 2 || interaction.data.data.type !== 1) return; + const opts = {} as Record; + let subcmd = ''; + + for (const opt of interaction.data.data.options || []) { + if (opt.type === 1) subcmd = opt.name; + if (opt.type === 3) opts[opt.name] = opt.value; + } + + plugin.emit('create_command', { + cmd: interaction.data.data.name, + subcmd, + replyfn: async msg => { + await interaction.api.interactions.reply( + interaction.data.id, + interaction.data.token, + await todiscord(msg) + ); + }, + channel: interaction.data.channel.id, + platform: 'bolt-discord', + opts, + timestamp: id_to_temporal(interaction.data.id) + }); + }); +} diff --git a/packages/bolt-discord/handlers.ts b/packages/bolt-discord/handlers.ts deleted file mode 100644 index 0a533de..0000000 --- a/packages/bolt-discord/handlers.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { - Bolt, - BoltThread, - GatewayDispatchEvents, - GatewayThreadDeleteDispatchData, - GatewayThreadUpdateDispatchData, - WithIntrinsicProps -} from './deps.ts'; -import { coreToMessage, messageToCore } from './messages.ts'; -import { default as DiscordPlugin } from './mod.ts'; - -export function threadEvent( - thread: WithIntrinsicProps< - GatewayThreadUpdateDispatchData | GatewayThreadDeleteDispatchData - >, - nojoin?: boolean -): BoltThread | void { - if ( - thread.data.type === 10 || - thread.data.type === 11 || - thread.data.type === 12 - ) { - if (!nojoin) thread.api.threads.join(thread.data.id); - return { - id: thread.data.id, - name: thread.data.name, - parent: thread.data.parent_id || '' - }; - } -} - -export function registerEvents(plugin: DiscordPlugin, bolt: Bolt) { - plugin.bot.on(GatewayDispatchEvents.Ready, ready => { - plugin.emit('ready', ready.data); - }); - plugin.bot.on(GatewayDispatchEvents.Resumed, ready => { - plugin.emit('ready', ready.data); - }); - plugin.bot.on(GatewayDispatchEvents.ThreadCreate, thread => { - const data = threadEvent(thread); - if (data) plugin.emit('threadCreate', data); - }); - plugin.bot.on(GatewayDispatchEvents.ThreadDelete, thread => { - const data = threadEvent(thread, true); - if (data) plugin.emit('threadDelete', data); - }); - plugin.bot.on(GatewayDispatchEvents.MessageCreate, async message => - plugin.emit('messageCreate', await messageToCore(message.api, message.data)) - ); - plugin.bot.on(GatewayDispatchEvents.MessageUpdate, async message => - plugin.emit('messageUpdate', await messageToCore(message.api, message.data)) - ); - plugin.bot.on(GatewayDispatchEvents.MessageDelete, message => - plugin.emit('messageDelete', { - id: message.data.id, - platform: { - name: 'bolt-discord', - message - }, - channel: message.data.channel_id, - guild: message.data.guild_id, - timestamp: Date.now() - }) - ); - plugin.bot.on(GatewayDispatchEvents.InteractionCreate, async interaction => { - if (interaction.data.type !== 2 || interaction.data.data.type !== 1) return; - const opts = interaction.data.data.options; - let arg = ''; - if (opts && opts[0]) { - const opt = opts[0]; - arg += opt.name; - if (opt.type === 1) { - for (const i of opt.options || []) { - arg += ` ${i.value}`; - } - } - } - await bolt.cmds.runCommand({ - name: interaction.data.data.name, - reply: async msg => { - await interaction.api.interactions.reply( - interaction.data.id, - interaction.data.token, - await coreToMessage(msg) - ); - }, - channel: interaction.data.channel.id, - platform: 'bolt-discord', - arg, - timestamp: Date.now() - }); - }); -} diff --git a/packages/bolt-discord/messages.ts b/packages/bolt-discord/messages.ts index 46a3cdc..5b003e3 100644 --- a/packages/bolt-discord/messages.ts +++ b/packages/bolt-discord/messages.ts @@ -1,54 +1,78 @@ import { API, - BoltMessage, - Buffer, - GatewayMessageUpdateDispatchData, - RESTPostAPIWebhookWithTokenJSONBody, - RESTPostAPIWebhookWithTokenQuery, - RawFile -} from './deps.ts'; + RawFile, + message, + update_data, + wh_query, + wh_token +} from './_deps.ts'; -const asyncFlatMap = (arr: A[], f: (a: A) => Promise) => - Promise.all(arr.map(f)).then(arr => arr.flat()); +export async function async_flat(arr: A[], f: (a: A) => Promise) { + return (await Promise.all(arr.map(f))).flat(); +} + +export function id_to_temporal(id: string) { + return Temporal.Instant.fromEpochMilliseconds( + Number(BigInt(id) >> 22n) + 1420070400000 + ); +} -export async function messageToCore( +export async function tocore( api: API, - message: GatewayMessageUpdateDispatchData, - excludeReply?: boolean -): Promise> { + message: Omit +): Promise>> { if (message.flags && message.flags & 128) message.content = 'Loading...'; - let color = '#FFFFFF'; - if (message.guild_id && message.member) { - const roles = await api.guilds.getRoles(message.guild_id); - const role = roles.find(i => message.member!.roles.includes(i.id)); - if (role) { - color = `#${role.color.toString(16)}`; + if (message.type === 7) message.content = '*joined on discord*'; + if (message.sticker_items) { + if (!message.attachments) message.attachments = []; + for (const sticker of message.sticker_items) { + let type; + if (sticker.format_type === 1) type = 'png'; + if (sticker.format_type === 2) type = 'apng'; + if (sticker.format_type === 3) type = 'lottie'; + if (sticker.format_type === 4) type = 'gif'; + const url = `https://media.discordapp.net/stickers/${sticker.id}.${type}`; + const req = await fetch(url, { method: 'HEAD' }); + if (req.ok) { + message.attachments.push({ + url, + description: sticker.name, + filename: `${sticker.name}.${type}`, + size: 0, + id: sticker.id, + proxy_url: url + }); + } else { + message.content = '*used sticker*'; + } } } - return { + const data = { author: { profile: `https://cdn.discordapp.com/avatars/${message.author?.id}/${message.author?.avatar}.png`, username: - message.member?.nick || message.author?.username || 'discord user', + message.member?.nick || + message.author?.global_name || + message.author?.username || + 'discord user', rawname: message.author?.username || 'discord user', id: message.author?.id || message.webhook_id || '', - color + color: '#5865F2' }, channel: message.channel_id, content: message.content, id: message.id, - timestamp: new Date( - message.edited_timestamp || message.timestamp || Date.now() - ).getTime(), + timestamp: id_to_temporal(message.id), embeds: message.embeds?.map(i => { return { ...i, timestamp: i.timestamp ? Number(i.timestamp) : undefined }; }), - reply: async (msg: BoltMessage) => { + reply: async (msg: message) => { + if (!data.author.id || data.author.id == '') return; await api.channels.createMessage(message.channel_id, { - ...(await coreToMessage(msg)), + ...(await todiscord(msg)), message_reference: { message_id: message.id } @@ -56,42 +80,25 @@ export async function messageToCore( }, platform: { name: 'bolt-discord', - message + message, + webhookid: message.webhook_id }, attachments: message.attachments?.map(i => { return { - file: i.proxy_url, + file: i.url, alt: i.description, name: i.filename, size: i.size / 1000000 }; }), - guild: message.guild_id, - replyto: await replyto(message, api, excludeReply), - threadId: message.thread?.id + replytoid: message.referenced_message?.id }; + return data; } -async function replyto( - message: GatewayMessageUpdateDispatchData, - api: API, - excludeReply?: boolean -) { - if (!message.referenced_message || excludeReply) return; - try { - return await messageToCore(api, message.referenced_message, true); - } catch { - return; - } -} - -export async function coreToMessage(message: BoltMessage): Promise< - RESTPostAPIWebhookWithTokenJSONBody & - RESTPostAPIWebhookWithTokenQuery & { - files?: RawFile[]; - wait: true; - } -> { +export async function todiscord( + message: message +): Promise { return { avatar_url: message.author.profile, content: message.content, @@ -102,18 +109,17 @@ export async function coreToMessage(message: BoltMessage): Promise< }; }), files: message.attachments - ? await asyncFlatMap(message.attachments, async a => { + ? await async_flat(message.attachments, async a => { if (a.size > 25) return []; if (!a.name) a.name = a.file.split('/').pop(); return [ { name: a.name || 'file', - data: Buffer.from(await (await fetch(a.file)).arrayBuffer()) + data: new Uint8Array(await (await fetch(a.file)).arrayBuffer()) } ]; }) : undefined, - thread_id: message.threadId, username: message.author.username, wait: true }; diff --git a/packages/bolt-discord/mod.ts b/packages/bolt-discord/mod.ts index 9fbaa5e..846505d 100644 --- a/packages/bolt-discord/mod.ts +++ b/packages/bolt-discord/mod.ts @@ -1,149 +1,100 @@ -import { registerCommands } from './commands.ts'; import { Bolt, - BoltBridgeMessage, - BoltBridgeMessageArgs, - BoltBridgeThreadArgs, - BoltPlugin, Client, - GatewayIntentBits as Intents, - REST, - WebSocketManager -} from './deps.ts'; -import { registerEvents } from './handlers.ts'; -import { coreToMessage, messageToCore } from './messages.ts'; + bolt_plugin, + bridge_platform, + deleted_message, + message, + rest, + socket +} from './_deps.ts'; +import { register_commands } from './commands.ts'; +import { register_events } from './events.ts'; +import { todiscord } from './messages.ts'; -type DiscordConfig = { - appId: string; +export type discord_config = { + app_id: string; token: string; - registerSlashCommands?: boolean; + slash_cmds?: boolean; }; -export default class DiscordPlugin extends BoltPlugin { +export class discord_plugin extends bolt_plugin { bot: Client; - config: DiscordConfig; - gateway: WebSocketManager; - rest: REST; name = 'bolt-discord'; - version = '0.5.4'; - constructor(config: DiscordConfig) { - super(); + version = '0.5.5'; + support = ['0.5.5']; + + constructor(bolt: Bolt, config: discord_config) { + super(bolt, config); this.config = config; - this.rest = new REST({ version: '10' }).setToken(config.token); - this.gateway = new WebSocketManager({ - rest: this.rest, + const rest_client = new rest({ version: '10' }).setToken(config.token); + const gateway = new socket({ + rest: rest_client, token: config.token, - intents: Intents.Guilds | Intents.GuildMessages | Intents.MessageContent + intents: 0 | 33281 }); - this.bot = new Client({ rest: this.rest, gateway: this.gateway }); + this.bot = new Client({ rest: rest_client, gateway }); + register_events(this); + register_commands(this.config, this.bot.api, bolt); + gateway.connect(); } - async start(bolt: Bolt) { - registerEvents(this, bolt); - await this.gateway.connect(); - if (this.config.registerSlashCommands) { - registerCommands(this, bolt); - } + + async create_bridge(channel: string) { + const wh = await this.bot.api.channels.createWebhook(channel, { + name: 'bolt bridge' + }); + return { id: wh.id, token: wh.token }; } - async stop() { - await this.gateway.destroy(); + + is_bridged() { + return 'query' as const; } - bridgeSupport = { - text: true, - threads: true, - forum: true - }; - async createSenddata(channel: string) { - return await this.bot.api.channels.createWebhook(channel, { - name: 'Bolt Bridge' - }); + + async create_message( + message: message, + bridge: bridge_platform + ): Promise { + const msg = await todiscord(message); + const senddata = bridge.senddata as { token: string; id: string }; + const wh = await this.bot.api.webhooks.execute( + senddata.id, + senddata.token, + msg + ); + return { + ...bridge, + id: wh.id + }; } - async bridgeMessage(data: BoltBridgeMessageArgs) { - switch (data.type) { - case 'create': - case 'update': { - const dat = data.data as BoltBridgeMessage; - let replyto; - try { - if (dat.replytoId) { - replyto = await messageToCore( - this.bot.api, - await this.bot.api.channels.getMessage(dat.channel, dat.replytoId) - ); - } - } catch { - replyto = undefined; - } - const msgd = await coreToMessage({ ...dat, replyto }); - const senddata = dat.senddata as { token: string; id: string }; - let wh; - let thread; - if (data.type === 'create') { - wh = await this.bot.api.webhooks.execute( - senddata.id, - senddata.token, - msgd - ); - } else { - wh = await this.bot.api.webhooks.editMessage( - senddata.id, - senddata.token, - dat.id, - msgd - ); - } - if (dat.threadId) { - thread = { - id: dat.threadId, - parent: wh.channel_id - }; - } - return { - channel: wh.channel_id, - id: wh.id, - plugin: 'bolt-discord', - senddata, - thread - }; - } - case 'delete': { - await this.bot.api.channels.deleteMessage( - data.data.channel, - data.data.id - ); - return { - channel: data.data.channel, - id: data.data.id, - plugin: 'bolt-discord', - senddata: data.data.senddata - }; - } - } + + async edit_message( + message: message, + bridge: bridge_platform & { id: string } + ): Promise { + const msg = await todiscord(message); + const senddata = bridge.senddata as { token: string; id: string }; + const wh = await this.bot.api.webhooks.editMessage( + senddata.id, + senddata.token, + bridge.id, + msg + ); + return { + ...bridge, + id: wh.id + }; } - async bridgeThread(data: BoltBridgeThreadArgs) { - if (data.type === 'create') { - const channel = await this.bot.api.channels.get( - data.data.bridgePlatform.channel - ); - const isForum = channel.type === 15; - const handler = isForum - ? this.bot.api.channels.createForumThread - : this.bot.api.channels.createThread; - const result = await handler(data.data.bridgePlatform.channel, { - message: { content: '.' }, - name: data.data.name || 'bridged thread', - type: 11 - }); - return { - id: result.id, - parent: data.data.bridgePlatform.channel, - name: result.name ? result.name : undefined - }; - } else { - await this.bot.api.channels.delete(data.data.id); - return { - id: data.data.id, - parent: data.data.parent - }; - } + + async delete_message( + _msg: deleted_message, + bridge: bridge_platform & { id: string } + ) { + const senddata = bridge.senddata as { token: string; id: string }; + await this.bot.api.webhooks.deleteMessage( + senddata.id, + senddata.token, + bridge.id + ); + return bridge; } } diff --git a/packages/bolt-guilded/_deps.ts b/packages/bolt-guilded/_deps.ts new file mode 100644 index 0000000..e5592ba --- /dev/null +++ b/packages/bolt-guilded/_deps.ts @@ -0,0 +1,21 @@ +export { + type APIEmbed, + type APIWebhookMessagePayload +} from 'npm:guilded-api-typings@0.13.2'; +export { + Channel, + Client, + Message, + WebhookClient, + type EmbedPayload, + type RESTPostWebhookBody +} from 'npm:guilded.js@0.23.7'; +export { + Bolt, + bolt_plugin, + create_message, + type bridge_platform, + type deleted_message, + type embed, + type message +} from '../bolt/mod.ts'; diff --git a/packages/bolt-guilded/deps.ts b/packages/bolt-guilded/deps.ts deleted file mode 100644 index 0f8927d..0000000 --- a/packages/bolt-guilded/deps.ts +++ /dev/null @@ -1,17 +0,0 @@ -export { - type APIEmbed, - type APIWebhookMessagePayloadResolvable -} from 'npm:guilded-api-typings@0.13.2'; -export { Client, Message } from 'npm:guilded.js@0.23.7'; -export { - BoltPlugin, - createBoltMessage, - getBoltBridge, - updateBoltBridge, - type BoltBridgeMessage, - type BoltBridgeMessageArgs, - type BoltBridgePlatform, - type BoltBridgeThreadArgs, - type BoltEmbed, - type BoltMessage -} from '../bolt/mod.ts'; diff --git a/packages/bolt-guilded/legacybridging.ts b/packages/bolt-guilded/legacybridging.ts index 2e9d59d..0652e20 100644 --- a/packages/bolt-guilded/legacybridging.ts +++ b/packages/bolt-guilded/legacybridging.ts @@ -1,24 +1,17 @@ -import { - BoltBridgeMessage, - BoltMessage, - createBoltMessage, - getBoltBridge, - updateBoltBridge -} from './deps.ts'; -import { idTransform } from './messages.ts'; -import GuildedPlugin from './mod.ts'; +import { create_message, message } from './_deps.ts'; +import { toguildedid } from './messages.ts'; +import { guilded_plugin } from './mod.ts'; -export async function bridgeLegacy( - this: GuildedPlugin, - dat: BoltBridgeMessage, - senddata: string | { id: string; token: string }, - replyto?: BoltMessage +export async function bridge_legacy( + guilded: guilded_plugin, + dat: message, + senddata: string ) { - const channel = await this.bot.channels.fetch(dat.channel); - const idtrsnd = idTransform({ ...dat, replyto }); - // @ts-ignore - const result = await channel.send(idtrsnd); try { + const result = await guilded.bot.messages.send( + senddata, + toguildedid({ ...dat }) + ); return { channel: result.channelId, id: result.id, @@ -27,36 +20,37 @@ export async function bridgeLegacy( }; } finally { try { - if ( - !(await dat.bolt.redis?.get(`guilded-embed-migration-${dat.channel}`)) - ) { - await dat.bolt.redis?.set( - `guilded-embed-migration-${dat.channel}`, - 'true' - ); - const currentbridge = await getBoltBridge(dat.bolt, { - channel: channel.id - })!; - const senddata = await this.createSenddata(channel.id); - if (currentbridge) { - const index = currentbridge.platforms.findIndex( - i => (i.channel = channel.id) - ); - currentbridge.platforms[index] = { - channel: channel.id, - plugin: 'bolt-guilded', - senddata - }; - await updateBoltBridge(dat.bolt, currentbridge); - } - } + await migrate_bridge(senddata, guilded); } catch { - const warning = createBoltMessage({ - content: - "In the next major version of Bolt, 1.0.0, embed-based bridges like this one won't be supported anymore. Take a look at https://github.com/williamhorning/bolt/issues/36 for more information and how to migrate to webhook-based bridges. This should be the last time you see this message." - }); - // @ts-ignore - channel.send(warning); + await guilded.bot.messages.send( + senddata, + toguildedid( + create_message({ + text: `In the next major version of Bolt, embed-based bridges like this one won't be supported anymore. + See https://github.com/williamhorning/bolt/issues/36 for more information.` + }) + ) + ); + } + } +} + +async function migrate_bridge(channel: string, guilded: guilded_plugin) { + if (!guilded.bolt.db.redis.get(`guilded-embed-migration-${channel}`)) { + await guilded.bolt.db.redis.set( + `guilded-embed-migration-${channel}`, + 'true' + ); + const current = await guilded.bolt.bridge.get_bridge({ channel: channel }); + if (current) { + current.platforms[ + current.platforms.findIndex(i => i.channel === channel) + ] = { + channel, + plugin: 'bolt-guilded', + senddata: await guilded.create_bridge(channel) + }; + await guilded.bolt.bridge.update_bridge(current); } } } diff --git a/packages/bolt-guilded/messages.ts b/packages/bolt-guilded/messages.ts index 839f166..28e525f 100644 --- a/packages/bolt-guilded/messages.ts +++ b/packages/bolt-guilded/messages.ts @@ -1,33 +1,35 @@ import { APIEmbed, - APIWebhookMessagePayloadResolvable, - BoltEmbed, - BoltMessage, - Message -} from './deps.ts'; -import GuildedPlugin from './mod.ts'; + embed, + EmbedPayload, + message, + Message, + RESTPostWebhookBody +} from './_deps.ts'; +import { guilded_plugin } from './mod.ts'; -export async function messageToCore( +export async function tocore( message: Message, - plugin: GuildedPlugin, - replybool = true -): Promise | undefined> { + plugin: guilded_plugin +): Promise | undefined> { if (!message.serverId) return; - const author = await plugin.bot.members.fetch( - message.serverId, - message.authorId - ); + let author; + if (!message.createdByWebhookId) + author = await plugin.bot.members.fetch(message.serverId, message.authorId); + const update_content = message.content.replaceAll('\n```\n```\n', '\n'); return { author: { - username: author.nickname || author.username || 'user on guilded', - rawname: author.username || 'user on guilded', - profile: author.user?.avatar || undefined, + username: author?.nickname || author?.username || 'user on guilded', + rawname: author?.username || 'user on guilded', + profile: author?.user?.avatar || undefined, id: message.authorId, color: '#F5C400' }, channel: message.channelId, id: message.id, - timestamp: message.createdAt.getTime(), + timestamp: Temporal.Instant.fromEpochMilliseconds( + message.createdAt.valueOf() + ), embeds: message.embeds?.map(embed => { return { ...embed, @@ -55,98 +57,48 @@ export async function messageToCore( video: embed.video || undefined }; }), - platform: { name: 'bolt-guilded', message }, - reply: async (msg: BoltMessage) => { - // @ts-ignore - await message.reply(coreToMessage(msg)); + platform: { + name: 'bolt-guilded', + message, + webhookid: message.createdByWebhookId || undefined }, - content: message.content, - guild: message.serverId, - replyto: (await replyto(message, plugin, replybool)) || undefined + reply: async (msg: message) => { + await message.reply(toguilded(msg)); + }, + content: update_content, + replytoid: message.isReply ? message.replyMessageIds[0] : undefined }; } -async function replyto( - message: Message, - plugin: GuildedPlugin, - replybool: boolean -) { - if (message.isReply && replybool) { - try { - return await messageToCore( - await plugin.bot.messages.fetch( - message.channelId, - message.replyMessageIds[0] - ), - plugin, - false - ); - } catch { - return; - } - } else { - return; - } -} - -function chooseValidGuildedUsername(msg: BoltMessage) { - if (validUsernameCheck(msg.author.username)) { - return msg.author.username; - } else if (validUsernameCheck(msg.author.rawname)) { - return msg.author.rawname; - } else { - return `${msg.author.id} on ${msg.platform.name}`; - } -} - -// FIXME: this logic is WRONG -function validUsernameCheck(username: string) { - return ( - username.length > 1 && - username.length < 32 && - username.match(/^[a-zA-Z0-9_ ()]*$/gms) - ); -} - -export function coreToMessage( - msg: BoltMessage -): APIWebhookMessagePayloadResolvable { - const message = { +export function toguilded(msg: message): guilded_msg { + const message: guilded_msg = { content: msg.content, avatar_url: msg.author.profile, - username: chooseValidGuildedUsername(msg), - embeds: msg.embeds?.map(embed => { - Object.keys(embed).forEach(key => { - embed[key as keyof BoltEmbed] === null - ? (embed[key as keyof BoltEmbed] = undefined) - : embed[key as keyof BoltEmbed]; - }); - return { - ...embed, - author: { - ...embed.author, - icon_url: embed.author?.iconUrl - }, - timestamp: embed.timestamp ? new Date(embed.timestamp) : undefined - }; - }) as APIEmbed[] | undefined + username: get_valid_username(msg), + embeds: fix_embed(msg.embeds, String) }; - if (msg.replyto) { + if (msg.replytoid) message.replyMessageIds = [msg.replytoid]; + if (message.embeds?.length == 0 || !message.embeds) delete message.embeds; + if (msg.attachments?.length) { if (!message.embeds) message.embeds = []; message.embeds.push({ - author: { - name: `replying to: ${msg.replyto.author.username}`, - icon_url: msg.replyto.author.profile - }, - description: msg.replyto.content + title: 'attachments', + description: msg.attachments + .slice(0, 5) + .map(a => { + return `![${a.alt || a.name}](${a.file})`; + }) + .join('\n') }); } - if (message.embeds?.length == 0) delete message.embeds; + return message; } -export function idTransform(msg: BoltMessage) { - const senddat = { +export function toguildedid(msg: message) { + const senddat: guilded_msg & { + embeds: APIEmbed[]; + } = { embeds: [ { author: { @@ -155,22 +107,18 @@ export function idTransform(msg: BoltMessage) { }, description: msg.content, footer: { - text: 'Please run `!bolt resetbridge` to migrate to Webhook bridges' + text: 'please migrate to webhook bridges' } }, - ...(msg.embeds || []).map(i => { - return { - ...i, - timestamp: i.timestamp ? new Date(i.timestamp) : undefined - }; + ...fix_embed(msg.embeds, d => { + return new Date(d); }) - ] + ], + replyMessageIds: msg.replytoid ? [msg.replytoid] : undefined }; - if (msg.replyto) { - senddat.embeds[0].description += `\n**In response to ${msg.replyto.author.username}'s message:**\n${msg.replyto.content}`; - } if (msg.attachments?.length) { senddat.embeds[0].description += `\n**Attachments:**\n${msg.attachments + .slice(0, 5) .map(a => { return `![${a.alt || a.name}](${a.file})`; }) @@ -178,3 +126,34 @@ export function idTransform(msg: BoltMessage) { } return senddat; } + +type guilded_msg = RESTPostWebhookBody & { replyMessageIds?: string[] }; + +function get_valid_username(msg: message) { + if (is_valid_username(msg.author.username)) { + return msg.author.username; + } else if (is_valid_username(msg.author.rawname)) { + return msg.author.rawname; + } else { + return `${msg.author.id}`; + } +} + +function is_valid_username(e: string) { + if (!e || e.length === 0 || e.length > 32) return false; + return /^[a-zA-Z0-9_ ()]*$/gms.test(e); +} + +function fix_embed(embeds: embed[] = [], timestamp_fix: (s: number) => t) { + return embeds.map(embed => { + Object.keys(embed).forEach(key => { + embed[key as keyof embed] === null + ? (embed[key as keyof embed] = undefined) + : embed[key as keyof embed]; + }); + return { + ...embed, + timestamp: embed.timestamp ? timestamp_fix(embed.timestamp) : undefined + }; + }) as (EmbedPayload & { timestamp: t })[]; +} diff --git a/packages/bolt-guilded/mod.ts b/packages/bolt-guilded/mod.ts index 0898a28..0365344 100644 --- a/packages/bolt-guilded/mod.ts +++ b/packages/bolt-guilded/mod.ts @@ -1,125 +1,100 @@ import { - BoltBridgeMessage, - BoltBridgeMessageArgs, - BoltPlugin, - Client -} from './deps.ts'; -import { bridgeLegacy } from './legacybridging.ts'; -import { coreToMessage, messageToCore } from './messages.ts'; + Bolt, + Client, + WebhookClient, + bolt_plugin, + bridge_platform, + deleted_message, + message +} from './_deps.ts'; +import { bridge_legacy } from './legacybridging.ts'; +import { tocore, toguilded } from './messages.ts'; -export default class GuildedPlugin extends BoltPlugin { +export class guilded_plugin extends bolt_plugin<{ token: string }> { bot: Client; name = 'bolt-guilded'; - version = '0.5.4'; - constructor(config: { token: string }) { - super(); + version = '0.5.5'; + support = ['0.5.5']; + + constructor(bolt: Bolt, config: { token: string }) { + super(bolt, config); this.bot = new Client(config); - this.setupClient(this.bot, config); - } - private setupClient(client: Client, config: { token: string }) { - client.on('debug', (data: unknown) => { - this.emit('debug', data); + this.bot.on('ready', () => { + this.emit('ready'); }); - client.on('messageCreated', async message => { - const msg = await messageToCore(message, this); - if (msg) this.emit('messageCreate', msg); + this.bot.on('messageCreated', async message => { + const msg = await tocore(message, this); + if (msg) this.emit('create_message', msg); }); - client.on('messageUpdated', async message => { - const msg = await messageToCore(message, this); - if (msg) this.emit('messageUpdate', msg); + this.bot.on('messageUpdated', async message => { + const msg = await tocore(message, this); + if (msg) this.emit('create_message', msg); }); - client.on('messageDeleted', message => { - this.emit('messageDelete', { - id: message.id, - platform: { name: 'guilded', message }, - channel: message.channelId, - guild: message.serverId, - timestamp: new Date(message.deletedAt).getTime() + this.bot.on('messageDeleted', del => { + this.emit('delete_message', { + channel: del.channelId, + id: del.id, + platform: { message: del, name: 'bolt-guilded' }, + timestamp: Temporal.Instant.from(del.deletedAt) }); }); - client.on('ready', () => { - this.emit('ready'); - }); - client.ws.emitter.on('exit', info => { - this.emit('debug', info); - this.bot = new Client(config); - this.setupClient(this.bot, config); + this.bot.ws.emitter.on('exit', () => { + this.bot.ws.connect(); }); - } - - start() { this.bot.login(); } - stop() { - this.bot.disconnect(); - } - bridgeSupport = { - text: true - }; - async createSenddata(channel: string) { + + async create_bridge(channel: string) { const ch = await this.bot.channels.fetch(channel); const wh = await this.bot.webhooks.create(ch.serverId, { - name: 'Bolt Bridge', + name: 'Bolt Bridges', channelId: channel }); - return { - id: wh.id, - token: wh.token || '' - }; + if (!wh.token) { + await this.bot.webhooks.delete(ch.serverId, wh.id); + throw new Error('No token!!!'); + } + return { id: wh.id, token: wh.token }; } - async bridgeMessage(data: BoltBridgeMessageArgs) { - switch (data.type) { - case 'create': { - const dat = data.data as BoltBridgeMessage & { - senddata: string | { token: string; id: string }; + + is_bridged(msg: message) { + if (msg.author.id === this.bot.user?.id && msg.embeds && !msg.replytoid) { + return true; + } + return 'query'; + } + + async create_message(message: message, platform: bridge_platform) { + if (typeof platform.senddata === 'string') { + return await bridge_legacy(this, message, platform.senddata); + } else { + try { + const resp = await new WebhookClient( + platform.senddata as { token: string; id: string } + ).send(toguilded(message)); + return { + channel: resp.channelId, + id: resp.id, + plugin: 'bolt-guilded', + senddata: platform.senddata }; - let replyto; - try { - if (dat.replytoId) { - replyto = await messageToCore( - await this.bot.messages.fetch(dat.channel, dat.replytoId), - this - ); - } - } catch { - replyto = undefined; - } - if (typeof dat.senddata === 'string') { - return await bridgeLegacy.bind(this)(dat, dat.senddata, replyto); - } else { - const msgd = coreToMessage({ ...dat, replyto }); - const resp = await fetch( - `https://media.guilded.gg/webhooks/${dat.senddata.id}/${dat.senddata.token}`, - { - method: 'POST', - body: JSON.stringify(msgd) - } - ); - if (resp.status === 404) { - // if the webhook disappeared, but the channel hasn't, try to legacy send it, which should recreate the webhook - return await bridgeLegacy.bind(this)( - dat, - dat.bridgePlatform.channel, - replyto - ); - } - const result = await resp.json(); - return { - channel: result.channelId, - id: result.id, - plugin: 'bolt-guilded', - senddata: dat.senddata - }; - } - } - case 'update': { - // editing is NOT supported - return { ...data.data.bridgePlatform, id: data.data.id }; - } - case 'delete': { - await this.bot.messages.delete(data.data.channel, data.data.id); - return { ...data.data.bridgePlatform, id: data.data.id }; + } catch { + return await bridge_legacy(this, message, platform.channel); } } } + + // deno-lint-ignore require-await + async edit_message(message: message, bridge: bridge_platform) { + return { id: message.id, ...bridge }; + } + + async delete_message( + _message: deleted_message, + bridge: bridge_platform + ) { + const msg = await this.bot.messages.fetch(bridge.channel, bridge.id!); + await msg.delete(); + return bridge; + } } diff --git a/packages/bolt-matrix/deps.ts b/packages/bolt-matrix/deps.ts index ba77d30..cccf237 100644 --- a/packages/bolt-matrix/deps.ts +++ b/packages/bolt-matrix/deps.ts @@ -10,8 +10,8 @@ export { } from 'npm:matrix-appservice-bridge@10.1.0'; export { Bolt, - BoltPlugin, - type BoltBridgeMessageArgs, - type BoltMessage + bolt_plugin, + type bridge_platform, + type message } from '../bolt/mod.ts'; -export { Buffer } from "node:buffer"; +export { Buffer } from 'node:buffer'; diff --git a/packages/bolt-matrix/events.ts b/packages/bolt-matrix/events.ts index 8b46249..6bdd545 100644 --- a/packages/bolt-matrix/events.ts +++ b/packages/bolt-matrix/events.ts @@ -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) { +export async function onEvent( + this: matrix_plugin, + request: Request +) { const event = request.getData(); const bot = this.bot.getBot(); const intent = this.bot.getIntent(); @@ -17,17 +20,23 @@ export async function onEvent(this: MatrixPlugin, request: Request) { } } if (event.type === 'm.room.message' && !event.content['m.new_content']) { - this.emit('messageCreate', await messageToCore(event, intent, this.config.homeserverUrl)); + this.emit( + 'create_message', + await messageToCore(event, intent, this.config.homeserverUrl) + ); } if (event.type === 'm.room.message' && event.content['m.new_content']) { - this.emit('messageUpdate', await messageToCore(event, intent, this.config.homeserverUrl)); + 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) }); } } @@ -35,30 +44,35 @@ export async function onEvent(this: MatrixPlugin, request: Request) { export async function messageToCore( event: WeakEvent, intent: Intent, - homeserverUrl: String -): Promise> { + homeserverUrl: string +): Promise> { const sender = await intent.getProfileInfo(event.sender); return { author: { username: sender.displayname || event.sender, rawname: event.sender, id: event.sender, - profile: `${sender.avatar_url.replace("mxc://", `${homeserverUrl}/_matrix/media/v3/thumbnail/`)}?width=96&height=96&method=scale` + profile: `${sender.avatar_url?.replace( + 'mxc://', + `${homeserverUrl}/_matrix/media/v3/thumbnail/` + )}?width=96&height=96&method=scale` }, channel: event.room_id, - id: event.content['m.relates_to']?.rel_type == "m.replace" - ? event.content['m.relates_to'].event_id - : event.event_id, - timestamp: event.origin_server_ts, - content: (event.content['m.new_content']?.body || event.content.body) as string, - reply: async (msg: BoltMessage) => { + 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) => { await intent.sendMessage(event.room_id, coreToMessage(msg)); }, platform: { name: 'bolt-matrix', message: event } }; } -export function coreToMessage(msg: BoltMessage) { +export function coreToMessage(msg: message) { return { body: msg.content ? msg.content diff --git a/packages/bolt-matrix/mod.ts b/packages/bolt-matrix/mod.ts index 6dddf0e..1158dc0 100644 --- a/packages/bolt-matrix/mod.ts +++ b/packages/bolt-matrix/mod.ts @@ -1,14 +1,13 @@ import { AppServiceRegistration, Bolt, - BoltBridgeMessageArgs, - BoltMessage, - BoltPlugin, Bridge, Buffer, - ClientEncryptionSession, MatrixUser, - existsSync + existsSync, + bolt_plugin, + message, + bridge_platform } from './deps.ts'; import { coreToMessage, onEvent } from './events.ts'; @@ -20,15 +19,14 @@ type MatrixConfig = { reg_path: string; }; -export default class MatrixPlugin extends BoltPlugin { +export class matrix_plugin extends bolt_plugin { bot: Bridge; - config: MatrixConfig; name = 'bolt-matrix'; - version = '0.5.4'; - bolt?: Bolt; - constructor(config: MatrixConfig) { - super(); - this.config = config; + 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, @@ -40,9 +38,6 @@ export default class MatrixPlugin extends BoltPlugin { 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.appserviceUrl); reg.setAppServiceToken(AppServiceRegistration.generateToken()); @@ -54,74 +49,91 @@ export default class MatrixPlugin extends BoltPlugin { 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 room = data.data.bridgePlatform.senddata as string; - switch (data.type) { - case 'create': - case 'update': { - const name = `@${data.data.platform.name}_${data.data.author.id}:${this.config.domain}`; - const intent = this.bot.getIntent(name); - // check for profile - await intent.ensureProfile(data.data.author.username); - const store = this.bot.getUserStore(); - let storeUser = await store?.getMatrixUser(name); - if (!storeUser) { - storeUser = new MatrixUser(name); - } - if (storeUser?.get("avatar") != data.data.author.profile) { - storeUser?.set("avatar", data.data.author.profile); - let b = await (await fetch(data.data.author.profile)).blob(); - const newMxc = await intent.uploadContent( - Buffer.from(await b.arrayBuffer()), - { type: b.type } - ); - await intent.ensureProfile(data.data.author.username, newMxc); - await store?.setMatrixUser(storeUser); - } - // now to our message - const message = coreToMessage( - data.data as unknown as BoltMessage - ); - 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) { + // TODO: implement this + return true; + } + + async create_message( + msg: message, + 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': { - const intent = this.bot.getIntent(); - await intent.botSdkIntent.underlyingClient.redactEvent( - room, data.data.id, 'bridge message deletion' - ); - 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, + platform: bridge_platform & { id: string } + ) { + return await this.create_message(msg, platform, true); + } + + async delete_message( + _msg: message, + 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 + }; } } diff --git a/packages/bolt-revolt/deps.ts b/packages/bolt-revolt/deps.ts index daa88a9..7ce2e50 100644 --- a/packages/bolt-revolt/deps.ts +++ b/packages/bolt-revolt/deps.ts @@ -10,9 +10,8 @@ export { UserSystemMessage } from 'npm:@williamhorning/revolt.js@7.0.0-beta.10'; export { - BoltPlugin, - type BoltBridgeMessage, - type BoltBridgeMessageArgs, - type BoltBridgeThreadArgs, - type BoltMessage + Bolt, + bolt_plugin, + type bridge_platform, + type message } from '../bolt/mod.ts'; diff --git a/packages/bolt-revolt/messages.ts b/packages/bolt-revolt/messages.ts index f4c30aa..588a666 100644 --- a/packages/bolt-revolt/messages.ts +++ b/packages/bolt-revolt/messages.ts @@ -1,35 +1,33 @@ -import { - API, - BoltMessage, - ChannelEditSystemMessage, - ChannelOwnershipChangeSystemMessage, - Message, - TextEmbed, - TextSystemMessage, - User, - UserSystemMessage -} from './deps.ts'; -import RevoltPlugin from './mod.ts'; +import { API, message, Message, TextEmbed } from './deps.ts'; -export async function coreToMessage( - message: BoltMessage, +export async function torevolt( + message: message, masquerade = true ): Promise> { - return { + const dat: API.DataMessageSend = { attachments: message.attachments && message.attachments.length > 0 ? await Promise.all( - message.attachments.map(async ({ file }) => { - const formdat = new FormData(); - formdat.append('file', await (await fetch(file)).blob()); + message.attachments.slice(0, 5).map(async ({ file, name }) => { + const formdata = new FormData(); + formdata.append( + 'file', + new File( + [await (await fetch(file)).arrayBuffer()], + name || 'file.name', + { + type: 'application/octet-stream' + } + ) + ); return ( await ( await fetch(`https://autumn.revolt.chat/attachments`, { method: 'POST', - body: formdat + body: formdata }) ).json() - ).id; + )?.id; }) ) : undefined, @@ -38,23 +36,35 @@ export async function coreToMessage( : message.embeds ? undefined : 'empty message', - embeds: message.embeds, + embeds: message.embeds?.map(embed => { + if (embed.fields) { + for (const field of embed.fields) { + embed.description += `\n\n**${field.name}**\n${field.value}`; + } + } + return embed; + }), masquerade: masquerade ? { avatar: message.author.profile, - name: message.author.username, + name: message.author.username.slice(0, 32), colour: message.author.color } + : undefined, + replies: message.replytoid + ? [{ id: message.replytoid, mention: true }] : undefined }; + + if (!dat.attachments) delete dat.attachments; + if (!dat.masquerade) delete dat.masquerade; + if (!dat.content) delete dat.content; + if (!dat.embeds) delete dat.embeds; + + return dat; } -export async function messageToCore( - plugin: RevoltPlugin, - message: Message, - getReply = true -): Promise> { - const content = systemMessages(message); +export function tocore(message: Message): message { return { author: { username: @@ -66,102 +76,37 @@ export async function messageToCore( `${message.authorId || 'unknown user'} on revolt`, profile: message.author?.avatarURL, id: message.authorId || 'unknown', - color: '#ff4654' + color: '#FF4654' }, channel: message.channelId, id: message.id, - timestamp: message.createdAt.valueOf(), + timestamp: Temporal.Instant.fromEpochMilliseconds( + message.createdAt.valueOf() + ), embeds: (message.embeds as TextEmbed[] | undefined)?.map(i => { return { - ...i, + icon_url: i.iconUrl ? i.iconUrl : undefined, + type: 'Text', description: i.description ? i.description : undefined, title: i.title ? i.title : undefined, url: i.url ? i.url : undefined }; }), platform: { name: 'bolt-revolt', message }, - reply: async (msg: BoltMessage, masquerade = true) => { - message.reply(await coreToMessage(msg, masquerade)); + reply: async (msg: message, masquerade = true) => { + message.reply(await torevolt(msg, masquerade as boolean)); }, attachments: message.attachments?.map( - ({ filename, size, downloadURL, isSpoiler }) => { + ({ filename, size, isSpoiler, id, tag }) => { return { - file: downloadURL, + file: `https://autumn.revolt.chat/${tag}/${id}/${filename}`, name: filename, - spoiler: isSpoiler, // change if revolt adds spoiler support + spoiler: isSpoiler, size: (size || 1) / 1000000 }; } ), - content: content !== null ? content : undefined, - guild: String(message.channel?.serverId), - replyto: await replyto(plugin, message, getReply) + content: message.content, + replytoid: message.replyIds ? message.replyIds[0] : undefined }; } - -function systemMessages(message: Message) { - let content = message.content; - const systemMessage = message.systemMessage; - function user(type: 'user' | 'from' | 'to') { - return ( - ((systemMessage as T)[type as keyof T] as User | null)?.username || - (systemMessage as T)[`${type}Id` as keyof T] - ); - } - if (systemMessage) { - const type = systemMessage.type; - const rest = type.split('_'); - rest.shift(); - const action = rest.join(' '); - if (type === 'text') { - content = (systemMessage as TextSystemMessage).content; - } else if ( - [ - 'user_added', - 'user_remove', - 'user_joined', - 'user_left', - 'user_kicked', - 'user_banned' - ].includes(type) - ) { - content = `${user('user')} ${action}`; - } else if (type === 'channel_ownership_changed') { - content = `${user( - 'from' - )} transfered this to ${user('to')}`; - } else if ( - [ - 'channel_description_changed', - 'channel_icon_changed', - 'channel_renamed' - ].includes(type) - ) { - content = `channel ${action} by ${user( - 'from' - )}`; - } else { - content = 'unknown system message'; - } - } - return content; -} - -async function replyto( - plugin: RevoltPlugin, - message: Message, - getReply: boolean -) { - if (message.replyIds && message.replyIds.length > 0 && getReply) { - try { - return await messageToCore( - plugin, - await plugin.bot.messages.fetch(message.channelId, message.replyIds[0]), - false - ); - } catch { - return; - } - } - return; -} diff --git a/packages/bolt-revolt/mod.ts b/packages/bolt-revolt/mod.ts index 16a0e4b..cf2625b 100644 --- a/packages/bolt-revolt/mod.ts +++ b/packages/bolt-revolt/mod.ts @@ -1,85 +1,70 @@ import { - BoltBridgeMessage, - BoltBridgeMessageArgs, - BoltPlugin, - Client + Bolt, + Client, + Message, + bolt_plugin, + bridge_platform, + message } from './deps.ts'; -import { coreToMessage, messageToCore } from './messages.ts'; +import { tocore, torevolt } from './messages.ts'; -export default class RevoltPlugin extends BoltPlugin { +export class revolt_plugin extends bolt_plugin<{ token: string }> { bot: Client; - token: string; name = 'bolt-revolt'; - version = '0.5.4'; - constructor(config: { token: string }) { - super(); + version = '0.5.5'; + support = ['0.5.5']; + + constructor(bolt: Bolt, config: { token: string }) { + super(bolt, config); this.bot = new Client(); - this.token = config.token; - this.bot.on('messageCreate', async message => { - this.emit('messageCreate', await messageToCore(this, message)); - }); - this.bot.on('messageUpdate', async message => { - this.emit('messageUpdate', await messageToCore(this, message)); - }); - this.bot.on('messageDelete', message => { - this.emit('messageDelete', { - id: message.id, - platform: { name: 'bolt-revolt', message }, - channel: message.channelId, - timestamp: Date.now() - }); + this.bot.on('messageCreate', message => { + if (message.systemMessage) return; + this.emit('create_message', tocore(message)); }); this.bot.on('ready', () => { this.emit('ready'); }); + this.bot.loginBot(this.config.token); } - async start() { - await this.bot.loginBot(this.token); - } - bridgeSupport = { text: true }; - async createSenddata(channel: string) { + + async create_bridge(channel: string) { const ch = await this.bot.channels.fetch(channel); - if (!ch.havePermission('Masquerade')) + if (!ch.havePermission('Masquerade')) { throw new Error('Please enable masquerade permissions!'); + } return ch.id; } - async bridgeMessage(data: BoltBridgeMessageArgs) { - switch (data.type) { - case 'create': - case 'update': { - const dat = data.data as BoltBridgeMessage; - const channel = await this.bot.channels.fetch(dat.channel); - let replyto; - try { - if (dat.replytoId) { - replyto = await messageToCore( - this, - await this.bot.messages.fetch(dat.channel, dat.replytoId) - ); - } - } catch {} - try { - const msg = await coreToMessage({ ...dat, replyto }); - const result = data.type === 'update' - ? await (await channel.fetchMessage(dat.id)).edit(msg) // TODO - : await channel.sendMessage(msg); - return { - channel: dat.channel, - id: 'id' in result ? result.id : result._id, - plugin: 'bolt-revolt', - senddata: dat.channel - }; - } catch (e) { - // TODO: proper error handling - return {}; - } - } - case 'delete': { - const channel = await this.bot.channels.fetch(data.data.channel); - const msg = await channel.fetchMessage(data.data.id); - await msg.delete(); - return { ...data.data.bridgePlatform, id: data.data.id }; - } - } + + is_bridged(msg: message) { + return Boolean( + msg.author.id === this.bot.user?.id && msg.platform.message.masquerade + ); + } + + async create_message(msg: message, bridge: bridge_platform) { + const channel = await this.bot.channels.fetch(bridge.channel); + const result = await channel.sendMessage(await torevolt(msg)); + return { + ...bridge, + id: result.id + }; + } + + async edit_message( + msg: message, + bridge: bridge_platform & { id: string } + ) { + const message = await this.bot.messages.fetch(bridge.channel, bridge.id); + await message.edit(await torevolt(msg)); + return bridge; + } + + async delete_message( + _msg: message, + bridge: bridge_platform & { id: string } + ) { + const message = await this.bot.messages.fetch(bridge.channel, bridge.id); + await message.delete(); + return bridge; } } diff --git a/packages/bolt/README.md b/packages/bolt/README.md deleted file mode 100644 index 0504836..0000000 --- a/packages/bolt/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Bolt - -Bolt is a cross-platform chat bot that bridges communities that's written in -Typescript and powered by Deno. To learn more, see the -[docs](https://williamhorning.dev/bolt/docs). diff --git a/packages/bolt/_deps.ts b/packages/bolt/_deps.ts new file mode 100644 index 0000000..d1e5692 --- /dev/null +++ b/packages/bolt/_deps.ts @@ -0,0 +1,8 @@ +export { EventEmitter } from 'https://deno.land/x/event@2.0.1/mod.ts'; +export { + MongoClient, + type Document, + type Collection +} from 'https://deno.land/x/mongo@v0.32.0/mod.ts'; +export { connect } from 'https://deno.land/x/redis@v0.32.0/mod.ts'; +export { parseArgs } from 'https://deno.land/std@0.215.0/cli/parse_args.ts'; diff --git a/packages/bolt/bolt.ts b/packages/bolt/bolt.ts new file mode 100644 index 0000000..20d37de --- /dev/null +++ b/packages/bolt/bolt.ts @@ -0,0 +1,78 @@ +import { bolt_bridges } from './bridges/mod.ts'; +import { bolt_commands } from './cmds/mod.ts'; +import { connect, EventEmitter, MongoClient } from './_deps.ts'; +import { + bolt_plugin, + config, + define_config, + log_error, + plugin_events, + create_plugin +} from './utils/mod.ts'; + +export class Bolt extends EventEmitter { + bridge: bolt_bridges; + cmds = new bolt_commands(); + config: config; + db: { + mongo: MongoClient; + redis: Awaited>; + name: string; + }; + plugins = new Map>(); + + static async setup(cfg: Partial) { + const config = define_config(cfg); + + Deno.env.set('BOLT_ERROR_HOOK', config.errorURL || ''); + + const mongo = new MongoClient(); + + let redis: Bolt['db']['redis'] | undefined; + + try { + await mongo.connect(config.mongo_uri); + redis = await connect({ + hostname: config.redis_host, + port: config.redis_port + }); + } catch (e) { + await log_error(e, { config }); + Deno.exit(1); + } + + return new Bolt(config, mongo, redis); + } + + private constructor( + config: config, + mongo: MongoClient, + redis: Bolt['db']['redis'] + ) { + super(); + this.config = config; + this.db = { mongo, redis, name: config.mongo_database }; + this.bridge = new bolt_bridges(this); + this.cmds.listen(this); + this.load(this.config.plugins); + } + + async load(plugins: { type: create_plugin; config: unknown }[]) { + for (const { type, config } of plugins) { + const plugin = new type(this, config); + if (!plugin.support.includes('0.5.5')) { + await log_error( + new Error(`plugin '${plugin.name}' doesn't support bolt 0.5.5`) + ); + Deno.exit(1); + } else { + this.plugins.set(plugin.name, plugin); + (async () => { + for await (const event of plugin) { + this.emit(event.name, ...event.value); + } + })(); + } + } + } +} diff --git a/packages/bolt/bridges/_command_functions.ts b/packages/bolt/bridges/_command_functions.ts new file mode 100644 index 0000000..d385b1d --- /dev/null +++ b/packages/bolt/bridges/_command_functions.ts @@ -0,0 +1,174 @@ +import { Bolt, command_arguments, create_message, log_error } from './_deps.ts'; + +/** join a bridge */ +export async function join( + { channel, platform, opts }: command_arguments, + bolt: Bolt +) { + const _id = `bridge-${opts.name?.split(' ')[0]}`; + const current = await bolt.bridge.get_bridge({ channel }); + const errorargs = { channel, platform, _id }; + const plugin = bolt.plugins.get(platform); + + if (current || !_id) { + return { + text: create_message({ + text: "to do this, you can't be in a bridge and need to name your bridge, see `!bolt help`" + }) + }; + } else if (!plugin || !plugin.create_bridge) { + return { + text: ( + await log_error(new Error(`can't find plugin#create_bridge`), errorargs) + ).message + }; + } else { + const bridge = (await bolt.bridge.get_bridge({ _id })) || { + _id, + platforms: [] + }; + try { + bridge.platforms.push({ + channel, + plugin: platform, + senddata: await plugin.create_bridge(channel) + }); + await bolt.bridge.update_bridge(bridge); + return { + text: create_message({ text: 'Joined a bridge!' }), + ok: true + }; + } catch (e) { + return { text: (await log_error(e, errorargs)).message }; + } + } +} + +/** leave a bridge */ +export async function leave( + { channel, platform }: command_arguments, + bolt: Bolt +) { + const current = await bolt.bridge.get_bridge({ channel }); + + if (!current) { + return { + text: create_message({ + text: 'To run this command you need to be in a bridge. To learn more, run `!bolt help`.' + }), + ok: true + }; + } else { + try { + await bolt.bridge.update_bridge({ + _id: current._id, + platforms: current.platforms.filter( + i => i.channel !== channel && i.plugin !== platform + ) + }); + + return { + text: create_message({ text: 'Left a bridge!' }), + ok: true + }; + } catch (e) { + return { + text: (await log_error(e, { channel, platform, current })).message + }; + } + } +} + +/** reset a bridge (leave then join) */ +export async function reset(args: command_arguments, bolt: Bolt) { + if (!args.opts.name) { + const [_, ...rest] = ( + (await bolt.bridge.get_bridge(args))?._id || '' + ).split('bridge-'); + args.opts.name = rest.join('bridge-'); + } + let result = await leave(args, bolt); + if (!result.ok) return result; + result = await join(args, bolt); + if (!result.ok) return result; + return { text: create_message({ text: 'Reset this bridge!' }) }; +} + +/** toggle a setting on a bridge */ +export async function toggle(args: command_arguments, bolt: Bolt) { + const current = await bolt.bridge.get_bridge(args); + + if (!current) { + return { + text: create_message({ + text: 'You need to be in a bridge to toggle settings' + }) + }; + } + + if (!args.opts.setting) { + return { + text: create_message({ + text: 'You need to specify a setting to toggle' + }) + }; + } + + if (!['realnames', 'editing_allowed'].includes(args.opts.setting)) { + return { + text: create_message({ text: "That setting doesn't exist" }) + }; + } + + const setting = args.opts.setting as 'realnames' | 'editing_allowed'; + + const bridge = { + ...current, + settings: { + ...(current.settings || {}), + [args.opts.setting]: !(current.settings?.[setting] || false) + } + }; + + try { + await bolt.bridge.update_bridge(bridge); + return { + text: create_message({ text: 'Toggled that setting!' }) + }; + } catch (e) { + return { + text: (await log_error(e, { ...args, bridge })).message + }; + } +} + +export async function status(args: command_arguments, bolt: Bolt) { + const current = await bolt.bridge.get_bridge(args); + + if (!current) { + return { + text: create_message({ + text: "You're not in any bridges right now." + }) + }; + } + + const settings_keys = Object.entries(current.settings || {}).filter( + i => i[1] + ); + + const settings_text = + settings_keys.length > 0 + ? ` as well as the following settings: \n\`${settings_keys.join( + '`, `' + )}\`` + : 'as well as no settings'; + + return { + text: create_message({ + text: `This channel is connected to \`${current._id}\`, a bridge with ${ + current.platforms.length - 1 + } other channels connected to it, ${settings_text}` + }) + }; +} diff --git a/packages/bolt/bridges/_commands.ts b/packages/bolt/bridges/_commands.ts new file mode 100644 index 0000000..08f74f1 --- /dev/null +++ b/packages/bolt/bridges/_commands.ts @@ -0,0 +1,48 @@ +import { join, leave, reset, toggle, status } from './_command_functions.ts'; +import { Bolt, command, create_message } from './_deps.ts'; + +export function bridge_commands(bolt: Bolt): command { + return { + name: 'bridge', + description: 'bridge this channel to somewhere else', + execute: () => + create_message({ + text: `Try running \`!bolt help\` for help with bridges` + }), + options: { + subcommands: [ + { + name: 'join', + description: 'join a bridge', + execute: async opts => (await join(opts, bolt)).text, + options: { argument_name: 'name', argument_required: true } + }, + { + name: 'leave', + description: 'leave a bridge', + execute: async opts => (await leave(opts, bolt)).text + }, + { + name: 'reset', + description: 'reset a bridge', + execute: async opts => (await reset(opts, bolt)).text, + options: { argument_name: 'name' } + }, + { + name: 'toggle', + description: 'toggle a setting on a bridge', + execute: async opts => (await toggle(opts, bolt)).text, + options: { + argument_name: 'setting', + argument_required: true + } + }, + { + name: 'status', + description: "see what bridges you're in", + execute: async opts => (await status(opts, bolt)).text + } + ] + } + }; +} diff --git a/packages/bolt/bridges/_deps.ts b/packages/bolt/bridges/_deps.ts new file mode 100644 index 0000000..f01fcc6 --- /dev/null +++ b/packages/bolt/bridges/_deps.ts @@ -0,0 +1,10 @@ +export { type Collection } from '../_deps.ts'; +export { Bolt } from '../bolt.ts'; +export { type command, type command_arguments } from '../cmds/mod.ts'; +export { + bolt_plugin, + create_message, + log_error, + type message, + type deleted_message +} from '../utils/mod.ts'; diff --git a/packages/bolt/bridges/mod.ts b/packages/bolt/bridges/mod.ts new file mode 100644 index 0000000..e4c71cb --- /dev/null +++ b/packages/bolt/bridges/mod.ts @@ -0,0 +1,198 @@ +import { bridge_commands } from './_commands.ts'; +import { + Bolt, + Collection, + message, + deleted_message, + log_error, + bolt_plugin +} from './_deps.ts'; +import { bridge_document, bridge_platform } from './types.ts'; + +export class bolt_bridges { + private bolt: Bolt; + private bridge_collection: Collection; + // TODO: find a better way to do this, maps work BUT don't't scale well + private bridged_message_id_map = new Map(); + + constructor(bolt: Bolt) { + this.bolt = bolt; + this.bridge_collection = bolt.db.mongo + .database(bolt.config.mongo_database) + .collection('bridges'); + this.bolt.on('create_message', async msg => { + await new Promise(res => setTimeout(res, 250)); + if (this.is_bridged(msg)) return; + bolt.emit('create_nonbridged_message', msg); + await this.handle_message(msg, 'create_message'); + }); + this.bolt.on('edit_message', async msg => { + await new Promise(res => setTimeout(res, 250)); + if (this.is_bridged(msg)) return; + await this.handle_message(msg, 'edit_message'); + }); + this.bolt.on('delete_message', async msg => { + await new Promise(res => setTimeout(res, 400)); + await this.handle_message(msg, 'delete_message'); + }); + this.bolt.cmds.set('bridge', bridge_commands(bolt)); + } + + async get_bridge_message(id: string) { + const redis_data = await this.bolt.db.redis.get(`bolt-bridge-${id}`); + if (redis_data === null) return [] as bridge_platform[]; + return JSON.parse(redis_data) as bridge_platform[]; + } + + is_bridged(msg: deleted_message) { + const platform = this.bolt.plugins.get(msg.platform.name); + if (!platform) return false; + const platsays = platform.is_bridged(msg); + if (platsays !== 'query') return platsays; + return Boolean(this.bridged_message_id_map.get(msg.id)); + } + + async get_bridge({ _id, channel }: { _id?: string; channel?: string }) { + const query = {} as Record; + + if (_id) { + query._id = _id; + } + if (channel) { + query['platforms.channel'] = channel; + } + return (await this.bridge_collection.findOne(query)) || undefined; + } + + async update_bridge(bridge: bridge_document) { + return await this.bridge_collection.replaceOne( + { _id: bridge._id }, + bridge, + { + upsert: true + } + ); + } + + private async handle_message( + msg: deleted_message, + action: 'delete_message' + ): Promise; + private async handle_message( + msg: message, + action: 'create_message' | 'edit_message' + ): Promise; + private async handle_message( + msg: message | deleted_message, + action: 'create_message' | 'edit_message' | 'delete_message' + ): Promise { + const bridge_info = await this.get_platforms(msg, action); + if (!bridge_info) return; + + if (bridge_info.bridge.settings?.realnames === true) { + if ('author' in msg && msg.author) { + msg.author.username = msg.author.rawname; + } + } + + const data: (bridge_platform & { id: string })[] = []; + + for (const plat of bridge_info.platforms) { + const { plugin, platform } = await this.get_sane_plugin(plat, action); + if (!plugin || !platform) continue; + + let dat; + + try { + dat = await plugin[action]( + { + ...msg, + replytoid: await this.get_replytoid(msg, platform) + } as message, + platform + ); + } catch (e) { + if (action === 'delete_message') continue; + const err = await log_error(e, { platform, action }); + try { + dat = await plugin[action](err.message, platform); + } catch (e) { + await log_error( + new Error(`logging failed for ${err.uuid}`, { cause: e }) + ); + continue; + } + } + this.bridged_message_id_map.set(dat.id!, true); + data.push(dat as bridge_platform & { id: string }); + } + + for (const i of data) { + await this.bolt.db.redis.set(`bolt-bridge-${i.id}`, JSON.stringify(data)); + } + + await this.bolt.db.redis.set(`bolt-bridge-${msg.id}`, JSON.stringify(data)); + } + + private async get_platforms( + msg: message | deleted_message, + action: 'create_message' | 'edit_message' | 'delete_message' + ) { + const bridge = await this.get_bridge(msg); + if (!bridge) return; + if ( + action !== 'create_message' && + bridge.settings?.editing_allowed !== true + ) + return; + + const platforms = + action === 'create_message' + ? bridge.platforms.filter(i => i.channel !== msg.channel) + : await this.get_bridge_message(msg.id); + if (!platforms || platforms.length < 1) return; + return { platforms, bridge }; + } + + private async get_replytoid( + msg: message | deleted_message, + platform: bridge_platform + ) { + let replytoid; + if ('replytoid' in msg && msg.replytoid) { + try { + replytoid = (await this.get_bridge_message(msg.replytoid)).find( + i => i.channel === platform.channel && i.plugin === platform.plugin + )?.id; + } catch { + replytoid = undefined; + } + } + return replytoid; + } + + private async get_sane_plugin( + platform: bridge_platform, + action: 'create_message' | 'edit_message' | 'delete_message' + ): Promise<{ + plugin?: bolt_plugin; + platform?: bridge_platform & { id: string }; + }> { + const plugin = this.bolt.plugins.get(platform.plugin); + + if (!plugin || !plugin[action]) { + await log_error(new Error(`plugin ${platform.plugin} has no ${action}`)); + return {}; + } + + if (!platform.senddata || (action !== 'create_message' && !platform.id)) + return {}; + + return { plugin, platform: platform } as { + plugin: bolt_plugin; + platform: bridge_platform & { id: string }; + }; + } +} + +export * from './types.ts'; diff --git a/packages/bolt/bridges/types.ts b/packages/bolt/bridges/types.ts new file mode 100644 index 0000000..dceccc6 --- /dev/null +++ b/packages/bolt/bridges/types.ts @@ -0,0 +1,15 @@ +export interface bridge_document { + _id: string; + platforms: bridge_platform[]; + settings?: { + realnames?: boolean; + editing_allowed?: boolean; + }; +} + +export interface bridge_platform { + channel: string; + plugin: string; + senddata: unknown; + id?: string; +} diff --git a/packages/bolt/cli.ts b/packages/bolt/cli.ts new file mode 100644 index 0000000..9652bbb --- /dev/null +++ b/packages/bolt/cli.ts @@ -0,0 +1,135 @@ +import { + apply_migrations, + get_migrations, + versions +} from './migrations/mod.ts'; +import { Bolt } from './bolt.ts'; +import { MongoClient, parseArgs } from './_deps.ts'; + +const c = { + name: 'bolt', + version: '0.5.5', + description: 'Cross-platform bot connecting communities', + colors: { + red: 'color: red', + blue: 'color: blue', + green: 'color: green', + purple: 'color: purple' + } +}; + +const f = parseArgs(Deno.args, { + boolean: ['help', 'version', 'run', 'migrations'], + string: ['config'] +}); + +if (f.help) { + run_help(); +} else if (f.version) { + console.log(c.version); + Deno.exit(); +} else if (f.run) { + await run_bolt(); +} else if (f.migrations) { + await run_migrations(); +} else { + run_help(); +} + +function run_help() { + console.log(`%c${c.name} v${c.version} - ${c.description}`, c.colors.blue); + console.log('%cUsage: bolt [options]', c.colors.purple); + console.log('%cOptions:', c.colors.green); + console.log('--help: Show this help.'); + console.log('--version: Show version.'); + console.log('--run: run an of bolt using the settings in config.ts'); + console.log('--config : absolute path to config file'); + console.log('--migrations: Start interactive tool to migrate databases'); + Deno.exit(); +} + +async function run_bolt() { + let cfg; + + try { + cfg = (await import(f.config || `${Deno.cwd()}/config.ts`))?.default; + } catch (e) { + console.error(`%cCan't load your config, exiting...\n`, c.colors.red); + console.error(e); + Deno.exit(1); + } + + await Bolt.setup(cfg); +} + +async function run_migrations() { + console.log( + `Available versions are: ${Object.values(versions).join(', ')}`, + c.colors.blue + ); + + const version_from = prompt('what version is the DB currently set up for?'); + const version_to = prompt('what version of bolt do you want to move to?'); + const db_uri = prompt('Input your MongoDB connection URI'); + const prod = confirm('Did you run Bolt in prod mode?'); + + if (!version_from || !version_to || !db_uri) Deno.exit(); + + if (version_from === version_to) { + console.log( + '%cYou are already on the version you want to move to', + c.colors.red + ); + Deno.exit(); + } + + if ( + !(Object.values(versions) as string[]).includes(version_from) || + !(Object.values(versions) as string[]).includes(version_to) + ) { + console.log('%cInvalid version(s) entered', c.colors.red); + Deno.exit(1); + } + + const migrationlist = get_migrations(version_from, version_to); + + if (migrationlist.length < 1) Deno.exit(); + + const mongo = new MongoClient(); + await mongo.connect(db_uri); + const database = mongo.database(prod ? 'bolt' : 'bolt-canary'); + + console.log(`%cDownloading your data...`, c.colors.blue); + + const dumped = await database + .collection(migrationlist[0].from_db) + .find() + .toArray(); + + console.log( + `%cDownloaded data from the DB! Migrating your data...`, + c.colors.blue + ); + + const data = apply_migrations(migrationlist, dumped); + + const filepath = Deno.makeTempFileSync({ suffix: 'bolt-db-migration' }); + Deno.writeTextFileSync(filepath, JSON.stringify(data)); + + const writeconfirm = confirm( + `Do you want to write the data at ${filepath} to the DB?` + ); + + if (!writeconfirm) Deno.exit(); + + const tocollection = database.collection(migrationlist.slice(-1)[0].to_db); + + await Promise.all( + data.map(i => { + return tocollection.replaceOne({ _id: i._id }, i, { upsert: true }); + }) + ); + + console.log('%cWrote data to the DB', c.colors.green); + Deno.exit(); +} diff --git a/packages/bolt/cli/deps.ts b/packages/bolt/cli/deps.ts deleted file mode 100644 index c040a14..0000000 --- a/packages/bolt/cli/deps.ts +++ /dev/null @@ -1,13 +0,0 @@ -export { colors } from 'https://deno.land/x/cliffy@v1.0.0-rc.3/ansi/colors.ts'; -export { Command as CliffyApp } from 'https://deno.land/x/cliffy@v1.0.0-rc.3/command/mod.ts'; -export { - Input, - Select, - Toggle, - prompt -} from 'https://deno.land/x/cliffy@v1.0.0-rc.3/prompt/mod.ts'; -export { - MongoClient, - type Document, - type ConnectOptions as MongoConnectOptions -} from 'https://deno.land/x/mongo@v0.32.0/mod.ts'; diff --git a/packages/bolt/cli/migrations.ts b/packages/bolt/cli/migrations.ts deleted file mode 100644 index 39f1138..0000000 --- a/packages/bolt/cli/migrations.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { - BoltMigrationVersions, - applyBoltMigrations, - getBoltMigrations -} from '../migrations/mod.ts'; -import { Input, MongoClient, Select, Toggle, colors, prompt } from './deps.ts'; - -export default async function migrations() { - const sharedversionprompt = { - type: Select, - options: Object.values(BoltMigrationVersions) - }; - const promptdata = await prompt([ - { - name: 'from', - message: 'what version of bolt is the DB currently set up for?', - ...sharedversionprompt - }, - { - name: 'to', - message: 'what version of bolt do you want to move to?', - ...sharedversionprompt - }, - { - name: 'mongo', - type: Input, - message: 'Input your MongoDB connection URI', - default: 'mongodb://localhost:27017' - }, - { - name: 'prod', - type: Toggle, - message: 'Did you run Bolt in prod mode?', - default: false - } - ]); - - if (!promptdata.from || !promptdata.to || !promptdata.mongo) Deno.exit(); - - const migrationlist = getBoltMigrations(promptdata.from, promptdata.to); - - if (migrationlist.length < 1) Deno.exit(); - - const mongo = new MongoClient(); - await mongo.connect(promptdata.mongo); - const database = mongo.database(promptdata.prod ? 'bolt' : 'bolt-canary'); - - console.log(colors.blue(`Downloading your data...`)); - - const dumped = await database - .collection(migrationlist[0].collectionNames.fromDB) - .find() - .toArray(); - - console.log( - colors.blue(`Downloaded data from the DB! Migrating your data...`) - ); - - const data = applyBoltMigrations(migrationlist, dumped); - - const filepath = Deno.makeTempFileSync({ suffix: 'bolt-db-migration' }); - Deno.writeTextFileSync(filepath, JSON.stringify(data)); - - const writeconfirm = await Toggle.prompt({ - message: `Do you want to write the data at ${filepath} to the DB?` - }); - - if (!writeconfirm) Deno.exit(); - - const tocollection = database.collection( - migrationlist.slice(-1)[0].collectionNames.toDB - ); - - await Promise.all( - data.map(i => { - return tocollection.replaceOne({ _id: i._id }, i, { upsert: true }); - }) - ); - - console.log(colors.green('Wrote data to the DB')); -} diff --git a/packages/bolt/cli/mod.ts b/packages/bolt/cli/mod.ts deleted file mode 100644 index be6281a..0000000 --- a/packages/bolt/cli/mod.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { CliffyApp } from './deps.ts'; -import migration from './migrations.ts'; -import run from './run.ts'; - -const cli = new CliffyApp() - .name('bolt') - .version('0.5.4') - .description('Cross-platform bot connecting communities') - .default('help') - .command('help', 'Show this help.') - .action(() => { - cli.showHelp(); - }) - .command('migration', 'Starts interactive tool to migrate databases') - .action(migration) - .command('run', 'runs an instance of bolt using the settings in config.ts') - .option( - '--config ', - 'path to config file relative to the current directory' - ) - .option('--debug', 'enables debug mode') - .action(run); - -cli.parse(Deno.args); diff --git a/packages/bolt/cli/run.ts b/packages/bolt/cli/run.ts deleted file mode 100644 index 2ec710b..0000000 --- a/packages/bolt/cli/run.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Bolt, defineBoltConfig } from '../lib/mod.ts'; -import { colors } from './deps.ts'; - -export default async function run({ - config, - debug -}: { - config?: string; - debug?: true | undefined; -}) { - let cfg; - - try { - cfg = (await import(config || `${Deno.cwd()}/config.ts`))?.default; - } catch (e) { - console.error(colors.red(`Can't load your config, exiting...\n`), e); - Deno.exit(1); - } - - const bolt = new Bolt(defineBoltConfig(cfg)); - - bolt.on('error', msg => { - console.error(msg); - }); - - bolt.on('warning', msg => { - console.warn(colors.yellow(msg)); - }); - - if (debug) { - bolt.on('debug', msg => { - console.debug(colors.blue(msg as string)); - }); - window.bolt = bolt; - } - - await bolt.setup(); -} diff --git a/packages/bolt/cmds/_default.ts b/packages/bolt/cmds/_default.ts new file mode 100644 index 0000000..612626c --- /dev/null +++ b/packages/bolt/cmds/_default.ts @@ -0,0 +1,45 @@ +import { create_message } from './_deps.ts'; +import { command } from './types.ts'; + +export const default_commands = [ + [ + 'help', + { + name: 'help', + description: 'get help', + execute: () => + create_message({ + embeds: [ + { + title: `bolt help`, + description: `Check out [the docs](https://williamhorning.dev/bolt/) for help.` + } + ] + }) + } + ], + [ + 'version', + { + name: 'version', + description: "get bolt's version", + execute: () => + create_message({ + text: `hello from bolt 0.5.5!` + }) + } + ], + [ + 'ping', + { + name: 'ping', + description: 'pong', + execute: ({ timestamp }) => + create_message({ + text: `Pong! 🏓 ${Temporal.Now.instant() + .since(timestamp) + .total('milliseconds')}ms` + }) + } + ] +] as [string, command][]; diff --git a/packages/bolt/cmds/_deps.ts b/packages/bolt/cmds/_deps.ts new file mode 100644 index 0000000..596d413 --- /dev/null +++ b/packages/bolt/cmds/_deps.ts @@ -0,0 +1,3 @@ +export { parseArgs } from '../_deps.ts'; +export { Bolt } from '../bolt.ts'; +export { create_message, log_error, type message } from '../utils/mod.ts'; diff --git a/packages/bolt/cmds/_testdata.ts b/packages/bolt/cmds/_testdata.ts new file mode 100644 index 0000000..edd6a64 --- /dev/null +++ b/packages/bolt/cmds/_testdata.ts @@ -0,0 +1,25 @@ +export const help_output = { + author: { + username: 'Bolt', + profile: + 'https://cdn.discordapp.com/icons/1011741670510968862/2d4ce9ff3f384c027d8781fa16a38b07.png?size=1024', + rawname: 'bolt', + id: 'bolt' + }, + embeds: [ + { + title: `bolt help`, + description: `Check out [the docs](https://williamhorning.dev/bolt/) for help.` + } + ], + content: undefined, + channel: '', + id: '', + reply: async () => {}, + timestamp: Temporal.Instant.from('2021-01-01T00:00:00Z'), + platform: { + name: 'bolt', + message: undefined + }, + uuid: undefined +}; diff --git a/packages/bolt/cmds/_tests.ts b/packages/bolt/cmds/_tests.ts new file mode 100644 index 0000000..4beb0ba --- /dev/null +++ b/packages/bolt/cmds/_tests.ts @@ -0,0 +1,36 @@ +import { assertEquals } from 'https://deno.land/std@0.216.0/assert/mod.ts'; +import { message } from './_deps.ts'; +import { help_output } from './_testdata.ts'; +import { bolt_commands } from './mod.ts'; + +const temporal_instant = Temporal.Instant.from('2021-01-01T00:00:00Z'); + +globalThis.Temporal.Now.instant = () => { + return temporal_instant; +}; + +Deno.test('Run help command', async () => { + const cmds = new bolt_commands(); + + let res: (value: message) => void; + + const promise = new Promise>(resolve => { + res = resolve; + }); + + await cmds.run({ + channel: '', + cmd: 'help', + opts: {}, + platform: 'bolt', + // deno-lint-ignore require-await + replyfn: async msg => res(msg), + timestamp: temporal_instant + }); + + const result = await promise; + + result.reply = help_output.reply; + + assertEquals(result, help_output); +}); diff --git a/packages/bolt/cmds/mod.ts b/packages/bolt/cmds/mod.ts new file mode 100644 index 0000000..8750d0b --- /dev/null +++ b/packages/bolt/cmds/mod.ts @@ -0,0 +1,56 @@ +import { default_commands } from './_default.ts'; +import { Bolt, log_error, parseArgs } from './_deps.ts'; +import { command, command_arguments } from './types.ts'; + +export class bolt_commands extends Map { + constructor() { + super(default_commands); + } + + listen(bolt: Bolt) { + bolt.on('create_nonbridged_message', msg => { + if (msg.content?.startsWith('!bolt')) { + const args = parseArgs(msg.content.split(' ')); + args._.shift(); + this.run({ + channel: msg.channel, + cmd: args._.shift() as string, + subcmd: args._.shift() as string, + opts: args as Record, + platform: msg.platform.name, + timestamp: msg.timestamp, + replyfn: msg.reply + }); + } + }); + + bolt.on('create_command', async cmd => { + await this.run(cmd); + }); + } + + async run(opts: command_arguments) { + const cmd = this.get(opts.cmd) || this.get('help')!; + const cmd_opts = { ...opts, commands: this }; + let reply; + try { + let execute; + if (cmd.options?.subcommands && opts.subcmd) { + execute = cmd.options.subcommands.find( + i => i.name === opts.subcmd + )?.execute; + } + if (!execute) execute = cmd.execute; + reply = await execute(cmd_opts); + } catch (e) { + reply = (await log_error(e, { ...opts, reply: undefined })).message; + } + try { + await opts.replyfn(reply, false); + } catch (e) { + await log_error(e, { ...opts, reply: undefined }); + } + } +} + +export * from './types.ts'; diff --git a/packages/bolt/cmds/types.ts b/packages/bolt/cmds/types.ts new file mode 100644 index 0000000..51127ac --- /dev/null +++ b/packages/bolt/cmds/types.ts @@ -0,0 +1,25 @@ +import { message } from './_deps.ts'; + +export type command_arguments = { + channel: string; + cmd: string; + opts: Record; + platform: string; + replyfn: message['reply']; + subcmd?: string; + timestamp: Temporal.Instant; +}; + +export type command = { + name: string; + description?: string; + options?: { + default?: boolean; + argument_name?: string; + argument_required?: boolean; + subcommands?: command[]; + }; + execute: ( + opts: command_arguments + ) => Promise> | message; +}; diff --git a/packages/bolt/lib/bolt.ts b/packages/bolt/lib/bolt.ts deleted file mode 100644 index b32105e..0000000 --- a/packages/bolt/lib/bolt.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { - BoltBridgeCommands, - bridgeBoltMessage, - bridgeBoltThread, - getBoltBridgedMessage -} from './bridge/mod.ts'; -import { BoltCommands } from './commands/mod.ts'; -import { EventEmitter, MongoClient, Redis, connect } from './deps.ts'; -import { BoltPluginEvents } from './types.ts'; -import { BoltConfig, BoltPlugin, logBoltError } from './utils.ts'; - -export class Bolt extends EventEmitter { - config: BoltConfig; - cmds = new BoltCommands(this); - database: string; - version = '0.5.4'; - plugins: BoltPlugin[] = []; - mongo = new MongoClient(); - redis?: Redis; - constructor(config: BoltConfig) { - super(); - this.config = config; - this.database = config.database.mongo.database; - } - getPlugin(name: string) { - return this.plugins.find(i => i.name === name); - } - async load(plugins: BoltPlugin[]) { - for (const plugin of plugins) { - if (plugin.boltversion !== '1') { - throw await logBoltError(this, { - message: `This plugin isn't supported by this version of Bolt.`, - extra: { plugin: plugin.name }, - code: 'PluginNotCompatible' - }); - } - this.plugins.push(plugin); - plugin.start(this); - if (plugin?.commands) { - this.cmds.registerCommands(...plugin.commands); - } - } - for (const plugin of plugins) { - (async () => { - for await (const event of plugin) { - this.emit(event.name, ...event.value); - } - })(); - } - } - async unload(plugins: BoltPlugin[]) { - for (const plugin of plugins) { - if (plugin.stop) await plugin.stop(); - this.plugins = this.plugins.filter(i => i.name !== plugin.name); - } - } - async setup() { - await this.dbsetup(); - this.registerPluginEvents(); - this.cmds.registerCommands(...BoltBridgeCommands); - this.load(this.config.plugins); - } - private async dbsetup() { - try { - await this.mongo.connect(this.config.database.mongo.connection); - } catch (e) { - throw await logBoltError(this, { - message: `Can't connect to MongoDB`, - cause: e, - extra: {}, - code: 'MongoDBConnectFailed' - }); - } - try { - if (this.config.database.redis) { - this.redis = await connect(this.config.database.redis); - } else { - this.emit( - 'warning', - 'not using Redis, things may slow down or be disabled.' - ); - } - } catch (e) { - await logBoltError(this, { - message: `Can't connect to Redis`, - cause: e, - extra: {}, - code: 'RedisConnectFailed' - }); - this.redis = undefined; - } - } - private registerPluginEvents() { - // TODO: move all code below to bridge folder - this.on('messageCreate', async msg => { - if (await getBoltBridgedMessage(this, true, msg.id)) return; - bridgeBoltMessage(this, 'create', msg); - }); - this.on('messageUpdate', async msg => { - if (await getBoltBridgedMessage(this, true, msg.id)) return; - bridgeBoltMessage(this, 'update', msg); - }); - this.on('messageDelete', async msg => { - bridgeBoltMessage(this, 'delete', msg); - }); - this.on('threadCreate', thread => bridgeBoltThread(this, 'create', thread)); - this.on('threadDelete', thread => bridgeBoltThread(this, 'delete', thread)); - } -} diff --git a/packages/bolt/lib/bridge/bridge.ts b/packages/bolt/lib/bridge/bridge.ts deleted file mode 100644 index 5f8e080..0000000 --- a/packages/bolt/lib/bridge/bridge.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { Bolt } from '../bolt.ts'; -import { BoltMessage } from '../mod.ts'; -import { BoltMessageBase, BoltThread } from '../types.ts'; -import { logBoltError } from '../utils.ts'; -import { BoltBridgePlatform, BoltBridgeSentPlatform } from './types.ts'; -import { getBoltBridge, getBoltBridgedMessage } from './utils.ts'; - -export async function bridgeBoltMessage( - bolt: Bolt, - type: 'create' | 'update' | 'delete', - message: - | (BoltMessageBase & { - replyto?: BoltMessage; - }) - | BoltMessage -) { - const data = []; - const bridge = await getBoltBridge(bolt, { channel: message.channel }); - if (!bridge) return; - - const platforms: (BoltBridgePlatform | BoltBridgeSentPlatform)[] | false = - type === 'create' - ? bridge.platforms.filter(i => i.channel !== message.channel) - : await getBoltBridgedMessage(bolt, false, message.id); - - if (!platforms || platforms.length < 1) return; - - for (const platform of platforms) { - const plugin = bolt.getPlugin(platform.plugin); - if ( - !platform?.senddata || - !plugin?.bridgeSupport?.text || - !plugin?.bridgeMessage || - (message.threadId && !plugin.bridgeSupport.threads) - ) - continue; - - const threadId = message.threadId - ? type === 'create' - ? ( - (await bolt.mongo - .database(bolt.database) - .collection('threads') - .findOne({ _id: message.threadId })) as BoltThread | undefined - )?.id - : (platform as BoltBridgeSentPlatform).thread?.id - : undefined; - - const replyto = await getBoltBridgedMessage(bolt, false, message.replyto?.id); - - if (bridge.settings?.realnames && 'author' in message) { - message.author.username = message.author.rawname; - } - - const bridgedata = { - ...platform, - threadId, - replytoId: replyto - ? replyto.find(i => i.channel === platform.channel)?.id - : undefined, - bridgePlatform: platform, - bolt - }; - - let handledat; - - try { - handledat = await plugin.bridgeMessage({ - data: { ...message, ...bridgedata }, - type - }); - } catch (e) { - const error = await logBoltError(bolt, { - message: `Bridging that message failed`, - cause: e, - extra: { - e, - type, - replyto, - message: { ...message, platform: undefined }, - data, - bridge, - platforms, - platform, - plugin: plugin.name - }, - code: 'BridgeFailed' - }); - try { - handledat = await plugin.bridgeMessage({ - data: { - ...error.boltmessage, - ...bridgedata - }, - type - }); - } catch (e2) { - await logBoltError(bolt, { - message: `Can't log bridge error`, - cause: e2, - extra: { ...error, e2 }, - code: 'BridgeErrorFailed' - }); - } - } - - if (handledat) data.push(handledat); - } - - if (type !== 'delete') { - for (const i of data) { - // since this key is used to prevent echo, 15 sec expiry should be enough - await bolt.redis?.set(`message-temp-${i.id}`, JSON.stringify(data), { - ex: 15 - }); - } - await bolt.redis?.set(`message-${message.id}`, JSON.stringify(data)); - } -} - -export async function bridgeBoltThread( - bolt: Bolt, - type: 'create' | 'delete', - thread: BoltThread -) { - const data = []; - const bridge = await getBoltBridge(bolt, { channel: thread.parent }); - if (!bridge) return; - - const platforms = bridge.platforms.filter( - (i: BoltBridgePlatform) => i.channel !== thread.parent - ); - - if (!platforms || platforms.length < 1) return; - - for (const platform of platforms) { - const plugin = bolt.getPlugin(platform.plugin); - if ( - !platform?.senddata || - !plugin?.bridgeSupport?.threads || - !plugin?.bridgeThread - ) - continue; - - try { - const handledat = await plugin.bridgeThread({ - type, - data: { - ...thread, - ...platform, - bridgePlatform: platform - } - }); - data.push(handledat); - } catch (e) { - await bridgeBoltMessage(bolt, 'create', { - ...( - await logBoltError(bolt, { - message: `Can't bridge thread events`, - cause: e, - extra: { bridge, e, type }, - code: `Thread${type}Failed` - }) - ).boltmessage, - channel: thread.parent - }); - } - } - - if (type !== 'delete') { - for (const i of data) { - await bolt.mongo - .database(bolt.database) - .collection('threads') - .replaceOne( - { _id: i.id }, - { ...data, _id: thread.id }, - { upsert: true } - ); - } - await bolt.mongo - .database(bolt.database) - .collection('threads') - .replaceOne( - { _id: thread.id }, - { ...data, _id: thread.id }, - { upsert: true } - ); - } -} diff --git a/packages/bolt/lib/bridge/commandinternal.ts b/packages/bolt/lib/bridge/commandinternal.ts deleted file mode 100644 index 9bc6447..0000000 --- a/packages/bolt/lib/bridge/commandinternal.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { Bolt } from '../bolt.ts'; -import { createBoltMessage, logBoltError } from '../utils.ts'; -import { BoltBridgePlatform } from './types.ts'; -import { getBoltBridge, updateBoltBridge } from './utils.ts'; - -export async function joinBoltBridge( - bolt: Bolt, - channel: string, - platform: string, - name?: string -) { - name = name?.split(' ')[0]; - const current = await getBoltBridge(bolt, { channel }); - if (current?._id) { - return { - message: createBoltMessage({ - content: - "To run this command you can't be in a bridge. To learn more, run `!bolt help`." - }), - code: 'InBridge' - }; - } - if (!name) { - return { - message: createBoltMessage({ - content: - 'Please provide a name for your bridge. To learn more, run `!bolt help`.' - }), - code: 'MissingParameter' - }; - } - const plugin = bolt.getPlugin(platform); - if (!plugin?.createSenddata) { - return logBoltError(bolt, { - message: `Can't find plugin while creating bridge`, - code: 'BridgeCreationNoPlugin', - extra: { plugin, channel, platform, name, current } - }); - } - const bridge = (await getBoltBridge(bolt, { _id: `bridge-${name}` })) || { - _id: `bridge-${name}`, - name, - platforms: [] - }; - try { - const senddata = await plugin.createSenddata(channel); - bridge.platforms.push({ - channel, - plugin: platform, - senddata - }); - await updateBoltBridge(bolt, bridge); - } catch (e) { - return logBoltError(bolt, { - message: `Can't update this bridge`, - cause: e, - code: 'BridgeCreationCreateUpdateFailed', - extra: { plugin, channel, platform, name, current } - }); - } - return { - message: createBoltMessage({ - content: 'Joined a bridge!' - }), - code: 'OK' - }; -} -export async function leaveBoltBridge( - bolt: Bolt, - channel: string, - platform: string -) { - const current = await getBoltBridge(bolt, { channel }); - if (!current?._id) { - return { - message: createBoltMessage({ - content: - 'To run this command you need to be in a bridge. To learn more, run `!bolt help`.' - }), - code: 'NotInBridge', - e: false - }; - } - try { - await updateBoltBridge(bolt, { - ...current, - platforms: current.platforms.filter( - (i: BoltBridgePlatform) => - i.channel === channel && i.plugin === platform - ) - }); - } catch (e) { - return logBoltError(bolt, { - cause: e, - message: `Can't leave that bridge`, - code: 'BridgeCreationLeaveFailed', - extra: { channel, platform, current } - }); - } - return { - message: createBoltMessage({ content: 'Left a bridge!' }), - code: 'OK', - e: false - }; -} -export async function resetBoltBridge( - bolt: Bolt, - channel: string, - platform: string, - name?: string -) { - name = name?.split(' ')[0]; - const current = await getBoltBridge(bolt, { channel }); - if (current?._id) { - const result = await leaveBoltBridge(bolt, channel, platform); - if (result.code !== 'OK') return result.message; - } - const result = await joinBoltBridge( - bolt, - channel, - platform, - name || current?._id.substring(6) - ); - if (result.code !== 'OK') return result.message; - return createBoltMessage({ content: 'Reset this bridge!' }); -} diff --git a/packages/bolt/lib/bridge/commands.ts b/packages/bolt/lib/bridge/commands.ts deleted file mode 100644 index c6b7067..0000000 --- a/packages/bolt/lib/bridge/commands.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { BoltCommand } from '../commands/mod.ts'; -import { createBoltMessage } from '../utils.ts'; -import { - joinBoltBridge, - leaveBoltBridge, - resetBoltBridge -} from './commandinternal.ts'; -import { getBoltBridge } from './utils.ts'; - -export const BoltBridgeCommands = [ - { - name: 'bridge', - description: 'bridge this channel to somewhere else', - execute: () => { - return createBoltMessage({ - content: `Try running \`!bolt help\` for help on bridges` - }); - }, - options: { - subcommands: [ - { - name: 'join', - description: 'join a bridge', - execute: async ({ bolt, channel, platform, arg: name }) => - (await joinBoltBridge(bolt, channel, platform, name)).message, - options: { hasArgument: true } - }, - { - name: 'leave', - description: 'leave a bridge', - execute: async ({ bolt, channel, platform }) => - (await leaveBoltBridge(bolt, channel, platform)).message, - options: { hasArgument: true } - }, - { - name: 'reset', - description: 'reset a bridge', - execute: async ({ bolt, channel, platform, arg: name }) => - await resetBoltBridge(bolt, channel, platform, name), - options: { hasArgument: true } - }, - { - name: 'status', - description: "see what bridges you're in", - execute: async ({ bolt, channel }) => { - const data = await getBoltBridge(bolt, { channel }); - if (data?._id) { - return createBoltMessage({ - content: `This channel is connected to \`${data._id}\`` - }); - } else { - return createBoltMessage({ - content: "You're not in any bridges right now." - }); - } - } - } - ] - } - } -] as BoltCommand[]; diff --git a/packages/bolt/lib/bridge/mod.ts b/packages/bolt/lib/bridge/mod.ts deleted file mode 100644 index d997709..0000000 --- a/packages/bolt/lib/bridge/mod.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './bridge.ts'; -export * from './commands.ts'; -export * from './types.ts'; -export * from './utils.ts'; diff --git a/packages/bolt/lib/bridge/types.ts b/packages/bolt/lib/bridge/types.ts deleted file mode 100644 index e03baf1..0000000 --- a/packages/bolt/lib/bridge/types.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Bolt } from '../mod.ts'; -import { BoltMessage, BoltMessageDelete, BoltThread } from '../types.ts'; - -export interface BoltBridgeDocument { - _id: string; - name: string; - platforms: BoltBridgePlatform[]; - settings?: { - realnames?: boolean; - }; -} - -export interface BoltBridgePlatform { - channel: string; - plugin: string; - senddata: unknown; -} - -export interface BoltBridgeSentPlatform extends BoltBridgePlatform { - id: string; - thread?: BoltThread; -} - -export interface BoltBridgeMessage - extends Omit, 'replyto'>, - BoltBridgePlatform { - bolt: Bolt; - bridgePlatform: BoltBridgePlatform; - replytoId?: string; -} - -export interface BoltBridgeMessageDelete - extends BoltMessageDelete, - BoltBridgePlatform { - bolt: Bolt; - bridgePlatform: BoltBridgePlatform; -} - -export interface BoltBridgeThread extends BoltThread, BoltBridgePlatform { - bridgePlatform: BoltBridgePlatform; -} - -export type BoltBridgeMessageArgs = { - type: 'create' | 'update' | 'delete'; - data: BoltBridgeMessage | BoltBridgeMessageDelete; -}; - -export type BoltBridgeThreadArgs = { - type: 'create' | 'update' | 'delete'; - data: BoltBridgeThread; -}; diff --git a/packages/bolt/lib/bridge/utils.ts b/packages/bolt/lib/bridge/utils.ts deleted file mode 100644 index 72055bc..0000000 --- a/packages/bolt/lib/bridge/utils.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Bolt } from '../bolt.ts'; -import { BoltBridgeDocument, BoltBridgeSentPlatform } from './types.ts'; - -export async function getBoltBridgedMessage(bolt: Bolt, bCheck: Boolean, id?: string) { - return JSON.parse((await bolt.redis?.get(`message${bCheck ? "-temp" : ""}-${id}`)) || 'false') as - | BoltBridgeSentPlatform[] - | false; -} - -export async function getBoltBridge( - bolt: Bolt, - { channel, _id }: { channel?: string; _id?: string } -) { - let query; - if (channel) { - // @ts-ignore: THIS IS A VALID QUERY, DON'T WORRY - query = { 'platforms.channel': channel }; - } else if (_id) { - query = { _id }; - } else { - throw new Error('Must provide one of channel or _id'); - } - return await bolt.mongo - .database(bolt.database) - .collection('bridges') - .findOne(query); -} - -export async function updateBoltBridge(bolt: Bolt, bridge: BoltBridgeDocument) { - return await bolt.mongo - .database(bolt.database) - .collection('bridges') - .replaceOne({ _id: bridge._id }, bridge, { upsert: true }); -} diff --git a/packages/bolt/lib/commands/commands.ts b/packages/bolt/lib/commands/commands.ts deleted file mode 100644 index 96df6c7..0000000 --- a/packages/bolt/lib/commands/commands.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Bolt } from '../bolt.ts'; -import { getBoltBridgedMessage } from '../bridge/mod.ts'; -import { logBoltError } from '../utils.ts'; -import defaultcommands from './default.ts'; -import { BoltCommand, BoltCommandArguments } from './types.ts'; - -export class BoltCommands { - bolt: Bolt; - commands = new Map(); - fallback = defaultcommands[0]; - defaultcommands = defaultcommands; - - constructor(bolt: Bolt) { - this.bolt = bolt; - this.registerCommands(...defaultcommands); - bolt.on('messageCreate', async msg => { - if (await getBoltBridgedMessage(bolt, true, msg.id)) return; - if (msg.content?.startsWith('!bolt')) { - let [_, name, ...arg] = msg.content.split(' '); - this.runCommand({ - name, - reply: msg.reply, - channel: msg.channel, - platform: msg.platform.name, - arg: arg.join(' '), - timestamp: msg.timestamp - }); - } - }); - } - - registerCommands(...cmds: BoltCommand[]) { - for (const cmd of cmds) { - this.commands.set(cmd.name, cmd); - if (cmd.options?.default) this.fallback = cmd; - } - } - - async runCommand(opts: Omit, 'commands'>) { - const cmd: BoltCommand = this.commands.get(opts.name) || this.fallback; - const cmdopts = { ...opts, ...this, commands: this }; - try { - let reply; - if (cmd.options?.subcommands && opts.arg) { - const [name, ...arg] = opts.arg.split(' '); - let subcmd = cmd.options.subcommands.find(i => i.name === name); - if (!subcmd) subcmd = cmd; - reply = await subcmd.execute({ ...cmdopts, arg: arg.join(' ') }); - } else { - reply = await cmd.execute(cmdopts); - } - cmdopts.reply(reply); - } catch (e) { - await opts.reply( - ( - await logBoltError(this.bolt, { - cause: e, - message: `Running that command failed:\n${e.message || e}`, - extra: opts, - code: 'CommandFailed' - }) - ).boltmessage - ); - } - } -} diff --git a/packages/bolt/lib/commands/default.ts b/packages/bolt/lib/commands/default.ts deleted file mode 100644 index d8b320d..0000000 --- a/packages/bolt/lib/commands/default.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { createBoltMessage } from '../utils.ts'; -import { BoltCommand } from './types.ts'; - -export default [ - { - name: 'help', - description: 'get help with bolt', - execute: ({ commands }) => { - return createBoltMessage({ - embeds: [ - { - title: 'Bolt Help', - description: - "Here's some basic help. Take a look at [the docs](https://williamhorning.dev/bolt/docs) for more information.", - fields: [ - { - name: 'Commands', - value: [...commands.commands.keys()] - .map(i => `\`${i}\``) - .join(', '), - inline: true - } - ] - } - ] - }); - }, - options: { - default: true - } - }, - { - name: 'info', - description: 'get information about bolt', - execute: ({ bolt }) => { - return createBoltMessage({ - content: `Bolt ${bolt.version} running on Deno ${Deno.version.deno} with ${bolt.plugins.length} plugins, MongoDB, Redis, and other open-source software.` - }); - } - }, - { - name: 'ping', - description: 'pong', - execute({ timestamp }) { - return createBoltMessage({ - content: `Pong! 🏓 ${Date.now() - timestamp}ms` - }); - } - }, - { - name: 'site', - description: 'links to the bolt site', - execute: ({ bolt }) => { - return createBoltMessage({ - content: `You can find the Bolt site at ${bolt.config.http.dashURL}` - }); - } - } -] as BoltCommand[]; diff --git a/packages/bolt/lib/commands/mod.ts b/packages/bolt/lib/commands/mod.ts deleted file mode 100644 index 7ed9e0b..0000000 --- a/packages/bolt/lib/commands/mod.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { BoltCommands } from './commands.ts'; -export * from './types.ts'; diff --git a/packages/bolt/lib/commands/types.ts b/packages/bolt/lib/commands/types.ts deleted file mode 100644 index ab0d285..0000000 --- a/packages/bolt/lib/commands/types.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Bolt } from '../bolt.ts'; -import { BoltMessage } from '../types.ts'; -import { BoltCommands } from './commands.ts'; - -export type BoltCommandArguments = { - arg?: string; - bolt: Bolt; - commands: BoltCommands; - channel: string; - name: string; - platform: string; - reply: (message: BoltMessage) => Promise; - timestamp: number; -}; - -export type BoltCommandOptions = { - default?: boolean; - hasArgument?: boolean; - subcommands?: BoltCommand[]; -}; - -export type BoltCommand = { - name: string; - description?: string; - options?: BoltCommandOptions; - execute: ( - opts: BoltCommandArguments - ) => Promise> | BoltMessage; -}; diff --git a/packages/bolt/lib/deps.ts b/packages/bolt/lib/deps.ts deleted file mode 100644 index c5c8ecb..0000000 --- a/packages/bolt/lib/deps.ts +++ /dev/null @@ -1,10 +0,0 @@ -export { EventEmitter } from 'https://deno.land/x/event@2.0.1/mod.ts'; -export { - MongoClient, - type ConnectOptions as MongoConnectOptions -} from 'https://deno.land/x/mongo@v0.32.0/mod.ts'; -export { - connect, - type Redis, - type RedisConnectOptions -} from 'https://deno.land/x/redis@v0.32.0/mod.ts'; diff --git a/packages/bolt/lib/mod.ts b/packages/bolt/lib/mod.ts deleted file mode 100644 index 4a5a4de..0000000 --- a/packages/bolt/lib/mod.ts +++ /dev/null @@ -1,28 +0,0 @@ -export { Bolt } from './bolt.ts'; -export { - getBoltBridge, - getBoltBridgedMessage, - updateBoltBridge, - type BoltBridgeDocument, - type BoltBridgeMessage, - type BoltBridgeMessageArgs, - type BoltBridgeMessageDelete, - type BoltBridgePlatform, - type BoltBridgeSentPlatform, - type BoltBridgeThread, - type BoltBridgeThreadArgs -} from './bridge/mod.ts'; -export { type BoltCommand } from './commands/mod.ts'; -export { - type BoltEmbed, - type BoltMessage, - type BoltMessageDelete, - type BoltPluginEvents, - type BoltThread -} from './types.ts'; -export { - BoltPlugin, - createBoltMessage, - defineBoltConfig, - logBoltError -} from './utils.ts'; diff --git a/packages/bolt/lib/types.ts b/packages/bolt/lib/types.ts deleted file mode 100644 index 456e2d5..0000000 --- a/packages/bolt/lib/types.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { BoltError } from './utils.ts'; - -type BoltMediaEmbed = { - url: string; - proxy_url?: string; - height?: number; - width?: number; -}; - -export interface BoltEmbed { - author?: { - name: string; - url?: string; - iconUrl?: string; - proxy_icon_url?: string; - }; - color?: number; - description?: string; - fields?: { - name: string; - value: string; - inline?: boolean; - }[]; - footer?: { - text: string; - icon_url?: string; - proxy_icon_url?: string; - }; - image?: BoltMediaEmbed; - provider?: { - name?: string; - url?: string; - }; - thumbnail?: BoltMediaEmbed; - timestamp?: number; - title?: string; - url?: string; - video?: { - url?: string; - proxy_url?: string; - height?: number; - width?: number; - }; -} - -export interface BoltMessageBase { - id: string; - platform: { - name: string; - message: Message; - }; - channel: string; - guild?: string; - threadId?: string; - timestamp: number; -} - -export interface BoltMessage extends BoltMessageBase { - attachments?: { - alt?: string; - file: string; - name?: string; - spoiler?: boolean; - size: number; - }[]; - author: { - username: string; - rawname: string; - profile?: string; - banner?: string; - id: string; - color?: string; - }; - content?: string; - embeds?: BoltEmbed[]; - reply: (message: BoltMessage) => Promise; - replyto?: Omit, 'replyto'>; -} - -export type BoltMessageDelete = BoltMessageBase; - -export type BoltThread = { - id: string; - parent: string; - name?: string; - topic?: string; -}; - -export type BoltPluginEvents = { - messageCreate: [BoltMessage]; - messageUpdate: [BoltMessage]; - messageDelete: [BoltMessageDelete]; - threadCreate: [BoltThread]; - threadUpdate: [BoltThread]; - threadDelete: [BoltThread]; - error: [BoltError]; - warning: [string]; - ready: [unknown?]; - debug: [unknown]; -}; diff --git a/packages/bolt/lib/utils.ts b/packages/bolt/lib/utils.ts deleted file mode 100644 index 95a4336..0000000 --- a/packages/bolt/lib/utils.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { Bolt } from './bolt.ts'; -import { - BoltBridgeMessageArgs, - BoltBridgeSentPlatform, - BoltBridgeThreadArgs -} from './bridge/mod.ts'; -import { BoltCommand } from './commands/types.ts'; -import { - EventEmitter, - MongoConnectOptions, - RedisConnectOptions -} from './deps.ts'; -import { - BoltEmbed, - BoltMessage, - BoltPluginEvents, - BoltThread -} from './types.ts'; - -export interface BoltConfig { - prod: boolean; - plugins: BoltPlugin[]; - database: { - mongo: { - connection: MongoConnectOptions | string; - database: string; - }; - redis?: RedisConnectOptions; - }; - http: { dashURL?: string; apiURL?: string; errorURL?: string }; -} - -export abstract class BoltPlugin extends EventEmitter { - abstract name: string; - abstract version: string; - boltversion = '1'; - bridgeSupport?: { - text?: boolean; - threads?: boolean; - forum?: boolean; - voice?: false; - } = {}; - commands?: BoltCommand[]; - createSenddata?(channel: string): Promise; - bridgeMessage?(data: BoltBridgeMessageArgs): Promise; - bridgeThread?(data: BoltBridgeThreadArgs): Promise; - abstract start(bolt: Bolt): Promise | void; - stop?(): Promise | void; -} - -export function defineBoltConfig(config?: Partial): BoltConfig { - if (!config) config = {}; - if (!config.prod) config.prod = false; - if (!config.plugins) config.plugins = []; - if (!config.database) - config.database = { - mongo: { - connection: 'mongodb://localhost:27017', - database: 'bolt-testing' - } - }; - if (!config.database.mongo) - config.database.mongo = { - connection: 'mongodb://localhost:27017', - database: config.prod ? 'bolt' : 'bolt-testing' - }; - if (!config.http) - config.http = { - apiURL: 'http://localhost:9090', - dashURL: 'http://localhost:9091' - }; - if (!config.http.apiURL) config.http.apiURL = 'http://localhost:9090'; - if (!config.http.dashURL) config.http.dashURL = 'http://localhost:9091'; - return config as BoltConfig; -} - -type CreateBoltMessageOptions = { - content?: string; - embeds?: [BoltEmbed, ...BoltEmbed[]]; -}; - -export function createBoltMessage( - opts: CreateBoltMessageOptions -): BoltMessage { - return { - author: { - rawname: 'Bolt', - username: 'Bolt', - id: 'bolt' - }, - ...opts, - channel: '', - id: '', - reply: async () => {}, - timestamp: Date.now(), - platform: { - name: 'bolt', - message: opts - } - }; -} - -export class BoltError extends Error { - code: string; - extra: Record; - id: string; - boltmessage: BoltMessage; - e = this; - name = 'BoltError'; - constructor( - message: string, - options: { - extra?: Record; - code: string; - cause?: Error; - } - ) { - super(message, { cause: options.cause }); - this.code = options.code; - this.extra = options.extra || {}; - this.id = crypto.randomUUID(); - this.boltmessage = createBoltMessage({ - content: `Something went wrong: ${this.code}! Join the Bolt support server and share the following: \`\`\`\n${message}\n${this.id}\`\`\`` - }); - } -} - -export async function logBoltError( - bolt: Bolt, - { - message, - cause, - extra, - code - }: { - message: string; - cause?: Error; - extra: Record; - code: string; - } -) { - const e = new BoltError(message, { - cause, - extra, - code - }); - if (bolt.config.http.errorURL) { - try { - const msg = `Bolt Error:\n${bolt.plugins.length} plugins - ${ - Deno.build.target - } - ${e.id} - ${code}\n${message}\n${ - cause ? `\`\`\`json\n${JSON.stringify(cause)}\n\`\`\`\n` : '' - }\`\`\`json\n${extra}\n\`\`\``; - await fetch(bolt.config.http.errorURL, { - method: 'POST', - body: msg - }); - } catch { - bolt.emit( - 'error', - new BoltError(`logging error ${e.id} failed`, { - code: 'ErrorLogFailed' - }) - ); - } - } - bolt.emit('error', e); - return e; -} diff --git a/packages/bolt/migrations/_deps.ts b/packages/bolt/migrations/_deps.ts new file mode 100644 index 0000000..ff1ace6 --- /dev/null +++ b/packages/bolt/migrations/_deps.ts @@ -0,0 +1 @@ +export { type Document } from '../_deps.ts'; diff --git a/packages/bolt/migrations/_testdata.ts b/packages/bolt/migrations/_testdata.ts new file mode 100644 index 0000000..1e5569c --- /dev/null +++ b/packages/bolt/migrations/_testdata.ts @@ -0,0 +1,67 @@ +export const four_one_platform = [ + { + _id: 'discord-bridge-a', + value: { id: '1', token: '2' } + }, + { + _id: 'discord-000000000000000000', + value: 'bridge-a' + } +]; + +export const four_two_platform = [ + { + _id: 'discord-bridge-a', + value: { id: '1', token: '2' } + }, + { + _id: 'discord-000000000000000000', + value: 'bridge-a' + }, + { + _id: 'guilded-bridge-a', + value: { id: '1', token: '2' } + }, + { + _id: 'guilded-6cb2f623-8eee-44a3-b5bf-cf9b147e46d7', + value: 'bridge-a' + } +]; + +export const fourbeta = [ + { + _id: 'bridge-a', + value: { + bridges: [ + { + platform: 'discord', + channel: '000000000000000000', + senddata: { id: '1', token: '2' } + }, + { + platform: 'guilded', + channel: '6cb2f623-8eee-44a3-b5bf-cf9b147e46d7', + senddata: { id: '1', token: '2' } + } + ] + } + } +]; + +export const five = [ + { + _id: 'bridge-a', + platforms: [ + { + plugin: 'bolt-discord', + channel: '000000000000000000', + senddata: { id: '1', token: '2' } + }, + { + plugin: 'bolt-guilded', + channel: '6cb2f623-8eee-44a3-b5bf-cf9b147e46d7', + senddata: { id: '1', token: '2' } + } + ] + } +]; diff --git a/packages/bolt/migrations/_tests.ts b/packages/bolt/migrations/_tests.ts new file mode 100644 index 0000000..c0e2a7a --- /dev/null +++ b/packages/bolt/migrations/_tests.ts @@ -0,0 +1,21 @@ +import { assertEquals } from 'https://deno.land/std@0.216.0/assert/mod.ts'; +import { + five, + four_one_platform, + fourbeta, + four_two_platform +} from './_testdata.ts'; +import fourbetatofive from './fourbetatofive.ts'; +import fourtofourbeta from './fourtofourbeta.ts'; + +Deno.test('four to fourbeta one platform', () => { + assertEquals(fourtofourbeta.translate(four_one_platform), []); +}); + +Deno.test('four to fourbeta two platforms', () => { + assertEquals(fourtofourbeta.translate(four_two_platform), fourbeta); +}); + +Deno.test('fourbeta to five', () => { + assertEquals(fourbetatofive.translate(fourbeta), five); +}); diff --git a/packages/bolt/migrations/deps.ts b/packages/bolt/migrations/deps.ts deleted file mode 100644 index 492f140..0000000 --- a/packages/bolt/migrations/deps.ts +++ /dev/null @@ -1 +0,0 @@ -export { type Document } from 'https://deno.land/x/mongo@v0.32.0/mod.ts'; diff --git a/packages/bolt/migrations/fourbetatofive.ts b/packages/bolt/migrations/fourbetatofive.ts index 2b8b62e..3643af1 100644 --- a/packages/bolt/migrations/fourbetatofive.ts +++ b/packages/bolt/migrations/fourbetatofive.ts @@ -1,13 +1,11 @@ -import { Document } from './deps.ts'; -import { mapPlugins } from './utils.ts'; +import { Document } from './_deps.ts'; +import { _map_plugins } from './mod.ts'; export default { - versionfrom: '0.4-beta', - versionto: '0.5', - collectionNames: { - fromDB: 'bridgev1', - toDB: 'bridges' - }, + from: '0.4-beta', + to: '0.5', + from_db: 'bridgev1', + to_db: 'bridges', translate: ( itemslist: ( | Document @@ -24,14 +22,12 @@ export default { return [ { _id, - name: `bridge-migrated-${_id}`, platforms: value.bridges.map( (i: { platform: string; channel: string; senddata: unknown }) => { return { - plugin: mapPlugins(i.platform), + plugin: _map_plugins(i.platform), channel: i.channel, - senddata: i.senddata, - name: `${i.channel} on ${i.platform}` + senddata: i.senddata }; } ) diff --git a/packages/bolt/migrations/fourtofourbeta.ts b/packages/bolt/migrations/fourtofourbeta.ts index f10e6b0..0465ea4 100644 --- a/packages/bolt/migrations/fourtofourbeta.ts +++ b/packages/bolt/migrations/fourtofourbeta.ts @@ -1,13 +1,11 @@ -import { Document } from './deps.ts'; -import { isChannel } from './utils.ts'; +import { Document } from './_deps.ts'; +import { _is_channel } from './mod.ts'; export default { - versionfrom: '0.4', - versionto: '0.4-beta', - collectionNames: { - fromDB: 'bridge', - toDB: 'bridgev1' - }, + from: '0.4', + to: '0.4-beta', + from_db: 'bridge', + to_db: 'bridgev1', translate: ( items: (Document | { _id: string; value: string | unknown })[] ) => { @@ -22,7 +20,7 @@ export default { for (const item of items) { const [platform, ...join] = item._id.split('-'); const name = join.join('-'); - if (isChannel(name)) continue; + if (_is_channel(name)) continue; const _id = items.find(i => { return i._id.startsWith(platform) && i.value === name; })?._id; @@ -40,7 +38,7 @@ export default { for (const _id in obj) { const value = obj[_id]; if (!value) continue; - if (isChannel(_id)) continue; + if (_is_channel(_id)) continue; if (value.length < 2) continue; documents.push({ _id, diff --git a/packages/bolt/migrations/mod.ts b/packages/bolt/migrations/mod.ts index 0da0186..aa9e761 100644 --- a/packages/bolt/migrations/mod.ts +++ b/packages/bolt/migrations/mod.ts @@ -1,30 +1,53 @@ -import { Document } from './deps.ts'; -import BoltFourToFive from './fourbetatofive.ts'; +import { Document } from './_deps.ts'; +import BoltFourToFourBeta from './fourtofourbeta.ts'; import BoltFourBetaToFive from './fourtofourbeta.ts'; -export enum BoltMigrationVersions { +const list_migrations = [BoltFourBetaToFive, BoltFourToFourBeta]; + +export type migration = (typeof list_migrations)[number]; + +export enum versions { Four = '0.4', FourBeta = '0.4-beta', Five = '0.5' } -export const BoltMigrationsList = [BoltFourBetaToFive, BoltFourToFive]; - -export function getBoltMigrations(versionFrom: string, versionTo: string) { - const indexoffrom = BoltMigrationsList.findIndex( - i => i.versionfrom === versionFrom - ); - const indexofto = BoltMigrationsList.findLastIndex( - i => i.versionto === versionTo - ); - return BoltMigrationsList.slice(indexoffrom, indexofto + 1); +export function get_migrations(from: string, to: string): migration[] { + const indexoffrom = list_migrations.findIndex(i => i.from === from); + const indexofto = list_migrations.findLastIndex(i => i.to === to); + return list_migrations.slice(indexoffrom, indexofto + 1); } -export function applyBoltMigrations( - migrations: typeof BoltMigrationsList, +export function apply_migrations( + migrations: migration[], data: Document[] -) { - return migrations.reduce((acc, migration) => { - return migration.translate(acc); - }, data); +): Document[] { + return migrations.reduce((acc, migration) => migration.translate(acc), data); +} + +export function _map_plugins(pluginname: string): string { + if (pluginname === 'discord') return 'bolt-discord'; + if (pluginname === 'guilded') return 'bolt-guilded'; + if (pluginname === 'revolt') return 'bolt-revolt'; + return 'unknown'; +} + +export function _is_channel(channel: string): boolean { + if ( + channel.match( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i + ) + ) { + return true; + } + if (channel.match(/[0-7][0-9A-HJKMNP-TV-Z]{25}/gm)) return true; + if (!isNaN(Number(channel))) return true; + if ( + channel.startsWith('discord-') || + channel.startsWith('guilded-') || + channel.startsWith('revolt-') + ) { + return true; + } + return false; } diff --git a/packages/bolt/migrations/utils.ts b/packages/bolt/migrations/utils.ts deleted file mode 100644 index 2963cf2..0000000 --- a/packages/bolt/migrations/utils.ts +++ /dev/null @@ -1,24 +0,0 @@ -export function isChannel(channel: string) { - if ( - channel.match( - /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i - ) - ) - return true; - if (channel.match(/[0-7][0-9A-HJKMNP-TV-Z]{25}/gm)) return true; - if (!isNaN(Number(channel))) return true; - if ( - channel.startsWith('discord-') || - channel.startsWith('guilded-') || - channel.startsWith('revolt-') - ) - return true; - return false; -} - -export function mapPlugins(pluginname: string): string { - if (pluginname === 'discord') return 'bolt-discord'; - if (pluginname === 'guilded') return 'bolt-guilded'; - if (pluginname === 'revolt') return 'bolt-revolt'; - return 'unknown'; -} diff --git a/packages/bolt/mod.ts b/packages/bolt/mod.ts index 05470a0..9316c54 100644 --- a/packages/bolt/mod.ts +++ b/packages/bolt/mod.ts @@ -1,6 +1,21 @@ -if (import.meta.main) { - await import('./cli/mod.ts'); -} - -export * from './lib/mod.ts'; -export * from './migrations/mod.ts'; +export { Bolt } from './bolt.ts'; +export { type bridge_platform } from './bridges/mod.ts'; +export { type command, type command_arguments } from './cmds/mod.ts'; +export { + apply_migrations, + get_migrations, + versions, + type migration +} from './migrations/mod.ts'; +export { + bolt_plugin, + create_message, + define_config, + log_error, + type config, + type deleted_message, + type embed, + type embed_media, + type message, + type plugin_events +} from './utils/mod.ts'; diff --git a/packages/bolt/utils/_deps.ts b/packages/bolt/utils/_deps.ts new file mode 100644 index 0000000..0b2a570 --- /dev/null +++ b/packages/bolt/utils/_deps.ts @@ -0,0 +1,5 @@ +export { nanoid } from 'https://deno.land/x/nanoid@v3.0.0/mod.ts'; +export { Bolt } from '../bolt.ts'; +export { type bridge_platform } from '../bridges/mod.ts'; +export { type command_arguments } from '../cmds/mod.ts'; +export { EventEmitter } from '../_deps.ts'; diff --git a/packages/bolt/utils/_testdata.ts b/packages/bolt/utils/_testdata.ts new file mode 100644 index 0000000..f54bb81 --- /dev/null +++ b/packages/bolt/utils/_testdata.ts @@ -0,0 +1,76 @@ +export const msg = { + author: { + username: 'Bolt', + profile: + 'https://cdn.discordapp.com/icons/1011741670510968862/2d4ce9ff3f384c027d8781fa16a38b07.png?size=1024', + rawname: 'bolt', + id: 'bolt' + }, + content: 'test', + embeds: [{ description: 'test' }], + channel: '', + id: '', + reply: async () => {}, + timestamp: Temporal.Instant.from('2021-01-01T00:00:00Z'), + platform: { + name: 'bolt', + message: undefined + }, + uuid: 'test' +}; + +export const cfg = { + prod: false, + plugins: [], + mongo_uri: 'mongodb://localhost:27017', + mongo_database: 'bolt-testing', + redis_host: 'localhost' +}; + +export const err = new Error('test'); + +export const extra = { test: 'test' }; + +export const err_id = () => 'test'; + +export const err_return = { + e: err, + uuid: 'test', + extra: { test: 'test' }, + message: { + author: { + username: 'Bolt', + profile: + 'https://cdn.discordapp.com/icons/1011741670510968862/2d4ce9ff3f384c027d8781fa16a38b07.png?size=1024', + rawname: 'bolt', + id: 'bolt' + }, + content: `Something went wrong! Check [the docs](https://williamhorning.dev/bolt/docs/Using/) for help.\n\`\`\`\ntest\ntest\n\`\`\``, + channel: '', + embeds: undefined, + id: '', + reply: async () => {}, + timestamp: Temporal.Instant.from('2021-01-01T00:00:00Z'), + platform: { + name: 'bolt', + message: undefined + }, + uuid: 'test' + } +}; + +export const err_hook = { + embeds: [ + { + title: err.message, + description: `\`\`\`${err.stack}\`\`\`\n\`\`\`js\n${JSON.stringify( + { + ...extra, + uuid: 'test' + }, + null, + 2 + )}\`\`\`` + } + ] +}; diff --git a/packages/bolt/utils/_tests.ts b/packages/bolt/utils/_tests.ts new file mode 100644 index 0000000..3407327 --- /dev/null +++ b/packages/bolt/utils/_tests.ts @@ -0,0 +1,65 @@ +import { assertEquals } from 'https://deno.land/std@0.216.0/assert/mod.ts'; +import { + cfg, + err, + err_hook, + err_id, + err_return, + extra, + msg +} from './_testdata.ts'; +import { create_message, define_config, log_error } from './mod.ts'; + +const temporal_instant = Temporal.Instant.from('2021-01-01T00:00:00Z'); + +globalThis.Temporal.Now.instant = () => { + return temporal_instant; +}; + +console.log = () => {}; + +console.error = () => {}; + +Deno.test('message creation', () => { + const msg_real = create_message({ + text: 'test', + embeds: [{ description: 'test' }], + uuid: 'test' + }); + + msg_real.reply = msg.reply; + + assertEquals(msg_real, msg); +}); + +Deno.test('config handling', () => { + assertEquals(define_config(), cfg); +}); + +Deno.test('error handling basic', async () => { + const err_return_real = await log_error(err, extra, err_id); + + err_return_real.message.reply = err_return.message.reply; + + assertEquals(err_return_real, err_return); +}); + +Deno.test('error handling webhook test', async () => { + Deno.env.set('BOLT_ERROR_HOOK', 'http://localhost:8000'); + + let res: (value: unknown) => void; + + const promise = new Promise(resolve => { + res = resolve; + }); + + const server = Deno.serve(async req => { + res(await req.json()); + return new Response(); + }); + + await log_error(err, extra, err_id); + await server.shutdown(); + + assertEquals(await promise, err_hook); +}); diff --git a/packages/bolt/utils/config.ts b/packages/bolt/utils/config.ts new file mode 100644 index 0000000..5d05d23 --- /dev/null +++ b/packages/bolt/utils/config.ts @@ -0,0 +1,22 @@ +import { create_plugin } from './plugins.ts'; + +export function define_config(config?: Partial): config { + if (!config) config = {}; + if (!config.prod) config.prod = false; + if (!config.plugins) config.plugins = []; + if (!config.mongo_uri) config.mongo_uri = 'mongodb://localhost:27017'; + if (!config.mongo_database) + config.mongo_database = config.prod ? 'bolt' : 'bolt-testing'; + if (!config.redis_host) config.redis_host = 'localhost'; + return config as config; +} + +export interface config { + prod: boolean; + plugins: { type: create_plugin; config: unknown }[]; + mongo_uri: string; + mongo_database: string; + redis_host: string; + redis_port?: number; + errorURL?: string; +} diff --git a/packages/bolt/utils/errors.ts b/packages/bolt/utils/errors.ts new file mode 100644 index 0000000..9f8e337 --- /dev/null +++ b/packages/bolt/utils/errors.ts @@ -0,0 +1,74 @@ +import { nanoid } from './_deps.ts'; +import { create_message } from './messages.ts'; + +function get_replacer() { + const seen = new WeakSet(); + // deno-lint-ignore no-explicit-any + return (_: any, value: any) => { + if (typeof value === 'object' && value !== null) { + if (seen.has(value)) { + return '[Circular]'; + } + seen.add(value); + } + if (typeof value === 'bigint') { + return value.toString(); + } + return value; + }; +} + +export async function log_error( + e: Error, + // deno-lint-ignore no-explicit-any + extra: Record = {}, + _id: () => string = nanoid +) { + const uuid = _id(); + + const error_hook = Deno.env.get('BOLT_ERROR_HOOK'); + + if (error_hook && error_hook !== '') { + delete extra.msg; + + await ( + await fetch(error_hook, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify( + { + embeds: [ + { + title: e.message, + description: `\`\`\`${ + e.stack + }\`\`\`\n\`\`\`js\n${JSON.stringify( + { + ...extra, + uuid + }, + get_replacer(), + 2 + )}\`\`\`` + } + ] + }, + get_replacer() + ) + }) + ).text(); + } + + console.error(`\x1b[1;31mBolt Error - '${uuid}'\x1b[0m`); + console.error(e, extra); + + return { + e, + uuid, + extra, + message: create_message({ + text: `Something went wrong! Check [the docs](https://williamhorning.dev/bolt/docs/Using/) for help.\n\`\`\`\n${e.message}\n${uuid}\n\`\`\``, + uuid + }) + }; +} diff --git a/packages/bolt/utils/messages.ts b/packages/bolt/utils/messages.ts new file mode 100644 index 0000000..e295eba --- /dev/null +++ b/packages/bolt/utils/messages.ts @@ -0,0 +1,87 @@ +export function create_message({ + text, + embeds, + uuid +}: { + text?: string; + embeds?: embed[]; + uuid?: string; +}): message & { uuid?: string } { + const data = { + author: { + username: 'Bolt', + profile: + 'https://cdn.discordapp.com/icons/1011741670510968862/2d4ce9ff3f384c027d8781fa16a38b07.png?size=1024', + rawname: 'bolt', + id: 'bolt' + }, + content: text, + embeds, + channel: '', + id: '', + reply: async () => {}, + timestamp: Temporal.Now.instant(), + platform: { + name: 'bolt', + message: undefined + }, + uuid + }; + return data; +} + +export type embed_media = { height?: number; url: string; width?: number }; + +export interface embed { + author?: { name: string; url?: string; icon_url?: string }; + color?: number; + description?: string; + fields?: { name: string; value: string; inline?: boolean }[]; + footer?: { text: string; icon_url?: string }; + image?: embed_media; + thumbnail?: embed_media; + timestamp?: number; + title?: string; + url?: string; + video?: Omit & { url?: string }; +} + +export interface message { + attachments?: { + alt?: string; + file: string; + name?: string; + spoiler?: boolean; + size: number; + }[]; + author: { + username: string; + rawname: string; + profile?: string; + banner?: string; + id: string; + color?: string; + }; + content?: string; + embeds?: embed[]; + reply: (message: message, optional?: unknown) => Promise; + replytoid?: string; + id: string; + platform: { + name: string; + message: t; + webhookid?: string; + }; + channel: string; + timestamp: Temporal.Instant; +} + +export interface deleted_message { + id: string; + channel: string; + platform: { + name: string; + message: t; + }; + timestamp: Temporal.Instant; +} diff --git a/packages/bolt/utils/mod.ts b/packages/bolt/utils/mod.ts new file mode 100644 index 0000000..0677158 --- /dev/null +++ b/packages/bolt/utils/mod.ts @@ -0,0 +1,4 @@ +export * from './config.ts'; +export * from './messages.ts'; +export * from './plugins.ts'; +export * from './errors.ts'; diff --git a/packages/bolt/utils/plugins.ts b/packages/bolt/utils/plugins.ts new file mode 100644 index 0000000..68831d9 --- /dev/null +++ b/packages/bolt/utils/plugins.ts @@ -0,0 +1,67 @@ +import { + Bolt, + EventEmitter, + bridge_platform, + command_arguments +} from './_deps.ts'; +import { deleted_message, message } from './messages.ts'; + +export abstract class bolt_plugin extends EventEmitter { + bolt: Bolt; + config: t; + + /** the name of your plugin (like bolt-discord) */ + abstract name: string; + + /** the version of your plugin (like 0.0.1) */ + abstract version: string; + + /** the versions of bolt your plugin was made for (array of strings like `[0.5.0, 0.5.5]` that only includes breaking releases) */ + abstract support: string[]; + + /** constructor */ + constructor(bolt: Bolt, config: t) { + super(); + this.bolt = bolt; + this.config = config; + } + /** create data needed to bridge */ + abstract create_bridge(channel: string): Promise; + + /** checks if message is bridged */ + abstract is_bridged(message: deleted_message): boolean | 'query'; + + /** bridge a message */ + abstract create_message( + message: message, + bridge: bridge_platform + ): Promise; + + /** edit a bridged message */ + abstract edit_message( + new_message: message, + bridge: bridge_platform & { id: string } + ): Promise; + + /** delete a bridged message */ + abstract delete_message( + message: deleted_message, + bridge: bridge_platform & { id: string } + ): Promise; +} + +export type plugin_events = { + create_message: [message]; + create_command: [command_arguments]; + create_nonbridged_message: [message]; + debug: [unknown]; + edit_message: [message]; + delete_message: [deleted_message]; + ready: []; +}; + +export interface create_plugin { + // deno-lint-ignore no-explicit-any + new (bolt: Bolt, config: any): bolt_plugin; + readonly prototype: bolt_plugin; +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..0f88897 --- /dev/null +++ b/readme.md @@ -0,0 +1,17 @@ +# bolt 0.5 + +bolt is a cross-platform chat bot that bridges communities that's written in +typescript. to learn more, see the [docs](https://williamhorning.dev/bolt). + +## feature support matrix + +| | text | threads | forums | +| ------------ | ----- | ------- | ------ | +| bolt | ✓ | ✓ | X | +| bolt-discord | ✓ | ✓ | ✓ | +| bolt-guilded | ✓\* | X | X | +| bolt-matrix | ✓\*\* | X | X | +| bolt-revolt | ✓ | X | X | + +\* bolt-guilded's text support is a bit iffy +\*\* bolt-matrix is a POC