From 5000ce98253df124654926eb99d87a006eb0ba40 Mon Sep 17 00:00:00 2001 From: Jersey Date: Thu, 3 Oct 2024 12:19:23 -0400 Subject: [PATCH 01/23] formatting changes --- .github/workflows/publish.yml | 4 ++-- packages/lightning-plugin-revolt/src/messages.ts | 10 +++------- packages/lightning-plugin-revolt/src/permissions.ts | 7 +------ packages/lightning/src/bridges/cmd_internals.ts | 4 ++-- 4 files changed, 8 insertions(+), 17 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1b04489..2324c4c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -34,8 +34,8 @@ jobs: run: cd packages/lightning - name: publish to jsr run: | - deno publish - cd packages/lightning + deno publish + cd packages/lightning - name: setup docker metadata id: metadata uses: docker/metadata-action@v5 diff --git a/packages/lightning-plugin-revolt/src/messages.ts b/packages/lightning-plugin-revolt/src/messages.ts index 57971ae..c91b3d0 100644 --- a/packages/lightning-plugin-revolt/src/messages.ts +++ b/packages/lightning-plugin-revolt/src/messages.ts @@ -47,8 +47,7 @@ export async function torvapi( embeds: message.embeds?.map((embed) => { if (embed.fields) { for (const field of embed.fields) { - embed.description += - `\n\n**${field.name}**\n${field.value}`; + embed.description += `\n\n**${field.name}**\n${field.value}`; } } return { @@ -128,17 +127,14 @@ export async function fromrvapi( : Temporal.Instant.fromEpochMilliseconds(decodeTime(message._id)), embeds: (message.embeds as Embed[] | undefined)?.map((i) => { return { - color: i.colour - ? parseInt(i.colour.replace('#', ''), 16) - : undefined, + color: i.colour ? parseInt(i.colour.replace('#', ''), 16) : undefined, ...i, } as embed; }), plugin: 'bolt-revolt', attachments: message.attachments?.map((i) => { return { - file: - `https://autumn.revolt.chat/attachments/${i._id}/${i.filename}`, + file: `https://autumn.revolt.chat/attachments/${i._id}/${i.filename}`, name: i.filename, size: i.size, }; diff --git a/packages/lightning-plugin-revolt/src/permissions.ts b/packages/lightning-plugin-revolt/src/permissions.ts index e5269ea..667e0e7 100644 --- a/packages/lightning-plugin-revolt/src/permissions.ts +++ b/packages/lightning-plugin-revolt/src/permissions.ts @@ -1,10 +1,5 @@ import type { Client } from '@jersey/rvapi'; -import type { - Channel, - Member, - Role, - Server, -} from '@jersey/revolt-api-types'; +import type { Channel, Member, Role, Server } from '@jersey/revolt-api-types'; export async function revolt_perms( client: Client, diff --git a/packages/lightning/src/bridges/cmd_internals.ts b/packages/lightning/src/bridges/cmd_internals.ts index 951e5e4..6cf5529 100644 --- a/packages/lightning/src/bridges/cmd_internals.ts +++ b/packages/lightning/src/bridges/cmd_internals.ts @@ -82,8 +82,8 @@ export async function leave( export async function reset(opts: command_arguments) { if (typeof opts.opts.name !== 'string') { - opts.opts.name = - (await get_channel_bridge(opts.lightning, opts.channel))?.id!; + opts.opts.name = (await get_channel_bridge(opts.lightning, opts.channel)) + ?.id!; } let [ok, text] = await leave(opts); From b5ffa7a9611c43fb4147a49d8e28fdf969e01824 Mon Sep 17 00:00:00 2001 From: Jersey Date: Thu, 3 Oct 2024 13:17:35 -0400 Subject: [PATCH 02/23] changes to temporal interface --- packages/lightning-plugin-telegram/src/messages.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lightning-plugin-telegram/src/messages.ts b/packages/lightning-plugin-telegram/src/messages.ts index da728f3..f3b09fd 100644 --- a/packages/lightning-plugin-telegram/src/messages.ts +++ b/packages/lightning-plugin-telegram/src/messages.ts @@ -115,7 +115,7 @@ async function get_base_msg( }, channel: msg.chat.id.toString(), id: msg.message_id.toString(), - timestamp: Temporal.Instant.fromEpochSeconds(msg.edit_date || msg.date), + timestamp: Temporal.Instant.fromEpochMilliseconds((msg.edit_date || msg.date) * 1000), plugin: 'bolt-telegram', reply: async (lmsg) => { for (const m of from_lightning(lmsg)) { From 686e387b78c01132b83dc01df22625b34b2c31d4 Mon Sep 17 00:00:00 2001 From: Jersey Date: Mon, 11 Nov 2024 19:14:18 -0500 Subject: [PATCH 03/23] steps towards postgres - redo event handling logic to be flatter - cleanup command types - move data handling for bridges to `bridge_data` - remove the `create_nonbridged_message` event - core no longer extends EventEmitter - make the types for log_error work - temporarily remove migrations code - temporarily remove CLI --- .gitignore | 1 + deno.jsonc | 3 + packages/lightning/deno.jsonc | 12 +- packages/lightning/dockerfile | 8 +- packages/lightning/logo.svg | 28 +++ packages/lightning/readme.md | 19 +- packages/lightning/src/bridge/cmd.ts | 191 ++++++++++++++++ packages/lightning/src/bridge/data.ts | 147 ++++++++++++ packages/lightning/src/bridge/msg.ts | 178 +++++++++++++++ .../lightning/src/bridges/cmd_internals.ts | 132 ----------- .../lightning/src/bridges/db_internals.ts | 44 ---- .../lightning/src/bridges/handle_message.ts | 212 ----------------- .../lightning/src/bridges/setup_bridges.ts | 55 ----- packages/lightning/src/cli/migrations.ts | 93 -------- packages/lightning/src/cli/mod.ts | 64 ------ packages/lightning/src/cmds.ts | 34 --- .../src/{commands.ts => commands/mod.ts} | 89 +++----- packages/lightning/src/commands/run.ts | 46 ++++ packages/lightning/src/errors.ts | 81 ++++--- packages/lightning/src/lightning.ts | 103 ++++++--- packages/lightning/src/messages.ts | 181 ++++++++++++++- packages/lightning/src/migrations.ts | 77 ------- packages/lightning/src/mod.ts | 13 -- packages/lightning/src/plugins.ts | 6 +- packages/lightning/src/types.ts | 214 ------------------ 25 files changed, 931 insertions(+), 1100 deletions(-) create mode 100644 packages/lightning/logo.svg create mode 100644 packages/lightning/src/bridge/cmd.ts create mode 100644 packages/lightning/src/bridge/data.ts create mode 100644 packages/lightning/src/bridge/msg.ts delete mode 100644 packages/lightning/src/bridges/cmd_internals.ts delete mode 100644 packages/lightning/src/bridges/db_internals.ts delete mode 100644 packages/lightning/src/bridges/handle_message.ts delete mode 100644 packages/lightning/src/bridges/setup_bridges.ts delete mode 100644 packages/lightning/src/cli/migrations.ts delete mode 100644 packages/lightning/src/cli/mod.ts delete mode 100644 packages/lightning/src/cmds.ts rename packages/lightning/src/{commands.ts => commands/mod.ts} (50%) create mode 100644 packages/lightning/src/commands/run.ts delete mode 100644 packages/lightning/src/migrations.ts delete mode 100644 packages/lightning/src/mod.ts delete mode 100644 packages/lightning/src/types.ts diff --git a/.gitignore b/.gitignore index 20944bb..690e926 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /.env /config /config.ts +packages/lightning-old \ No newline at end of file diff --git a/deno.jsonc b/deno.jsonc index 504a322..b375ef0 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -21,6 +21,9 @@ }, "workspace": [ "./packages/lightning", + // TODO(jersey): remove these two + "./packages/lightning-old", + "./packages/postgres", "./packages/lightning-plugin-telegram", "./packages/lightning-plugin-revolt", "./packages/lightning-plugin-guilded", diff --git a/packages/lightning/deno.jsonc b/packages/lightning/deno.jsonc index 6466970..cbdcc1d 100644 --- a/packages/lightning/deno.jsonc +++ b/packages/lightning/deno.jsonc @@ -1,19 +1,15 @@ { "name": "@jersey/lightning", - "version": "0.7.4", + "version": "0.8.0-alpha.0", "exports": { ".": "./src/mod.ts", + // TODO(jersey): add the cli back in along with migrations, except make migrations not suck as much? "./cli": "./src/cli/mod.ts" }, - "publish": { - "exclude": ["./src/tests/*"] - }, - "test": { - "include": ["./src/tests/*"] - }, "imports": { "@denosaurs/event": "jsr:@denosaurs/event@^2.0.2", - "@iuioiua/r2d2": "jsr:@iuioiua/r2d2@^2.1.1", + "@db/postgres": "jsr:@db/postgres@^0.19.4", + "@std/ulid": "jsr:@std/ulid@^1.0.0", "@std/cli/parse-args": "jsr:@std/cli@^1.0.3/parse-args" } } diff --git a/packages/lightning/dockerfile b/packages/lightning/dockerfile index ff0f899..b1fec7b 100644 --- a/packages/lightning/dockerfile +++ b/packages/lightning/dockerfile @@ -1,11 +1,11 @@ -ARG DENO_VERSION=1.45.5 - -FROM docker.io/denoland/deno:${DENO_VERSION} +FROM docker.io/denoland/deno:2.0.3 # add lightning to the image -RUN deno install -A --unstable-temporal jsr:@jersey/lightning@0.7.4/cli +RUN deno install -g -A --unstable-temporal jsr:@jersey/lightning@0.8.0-alpha.0/cli RUN mkdir -p /app/data +WORKDIR /app/data # set lightning as the entrypoint and use the run command by default ENTRYPOINT [ "lightning" ] +# TODO(jersey): do i need to do this? CMD [ "run", "--config", "file:///app/data/config.ts"] diff --git a/packages/lightning/logo.svg b/packages/lightning/logo.svg new file mode 100644 index 0000000..c9db9d1 --- /dev/null +++ b/packages/lightning/logo.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/lightning/readme.md b/packages/lightning/readme.md index 60e5b73..cc12d72 100644 --- a/packages/lightning/readme.md +++ b/packages/lightning/readme.md @@ -1,3 +1,5 @@ +![lightning](logo.svg) + # @jersey/lightning lightning is a typescript-based chatbot that supports bridging multiple chat @@ -5,19 +7,4 @@ apps via plugins ## [docs](https://williamhorning.eu.org/bolt) -## example config - -```ts -import type { config } from 'jsr:@jersey/lightning@0.7.4'; -import { discord_plugin } from 'jsr:@jersey/lightning-plugin-discord@0.7.4'; - -export default { - redis_host: 'localhost', - redis_port: 6379, - plugins: [ - discord_plugin.new({ - // ... - }), - ], -} as config; -``` + diff --git a/packages/lightning/src/bridge/cmd.ts b/packages/lightning/src/bridge/cmd.ts new file mode 100644 index 0000000..5ce6ed3 --- /dev/null +++ b/packages/lightning/src/bridge/cmd.ts @@ -0,0 +1,191 @@ +import type { command } from '../commands/mod.ts'; +import { log_error } from '../errors.ts'; + +export const bridge_command = { + name: 'bridge', + description: 'bridge commands', + execute: () => 'take a look at the docs for help with bridges', + options: { + subcommands: [ + // TODO(jersey): eventually reimplement reset command? + { + name: 'join', + description: 'join a bridge', + // TODO(jersey): update this to support multiple options + // TODO(jersey): make command options more flexible + options: { argument_name: 'name', argument_required: true }, + execute: async ({ lightning, channel, opts, plugin }) => { + const current_bridge = await lightning.data + .get_bridge_by_channel( + channel, + ); + + // live laugh love validation + + if (current_bridge) { + return `You are already in a bridge called ${current_bridge.name}`; + } + if (opts.id && opts.name) { + return `You can only provide an id or a name, not both`; + } + if (!opts.id && !opts.name) { + return `You must provide either an id or a name`; + } + + const bridge_channel = { + id: channel, + data: undefined as unknown, + disabled: false, + plugin, + }; + + try { + bridge_channel.data = lightning.plugins.get(plugin) + ?.create_bridge(channel); + } catch (e) { + return (await log_error( + new Error('error creating bridge', { cause: e }), + { + channel, + plugin_name: plugin, + }, + )).message.content as string; + } + + if (opts.id) { + const bridge = await lightning.data.get_bridge_by_id( + opts.id, + ); + + if (!bridge) return `No bridge found with that id`; + + bridge.channels.push(bridge_channel); + + try { + await lightning.data.update_bridge(bridge); + return `Bridge joined successfully`; + } catch (e) { + return (await log_error( + new Error('error updating bridge', { cause: e }), + { + bridge, + }, + )).message.content as string; + } + } else { + try { + await lightning.data.new_bridge({ + name: opts.name, + channels: [bridge_channel], + settings: { + allow_editing: true, + allow_everyone: false, + use_rawname: false, + }, + }); + return `Bridge joined successfully`; + } catch (e) { + return (await log_error( + new Error('error inserting bridge', { cause: e }), + { + bridge: { + name: opts.name, + channels: [bridge_channel], + settings: { + allow_editing: true, + allow_everyone: false, + use_rawname: false, + }, + }, + }, + )).message.content as string; + } + } + }, + }, + { + name: 'leave', + description: 'leave a bridge', + execute: async ({ lightning, channel }) => { + const bridge = await lightning.data.get_bridge_by_channel( + channel, + ); + + if (!bridge) return `You are not in a bridge`; + + bridge.channels = bridge.channels.filter(( + ch, + ) => ch.id !== channel); + + try { + await lightning.data.update_bridge( + bridge, + ); + return `Bridge left successfully`; + } catch (e) { + return await log_error( + new Error('error updating bridge', { cause: e }), + { + bridge, + }, + ); + } + }, + }, + { + name: 'toggle', + description: 'toggle a setting on a bridge', + options: { argument_name: 'setting', argument_required: true }, + execute: async ({ opts, lightning, channel }) => { + const bridge = await lightning.data.get_bridge_by_channel( + channel, + ); + + if (!bridge) return `You are not in a bridge`; + + if ( + !['allow_editing', 'allow_everyone', 'use_rawname'] + .includes(opts.setting) + ) { + return `that setting does not exist`; + } + + const key = opts.setting as keyof typeof bridge.settings; + + bridge.settings[key] = !bridge + .settings[key]; + + try { + await lightning.data.update_bridge( + bridge, + ); + return `Setting toggled successfully`; + } catch (e) { + return await log_error( + new Error('error updating bridge', { cause: e }), + { + bridge, + }, + ); + } + }, + }, + { + name: 'status', + description: 'see what bridges you are in', + execute: async ({ lightning, channel }) => { + const existing_bridge = await lightning.data + .get_bridge_by_channel( + channel, + ); + + if (!existing_bridge) return `You are not in a bridge`; + + return `You are in a bridge called ${existing_bridge.name} that's connected to ${ + existing_bridge.channels.length - 1 + } other channels`; + }, + }, + ], + }, +} as command; diff --git a/packages/lightning/src/bridge/data.ts b/packages/lightning/src/bridge/data.ts new file mode 100644 index 0000000..f9dd4ba --- /dev/null +++ b/packages/lightning/src/bridge/data.ts @@ -0,0 +1,147 @@ +import { Client, type ClientOptions } from '@db/postgres'; +import { ulid } from '@std/ulid'; + +export interface bridge { + id: string; /* ulid */ + name: string; /* name of the bridge */ + channels: bridge_channel[]; /* channels bridged */ + settings: bridge_settings; /* settings for the bridge */ +} + +export interface bridge_channel { + id: string; /* from the platform */ + data: unknown; /* data needed to bridge this channel */ + disabled: boolean; /* whether the channel is disabled */ + plugin: string; /* the plugin used to bridge this channel */ +} + +export interface bridge_settings { + allow_editing: boolean; /* allow editing/deletion */ + allow_everyone: boolean; /* @everyone/@here/@room */ + use_rawname: boolean; /* rawname = username */ +} + +export interface bridge_message extends bridge { + original_id: string; /* original message id */ + messages: bridged_message[]; /* bridged messages */ +} + +export interface bridged_message { + id: string[]; /* message id */ + channel: string; /* channel id */ + plugin: string; /* plugin id */ +} + +export class bridge_data { + private pg: Client; + + static async create(pg_options: ClientOptions) { + const pg = new Client(pg_options); + await pg.connect(); + + await this.create_table(pg); + + return new bridge_data(pg); + } + + private static async create_table(pg: Client) { + const exists = (await pg.queryArray`SELECT relname FROM pg_class + WHERE relname = 'bridges'`).rows.length > 0; + + if (exists) return; + + await pg.queryArray`CREATE TABLE bridges ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + channels JSONB NOT NULL, + settings JSONB NOT NULL + )`; + await pg.queryArray`CREATE TABLE bridge_messages ( + original_id TEXT PRIMARY KEY, + id TEXT NOT NULL, + name TEXT NOT NULL, + channels JSONB NOT NULL REFERENCES bridges(channels), + messages JSONB NOT NULL, + settings JSONB NOT NULL REFERENCES bridges(settings), + CONSTRAINT fk_id FOREIGN KEY(id) REFERENCES bridges(id) + )`; + } + + private constructor(pg_client: Client) { + this.pg = pg_client; + } + + async new_bridge(bridge: Omit): Promise { + const id = ulid(); + + await this.pg.queryArray`INSERT INTO bridges + (id, name, channels, settings) VALUES + (${id}, ${bridge.name}, ${bridge.channels}, ${bridge.settings})`; + + return { id, ...bridge }; + } + + async update_bridge(bridge: bridge): Promise { + await this.pg.queryArray`UPDATE bridges SET + name = ${bridge.name}, + channels = ${bridge.channels}, + settings = ${bridge.settings} + WHERE id = ${bridge.id}`; + + return bridge; + } + + async get_bridge_by_id(id: string): Promise { + const resp = await this.pg.queryObject` + SELECT * FROM bridges WHERE id = ${id}`; + + return resp.rows[0]; + } + + async get_bridge_by_channel(channel: string): Promise { + const resp = await this.pg.queryObject` + SELECT * FROM bridges WHERE JSON_QUERY(channels, '$[*].id') = ${channel}`; + + return resp.rows[0]; + } + + async new_bridge_message(message: bridge_message): Promise { + await this.pg.queryArray`INSERT INTO bridge_messages + (original_id, id, name, channels, messages, settings) VALUES + (${message.original_id}, ${message.id}, ${message.name}, ${message.channels}, ${message.messages}, ${message.settings})`; + + return message; + } + + async update_bridge_message( + message: bridge_message, + ): Promise { + await this.pg.queryArray`UPDATE bridge_messages SET + id = ${message.id}, + channels = ${message.channels}, + messages = ${message.messages}, + settings = ${message.settings} + WHERE original_id = ${message.original_id}`; + + return message; + } + + async delete_bridge_message(id: string): Promise { + await this.pg + .queryArray`DELETE FROM bridge_messages WHERE original_id = ${id}`; + } + + async get_bridge_message(id: string): Promise { + const resp = await this.pg.queryObject` + SELECT * FROM bridge_messages WHERE original_id = ${id}`; + + return resp.rows[0]; + } + + async is_bridged_message(id: string): Promise { + const resp = await this.pg.queryObject` + SELECT * FROM bridge_messages WHERE JSON_QUERY(messages, '$[*].id') = ${id}`; + + return resp.rows.length > 0; + } +} diff --git a/packages/lightning/src/bridge/msg.ts b/packages/lightning/src/bridge/msg.ts new file mode 100644 index 0000000..b62a373 --- /dev/null +++ b/packages/lightning/src/bridge/msg.ts @@ -0,0 +1,178 @@ +import type { lightning } from '../lightning.ts'; +import { log_error } from '../errors.ts'; +import type { + deleted_message, + message, + unprocessed_message, +} from '../messages.ts'; +import type { + bridge, + bridge_channel, + bridge_message, + bridged_message, +} from './data.ts'; + +export async function handle_message( + core: lightning, + msg: message | deleted_message, + type: 'create' | 'edit' | 'delete', +): Promise { + const br = type === 'create' + ? await core.data.get_bridge_by_channel(msg.channel) + : await core.data.get_bridge_message(msg.id); + + if (!br) return; + + if (type !== 'create' && br.settings.allow_editing !== true) return; + + if ( + br.channels.find((i) => + i.id === msg.channel && i.plugin === msg.plugin && i.disabled + ) + ) return; + + const channels = br.channels.filter( + (i) => i.id !== msg.channel || i.plugin !== msg.plugin, + ); + + if (channels.length < 1) return; + + const messages = [] as bridged_message[]; + + for (const ch of channels) { + if (!ch.data || ch.disabled) continue; + + const bridged_id = (br as Partial).messages?.find((i) => + i.channel === ch.id && i.plugin === ch.plugin + ); + + if ((type !== 'create' && !bridged_id)) { + continue; + } + + const plugin = core.plugins.get(ch.plugin); + + if (!plugin) { + await disable_channel( + ch, + br, + core, + (await log_error( + new Error(`plugin ${ch.plugin} doesn't exist`), + { channel: ch, bridged_id }, + )).cause, + ); + + continue; + } + + const reply_id = await get_reply_id(core, msg as message, ch); + + let res; + + try { + res = await plugin.process_message({ + action: type as 'edit', + channel: ch, + message: msg as message, + edit_id: bridged_id?.id as string[], + reply_id, + }); + + if (res.error) throw res.error; + } catch (e) { + if (type === 'delete') continue; + + if ((res as unprocessed_message).disable) { + await disable_channel(ch, br, core, e); + + continue; + } + + const err = await log_error(e, { + channel: ch, + bridged_id, + message: msg, + }); + + try { + res = await plugin.process_message({ + action: type as 'edit', + channel: ch, + message: err.message as message, + edit_id: bridged_id?.id as string[], + reply_id, + }); + + if (res.error) throw res.error; + } catch (e) { + await log_error( + new Error(`failed to log error`, { cause: e }), + { channel: ch, bridged_id, original_id: err.id }, + ); + + continue; + } + } + + for (const id of res.id) { + sessionStorage.setItem(`${ch.plugin}-${id}`, '1'); + } + + messages.push({ + id: res.id, + channel: ch.id, + plugin: ch.plugin, + }); + } + + const method = type === 'create' ? 'new' : 'update'; + + await core.data[`${method}_bridge_message`]({ + ...br, + original_id: msg.id, + messages, + }); +} + +async function disable_channel( + channel: bridge_channel, + bridge: bridge, + core: lightning, + error: unknown, +): Promise { + await log_error(error, { channel, bridge }); + + await core.data.update_bridge({ + ...bridge, + channels: bridge.channels.map((i) => + i.id === channel.id && i.plugin === channel.plugin + ? { ...i, disabled: true, data: error } + : i + ), + }); +} + +async function get_reply_id( + core: lightning, + msg: message, + channel: bridge_channel, +): Promise { + if (msg.reply_id) { + try { + const bridged = await core.data.get_bridge_message(msg.reply_id); + + if (!bridged) return; + + const br_ch = bridged.channels.find((i) => + i.id === channel.id && i.plugin === channel.plugin + ); + + if (!br_ch) return; + + return br_ch.id; + } catch { + return; + } + } +} diff --git a/packages/lightning/src/bridges/cmd_internals.ts b/packages/lightning/src/bridges/cmd_internals.ts deleted file mode 100644 index 6cf5529..0000000 --- a/packages/lightning/src/bridges/cmd_internals.ts +++ /dev/null @@ -1,132 +0,0 @@ -import type { command_arguments } from '../commands.ts'; -import { log_error } from '../errors.ts'; -import { - del_key, - get_bridge, - get_channel_bridge, - set_bridge, -} from './db_internals.ts'; - -export async function join( - opts: command_arguments, -): Promise<[boolean, string]> { - if ( - await get_channel_bridge( - opts.lightning, - `lightning-bchannel-${opts.channel}`, - ) - ) { - return [ - false, - "To do this, you can't be in a bridge. Try leaving your bridge first.", - ]; - } - - const id = opts.opts.name?.split(' ')[0]; - - if (!id) { - return [ - false, - 'You need to provide a name your bridge. Try `join --name=` instead.', - ]; - } - - const plugin = opts.lightning.plugins.get(opts.plugin); - - const bridge = (await get_bridge(opts.lightning, id)) || { - allow_editing: false, - channels: [], - id, - use_rawname: false, - }; - - try { - const data = await plugin!.create_bridge(opts.channel); - - bridge.channels.push({ - id: opts.channel, - disabled: false, - plugin: opts.plugin, - data, - }); - - await set_bridge(opts.lightning, bridge); - - return [true, 'Joined a bridge!']; - } catch (e) { - const err = await log_error(e, { opts }); - return [false, err.message.content!]; - } -} - -export async function leave( - opts: command_arguments, -): Promise<[boolean, string]> { - const bridge = await get_channel_bridge(opts.lightning, opts.channel); - - if (!bridge) { - return [true, "You're not in a bridge, so try joining a bridge first."]; - } - - await set_bridge(opts.lightning, { - ...bridge, - channels: bridge.channels.filter( - (i) => i.id !== opts.channel && i.plugin !== opts.plugin, - ), - }); - - await del_key(opts.lightning, `lightning-bchannel-${opts.channel}`); - - return [true, 'Left a bridge!']; -} - -export async function reset(opts: command_arguments) { - if (typeof opts.opts.name !== 'string') { - opts.opts.name = (await get_channel_bridge(opts.lightning, opts.channel)) - ?.id!; - } - - let [ok, text] = await leave(opts); - if (!ok) return text; - [ok, text] = await join(opts); - if (!ok) return text; - return 'Reset this bridge!'; -} - -export async function toggle(opts: command_arguments) { - const bridge = await get_channel_bridge(opts.lightning, opts.channel); - - if (!bridge) { - return "You're not in a bridge right now. Try joining one first."; - } - - if (!opts.opts.setting) { - return 'You need to specify a setting to toggle. Try `toggle --setting=` instead.'; - } - - if (!['allow_editing', 'use_rawname'].includes(opts.opts.setting)) { - return "That setting doesn't exist! Try `allow_editing` or `use_rawname` instead."; - } - - const setting = opts.opts.setting as 'allow_editing' | 'use_rawname'; - - bridge[setting] = !bridge[setting]; - - await set_bridge(opts.lightning, bridge); - - return 'Toggled that setting!'; -} - -export async function status(args: command_arguments) { - const current = await get_channel_bridge(args.lightning, args.channel); - - if (!current) { - return "You're not in any bridges right now."; - } - - return `This channel is connected to \`${current.id}\`, a bridge with ${ - current.channels.length - 1 - } other channels connected to it, with editing ${ - current.allow_editing ? 'enabled' : 'disabled' - } and nicknames ${current.use_rawname ? 'disabled' : 'enabled'}`; -} diff --git a/packages/lightning/src/bridges/db_internals.ts b/packages/lightning/src/bridges/db_internals.ts deleted file mode 100644 index f08befc..0000000 --- a/packages/lightning/src/bridges/db_internals.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { lightning } from '../lightning.ts'; -import type { bridge_document } from '../types.ts'; - -export async function get_json( - l: lightning, - key: string, -): Promise { - const reply = await l.redis.sendCommand(['GET', key]); - if (!reply || reply === 'OK') return; - return JSON.parse(reply as string) as T; -} - -export async function del_key(l: lightning, key: string) { - await l.redis.sendCommand(['DEL', key]); -} - -export async function set_json(l: lightning, key: string, value: unknown) { - await l.redis.sendCommand(['SET', key, JSON.stringify(value)]); -} - -export async function get_bridge(l: lightning, id: string) { - return await get_json(l, `lightning-bridge-${id}`); -} - -export async function get_channel_bridge(l: lightning, id: string) { - const ch = await l.redis.sendCommand(['GET', `lightning-bchannel-${id}`]); - return await get_bridge(l, ch as string); -} - -export async function get_message_bridge(l: lightning, id: string) { - return await get_json(l, `lightning-bridged-${id}`); -} - -export async function set_bridge(l: lightning, bridge: bridge_document) { - set_json(l, `lightning-bridge-${bridge.id}`, bridge); - - for (const channel of bridge.channels) { - await l.redis.sendCommand([ - 'SET', - `lightning-bchannel-${channel.id}`, - bridge.id, - ]); - } -} diff --git a/packages/lightning/src/bridges/handle_message.ts b/packages/lightning/src/bridges/handle_message.ts deleted file mode 100644 index 874b8b1..0000000 --- a/packages/lightning/src/bridges/handle_message.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { log_error } from '../errors.ts'; -import type { lightning } from '../lightning.ts'; -import type { - bridge_channel, - bridge_document, - bridge_message, - deleted_message, - message, - process_result, -} from '../types.ts'; -import { - get_channel_bridge, - get_message_bridge, - set_json, -} from './db_internals.ts'; - -export async function handle_message( - lightning: lightning, - msg: message | deleted_message, - type: 'create_message' | 'edit_message' | 'delete_message', -): Promise { - await new Promise((res) => setTimeout(res, 150)); - - if (type !== 'delete_message') { - if (sessionStorage.getItem(`${msg.plugin}-${msg.id}`)) { - return sessionStorage.removeItem(`${msg.plugin}-${msg.id}`); - } else if (type === 'create_message') { - lightning.emit(`create_nonbridged_message`, msg as message); - } - } - - const bridge = type === 'create_message' - ? await get_channel_bridge(lightning, msg.channel) - : await get_message_bridge(lightning, msg.id); - - if (!bridge) return; - - if ( - bridge.channels.find((i) => - i.id === msg.channel && i.plugin === msg.plugin && i.disabled - ) - ) return; - - if (type !== 'create_message' && bridge.allow_editing !== true) return; - - const channels = bridge.channels.filter( - (i) => i.id !== msg.channel || i.plugin !== msg.plugin, - ); - - if (channels.length < 1) return; - - const messages = [] as bridge_message[]; - - for (const channel of channels) { - if (!channel.data || channel.disabled) continue; - - const bridged_id = bridge.messages?.find((i) => - i.channel === channel.id && i.plugin === channel.plugin - ); - - if ((type !== 'create_message' && !bridged_id)) { - continue; - } - - const plugin = lightning.plugins.get(channel.plugin); - - if (!plugin) { - const err = await log_error( - new Error(`plugin ${channel.plugin} doesn't exist`), - { channel, bridged_id }, - ); - - await disable_channel(channel, bridge, lightning, err.e); - - continue; - } - - let dat: process_result; - - const reply_id = await get_reply_id(lightning, msg as message, channel); - - try { - dat = await plugin.process_message({ - // maybe find a better way to deal w/types - action: type.replace('_message', '') as 'edit', - channel, - message: msg as message, - edit_id: bridged_id?.id as string[], - reply_id, - }); - } catch (e) { - dat = { - channel, - disable: false, - error: e, - }; - - if (type === 'delete_message') continue; - } - - if (dat.error) { - if (type === 'delete_message') continue; - - if (dat.disable) { - await disable_channel(channel, bridge, lightning, dat.error); - - continue; - } - - const logged = await log_error(dat.error, { - channel, - dat, - bridged_id, - }); - - try { - dat = await plugin.process_message({ - action: type.replace('_message', '') as 'edit', - channel, - message: logged.message, - edit_id: bridged_id?.id as string[], - reply_id, - }); - - if (dat.error) throw dat.error; - } catch (e) { - await log_error( - new Error('failed to send error', { cause: e }), - { channel, dat, bridged_id, logged: logged.uuid }, - ); - - continue; - } - } - - for (const i of dat.id) { - sessionStorage.setItem(`${channel.plugin}-${i}`, '1'); - } - - messages.push({ - id: dat.id, - channel: channel.id, - plugin: channel.plugin, - }); - } - - for (const i of messages) { - await set_json(lightning, `lightning-bridged-${i.id}`, { - ...bridge, - messages, - }); - } - - await set_json(lightning, `lightning-bridged-${msg.id}`, { - ...bridge, - messages, - }); -} - -async function disable_channel( - channel: bridge_channel, - bridge: bridge_document, - lightning: lightning, - e: Error, -) { - channel.disabled = true; - - bridge.channels = bridge.channels.map((i) => { - if (i.id === channel.id && i.plugin === channel.plugin) { - i.disabled = true; - } - return i; - }); - - await set_json( - lightning, - `lightning-bridge-${bridge.id}`, - bridge, - ); - - const err = new Error( - `disabled channel ${channel.id} on ${channel.plugin}`, - { cause: e }, - ); - - await log_error(err, { channel }); -} - -async function get_reply_id( - lightning: lightning, - msg: message, - channel: bridge_channel, -) { - if (msg.reply_id) { - try { - const bridged = await get_message_bridge(lightning, msg.reply_id); - - if (!bridged) return; - - const bridge_channel = bridged.messages?.find( - (i) => i.channel === channel.id && i.plugin === channel.plugin, - ); - - if (!bridge_channel) return; - - return bridge_channel.id[0]; - } catch { - return; - } - } - return; -} diff --git a/packages/lightning/src/bridges/setup_bridges.ts b/packages/lightning/src/bridges/setup_bridges.ts deleted file mode 100644 index 82d55ae..0000000 --- a/packages/lightning/src/bridges/setup_bridges.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { lightning } from '../lightning.ts'; -import { join, leave, reset, status, toggle } from './cmd_internals.ts'; -import { handle_message } from './handle_message.ts'; - -export function setup_bridges(l: lightning) { - l.on('create_message', (msg) => { - handle_message(l, msg, 'create_message'); - }); - - l.on('edit_message', (msg) => { - handle_message(l, msg, 'edit_message'); - }); - - l.on('delete_message', (msg) => { - handle_message(l, msg, 'delete_message'); - }); - - l.commands.set('bridge', { - name: 'bridge', - description: 'bridge this channel to somewhere else', - execute: () => `Try running the help command for help with bridges`, - options: { - subcommands: [ - { - name: 'join', - description: 'join a bridge', - execute: async (opts) => (await join(opts))[1], - options: { argument_name: 'name', argument_required: true }, - }, - { - name: 'leave', - description: 'leave a bridge', - execute: async (opts) => (await leave(opts))[1], - }, - { - name: 'reset', - description: 'reset a bridge', - execute: async (opts) => await reset(opts), - options: { argument_name: 'name' }, - }, - { - name: 'toggle', - description: 'toggle a setting on a bridge', - execute: async (opts) => await toggle(opts), - options: { argument_name: 'setting', argument_required: true }, - }, - { - name: 'status', - description: 'see what bridges you are in', - execute: async (opts) => await status(opts), - }, - ], - }, - }); -} diff --git a/packages/lightning/src/cli/migrations.ts b/packages/lightning/src/cli/migrations.ts deleted file mode 100644 index 8c7aec6..0000000 --- a/packages/lightning/src/cli/migrations.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { RedisClient } from '@iuioiua/r2d2'; -import { get_migrations, versions } from '../migrations.ts'; - -const redis_hostname = prompt( - `what hostname is used by your redis instance?`, - 'localhost', -); -const redis_port = prompt(`what port is used by your redis instance?`, '6379'); - -if (!redis_hostname || !redis_port) Deno.exit(); - -const redis = new RedisClient( - await Deno.connect({ - hostname: redis_hostname, - port: Number(redis_port), - }), -); - -console.log('connected to redis!'); - -console.log(`available versions: ${Object.values(versions).join(', ')}`); - -const from_version = prompt('what version are you migrating from?') as - | versions - | undefined; -const to_version = prompt('what version are you migrating to?') as - | versions - | undefined; - -if (!from_version || !to_version) Deno.exit(); - -const migrations = get_migrations(from_version, to_version); - -if (migrations.length < 1) Deno.exit(); - -console.log(`downloading data from redis...`); - -const keys = (await redis.sendCommand(['KEYS', 'lightning-*'])) as string[]; -const redis_data = [] as [string, unknown][]; - -// sorry database :( - -for (const key of keys) { - const type = await redis.sendCommand(['TYPE', key]); - const val = await redis.sendCommand([ - type === 'string' ? 'GET' : 'JSON.GET', - key, - ]); - - try { - redis_data.push([ - key, - JSON.parse(val as string), - ]); - } catch { - redis_data.push([ - key, - val as string, - ]); - continue; - } -} - -console.log(`downloaded data from redis!`); -console.log(`applying migrations...`); - -const data = migrations.reduce((r, m) => m.translate(r), redis_data); - -const final_data = data.map(([key, value]) => { - return [key, typeof value !== 'string' ? JSON.stringify(value) : value]; -}); - -console.log(`migrated your data!`); - -const file = await Deno.makeTempFile(); - -await Deno.writeTextFile(file, JSON.stringify(final_data, null, 2)); - -const write = confirm( - `do you want the data in ${file} to be written to the database?`, -); - -if (!write) Deno.exit(); - -await redis.sendCommand(['DEL', ...keys]); - -const reply = await redis.sendCommand(['MSET', ...final_data.flat()]); - -if (reply === 'OK') { - console.log('data written to database'); -} else { - console.log('error writing data to database', reply); -} diff --git a/packages/lightning/src/cli/mod.ts b/packages/lightning/src/cli/mod.ts deleted file mode 100644 index ea35474..0000000 --- a/packages/lightning/src/cli/mod.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { parseArgs } from '@std/cli/parse-args'; -import { log_error } from '../errors.ts'; -import { type config, lightning } from '../lightning.ts'; - -const _ = parseArgs(Deno.args, { - string: ['config'], -}); - -const cmd = _._[0]; - -if (cmd === 'version') { - console.log('0.7.4'); -} else if (cmd === 'run') { - const cfg = (await import(_.config || `${Deno.cwd()}/config.ts`)) - ?.default as config; - - Deno.env.set('LIGHTNING_ERROR_HOOK', cfg.errorURL || ''); - - addEventListener('unhandledrejection', async (e) => { - if (e.reason instanceof Error) { - await log_error(e.reason); - } else { - await log_error(new Error('global rejection'), { - extra: e.reason, - }); - } - - Deno.exit(1); - }); - - addEventListener('error', async (e) => { - if (e.error instanceof Error) { - await log_error(e.error); - } else { - await log_error(new Error('global error'), { extra: e.error }); - } - - Deno.exit(1); - }); - - try { - new lightning( - cfg, - await Deno.connect({ - hostname: cfg.redis_host || 'localhost', - port: cfg.redis_port || 6379, - }), - ); - } catch (e) { - await log_error(e); - Deno.exit(1); - } -} else if (cmd === 'migrations') { - import('./migrations.ts'); -} else { - console.log('lightning v0.7.4 - extensible chatbot connecting communities'); - console.log(' Usage: lightning [subcommand] '); - console.log(' Subcommands:'); - console.log(' run: run an of lightning using the settings in config.ts'); - console.log(' migrations: run migration script'); - console.log(' version: shows version'); - console.log(' Options:'); - console.log(' --config : path to config file'); -} diff --git a/packages/lightning/src/cmds.ts b/packages/lightning/src/cmds.ts deleted file mode 100644 index 96c7f33..0000000 --- a/packages/lightning/src/cmds.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { command } from './commands.ts'; - -export const default_cmds = [ - [ - 'help', - { - name: 'help', - description: 'get help', - execute: () => - 'check out [the docs](https://williamhorning.eu.org/bolt/) for help.', - }, - ], - [ - 'version', - { - name: 'version', - description: 'get the bots version', - execute: () => 'hello from v0.7.4!', - }, - ], - [ - 'ping', - { - name: 'ping', - description: 'pong', - execute: ({ timestamp }) => - `Pong! 🏓 ${ - Temporal.Now.instant() - .since(timestamp) - .total('milliseconds') - }ms`, - }, - ], -] as [string, command][]; diff --git a/packages/lightning/src/commands.ts b/packages/lightning/src/commands/mod.ts similarity index 50% rename from packages/lightning/src/commands.ts rename to packages/lightning/src/commands/mod.ts index 52fe30f..0eadcff 100644 --- a/packages/lightning/src/commands.ts +++ b/packages/lightning/src/commands/mod.ts @@ -1,32 +1,5 @@ -import { parseArgs } from '@std/cli/parse-args'; -import { log_error } from './errors.ts'; -import type { lightning } from './lightning.ts'; -import type { message } from './types.ts'; -import { create_message } from './messages.ts'; - -/** setup commands on an instance of lightning */ -export function setup_commands(l: lightning) { - const prefix = l.config.cmd_prefix || 'l!'; - - l.on('create_nonbridged_message', (m) => { - if (!m.content?.startsWith(prefix)) return; - - const { - _: [cmd, subcmd], - ...opts - } = parseArgs(m.content.replace(prefix, '').split(' ')); - - run_command({ - lightning: l, - cmd: cmd as string, - subcmd: subcmd as string, - opts, - ...m, - }); - }); - - l.on('run_command', (i) => run_command({ lightning: l, ...i })); -} +import type { lightning } from '../lightning.ts'; +import type { message } from '../messages.ts'; /** arguments passed to a command */ export interface command_arguments { @@ -38,6 +11,8 @@ export interface command_arguments { channel: string; /** the plugin its being run on */ plugin: string; + /** the id of the associated event */ + id: string; /** timestamp given */ timestamp: Temporal.Instant; /** options passed by the user */ @@ -49,6 +24,7 @@ export interface command_arguments { } /** options when parsing a command */ +// TODO(jersey): make the options more flexible export interface command_options { /** this will be the key passed to options.opts in the execute function */ argument_name?: string; @@ -70,26 +46,35 @@ export interface command { execute: (options: command_arguments) => Promise | string; } -async function run_command(args: command_arguments) { - let reply; - - try { - const cmd = args.lightning.commands.get(args.cmd) || - args.lightning.commands.get('help')!; - - const exec = cmd.options?.subcommands?.find((i) => - i.name === args.subcmd - )?.execute || - cmd.execute; - - reply = create_message(await exec(args)); - } catch (e) { - reply = (await log_error(e, { ...args, reply: undefined })).message; - } - - try { - await args.reply(reply, false); - } catch (e) { - await log_error(e, { ...args, reply: undefined }); - } -} +export const default_commands = [ + [ + 'help', + { + name: 'help', + description: 'get help', + execute: () => + 'check out [the docs](https://williamhorning.eu.org/bolt/) for help.', + }, + ], + [ + 'version', + { + name: 'version', + description: 'get the bots version', + execute: () => 'hello from v0.7.4!', + }, + ], + [ + 'ping', + { + name: 'ping', + description: 'pong', + execute: ({ timestamp }) => + `Pong! 🏓 ${ + Temporal.Now.instant() + .since(timestamp) + .total('milliseconds') + }ms`, + }, + ], +] as [string, command][]; diff --git a/packages/lightning/src/commands/run.ts b/packages/lightning/src/commands/run.ts new file mode 100644 index 0000000..69d855f --- /dev/null +++ b/packages/lightning/src/commands/run.ts @@ -0,0 +1,46 @@ +import type { command_arguments } from './mod.ts'; +import { create_message, type message } from '../messages.ts'; +import { log_error } from '../errors.ts'; +import type { lightning } from '../lightning.ts'; +import { parseArgs } from '@std/cli/parse-args'; + +export function handle_command_message(m: message, l: lightning) { + if (!m.content?.startsWith(l.config.cmd_prefix)) return; + + const { + _: [cmd, subcmd], + ...opts + } = parseArgs(m.content.replace(l.config.cmd_prefix, '').split(' ')); + + run_command({ + lightning: l, + cmd: cmd as string, + subcmd: subcmd as string, + opts, + ...m, + }); +} + +export async function run_command(args: command_arguments) { + let reply; + + try { + const cmd = args.lightning.commands.get(args.cmd) || + args.lightning.commands.get('help')!; + + const exec = cmd.options?.subcommands?.find((i) => + i.name === args.subcmd + )?.execute || + cmd.execute; + + reply = create_message(await exec(args)); + } catch (e) { + reply = (await log_error(e, { ...args, reply: undefined })).message; + } + + try { + await args.reply(reply, false); + } catch (e) { + await log_error(e, { ...args, reply: undefined }); + } +} diff --git a/packages/lightning/src/errors.ts b/packages/lightning/src/errors.ts index 6cb12d2..b4a44fa 100644 --- a/packages/lightning/src/errors.ts +++ b/packages/lightning/src/errors.ts @@ -1,14 +1,13 @@ -import { create_message } from './messages.ts'; -import type { message } from './types.ts'; +import { create_message, type message } from './messages.ts'; /** the error returned from log_error */ export interface err { + /** id of the error */ + id: string; /** the original error */ - e: Error; + cause: Error; /** extra information about the error */ extra: Record; - /** the uuid associated with the error */ - uuid: string; /** the message associated with the error */ message: message; } @@ -19,59 +18,55 @@ export interface err { * @param extra any extra data to log */ export async function log_error( - e: Error, + e: unknown, extra: Record = {}, ): Promise { - const uuid = crypto.randomUUID(); - const error_hook = Deno.env.get('LIGHTNING_ERROR_HOOK'); + const id = crypto.randomUUID(); + const webhook = Deno.env.get('LIGHTNING_ERROR_HOOK'); + const cause = e instanceof Error + ? e + : e instanceof Object + ? new Error(JSON.stringify(e)) + : new Error(String(e)); + const user_facing_text = + `Something went wrong! Take a look at [the docs](https://williamhorning.eu.org/lightning).\n\`\`\`\n${cause.message}\n${id}\n\`\`\``; - if ('lightning' in extra) delete extra.lightning; + for (const key in extra) { + if (key === 'lightning') { + delete extra[key]; + } + + if (typeof extra[key] === 'object' && extra[key] !== null) { + if ('lightning' in extra[key]) { + delete extra[key].lightning; + } + } + } + + // TODO(jersey): this is a really bad way of error handling-especially given it doesn't do a lot of stuff that would help debug errors-but it'll be replaced + + console.error(`%clightning error ${id}`, 'color: red'); + console.error(cause, extra); - if ( - 'opts' in extra && - 'lightning' in (extra.opts as Record) - ) delete (extra.opts as Record).lightning; + if (webhook && webhook.length > 0) { + let json_str = `\`\`\`json\n${JSON.stringify(extra, null, 2)}\n\`\`\``; - if (error_hook && error_hook.length > 0) { - const resp = await fetch(error_hook, { + if (json_str.length > 2000) json_str = '*see console*'; + + await fetch(webhook, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - content: `# ${e.message}\n*${uuid}*`, + content: `# ${cause.message}\n*${id}*`, embeds: [ { title: 'extra', - description: `\`\`\`json\n${ - JSON.stringify(extra, null, 2) - }\n\`\`\``, + description: json_str, }, ], }), }); - - if (!resp.ok) { - await fetch(error_hook, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - content: `# ${e.message}\n*${uuid}*`, - embeds: [ - { - title: 'extra', - description: '*see console*', - }, - ], - }), - }); - } } - console.error(`%clightning error ${uuid}`, 'color: red'); - console.error(e, extra); - - const message = create_message( - `Something went wrong! [Look here](https://williamhorning.eu.org/bolt) for help.\n\`\`\`\n${e.message}\n${uuid}\n\`\`\``, - ); - - return { e, uuid, extra, message }; + return { id, cause, extra, message: create_message(user_facing_text) }; } diff --git a/packages/lightning/src/lightning.ts b/packages/lightning/src/lightning.ts index d3fb132..e30e2cf 100644 --- a/packages/lightning/src/lightning.ts +++ b/packages/lightning/src/lightning.ts @@ -1,60 +1,95 @@ -import { EventEmitter } from '@denosaurs/event'; -import { RedisClient } from '@iuioiua/r2d2'; -import { setup_bridges } from './bridges/setup_bridges.ts'; -import { default_cmds } from './cmds.ts'; -import { type command, setup_commands } from './commands.ts'; -import type { create_plugin, plugin, plugin_events } from './plugins.ts'; +import type { ClientOptions } from '@db/postgres'; +import { + type command, + type command_arguments, + default_commands, +} from './commands/mod.ts'; +import type { create_plugin, plugin } from './plugins.ts'; +import { bridge_data } from './bridge/data.ts'; +import { handle_message } from './bridge/msg.ts'; +import { run_command } from './commands/run.ts'; +import { handle_command_message } from './commands/run.ts'; +import type { message } from './messages.ts'; /** configuration options for lightning */ export interface config { + /** database options */ + postgres_options: ClientOptions; /** a list of plugins */ // deno-lint-ignore no-explicit-any - plugins?: create_plugin[]; + plugins?: create_plugin>[]; /** the prefix used for commands */ - cmd_prefix?: string; - /** the set of commands to use */ - commands?: [string, command][]; - /** the hostname of your redis instance */ - redis_host?: string; - /** the port of your redis instance */ - redis_port?: number; - /** the webhook used to send errors to */ - errorURL?: string; + cmd_prefix: string; } /** an instance of lightning */ -export class lightning extends EventEmitter { +export class lightning { + /** bridge data handling */ + data: bridge_data; /** the commands registered */ - commands: Map; + commands: Map = new Map(default_commands); /** the config used */ config: config; - /** a redis client */ - redis: RedisClient; + /** set of processed messages */ + private processed: Set<`${string}-${string}`> = new Set(); /** the plugins loaded */ plugins: Map>; - /** setup an instance with the given config and redis connection */ - constructor(config: config, redis_conn: Deno.TcpConn) { - super(); - + /** setup an instance with the given config and bridge data */ + constructor(bridge_data: bridge_data, config: config) { + this.data = bridge_data; this.config = config; - this.commands = new Map(config.commands || default_cmds); - this.redis = new RedisClient(redis_conn); this.plugins = new Map>(); - setup_commands(this); - setup_bridges(this); - for (const p of this.config.plugins || []) { if (p.support.some((v) => ['0.7.3'].includes(v))) { const plugin = new p.type(this, p.config); this.plugins.set(plugin.name, plugin); - (async () => { - for await (const event of plugin) { - this.emit(event.name, ...event.value); - } - })(); + this._handle_events(plugin); } } } + + private async _handle_events(plugin: plugin) { + for await (const event of plugin) { + await new Promise((res) => setTimeout(res, 150)); + + const id = `${event.value[0].plugin}-${event.value[0].id}` as const; + + if (!this.processed.has(id)) { + this.processed.add(id); + + if (event.name === 'run_command') { + run_command({ + ...(event.value[0] as Omit< + command_arguments, + 'lightning' + >), + lightning: this, + }); + + continue; + } + + if (event.name === 'create_message') { + handle_command_message(event.value[0] as message, this); + } + + handle_message( + this, + event.value[0] as message, + event.name.split('_')[0] as 'create', + ); + } else { + this.processed.delete(id); + } + } + } + + /** create a new instance of lightning */ + static async create(config: config) { + const data = await bridge_data.create(config.postgres_options); + + return new lightning(data, config); + } } diff --git a/packages/lightning/src/messages.ts b/packages/lightning/src/messages.ts index f959efc..4dec59b 100644 --- a/packages/lightning/src/messages.ts +++ b/packages/lightning/src/messages.ts @@ -1,4 +1,4 @@ -import type { message } from './types.ts'; +import type { bridge_channel } from './bridge/data.ts'; /** * creates a message that can be sent using lightning @@ -21,3 +21,182 @@ export function create_message(text: string): message { }; return data; } + +/** attachments within a message */ +export interface attachment { + /** alt text for images */ + alt?: string; + /** a URL pointing to the file */ + file: string; + /** the file's name */ + name?: string; + /** whether or not the file has a spoiler */ + spoiler?: boolean; + /** file size */ + size: number; +} + +/** a representation of a message that has been deleted */ +export interface deleted_message { + /** the message's id */ + id: string; + /** the channel the message was sent in */ + channel: string; + /** the plugin that recieved the message */ + plugin: string; + /** the time the message was sent/edited as a temporal instant */ + timestamp: Temporal.Instant; +} + +/** a discord-style embed */ +export interface embed { + /** the author of the embed */ + author?: { + /** the name of the author */ + name: string; + /** the url of the author */ + url?: string; + /** the icon of the author */ + icon_url?: string; + }; + /** the color of the embed */ + color?: number; + /** the text in an embed */ + description?: string; + /** fields within the embed */ + fields?: { + /** the name of the field */ + name: string; + /** the value of the field */ + value: string; + /** whether or not the field is inline */ + inline?: boolean; + }[]; + /** a footer shown in the embed */ + footer?: { + /** the footer text */ + text: string; + /** the icon of the footer */ + icon_url?: string; + }; + /** an image shown in the embed */ + image?: media; + /** a thumbnail shown in the embed */ + thumbnail?: media; + /** the time (in epoch ms) shown in the embed */ + timestamp?: number; + /** the title of the embed */ + title?: string; + /** a site linked to by the embed */ + url?: string; + /** a video inside of the embed */ + video?: media; +} + +/** media inside of an embed */ +export interface media { + /** the height of the media */ + height?: number; + /** the url of the media */ + url: string; + /** the width of the media */ + width?: number; +} + +/** a message recieved by a plugin */ +export interface message extends deleted_message { + /** the attachments sent with the message */ + attachments?: attachment[]; + /** the author of the message */ + author: { + /** the nickname of the author */ + username: string; + /** the author's username */ + rawname: string; + /** a url pointing to the authors profile picture */ + profile?: string; + /** a url pointing to the authors banner */ + banner?: string; + /** the author's id */ + id: string; + /** the color of an author */ + color?: string; + }; + /** message content (can be markdown) */ + content?: string; + /** discord-style embeds */ + embeds?: embed[]; + /** a function to reply to a message */ + reply: (message: message, optional?: unknown) => Promise; + /** the id of the message replied to */ + reply_id?: string; +} + +/** the options given to plugins when a message needs to be sent */ +export interface create_message_opts { + /** the action to take */ + action: 'create'; + /** the channel to send the message to */ + channel: bridge_channel; + /** the message to send */ + message: message; + /** the id of the message to reply to */ + reply_id?: string; +} + +/** the options given to plugins when a message needs to be edited */ +export interface edit_message_opts { + /** the action to take */ + action: 'edit'; + /** the channel to send the message to */ + channel: bridge_channel; + /** the message to send */ + message: message; + /** the id of the message to edit */ + edit_id: string[]; + /** the id of the message to reply to */ + reply_id?: string; +} + +/** the options given to plugins when a message needs to be deleted */ +export interface delete_message_opts { + /** the action to take */ + action: 'delete'; + /** the channel to send the message to */ + channel: bridge_channel; + /** the message to send */ + message: deleted_message; + /** the id of the message to delete */ + edit_id: string[]; + /** the id of the message to reply to */ + reply_id?: string; +} + +/** the options given to plugins when a message needs to be processed */ +export type message_options = + | create_message_opts + | edit_message_opts + | delete_message_opts; + +/** successfully processed message */ +export interface processed_message { + /** whether there was an error */ + error?: undefined; + /** the message that was processed */ + id: string[]; + /** the channel the message was sent to */ + channel: bridge_channel; +} + +/** messages not processed */ +export interface unprocessed_message { + /** the channel the message was to be sent to */ + channel: bridge_channel; + /** whether the channel should be disabled */ + disable: boolean; + /** the error causing this */ + error: Error; +} + +/** process result */ +export type process_result = processed_message | unprocessed_message; diff --git a/packages/lightning/src/migrations.ts b/packages/lightning/src/migrations.ts deleted file mode 100644 index a4a6409..0000000 --- a/packages/lightning/src/migrations.ts +++ /dev/null @@ -1,77 +0,0 @@ -/** - * get migrations that can then be applied using apply_migrations - * @param from the version that the data is currently in - * @param to the version that the data will be migrated to - */ -export function get_migrations(from: versions, to: versions): migration[] { - return migrations.slice( - migrations.findIndex((i) => i.from === from), - migrations.findLastIndex((i) => i.to === to) + 1, - ); -} - -/** the type of a migration */ -export interface migration { - /** the version to translate from */ - from: versions; - /** the version to translate to */ - to: versions; - /** a function to translate a document */ - translate: (data: [string, unknown][]) => [string, unknown][]; -} - -/** all of the versions with migrations to/from them */ -export enum versions { - /** versions 0.7 through 0.7.2 */ - Seven = '0.7', - /** versions 0.7.3 and higher */ - SevenDotThree = '0.7.3', -} - -/** the internal list of migrations */ -const migrations = [ - { - from: versions.Seven, - to: versions.SevenDotThree, - translate: (items) => - items.map(([key, val]) => { - if (!key.startsWith('lightning-bridge')) return [key, val]; - - return [ - key, - { - ...(val as Record), - channels: (val as { - channels: { - id: string; - data: unknown; - plugin: string; - }[]; - }).channels?.map((i) => { - return { - data: i.data, - disabled: false, - id: i.id, - plugin: i.plugin, - }; - }), - messages: (val as { - messages: { - id: string | string[]; - channel: string; - plugin: string; - }[]; - }).messages?.map((i) => { - if (typeof i.id === 'string') { - return { - ...i, - id: [i.id], - }; - } - return i; - }), - }, - ]; - }), - }, -] as migration[]; diff --git a/packages/lightning/src/mod.ts b/packages/lightning/src/mod.ts deleted file mode 100644 index 07325aa..0000000 --- a/packages/lightning/src/mod.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * lightning is a typescript-based chatbot that supports - * bridging multiple chat apps via plugins. - * @module - */ - -export type * from './commands.ts'; -export * from './errors.ts'; -export * from './lightning.ts'; -export * from './messages.ts'; -export * from './migrations.ts'; -export * from './plugins.ts'; -export * from './types.ts'; diff --git a/packages/lightning/src/plugins.ts b/packages/lightning/src/plugins.ts index e37a178..775aa12 100644 --- a/packages/lightning/src/plugins.ts +++ b/packages/lightning/src/plugins.ts @@ -1,12 +1,12 @@ import { EventEmitter } from '@denosaurs/event'; -import type { command_arguments } from './commands.ts'; +import type { command_arguments } from './commands/mod.ts'; import type { lightning } from './lightning.ts'; import type { deleted_message, message, message_options, process_result, -} from './types.ts'; +} from './messages.ts'; /** the way to make a plugin */ export interface create_plugin< @@ -24,8 +24,6 @@ export interface create_plugin< export type plugin_events = { /** when a message is created */ create_message: [message]; - /** when a message isn't already bridged (don't emit outside of core) */ - create_nonbridged_message: [message]; /** when a message is edited */ edit_message: [message]; /** when a message is deleted */ diff --git a/packages/lightning/src/types.ts b/packages/lightning/src/types.ts deleted file mode 100644 index 9ed989d..0000000 --- a/packages/lightning/src/types.ts +++ /dev/null @@ -1,214 +0,0 @@ -/** attachments within a message */ -export interface attachment { - /** alt text for images */ - alt?: string; - /** a URL pointing to the file */ - file: string; - /** the file's name */ - name?: string; - /** whether or not the file has a spoiler */ - spoiler?: boolean; - /** file size */ - size: number; -} - -/** channel within a bridge */ -export interface bridge_channel { - /** the id of this channel */ - id: string; - /** the data needed to bridge this channel */ - data: unknown; - /** whether the channel is disabled */ - disabled: boolean; - /** the plugin used to bridge this channel */ - plugin: string; -} - -/** the representation of a bridge */ -export interface bridge_document { - /** whether or not to allow editing */ - allow_editing: boolean; - /** the channels to be bridged */ - channels: bridge_channel[]; - /** the id of the bridge */ - id: string; - /** messages bridged using these channels */ - messages?: bridge_message[]; - /** whether or not to use nicknames */ - use_rawname: boolean; -} - -/** bridged messages */ -export interface bridge_message { - /** the id of the message */ - id: string[]; - /** the id of the channel the message was sent in */ - channel: string; - /** the plugin the message was sent using */ - plugin: string; -} - -/** a representation of a message that has been deleted */ -export interface deleted_message { - /** the message's id */ - id: string; - /** the channel the message was sent in */ - channel: string; - /** the plugin that recieved the message */ - plugin: string; - /** the time the message was sent/edited as a temporal instant */ - timestamp: Temporal.Instant; -} - -/** a discord-style embed */ -export interface embed { - /** the author of the embed */ - author?: { - /** the name of the author */ - name: string; - /** the url of the author */ - url?: string; - /** the icon of the author */ - icon_url?: string; - }; - /** the color of the embed */ - color?: number; - /** the text in an embed */ - description?: string; - /** fields within the embed */ - fields?: { - /** the name of the field */ - name: string; - /** the value of the field */ - value: string; - /** whether or not the field is inline */ - inline?: boolean; - }[]; - /** a footer shown in the embed */ - footer?: { - /** the footer text */ - text: string; - /** the icon of the footer */ - icon_url?: string; - }; - /** an image shown in the embed */ - image?: media; - /** a thumbnail shown in the embed */ - thumbnail?: media; - /** the time (in epoch ms) shown in the embed */ - timestamp?: number; - /** the title of the embed */ - title?: string; - /** a site linked to by the embed */ - url?: string; - /** a video inside of the embed */ - video?: media; -} - -/** media inside of an embed */ -export interface media { - /** the height of the media */ - height?: number; - /** the url of the media */ - url: string; - /** the width of the media */ - width?: number; -} - -/** a message recieved by a plugin */ -export interface message extends deleted_message { - /** the attachments sent with the message */ - attachments?: attachment[]; - /** the author of the message */ - author: { - /** the nickname of the author */ - username: string; - /** the author's username */ - rawname: string; - /** a url pointing to the authors profile picture */ - profile?: string; - /** a url pointing to the authors banner */ - banner?: string; - /** the author's id */ - id: string; - /** the color of an author */ - color?: string; - }; - /** message content (can be markdown) */ - content?: string; - /** discord-style embeds */ - embeds?: embed[]; - /** a function to reply to a message */ - reply: (message: message, optional?: unknown) => Promise; - /** the id of the message replied to */ - reply_id?: string; -} - -/** the options given to plugins when a message needs to be sent */ -export interface create_message_opts { - /** the action to take */ - action: 'create'; - /** the channel to send the message to */ - channel: bridge_channel; - /** the message to send */ - message: message; - /** the id of the message to reply to */ - reply_id?: string; -} - -/** the options given to plugins when a message needs to be edited */ -export interface edit_message_opts { - /** the action to take */ - action: 'edit'; - /** the channel to send the message to */ - channel: bridge_channel; - /** the message to send */ - message: message; - /** the id of the message to edit */ - edit_id: string[]; - /** the id of the message to reply to */ - reply_id?: string; -} - -/** the options given to plugins when a message needs to be deleted */ -export interface delete_message_opts { - /** the action to take */ - action: 'delete'; - /** the channel to send the message to */ - channel: bridge_channel; - /** the message to send */ - message: deleted_message; - /** the id of the message to delete */ - edit_id: string[]; - /** the id of the message to reply to */ - reply_id?: string; -} - -/** the options given to plugins when a message needs to be processed */ -export type message_options = - | create_message_opts - | edit_message_opts - | delete_message_opts; - -/** successfully processed message */ -export interface processed_message { - /** whether there was an error */ - error?: undefined; - /** the message that was processed */ - id: string[]; - /** the channel the message was sent to */ - channel: bridge_channel; -} - -/** messages not processed */ -export interface unprocessed_message { - /** the channel the message was to be sent to */ - channel: bridge_channel; - /** whether the channel should be disabled */ - disable: boolean; - /** the error causing this */ - error: Error; -} - -/** process result */ -export type process_result = processed_message | unprocessed_message; From da596ca1382dd85693c96e29872d6bd0d525766a Mon Sep 17 00:00:00 2001 From: Jersey Date: Sat, 16 Nov 2024 17:11:08 -0500 Subject: [PATCH 04/23] cleanup and add back cli --- .gitignore | 3 +- packages/lightning/deno.jsonc | 8 ++--- packages/lightning/src/bridge/cmd.ts | 2 -- packages/lightning/src/cli/mod.ts | 53 ++++++++++++++++++++++++++++ packages/lightning/src/errors.ts | 2 +- 5 files changed, 60 insertions(+), 8 deletions(-) create mode 100644 packages/lightning/src/cli/mod.ts diff --git a/.gitignore b/.gitignore index 690e926..c3da77e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /.env /config /config.ts -packages/lightning-old \ No newline at end of file +packages/lightning-old +packages/postgres \ No newline at end of file diff --git a/packages/lightning/deno.jsonc b/packages/lightning/deno.jsonc index cbdcc1d..d589a24 100644 --- a/packages/lightning/deno.jsonc +++ b/packages/lightning/deno.jsonc @@ -3,13 +3,13 @@ "version": "0.8.0-alpha.0", "exports": { ".": "./src/mod.ts", - // TODO(jersey): add the cli back in along with migrations, except make migrations not suck as much? "./cli": "./src/cli/mod.ts" }, "imports": { - "@denosaurs/event": "jsr:@denosaurs/event@^2.0.2", "@db/postgres": "jsr:@db/postgres@^0.19.4", - "@std/ulid": "jsr:@std/ulid@^1.0.0", - "@std/cli/parse-args": "jsr:@std/cli@^1.0.3/parse-args" + "@denosaurs/event": "jsr:@denosaurs/event@^2.0.2", + "@std/cli/parse-args": "jsr:@std/cli@^1.0.3/parse-args", + "@std/path": "jsr:@std/path@^1.0.0", + "@std/ulid": "jsr:@std/ulid@^1.0.0" } } diff --git a/packages/lightning/src/bridge/cmd.ts b/packages/lightning/src/bridge/cmd.ts index 5ce6ed3..2663b71 100644 --- a/packages/lightning/src/bridge/cmd.ts +++ b/packages/lightning/src/bridge/cmd.ts @@ -11,8 +11,6 @@ export const bridge_command = { { name: 'join', description: 'join a bridge', - // TODO(jersey): update this to support multiple options - // TODO(jersey): make command options more flexible options: { argument_name: 'name', argument_required: true }, execute: async ({ lightning, channel, opts, plugin }) => { const current_bridge = await lightning.data diff --git a/packages/lightning/src/cli/mod.ts b/packages/lightning/src/cli/mod.ts new file mode 100644 index 0000000..dc432da --- /dev/null +++ b/packages/lightning/src/cli/mod.ts @@ -0,0 +1,53 @@ +import { parseArgs } from '@std/cli/parse-args'; +import { join, toFileUrl } from '@std/path'; +import { log_error } from '../errors.ts'; +import { type config, lightning } from '../lightning.ts'; + +const version = '0.8.0-alpha.0'; +const _ = parseArgs(Deno.args); + +if (_.v || _.version) { + console.log(version); +} else if (_.h || _.help) { + run_help(); +} else if (_._[0] === 'run') { + if (!_.config) _.config = join(Deno.cwd(), 'config.ts'); + + const config: config = await import(toFileUrl(_.config).toString()); + + addEventListener('error', async (ev) => { + await log_error(ev.error, { type: 'global error' }); + Deno.exit(1); + }); + + addEventListener('unhandledrejection', async (ev) => { + await log_error(ev.reason, { type: 'global rejection' }); + Deno.exit(1); + }); + + try { + await lightning.create(config); + } catch (e) { + await log_error(e, { type: 'global class error' }); + Deno.exit(1); + } +} else if (_._[0] === 'migrations') { + // TODO(jersey): implement migrations +} else { + console.log('[lightning] command not found, showing help'); + run_help(); +} + +function run_help() { + console.log(`lightning v${version} - extensible chatbot connecting communities`); + console.log(' Usage: lightning [subcommand] '); + console.log(' Subcommands:'); + console.log(' run: run a lightning instance'); + console.log(' migrations: run migration script'); + console.log(' Options:'); + console.log(' -h, --help: display this help message'); + console.log(' -v, --version: display the version number'); + console.log(' -c, --config: the config file to use'); + console.log(' Environment Variables:'); + console.log(' LIGHTNING_ERROR_WEBHOOK: the webhook to send errors to'); +} diff --git a/packages/lightning/src/errors.ts b/packages/lightning/src/errors.ts index b4a44fa..2a76423 100644 --- a/packages/lightning/src/errors.ts +++ b/packages/lightning/src/errors.ts @@ -22,7 +22,7 @@ export async function log_error( extra: Record = {}, ): Promise { const id = crypto.randomUUID(); - const webhook = Deno.env.get('LIGHTNING_ERROR_HOOK'); + const webhook = Deno.env.get('LIGHTNING_ERROR_WEBHOOK'); const cause = e instanceof Error ? e : e instanceof Object From 5b35253d91e89c171dba303634c020ba3c80113a Mon Sep 17 00:00:00 2001 From: Jersey Date: Sat, 16 Nov 2024 17:38:22 -0500 Subject: [PATCH 05/23] cleanup some stuff --- packages/lightning-plugin-discord/deno.json | 4 +- .../src/process_message.ts | 9 ++- packages/lightning-plugin-guilded/deno.json | 4 +- packages/lightning-plugin-guilded/src/mod.ts | 12 +++- packages/lightning-plugin-revolt/deno.json | 4 +- packages/lightning-plugin-revolt/src/mod.ts | 11 ++- packages/lightning-plugin-telegram/deno.json | 4 +- .../lightning-plugin-telegram/src/messages.ts | 4 +- packages/lightning-plugin-telegram/src/mod.ts | 4 +- packages/lightning/deno.jsonc | 2 +- packages/lightning/dockerfile | 2 +- packages/lightning/src/bridge/data.ts | 2 +- packages/lightning/src/cli/mod.ts | 48 +++++++------ packages/lightning/src/commands/run.ts | 70 +++++++++---------- packages/lightning/src/lightning.ts | 2 +- packages/lightning/src/messages.ts | 1 + packages/lightning/src/mod.ts | 9 +++ 17 files changed, 110 insertions(+), 82 deletions(-) create mode 100644 packages/lightning/src/mod.ts diff --git a/packages/lightning-plugin-discord/deno.json b/packages/lightning-plugin-discord/deno.json index c2e5f6b..b22054a 100644 --- a/packages/lightning-plugin-discord/deno.json +++ b/packages/lightning-plugin-discord/deno.json @@ -1,9 +1,9 @@ { "name": "@jersey/lightning-plugin-discord", - "version": "0.7.4", + "version": "0.8.0", "exports": "./src/mod.ts", "imports": { - "@jersey/lightning": "jsr:@jersey/lightning@^0.7.3", + "@jersey/lightning": "jsr:@jersey/lightning@^0.8.0", "@discordjs/core": "npm:@discordjs/core@^1.2.0", "@discordjs/rest": "npm:@discordjs/rest@^2.3.0", "@discordjs/ws": "npm:@discordjs/ws@^1.1.1", diff --git a/packages/lightning-plugin-discord/src/process_message.ts b/packages/lightning-plugin-discord/src/process_message.ts index c8068b7..ef5f881 100644 --- a/packages/lightning-plugin-discord/src/process_message.ts +++ b/packages/lightning-plugin-discord/src/process_message.ts @@ -46,10 +46,13 @@ export async function process_message(api: API, opts: message_options) { channel: opts.channel, }; } catch (e) { - if (e.status === 404 && opts.action !== 'edit') { + if ( + (e as { status: number }).status === 404 && + opts.action !== 'edit' + ) { return { channel: opts.channel, - error: e, + error: e as Error, disable: true, }; } else { @@ -71,7 +74,7 @@ export async function process_message(api: API, opts: message_options) { } catch (e) { return { channel: opts.channel, - error: e, + error: e as Error, disable: false, }; } diff --git a/packages/lightning-plugin-guilded/deno.json b/packages/lightning-plugin-guilded/deno.json index aa51830..622bc6b 100644 --- a/packages/lightning-plugin-guilded/deno.json +++ b/packages/lightning-plugin-guilded/deno.json @@ -1,9 +1,9 @@ { "name": "@jersey/lightning-plugin-guilded", - "version": "0.7.4", + "version": "0.8.0", "exports": "./src/mod.ts", "imports": { - "@jersey/lightning": "jsr:@jersey/lightning@^0.7.3", + "@jersey/lightning": "jsr:@jersey/lightning@^0.8.0", "guilded.js": "npm:guilded.js@^0.25.0", "@guildedjs/api": "npm:@guildedjs/api@^0.4.0" } diff --git a/packages/lightning-plugin-guilded/src/mod.ts b/packages/lightning-plugin-guilded/src/mod.ts index 47716f5..948a2f8 100644 --- a/packages/lightning-plugin-guilded/src/mod.ts +++ b/packages/lightning-plugin-guilded/src/mod.ts @@ -73,13 +73,19 @@ export class guilded_plugin extends plugin { channel: opts.channel, }; } catch (e) { - if (e.response.status === 404) { + if ( + (e as { response: { status: number } }).response + .status === 404 + ) { return { channel: opts.channel, disable: true, error: new Error('webhook not found!'), }; - } else if (e.response.status === 403) { + } else if ( + (e as { response: { status: number } }).response + .status === 403 + ) { return { channel: opts.channel, disable: true, @@ -111,7 +117,7 @@ export class guilded_plugin extends plugin { // TODO(@williamhorning): improve error handling return { channel: opts.channel, - error: e, + error: e as Error, disable: false, }; } diff --git a/packages/lightning-plugin-revolt/deno.json b/packages/lightning-plugin-revolt/deno.json index ebdf6b8..36faa94 100644 --- a/packages/lightning-plugin-revolt/deno.json +++ b/packages/lightning-plugin-revolt/deno.json @@ -1,9 +1,9 @@ { "name": "@jersey/lightning-plugin-revolt", - "version": "0.7.4", + "version": "0.8.0", "exports": "./src/mod.ts", "imports": { - "@jersey/lightning": "jsr:@jersey/lightning@^0.7.3", + "@jersey/lightning": "jsr:@jersey/lightning@^0.8.0", "@jersey/rvapi": "jsr:@jersey/rvapi@^0.0.3", "@jersey/revolt-api-types": "jsr:@jersey/revolt-api-types@^0.7.14", "@std/ulid": "jsr:@std/ulid@^1.0.0" diff --git a/packages/lightning-plugin-revolt/src/mod.ts b/packages/lightning-plugin-revolt/src/mod.ts index 0b283d8..675bea1 100644 --- a/packages/lightning-plugin-revolt/src/mod.ts +++ b/packages/lightning-plugin-revolt/src/mod.ts @@ -88,11 +88,16 @@ export class revolt_plugin extends plugin { id: [resp._id], }; } catch (e) { - if (e.cause.status === 403 || e.cause.status === 404) { + if ( + (e as { cause: { status: number } }).cause.status === + 403 || + (e as { cause: { status: number } }).cause.status === + 404 + ) { return { channel: opts.channel, disable: true, - error: e, + error: e as Error, }; } else { throw e; @@ -128,7 +133,7 @@ export class revolt_plugin extends plugin { return { channel: opts.channel, disable: false, - error: e, + error: e as Error, }; } } diff --git a/packages/lightning-plugin-telegram/deno.json b/packages/lightning-plugin-telegram/deno.json index eabd50d..82137b3 100644 --- a/packages/lightning-plugin-telegram/deno.json +++ b/packages/lightning-plugin-telegram/deno.json @@ -1,9 +1,9 @@ { "name": "@jersey/lightning-plugin-telegram", - "version": "0.7.4", + "version": "0.8.0", "exports": "./src/mod.ts", "imports": { - "@jersey/lightning": "jsr:@jersey/lightning@^0.7.3", + "@jersey/lightning": "jsr:@jersey/lightning@^0.8.0", "telegramify-markdown": "npm:telegramify-markdown@^1.2.2", "grammy": "npm:grammy@^1.28.0", "grammy/types": "npm:grammy@^1.28.0/types" diff --git a/packages/lightning-plugin-telegram/src/messages.ts b/packages/lightning-plugin-telegram/src/messages.ts index f3b09fd..6e6cd56 100644 --- a/packages/lightning-plugin-telegram/src/messages.ts +++ b/packages/lightning-plugin-telegram/src/messages.ts @@ -115,7 +115,9 @@ async function get_base_msg( }, channel: msg.chat.id.toString(), id: msg.message_id.toString(), - timestamp: Temporal.Instant.fromEpochMilliseconds((msg.edit_date || msg.date) * 1000), + timestamp: Temporal.Instant.fromEpochMilliseconds( + (msg.edit_date || msg.date) * 1000, + ), plugin: 'bolt-telegram', reply: async (lmsg) => { for (const m of from_lightning(lmsg)) { diff --git a/packages/lightning-plugin-telegram/src/mod.ts b/packages/lightning-plugin-telegram/src/mod.ts index 02bb3c9..1dc58c7 100644 --- a/packages/lightning-plugin-telegram/src/mod.ts +++ b/packages/lightning-plugin-telegram/src/mod.ts @@ -118,9 +118,9 @@ export class telegram_plugin extends plugin { } catch (e) { // TODO(@williamhorning): improve error handling logic return { - error: e, - id: [opts.message.id], + error: e as Error, channel: opts.channel, + disable: false, }; } } diff --git a/packages/lightning/deno.jsonc b/packages/lightning/deno.jsonc index d589a24..3c2c21f 100644 --- a/packages/lightning/deno.jsonc +++ b/packages/lightning/deno.jsonc @@ -1,6 +1,6 @@ { "name": "@jersey/lightning", - "version": "0.8.0-alpha.0", + "version": "0.8.0", "exports": { ".": "./src/mod.ts", "./cli": "./src/cli/mod.ts" diff --git a/packages/lightning/dockerfile b/packages/lightning/dockerfile index b1fec7b..4bb8831 100644 --- a/packages/lightning/dockerfile +++ b/packages/lightning/dockerfile @@ -1,7 +1,7 @@ FROM docker.io/denoland/deno:2.0.3 # add lightning to the image -RUN deno install -g -A --unstable-temporal jsr:@jersey/lightning@0.8.0-alpha.0/cli +RUN deno install -g -A --unstable-temporal jsr:@jersey/lightning@0.8.0/cli RUN mkdir -p /app/data WORKDIR /app/data diff --git a/packages/lightning/src/bridge/data.ts b/packages/lightning/src/bridge/data.ts index f9dd4ba..56407e4 100644 --- a/packages/lightning/src/bridge/data.ts +++ b/packages/lightning/src/bridge/data.ts @@ -35,7 +35,7 @@ export interface bridged_message { export class bridge_data { private pg: Client; - static async create(pg_options: ClientOptions) { + static async create(pg_options: ClientOptions): Promise { const pg = new Client(pg_options); await pg.connect(); diff --git a/packages/lightning/src/cli/mod.ts b/packages/lightning/src/cli/mod.ts index dc432da..fa945bb 100644 --- a/packages/lightning/src/cli/mod.ts +++ b/packages/lightning/src/cli/mod.ts @@ -3,43 +3,45 @@ import { join, toFileUrl } from '@std/path'; import { log_error } from '../errors.ts'; import { type config, lightning } from '../lightning.ts'; -const version = '0.8.0-alpha.0'; +const version = '0.8.0'; const _ = parseArgs(Deno.args); if (_.v || _.version) { - console.log(version); + console.log(version); } else if (_.h || _.help) { - run_help(); + run_help(); } else if (_._[0] === 'run') { - if (!_.config) _.config = join(Deno.cwd(), 'config.ts'); + if (!_.config) _.config = join(Deno.cwd(), 'config.ts'); - const config: config = await import(toFileUrl(_.config).toString()); + const config: config = await import(toFileUrl(_.config).toString()); - addEventListener('error', async (ev) => { - await log_error(ev.error, { type: 'global error' }); - Deno.exit(1); - }); + addEventListener('error', async (ev) => { + await log_error(ev.error, { type: 'global error' }); + Deno.exit(1); + }); - addEventListener('unhandledrejection', async (ev) => { - await log_error(ev.reason, { type: 'global rejection' }); - Deno.exit(1); - }); + addEventListener('unhandledrejection', async (ev) => { + await log_error(ev.reason, { type: 'global rejection' }); + Deno.exit(1); + }); - try { - await lightning.create(config); - } catch (e) { - await log_error(e, { type: 'global class error' }); - Deno.exit(1); - } + try { + await lightning.create(config); + } catch (e) { + await log_error(e, { type: 'global class error' }); + Deno.exit(1); + } } else if (_._[0] === 'migrations') { - // TODO(jersey): implement migrations + // TODO(jersey): implement migrations } else { - console.log('[lightning] command not found, showing help'); - run_help(); + console.log('[lightning] command not found, showing help'); + run_help(); } function run_help() { - console.log(`lightning v${version} - extensible chatbot connecting communities`); + console.log( + `lightning v${version} - extensible chatbot connecting communities`, + ); console.log(' Usage: lightning [subcommand] '); console.log(' Subcommands:'); console.log(' run: run a lightning instance'); diff --git a/packages/lightning/src/commands/run.ts b/packages/lightning/src/commands/run.ts index 69d855f..04b48f2 100644 --- a/packages/lightning/src/commands/run.ts +++ b/packages/lightning/src/commands/run.ts @@ -5,42 +5,42 @@ import type { lightning } from '../lightning.ts'; import { parseArgs } from '@std/cli/parse-args'; export function handle_command_message(m: message, l: lightning) { - if (!m.content?.startsWith(l.config.cmd_prefix)) return; - - const { - _: [cmd, subcmd], - ...opts - } = parseArgs(m.content.replace(l.config.cmd_prefix, '').split(' ')); - - run_command({ - lightning: l, - cmd: cmd as string, - subcmd: subcmd as string, - opts, - ...m, - }); + if (!m.content?.startsWith(l.config.cmd_prefix)) return; + + const { + _: [cmd, subcmd], + ...opts + } = parseArgs(m.content.replace(l.config.cmd_prefix, '').split(' ')); + + run_command({ + lightning: l, + cmd: cmd as string, + subcmd: subcmd as string, + opts, + ...m, + }); } export async function run_command(args: command_arguments) { - let reply; - - try { - const cmd = args.lightning.commands.get(args.cmd) || - args.lightning.commands.get('help')!; - - const exec = cmd.options?.subcommands?.find((i) => - i.name === args.subcmd - )?.execute || - cmd.execute; - - reply = create_message(await exec(args)); - } catch (e) { - reply = (await log_error(e, { ...args, reply: undefined })).message; - } - - try { - await args.reply(reply, false); - } catch (e) { - await log_error(e, { ...args, reply: undefined }); - } + let reply; + + try { + const cmd = args.lightning.commands.get(args.cmd) || + args.lightning.commands.get('help')!; + + const exec = cmd.options?.subcommands?.find((i) => + i.name === args.subcmd + )?.execute || + cmd.execute; + + reply = create_message(await exec(args)); + } catch (e) { + reply = (await log_error(e, { ...args, reply: undefined })).message; + } + + try { + await args.reply(reply, false); + } catch (e) { + await log_error(e, { ...args, reply: undefined }); + } } diff --git a/packages/lightning/src/lightning.ts b/packages/lightning/src/lightning.ts index e30e2cf..d7c2d7d 100644 --- a/packages/lightning/src/lightning.ts +++ b/packages/lightning/src/lightning.ts @@ -87,7 +87,7 @@ export class lightning { } /** create a new instance of lightning */ - static async create(config: config) { + static async create(config: config): Promise { const data = await bridge_data.create(config.postgres_options); return new lightning(data, config); diff --git a/packages/lightning/src/messages.ts b/packages/lightning/src/messages.ts index 4dec59b..1720f75 100644 --- a/packages/lightning/src/messages.ts +++ b/packages/lightning/src/messages.ts @@ -195,6 +195,7 @@ export interface unprocessed_message { /** whether the channel should be disabled */ disable: boolean; /** the error causing this */ + // TODO(jersey): make this unknown ideally error: Error; } diff --git a/packages/lightning/src/mod.ts b/packages/lightning/src/mod.ts new file mode 100644 index 0000000..57180e6 --- /dev/null +++ b/packages/lightning/src/mod.ts @@ -0,0 +1,9 @@ +export type { + command, + command_arguments, + command_options, +} from './commands/mod.ts'; +export { log_error } from './errors.ts'; +export { type config, lightning } from './lightning.ts'; +export * from './messages.ts'; +export * from './plugins.ts'; From 9fef90133e414d00a65919fc60feedcd33cf86d7 Mon Sep 17 00:00:00 2001 From: Jersey Date: Sat, 16 Nov 2024 17:48:09 -0500 Subject: [PATCH 06/23] clean up error handling a little --- .../src/process_message.ts | 1 - packages/lightning-plugin-guilded/src/mod.ts | 1 - packages/lightning-plugin-revolt/src/mod.ts | 1 - packages/lightning-plugin-telegram/src/mod.ts | 1 - packages/lightning/dockerfile | 5 ++--- packages/lightning/src/bridge/cmd.ts | 17 +++++++---------- packages/lightning/src/messages.ts | 2 +- 7 files changed, 10 insertions(+), 18 deletions(-) diff --git a/packages/lightning-plugin-discord/src/process_message.ts b/packages/lightning-plugin-discord/src/process_message.ts index ef5f881..6edbe54 100644 --- a/packages/lightning-plugin-discord/src/process_message.ts +++ b/packages/lightning-plugin-discord/src/process_message.ts @@ -75,7 +75,6 @@ export async function process_message(api: API, opts: message_options) { return { channel: opts.channel, error: e as Error, - disable: false, }; } } diff --git a/packages/lightning-plugin-guilded/src/mod.ts b/packages/lightning-plugin-guilded/src/mod.ts index 948a2f8..d034e04 100644 --- a/packages/lightning-plugin-guilded/src/mod.ts +++ b/packages/lightning-plugin-guilded/src/mod.ts @@ -118,7 +118,6 @@ export class guilded_plugin extends plugin { return { channel: opts.channel, error: e as Error, - disable: false, }; } } diff --git a/packages/lightning-plugin-revolt/src/mod.ts b/packages/lightning-plugin-revolt/src/mod.ts index 675bea1..03b87f7 100644 --- a/packages/lightning-plugin-revolt/src/mod.ts +++ b/packages/lightning-plugin-revolt/src/mod.ts @@ -132,7 +132,6 @@ export class revolt_plugin extends plugin { } catch (e) { return { channel: opts.channel, - disable: false, error: e as Error, }; } diff --git a/packages/lightning-plugin-telegram/src/mod.ts b/packages/lightning-plugin-telegram/src/mod.ts index 1dc58c7..4e211d9 100644 --- a/packages/lightning-plugin-telegram/src/mod.ts +++ b/packages/lightning-plugin-telegram/src/mod.ts @@ -120,7 +120,6 @@ export class telegram_plugin extends plugin { return { error: e as Error, channel: opts.channel, - disable: false, }; } } diff --git a/packages/lightning/dockerfile b/packages/lightning/dockerfile index 4bb8831..817f3e7 100644 --- a/packages/lightning/dockerfile +++ b/packages/lightning/dockerfile @@ -1,11 +1,10 @@ FROM docker.io/denoland/deno:2.0.3 # add lightning to the image -RUN deno install -g -A --unstable-temporal jsr:@jersey/lightning@0.8.0/cli +RUN deno install -g -A --unstable-temporal ./cli/mod.ts RUN mkdir -p /app/data WORKDIR /app/data # set lightning as the entrypoint and use the run command by default ENTRYPOINT [ "lightning" ] -# TODO(jersey): do i need to do this? -CMD [ "run", "--config", "file:///app/data/config.ts"] +CMD [ "run"] diff --git a/packages/lightning/src/bridge/cmd.ts b/packages/lightning/src/bridge/cmd.ts index 2663b71..4ff4d93 100644 --- a/packages/lightning/src/bridge/cmd.ts +++ b/packages/lightning/src/bridge/cmd.ts @@ -3,14 +3,13 @@ import { log_error } from '../errors.ts'; export const bridge_command = { name: 'bridge', - description: 'bridge commands', - execute: () => 'take a look at the docs for help with bridges', + description: 'Bridge commands', + execute: () => 'Take a look at the docs for help with bridges', options: { subcommands: [ - // TODO(jersey): eventually reimplement reset command? { name: 'join', - description: 'join a bridge', + description: 'Join a bridge', options: { argument_name: 'name', argument_required: true }, execute: async ({ lightning, channel, opts, plugin }) => { const current_bridge = await lightning.data @@ -18,8 +17,6 @@ export const bridge_command = { channel, ); - // live laugh love validation - if (current_bridge) { return `You are already in a bridge called ${current_bridge.name}`; } @@ -103,7 +100,7 @@ export const bridge_command = { }, { name: 'leave', - description: 'leave a bridge', + description: 'Leave a bridge', execute: async ({ lightning, channel }) => { const bridge = await lightning.data.get_bridge_by_channel( channel, @@ -132,7 +129,7 @@ export const bridge_command = { }, { name: 'toggle', - description: 'toggle a setting on a bridge', + description: 'Toggle a setting on a bridge', options: { argument_name: 'setting', argument_required: true }, execute: async ({ opts, lightning, channel }) => { const bridge = await lightning.data.get_bridge_by_channel( @@ -145,7 +142,7 @@ export const bridge_command = { !['allow_editing', 'allow_everyone', 'use_rawname'] .includes(opts.setting) ) { - return `that setting does not exist`; + return `That setting does not exist`; } const key = opts.setting as keyof typeof bridge.settings; @@ -170,7 +167,7 @@ export const bridge_command = { }, { name: 'status', - description: 'see what bridges you are in', + description: 'See what bridges you are in', execute: async ({ lightning, channel }) => { const existing_bridge = await lightning.data .get_bridge_by_channel( diff --git a/packages/lightning/src/messages.ts b/packages/lightning/src/messages.ts index 1720f75..c8a6ba9 100644 --- a/packages/lightning/src/messages.ts +++ b/packages/lightning/src/messages.ts @@ -193,7 +193,7 @@ export interface unprocessed_message { /** the channel the message was to be sent to */ channel: bridge_channel; /** whether the channel should be disabled */ - disable: boolean; + disable?: boolean; /** the error causing this */ // TODO(jersey): make this unknown ideally error: Error; From 93833f792016dbee66ac5d9881d5e8b0ad0a652e Mon Sep 17 00:00:00 2001 From: Jersey Date: Wed, 27 Nov 2024 16:56:32 -0500 Subject: [PATCH 07/23] clean up the data structures a little bit? --- packages/lightning/src/bridge/data.ts | 29 ++++++++++++--------------- packages/lightning/src/bridge/msg.ts | 8 +++++--- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/packages/lightning/src/bridge/data.ts b/packages/lightning/src/bridge/data.ts index 56407e4..d16fa87 100644 --- a/packages/lightning/src/bridge/data.ts +++ b/packages/lightning/src/bridge/data.ts @@ -21,9 +21,12 @@ export interface bridge_settings { use_rawname: boolean; /* rawname = username */ } -export interface bridge_message extends bridge { - original_id: string; /* original message id */ +export interface bridge_message { + id: string; /* original message id */ + bridge_id: string; /* bridge id */ + channels: bridge_channel[]; /* channels bridged */ messages: bridged_message[]; /* bridged messages */ + settings: bridge_settings; /* settings for the bridge */ } export interface bridged_message { @@ -57,13 +60,11 @@ export class bridge_data { settings JSONB NOT NULL )`; await pg.queryArray`CREATE TABLE bridge_messages ( - original_id TEXT PRIMARY KEY, - id TEXT NOT NULL, - name TEXT NOT NULL, - channels JSONB NOT NULL REFERENCES bridges(channels), + id TEXT PRIMARY KEY, + bridge_id TEXT NOT NULL, + channels JSONB NOT NULL, messages JSONB NOT NULL, - settings JSONB NOT NULL REFERENCES bridges(settings), - CONSTRAINT fk_id FOREIGN KEY(id) REFERENCES bridges(id) + settings JSONB NOT NULL )`; } @@ -81,14 +82,11 @@ export class bridge_data { return { id, ...bridge }; } - async update_bridge(bridge: bridge): Promise { + async update_bridge(bridge: {channels: bridge_channel[], settings: bridge_settings, id: string}): Promise { await this.pg.queryArray`UPDATE bridges SET - name = ${bridge.name}, channels = ${bridge.channels}, settings = ${bridge.settings} WHERE id = ${bridge.id}`; - - return bridge; } async get_bridge_by_id(id: string): Promise { @@ -107,8 +105,8 @@ export class bridge_data { async new_bridge_message(message: bridge_message): Promise { await this.pg.queryArray`INSERT INTO bridge_messages - (original_id, id, name, channels, messages, settings) VALUES - (${message.original_id}, ${message.id}, ${message.name}, ${message.channels}, ${message.messages}, ${message.settings})`; + (id, bridge_id, channels, messages, settings) VALUES + (${message.id}, ${message.bridge_id}, ${message.channels}, ${message.messages}, ${message.settings})`; return message; } @@ -117,11 +115,10 @@ export class bridge_data { message: bridge_message, ): Promise { await this.pg.queryArray`UPDATE bridge_messages SET - id = ${message.id}, channels = ${message.channels}, messages = ${message.messages}, settings = ${message.settings} - WHERE original_id = ${message.original_id}`; + WHERE id = ${message.id}`; return message; } diff --git a/packages/lightning/src/bridge/msg.ts b/packages/lightning/src/bridge/msg.ts index b62a373..1e5f186 100644 --- a/packages/lightning/src/bridge/msg.ts +++ b/packages/lightning/src/bridge/msg.ts @@ -130,26 +130,28 @@ export async function handle_message( await core.data[`${method}_bridge_message`]({ ...br, - original_id: msg.id, + id: msg.id, messages, + bridge_id: br.id, }); } async function disable_channel( channel: bridge_channel, - bridge: bridge, + bridge: bridge | bridge_message, core: lightning, error: unknown, ): Promise { await log_error(error, { channel, bridge }); await core.data.update_bridge({ - ...bridge, + id: "bridge_id" in bridge ? bridge.bridge_id : bridge.id, channels: bridge.channels.map((i) => i.id === channel.id && i.plugin === channel.plugin ? { ...i, disabled: true, data: error } : i ), + settings: bridge.settings }); } From cc0d5cdde0c08672c0d627a5c6ba0e8e70a60e44 Mon Sep 17 00:00:00 2001 From: Jersey Date: Wed, 27 Nov 2024 17:00:51 -0500 Subject: [PATCH 08/23] clean up unnecessary catches --- .../src/process_message.ts | 109 ++++++++---------- packages/lightning-plugin-guilded/src/mod.ts | 92 +++++++-------- packages/lightning-plugin-revolt/src/mod.ts | 101 ++++++++-------- packages/lightning-plugin-telegram/src/mod.ts | 94 +++++++-------- 4 files changed, 183 insertions(+), 213 deletions(-) diff --git a/packages/lightning-plugin-discord/src/process_message.ts b/packages/lightning-plugin-discord/src/process_message.ts index 6edbe54..04327ca 100644 --- a/packages/lightning-plugin-discord/src/process_message.ts +++ b/packages/lightning-plugin-discord/src/process_message.ts @@ -3,78 +3,71 @@ import type { message_options } from '@jersey/lightning'; import { to_discord } from './discord.ts'; export async function process_message(api: API, opts: message_options) { - try { - const data = opts.channel.data as { token: string; id: string }; + const data = opts.channel.data as { token: string; id: string }; - if (opts.action !== 'delete') { - let replied_message; + if (opts.action !== 'delete') { + let replied_message; - if (opts.reply_id) { - try { - replied_message = await api.channels - .getMessage(opts.channel.id, opts.reply_id); - } catch { - // safe to ignore - } + if (opts.reply_id) { + try { + replied_message = await api.channels + .getMessage(opts.channel.id, opts.reply_id); + } catch { + // safe to ignore } + } - const msg = await to_discord( - opts.message, - replied_message, - ); - - try { - let wh; + const msg = await to_discord( + opts.message, + replied_message, + ); - if (opts.action === 'edit') { - wh = await api.webhooks.editMessage( - data.id, - data.token, - opts.edit_id[0], - msg, - ); - } else { - wh = await api.webhooks.execute( - data.id, - data.token, - msg, - ); - } + try { + let wh; - return { - id: [wh.id], - channel: opts.channel, - }; - } catch (e) { - if ( - (e as { status: number }).status === 404 && - opts.action !== 'edit' - ) { - return { - channel: opts.channel, - error: e as Error, - disable: true, - }; - } else { - throw e; - } + if (opts.action === 'edit') { + wh = await api.webhooks.editMessage( + data.id, + data.token, + opts.edit_id[0], + msg, + ); + } else { + wh = await api.webhooks.execute( + data.id, + data.token, + msg, + ); } - } else { - await api.webhooks.deleteMessage( - data.id, - data.token, - opts.edit_id[0], - ); return { - id: opts.edit_id, + id: [wh.id], channel: opts.channel, }; + } catch (e) { + if ( + (e as { status: number }).status === 404 && + opts.action !== 'edit' + ) { + return { + channel: opts.channel, + error: e as Error, + disable: true, + }; + } else { + throw e; + } } - } catch (e) { + } else { + await api.webhooks.deleteMessage( + data.id, + data.token, + opts.edit_id[0], + ); + return { + id: opts.edit_id, channel: opts.channel, - error: e as Error, }; } } diff --git a/packages/lightning-plugin-guilded/src/mod.ts b/packages/lightning-plugin-guilded/src/mod.ts index d034e04..90b34c5 100644 --- a/packages/lightning-plugin-guilded/src/mod.ts +++ b/packages/lightning-plugin-guilded/src/mod.ts @@ -59,65 +59,57 @@ export class guilded_plugin extends plugin { } async process_message(opts: message_options): Promise { - try { - if (opts.action === 'create') { - try { - const { id } = await new WebhookClient( - opts.channel.data as { token: string; id: string }, - ).send( - await convert_msg(opts.message, opts.channel.id, this), - ); + if (opts.action === 'create') { + try { + const { id } = await new WebhookClient( + opts.channel.data as { token: string; id: string }, + ).send( + await convert_msg(opts.message, opts.channel.id, this), + ); + return { + id: [id], + channel: opts.channel, + }; + } catch (e) { + if ( + (e as { response: { status: number } }).response + .status === 404 + ) { + return { + channel: opts.channel, + disable: true, + error: new Error('webhook not found!'), + }; + } else if ( + (e as { response: { status: number } }).response + .status === 403 + ) { return { - id: [id], channel: opts.channel, + disable: true, + error: new Error('no permission to send messages!'), }; - } catch (e) { - if ( - (e as { response: { status: number } }).response - .status === 404 - ) { - return { - channel: opts.channel, - disable: true, - error: new Error('webhook not found!'), - }; - } else if ( - (e as { response: { status: number } }).response - .status === 403 - ) { - return { - channel: opts.channel, - disable: true, - error: new Error('no permission to send messages!'), - }; - } else { - throw e; - } + } else { + throw e; } - } else if (opts.action === 'delete') { - const msg = await this.bot.messages.fetch( - opts.channel.id, - opts.edit_id[0], - ); + } + } else if (opts.action === 'delete') { + const msg = await this.bot.messages.fetch( + opts.channel.id, + opts.edit_id[0], + ); - await msg.delete(); + await msg.delete(); - return { - channel: opts.channel, - id: opts.edit_id, - }; - } else { - return { - channel: opts.channel, - id: opts.edit_id, - }; - } - } catch (e) { - // TODO(@williamhorning): improve error handling return { channel: opts.channel, - error: e as Error, + id: opts.edit_id, + }; + } else { + return { + channel: opts.channel, + id: opts.edit_id, }; } } diff --git a/packages/lightning-plugin-revolt/src/mod.ts b/packages/lightning-plugin-revolt/src/mod.ts index 03b87f7..8d035be 100644 --- a/packages/lightning-plugin-revolt/src/mod.ts +++ b/packages/lightning-plugin-revolt/src/mod.ts @@ -69,70 +69,63 @@ export class revolt_plugin extends plugin { /** process a message */ async process_message(opts: message_options): Promise { - try { - if (opts.action === 'create') { - try { - const msg = await torvapi(this.bot, { - ...opts.message, - reply_id: opts.reply_id, - }); + if (opts.action === 'create') { + try { + const msg = await torvapi(this.bot, { + ...opts.message, + reply_id: opts.reply_id, + }); - const resp = (await this.bot.request( - 'post', - `/channels/${opts.channel.id}/messages`, - msg, - )) as Message; + const resp = (await this.bot.request( + 'post', + `/channels/${opts.channel.id}/messages`, + msg, + )) as Message; + return { + channel: opts.channel, + id: [resp._id], + }; + } catch (e) { + if ( + (e as { cause: { status: number } }).cause.status === + 403 || + (e as { cause: { status: number } }).cause.status === + 404 + ) { return { channel: opts.channel, - id: [resp._id], + disable: true, + error: e as Error, }; - } catch (e) { - if ( - (e as { cause: { status: number } }).cause.status === - 403 || - (e as { cause: { status: number } }).cause.status === - 404 - ) { - return { - channel: opts.channel, - disable: true, - error: e as Error, - }; - } else { - throw e; - } + } else { + throw e; } - } else if (opts.action === 'edit') { - await this.bot.request( - 'patch', - `/channels/${opts.channel.id}/messages/${opts.edit_id[0]}`, - await torvapi(this.bot, { - ...opts.message, - reply_id: opts.reply_id, - }), - ); + } + } else if (opts.action === 'edit') { + await this.bot.request( + 'patch', + `/channels/${opts.channel.id}/messages/${opts.edit_id[0]}`, + await torvapi(this.bot, { + ...opts.message, + reply_id: opts.reply_id, + }), + ); - return { - channel: opts.channel, - id: opts.edit_id, - }; - } else { - await this.bot.request( - 'delete', - `/channels/${opts.channel.id}/messages/${opts.edit_id[0]}`, - undefined, - ); + return { + channel: opts.channel, + id: opts.edit_id, + }; + } else { + await this.bot.request( + 'delete', + `/channels/${opts.channel.id}/messages/${opts.edit_id[0]}`, + undefined, + ); - return { - channel: opts.channel, - id: opts.edit_id, - }; - } - } catch (e) { return { channel: opts.channel, - error: e as Error, + id: opts.edit_id, }; } } diff --git a/packages/lightning-plugin-telegram/src/mod.ts b/packages/lightning-plugin-telegram/src/mod.ts index 4e211d9..b461071 100644 --- a/packages/lightning-plugin-telegram/src/mod.ts +++ b/packages/lightning-plugin-telegram/src/mod.ts @@ -58,69 +58,61 @@ export class telegram_plugin extends plugin { /** process a message event */ async process_message(opts: message_options): Promise { - try { - if (opts.action === 'delete') { - for (const id of opts.edit_id) { - await this.bot.api.deleteMessage( - opts.channel.id, - Number(id), - ); - } + if (opts.action === 'delete') { + for (const id of opts.edit_id) { + await this.bot.api.deleteMessage( + opts.channel.id, + Number(id), + ); + } + + return { + id: opts.edit_id, + channel: opts.channel, + }; + } else if (opts.action === 'edit') { + const content = from_lightning(opts.message)[0]; + + await this.bot.api.editMessageText( + opts.channel.id, + Number(opts.edit_id[0]), + content.value, + { + parse_mode: 'MarkdownV2', + }, + ); - return { - id: opts.edit_id, - channel: opts.channel, - }; - } else if (opts.action === 'edit') { - const content = from_lightning(opts.message)[0]; + return { + id: opts.edit_id, + channel: opts.channel, + }; + } else if (opts.action === 'create') { + const content = from_lightning(opts.message); + const messages = []; - await this.bot.api.editMessageText( + for (const msg of content) { + const result = await this.bot.api[msg.function]( opts.channel.id, - Number(opts.edit_id[0]), - content.value, + msg.value, { + reply_parameters: opts.reply_id + ? { + message_id: Number(opts.reply_id), + } + : undefined, parse_mode: 'MarkdownV2', }, ); - return { - id: opts.edit_id, - channel: opts.channel, - }; - } else if (opts.action === 'create') { - const content = from_lightning(opts.message); - const messages = []; - - for (const msg of content) { - const result = await this.bot.api[msg.function]( - opts.channel.id, - msg.value, - { - reply_parameters: opts.reply_id - ? { - message_id: Number(opts.reply_id), - } - : undefined, - parse_mode: 'MarkdownV2', - }, - ); - - messages.push(String(result.message_id)); - } - - return { - id: messages, - channel: opts.channel, - }; - } else { - throw new Error('unknown action'); + messages.push(String(result.message_id)); } - } catch (e) { - // TODO(@williamhorning): improve error handling logic + return { - error: e as Error, + id: messages, channel: opts.channel, }; + } else { + throw new Error('unknown action'); } } } From a349663b2c957d0dad41204b8067d038c62f17ad Mon Sep 17 00:00:00 2001 From: Jersey Date: Wed, 27 Nov 2024 22:44:36 -0500 Subject: [PATCH 09/23] working postgres --- packages/lightning/src/bridge/cmd.ts | 36 +++++--- packages/lightning/src/bridge/data.ts | 126 ++++++++++++++------------ packages/lightning/src/bridge/msg.ts | 6 +- packages/lightning/src/cli/mod.ts | 3 +- packages/lightning/src/lightning.ts | 52 +++++------ 5 files changed, 117 insertions(+), 106 deletions(-) diff --git a/packages/lightning/src/bridge/cmd.ts b/packages/lightning/src/bridge/cmd.ts index 4ff4d93..59a74e1 100644 --- a/packages/lightning/src/bridge/cmd.ts +++ b/packages/lightning/src/bridge/cmd.ts @@ -26,17 +26,20 @@ export const bridge_command = { if (!opts.id && !opts.name) { return `You must provide either an id or a name`; } + + const p = lightning.plugins.get(plugin); - const bridge_channel = { - id: channel, - data: undefined as unknown, - disabled: false, - plugin, - }; + if (!p) return (await log_error( + new Error('plugin not found'), + { + plugin, + }, + )).message.content as string; + + let data; try { - bridge_channel.data = lightning.plugins.get(plugin) - ?.create_bridge(channel); + data = await p.create_bridge(channel); } catch (e) { return (await log_error( new Error('error creating bridge', { cause: e }), @@ -47,6 +50,13 @@ export const bridge_command = { )).message.content as string; } + const bridge_channel = { + id: channel, + data, + disabled: false, + plugin, + }; + if (opts.id) { const bridge = await lightning.data.get_bridge_by_id( opts.id, @@ -57,7 +67,7 @@ export const bridge_command = { bridge.channels.push(bridge_channel); try { - await lightning.data.update_bridge(bridge); + await lightning.data.edit_bridge(bridge); return `Bridge joined successfully`; } catch (e) { return (await log_error( @@ -69,7 +79,7 @@ export const bridge_command = { } } else { try { - await lightning.data.new_bridge({ + await lightning.data.create_bridge({ name: opts.name, channels: [bridge_channel], settings: { @@ -113,7 +123,7 @@ export const bridge_command = { ) => ch.id !== channel); try { - await lightning.data.update_bridge( + await lightning.data.edit_bridge( bridge, ); return `Bridge left successfully`; @@ -151,7 +161,7 @@ export const bridge_command = { .settings[key]; try { - await lightning.data.update_bridge( + await lightning.data.edit_bridge( bridge, ); return `Setting toggled successfully`; @@ -176,7 +186,7 @@ export const bridge_command = { if (!existing_bridge) return `You are not in a bridge`; - return `You are in a bridge called ${existing_bridge.name} that's connected to ${ + return `You are in a bridge called "${existing_bridge.name}" that's connected to ${ existing_bridge.channels.length - 1 } other channels`; }, diff --git a/packages/lightning/src/bridge/data.ts b/packages/lightning/src/bridge/data.ts index d16fa87..fdd9af8 100644 --- a/packages/lightning/src/bridge/data.ts +++ b/packages/lightning/src/bridge/data.ts @@ -42,7 +42,7 @@ export class bridge_data { const pg = new Client(pg_options); await pg.connect(); - await this.create_table(pg); + await bridge_data.create_table(pg); return new bridge_data(pg); } @@ -53,92 +53,100 @@ export class bridge_data { if (exists) return; - await pg.queryArray`CREATE TABLE bridges ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - channels JSONB NOT NULL, - settings JSONB NOT NULL - )`; - await pg.queryArray`CREATE TABLE bridge_messages ( - id TEXT PRIMARY KEY, - bridge_id TEXT NOT NULL, - channels JSONB NOT NULL, - messages JSONB NOT NULL, - settings JSONB NOT NULL - )`; + await pg.queryArray` + CREATE TABLE bridges ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + channels JSONB NOT NULL, + settings JSONB NOT NULL + ); + + CREATE TABLE bridge_messages ( + id TEXT PRIMARY KEY, + bridge_id TEXT NOT NULL, + channels JSONB NOT NULL, + messages JSONB NOT NULL, + settings JSONB NOT NULL + ); + `; } private constructor(pg_client: Client) { this.pg = pg_client; } - async new_bridge(bridge: Omit): Promise { + async create_bridge(br: Omit): Promise { const id = ulid(); - await this.pg.queryArray`INSERT INTO bridges - (id, name, channels, settings) VALUES - (${id}, ${bridge.name}, ${bridge.channels}, ${bridge.settings})`; + await this.pg.queryArray` + INSERT INTO bridges (id, name, channels, settings) + VALUES (${id}, ${br.name}, ${JSON.stringify(br.channels)}, ${JSON.stringify(br.settings)}) + `; - return { id, ...bridge }; + return { id, ...br }; } - async update_bridge(bridge: {channels: bridge_channel[], settings: bridge_settings, id: string}): Promise { - await this.pg.queryArray`UPDATE bridges SET - channels = ${bridge.channels}, - settings = ${bridge.settings} - WHERE id = ${bridge.id}`; + async edit_bridge(br: Omit): Promise { + await this.pg.queryArray` + UPDATE bridges + SET channels = ${JSON.stringify(br.channels)}, settings = ${JSON.stringify(br.settings)} + WHERE id = ${br.id} + `; } async get_bridge_by_id(id: string): Promise { - const resp = await this.pg.queryObject` - SELECT * FROM bridges WHERE id = ${id}`; + const res = await this.pg.queryObject` + SELECT * FROM bridges + WHERE id = ${id} + `; - return resp.rows[0]; + return res.rows[0]; } - async get_bridge_by_channel(channel: string): Promise { - const resp = await this.pg.queryObject` - SELECT * FROM bridges WHERE JSON_QUERY(channels, '$[*].id') = ${channel}`; + async get_bridge_by_channel(ch: string): Promise { + const res = await this.pg.queryObject(` + SELECT * FROM bridges + WHERE EXISTS ( + SELECT 1 FROM jsonb_array_elements(channels) AS ch + WHERE ch->>'id' = '${ch}' + ) + `); - return resp.rows[0]; + return res.rows[0]; } - async new_bridge_message(message: bridge_message): Promise { + async create_bridge_message(msg: bridge_message): Promise { await this.pg.queryArray`INSERT INTO bridge_messages (id, bridge_id, channels, messages, settings) VALUES - (${message.id}, ${message.bridge_id}, ${message.channels}, ${message.messages}, ${message.settings})`; - - return message; + (${msg.id}, ${msg.bridge_id}, ${JSON.stringify(msg.channels)}, ${JSON.stringify(msg.messages)}, ${JSON.stringify(msg.settings)})`; } - async update_bridge_message( - message: bridge_message, - ): Promise { - await this.pg.queryArray`UPDATE bridge_messages SET - channels = ${message.channels}, - messages = ${message.messages}, - settings = ${message.settings} - WHERE id = ${message.id}`; - - return message; + async edit_bridge_message(msg: bridge_message): Promise { + await this.pg.queryArray` + UPDATE bridge_messages + SET messages = ${JSON.stringify(msg.messages)}, channels = ${JSON.stringify(msg.channels)}, settings = ${JSON.stringify(msg.settings)} + WHERE id = ${msg.id} + `; } - async delete_bridge_message(id: string): Promise { - await this.pg - .queryArray`DELETE FROM bridge_messages WHERE original_id = ${id}`; + async delete_bridge_message({ id }: bridge_message): Promise { + await this.pg.queryArray` + DELETE FROM bridge_messages WHERE id = ${id} + `; } async get_bridge_message(id: string): Promise { - const resp = await this.pg.queryObject` - SELECT * FROM bridge_messages WHERE original_id = ${id}`; - - return resp.rows[0]; - } - - async is_bridged_message(id: string): Promise { - const resp = await this.pg.queryObject` - SELECT * FROM bridge_messages WHERE JSON_QUERY(messages, '$[*].id') = ${id}`; - - return resp.rows.length > 0; + const res = await this.pg.queryObject(` + SELECT * FROM bridge_messages + WHERE id = '${id}' OR EXISTS ( + SELECT 1 FROM jsonb_array_elements(messages) AS msg + WHERE EXISTS ( + SELECT 1 FROM jsonb_array_elements(msg->'id') AS id + WHERE id = '${id}' + ) + ) + `); + + return res.rows[0]; } } diff --git a/packages/lightning/src/bridge/msg.ts b/packages/lightning/src/bridge/msg.ts index 1e5f186..47525d3 100644 --- a/packages/lightning/src/bridge/msg.ts +++ b/packages/lightning/src/bridge/msg.ts @@ -126,9 +126,7 @@ export async function handle_message( }); } - const method = type === 'create' ? 'new' : 'update'; - - await core.data[`${method}_bridge_message`]({ + await core.data[`${type}_bridge_message`]({ ...br, id: msg.id, messages, @@ -144,7 +142,7 @@ async function disable_channel( ): Promise { await log_error(error, { channel, bridge }); - await core.data.update_bridge({ + await core.data.edit_bridge({ id: "bridge_id" in bridge ? bridge.bridge_id : bridge.id, channels: bridge.channels.map((i) => i.id === channel.id && i.plugin === channel.plugin diff --git a/packages/lightning/src/cli/mod.ts b/packages/lightning/src/cli/mod.ts index fa945bb..02ba47e 100644 --- a/packages/lightning/src/cli/mod.ts +++ b/packages/lightning/src/cli/mod.ts @@ -11,9 +11,10 @@ if (_.v || _.version) { } else if (_.h || _.help) { run_help(); } else if (_._[0] === 'run') { + // TODO(jersey): this is somewhat broken when acting as a JSR package if (!_.config) _.config = join(Deno.cwd(), 'config.ts'); - const config: config = await import(toFileUrl(_.config).toString()); + const config: config = (await import(toFileUrl(_.config).toString())).default; addEventListener('error', async (ev) => { await log_error(ev.error, { type: 'global error' }); diff --git a/packages/lightning/src/lightning.ts b/packages/lightning/src/lightning.ts index d7c2d7d..d7623fb 100644 --- a/packages/lightning/src/lightning.ts +++ b/packages/lightning/src/lightning.ts @@ -10,6 +10,7 @@ import { handle_message } from './bridge/msg.ts'; import { run_command } from './commands/run.ts'; import { handle_command_message } from './commands/run.ts'; import type { message } from './messages.ts'; +import { bridge_command } from './bridge/cmd.ts'; /** configuration options for lightning */ export interface config { @@ -17,7 +18,7 @@ export interface config { postgres_options: ClientOptions; /** a list of plugins */ // deno-lint-ignore no-explicit-any - plugins?: create_plugin>[]; + plugins?: create_plugin[]; /** the prefix used for commands */ cmd_prefix: string; } @@ -30,8 +31,6 @@ export class lightning { commands: Map = new Map(default_commands); /** the config used */ config: config; - /** set of processed messages */ - private processed: Set<`${string}-${string}`> = new Set(); /** the plugins loaded */ plugins: Map>; @@ -39,6 +38,7 @@ export class lightning { constructor(bridge_data: bridge_data, config: config) { this.data = bridge_data; this.config = config; + this.commands.set('bridge', bridge_command); this.plugins = new Map>(); for (const p of this.config.plugins || []) { @@ -51,38 +51,32 @@ export class lightning { } private async _handle_events(plugin: plugin) { - for await (const event of plugin) { + for await (const { name, value } of plugin) { await new Promise((res) => setTimeout(res, 150)); - const id = `${event.value[0].plugin}-${event.value[0].id}` as const; + if (sessionStorage.getItem(`${value[0].plugin}-${value[0].id}`)) continue; - if (!this.processed.has(id)) { - this.processed.add(id); + if (name === 'run_command') { + run_command({ + ...(value[0] as Omit< + command_arguments, + 'lightning' + >), + lightning: this, + }); - if (event.name === 'run_command') { - run_command({ - ...(event.value[0] as Omit< - command_arguments, - 'lightning' - >), - lightning: this, - }); - - continue; - } - - if (event.name === 'create_message') { - handle_command_message(event.value[0] as message, this); - } + continue; + } - handle_message( - this, - event.value[0] as message, - event.name.split('_')[0] as 'create', - ); - } else { - this.processed.delete(id); + if (name === 'create_message') { + handle_command_message(value[0] as message, this); } + + handle_message( + this, + value[0] as message, + name.split('_')[0] as 'create', + ); } } From d9a8aa55473c800d8f7839c00245fdadb96f0551 Mon Sep 17 00:00:00 2001 From: Jersey Date: Thu, 28 Nov 2024 00:28:54 -0500 Subject: [PATCH 10/23] commands v2 starter --- .../lightning-plugin-discord/src/commands.ts | 2 + packages/lightning/src/bridge/cmd.ts | 196 ------------------ packages/lightning/src/commands/mod.ts | 80 ------- packages/lightning/src/commands/run.ts | 2 + .../lightning/src/commands_v2/bridge/add.ts | 120 +++++++++++ .../lightning/src/commands_v2/bridge/mod.ts | 54 +++++ .../src/commands_v2/bridge/modify.ts | 61 ++++++ .../src/commands_v2/bridge/status.ts | 26 +++ packages/lightning/src/commands_v2/mod.ts | 54 +++++ packages/lightning/src/lightning.ts | 17 +- packages/lightning/src/mod.ts | 6 +- packages/lightning/src/plugins.ts | 4 +- 12 files changed, 325 insertions(+), 297 deletions(-) delete mode 100644 packages/lightning/src/bridge/cmd.ts delete mode 100644 packages/lightning/src/commands/mod.ts create mode 100644 packages/lightning/src/commands_v2/bridge/add.ts create mode 100644 packages/lightning/src/commands_v2/bridge/mod.ts create mode 100644 packages/lightning/src/commands_v2/bridge/modify.ts create mode 100644 packages/lightning/src/commands_v2/bridge/status.ts create mode 100644 packages/lightning/src/commands_v2/mod.ts diff --git a/packages/lightning-plugin-discord/src/commands.ts b/packages/lightning-plugin-discord/src/commands.ts index 1915396..8b6e24f 100644 --- a/packages/lightning-plugin-discord/src/commands.ts +++ b/packages/lightning-plugin-discord/src/commands.ts @@ -4,6 +4,8 @@ import type { APIInteraction } from 'discord-api-types'; import { to_discord } from './discord.ts'; import { instant } from './lightning.ts'; +// TODO(jersey): migrate over to commands_v2 + export function to_command(interaction: { api: API; data: APIInteraction }) { if (interaction.data.type !== 2 || interaction.data.data.type !== 1) return; const opts = {} as Record; diff --git a/packages/lightning/src/bridge/cmd.ts b/packages/lightning/src/bridge/cmd.ts deleted file mode 100644 index 59a74e1..0000000 --- a/packages/lightning/src/bridge/cmd.ts +++ /dev/null @@ -1,196 +0,0 @@ -import type { command } from '../commands/mod.ts'; -import { log_error } from '../errors.ts'; - -export const bridge_command = { - name: 'bridge', - description: 'Bridge commands', - execute: () => 'Take a look at the docs for help with bridges', - options: { - subcommands: [ - { - name: 'join', - description: 'Join a bridge', - options: { argument_name: 'name', argument_required: true }, - execute: async ({ lightning, channel, opts, plugin }) => { - const current_bridge = await lightning.data - .get_bridge_by_channel( - channel, - ); - - if (current_bridge) { - return `You are already in a bridge called ${current_bridge.name}`; - } - if (opts.id && opts.name) { - return `You can only provide an id or a name, not both`; - } - if (!opts.id && !opts.name) { - return `You must provide either an id or a name`; - } - - const p = lightning.plugins.get(plugin); - - if (!p) return (await log_error( - new Error('plugin not found'), - { - plugin, - }, - )).message.content as string; - - let data; - - try { - data = await p.create_bridge(channel); - } catch (e) { - return (await log_error( - new Error('error creating bridge', { cause: e }), - { - channel, - plugin_name: plugin, - }, - )).message.content as string; - } - - const bridge_channel = { - id: channel, - data, - disabled: false, - plugin, - }; - - if (opts.id) { - const bridge = await lightning.data.get_bridge_by_id( - opts.id, - ); - - if (!bridge) return `No bridge found with that id`; - - bridge.channels.push(bridge_channel); - - try { - await lightning.data.edit_bridge(bridge); - return `Bridge joined successfully`; - } catch (e) { - return (await log_error( - new Error('error updating bridge', { cause: e }), - { - bridge, - }, - )).message.content as string; - } - } else { - try { - await lightning.data.create_bridge({ - name: opts.name, - channels: [bridge_channel], - settings: { - allow_editing: true, - allow_everyone: false, - use_rawname: false, - }, - }); - return `Bridge joined successfully`; - } catch (e) { - return (await log_error( - new Error('error inserting bridge', { cause: e }), - { - bridge: { - name: opts.name, - channels: [bridge_channel], - settings: { - allow_editing: true, - allow_everyone: false, - use_rawname: false, - }, - }, - }, - )).message.content as string; - } - } - }, - }, - { - name: 'leave', - description: 'Leave a bridge', - execute: async ({ lightning, channel }) => { - const bridge = await lightning.data.get_bridge_by_channel( - channel, - ); - - if (!bridge) return `You are not in a bridge`; - - bridge.channels = bridge.channels.filter(( - ch, - ) => ch.id !== channel); - - try { - await lightning.data.edit_bridge( - bridge, - ); - return `Bridge left successfully`; - } catch (e) { - return await log_error( - new Error('error updating bridge', { cause: e }), - { - bridge, - }, - ); - } - }, - }, - { - name: 'toggle', - description: 'Toggle a setting on a bridge', - options: { argument_name: 'setting', argument_required: true }, - execute: async ({ opts, lightning, channel }) => { - const bridge = await lightning.data.get_bridge_by_channel( - channel, - ); - - if (!bridge) return `You are not in a bridge`; - - if ( - !['allow_editing', 'allow_everyone', 'use_rawname'] - .includes(opts.setting) - ) { - return `That setting does not exist`; - } - - const key = opts.setting as keyof typeof bridge.settings; - - bridge.settings[key] = !bridge - .settings[key]; - - try { - await lightning.data.edit_bridge( - bridge, - ); - return `Setting toggled successfully`; - } catch (e) { - return await log_error( - new Error('error updating bridge', { cause: e }), - { - bridge, - }, - ); - } - }, - }, - { - name: 'status', - description: 'See what bridges you are in', - execute: async ({ lightning, channel }) => { - const existing_bridge = await lightning.data - .get_bridge_by_channel( - channel, - ); - - if (!existing_bridge) return `You are not in a bridge`; - - return `You are in a bridge called "${existing_bridge.name}" that's connected to ${ - existing_bridge.channels.length - 1 - } other channels`; - }, - }, - ], - }, -} as command; diff --git a/packages/lightning/src/commands/mod.ts b/packages/lightning/src/commands/mod.ts deleted file mode 100644 index 0eadcff..0000000 --- a/packages/lightning/src/commands/mod.ts +++ /dev/null @@ -1,80 +0,0 @@ -import type { lightning } from '../lightning.ts'; -import type { message } from '../messages.ts'; - -/** arguments passed to a command */ -export interface command_arguments { - /** the name of the command */ - cmd: string; - /** the subcommand being run, if any */ - subcmd?: string; - /** the channel its being run in */ - channel: string; - /** the plugin its being run on */ - plugin: string; - /** the id of the associated event */ - id: string; - /** timestamp given */ - timestamp: Temporal.Instant; - /** options passed by the user */ - opts: Record; - /** the function to reply to the command */ - reply: (message: message, optional?: unknown) => Promise; - /** the instance of lightning the command is ran against */ - lightning: lightning; -} - -/** options when parsing a command */ -// TODO(jersey): make the options more flexible -export interface command_options { - /** this will be the key passed to options.opts in the execute function */ - argument_name?: string; - /** whether or not the argument provided is required */ - argument_required?: boolean; - /** an array of commands that show as subcommands */ - subcommands?: command[]; -} - -/** commands are a way for users to interact with the bot */ -export interface command { - /** the name of the command */ - name: string; - /** an optional description */ - description?: string; - /** options when parsing the command */ - options?: command_options; - /** a function that returns a message */ - execute: (options: command_arguments) => Promise | string; -} - -export const default_commands = [ - [ - 'help', - { - name: 'help', - description: 'get help', - execute: () => - 'check out [the docs](https://williamhorning.eu.org/bolt/) for help.', - }, - ], - [ - 'version', - { - name: 'version', - description: 'get the bots version', - execute: () => 'hello from v0.7.4!', - }, - ], - [ - 'ping', - { - name: 'ping', - description: 'pong', - execute: ({ timestamp }) => - `Pong! 🏓 ${ - Temporal.Now.instant() - .since(timestamp) - .total('milliseconds') - }ms`, - }, - ], -] as [string, command][]; diff --git a/packages/lightning/src/commands/run.ts b/packages/lightning/src/commands/run.ts index 04b48f2..f803b07 100644 --- a/packages/lightning/src/commands/run.ts +++ b/packages/lightning/src/commands/run.ts @@ -4,6 +4,8 @@ import { log_error } from '../errors.ts'; import type { lightning } from '../lightning.ts'; import { parseArgs } from '@std/cli/parse-args'; +// TODO(jersey): migrate over to commands_v2 + export function handle_command_message(m: message, l: lightning) { if (!m.content?.startsWith(l.config.cmd_prefix)) return; diff --git a/packages/lightning/src/commands_v2/bridge/add.ts b/packages/lightning/src/commands_v2/bridge/add.ts new file mode 100644 index 0000000..6fde26a --- /dev/null +++ b/packages/lightning/src/commands_v2/bridge/add.ts @@ -0,0 +1,120 @@ +import type { bridge_channel } from '../../bridge/data.ts'; +import { log_error } from '../../errors.ts'; +import { create_message, type message } from '../../messages.ts'; +import type { command_execute_options } from '../mod.ts'; + +export async function create(opts: command_execute_options): Promise { + const result = await _lightning_bridge_add_common(opts, 'name'); + + if (!('data' in result)) return result; + + const bridge_data = { + name: opts.arguments.name, + channels: [result], + settings: { + allow_editing: true, + allow_everyone: false, + use_rawname: false, + }, + }; + + try { + await opts.lightning.data.create_bridge(bridge_data); + return create_message( + `Bridge created successfully! You can now join it using \`${opts.lightning.config.cmd_prefix}join ${result.id}\`. Keep this id safe, don't share it with anyone, and delete this message.`, + ); + } catch (e) { + return (await log_error( + new Error('Failed to insert bridge into database', { cause: e }), + bridge_data, + )).message; + } +} + +export async function join(opts: command_execute_options): Promise { + const target_bridge = await opts.lightning.data.get_bridge_by_id( + opts.arguments.id, + ); + + if (!target_bridge) { + return create_message( + `Bridge with id \`${opts.arguments.id}\` not found. Make sure you have the correct id.`, + ); + } + + const result = await _lightning_bridge_add_common(opts, 'id'); + + if (!('data' in result)) return result; + + target_bridge.channels.push(result); + + try { + await opts.lightning.data.edit_bridge(target_bridge); + + return create_message( + `Bridge joined successfully!`, + ); + } catch (e) { + return (await log_error( + new Error('Failed to update bridge in database', { + cause: e, + }), + { + bridge: target_bridge, + }, + )).message; + } +} + +async function _lightning_bridge_add_common( + opts: command_execute_options, + option_name: 'name' | 'id', +): Promise { + const existing_bridge = await opts.lightning.data.get_bridge_by_channel( + opts.channel, + ); + + if (existing_bridge) { + return create_message( + `You are already in a bridge called \`${existing_bridge.name}\`. You must leave it before being in another bridge. Try using \`${opts.lightning.config.cmd_prefix}leave\` or \`${opts.lightning.config.cmd_prefix}help\` commands.`, + ); + } + + if (!opts.arguments[option_name]) { + return create_message( + `Please provide the \`${option_name}\` argument. Try using \`${opts.lightning.config.cmd_prefix}help\` command.`, + ); + } + + const plugin = opts.lightning.plugins.get(opts.plugin); + + if (!plugin) { + return (await log_error( + new Error('Internal error: platform support not found'), + { + plugin: opts.plugin, + }, + )).message; + } + + let bridge_data; + + try { + bridge_data = await plugin.create_bridge(opts.channel); + } catch (e) { + return (await log_error( + new Error('Failed to create bridge using plugin', { cause: e }), + { + channel: opts.channel, + plugin_name: opts.plugin, + }, + )).message; + } + + return { + id: opts.channel, + data: bridge_data, + disabled: false, + plugin: opts.plugin, + }; +} diff --git a/packages/lightning/src/commands_v2/bridge/mod.ts b/packages/lightning/src/commands_v2/bridge/mod.ts new file mode 100644 index 0000000..b3fec42 --- /dev/null +++ b/packages/lightning/src/commands_v2/bridge/mod.ts @@ -0,0 +1,54 @@ +import { create, join } from './add.ts'; +import { leave, toggle } from './modify.ts'; +import { status } from './status.ts'; +import type { command } from '../mod.ts'; +import { create_message } from '../../messages.ts'; + +export const bridge_command = { + name: 'bridge', + description: 'bridge commands', + execute: () => + create_message('take a look at the subcommands of this command'), + subcommands: [ + { + name: 'create', + description: 'create a new bridge', + arguments: [{ + name: 'name', + description: 'name of the bridge', + required: true, + }], + execute: create, + }, + { + name: 'join', + description: 'join an existing bridge', + arguments: [{ + name: 'id', + description: 'id of the bridge', + required: true, + }], + execute: join, + }, + { + name: 'leave', + description: 'leave the current bridge', + execute: leave, + }, + { + name: 'toggle', + description: 'toggle a setting on the current bridge', + arguments: [{ + name: 'setting', + description: 'setting to toggle', + required: true, + }], + execute: toggle, + }, + { + name: 'status', + description: 'get the status of the current bridge', + execute: status, + }, + ], +} as command; diff --git a/packages/lightning/src/commands_v2/bridge/modify.ts b/packages/lightning/src/commands_v2/bridge/modify.ts new file mode 100644 index 0000000..e51844e --- /dev/null +++ b/packages/lightning/src/commands_v2/bridge/modify.ts @@ -0,0 +1,61 @@ +import { log_error } from '../../errors.ts'; +import { create_message, type message } from '../../messages.ts'; +import type { command_execute_options } from '../mod.ts'; + +export async function leave(opts: command_execute_options): Promise { + const bridge = await opts.lightning.data.get_bridge_by_channel( + opts.channel, + ); + + if (!bridge) return create_message(`You are not in a bridge`); + + bridge.channels = bridge.channels.filter(( + ch, + ) => ch.id !== opts.channel); + + try { + await opts.lightning.data.edit_bridge( + bridge, + ); + return create_message(`Bridge left successfully`); + } catch (e) { + return (await log_error( + new Error('Error updating bridge', { cause: e }), + { + bridge, + }, + )).message; + } +} + +const settings = ['allow_editing', 'allow_everyone', 'use_rawname']; + +export async function toggle(opts: command_execute_options): Promise { + const bridge = await opts.lightning.data.get_bridge_by_channel( + opts.channel, + ); + + if (!bridge) return create_message(`You are not in a bridge`); + + if (!settings.includes(opts.arguments.setting)) { + return create_message(`That setting does not exist`); + } + + const key = opts.arguments.setting as keyof typeof bridge.settings; + + bridge.settings[key] = !bridge.settings[key]; + + try { + await opts.lightning.data.edit_bridge( + bridge, + ); + return create_message(`Bridge settings updated successfully`); + } catch (e) { + return (await log_error( + new Error('Error updating bridge', { cause: e }), + { + bridge, + }, + )).message; + } +} diff --git a/packages/lightning/src/commands_v2/bridge/status.ts b/packages/lightning/src/commands_v2/bridge/status.ts new file mode 100644 index 0000000..c700b55 --- /dev/null +++ b/packages/lightning/src/commands_v2/bridge/status.ts @@ -0,0 +1,26 @@ +import { create_message, type message } from '../../messages.ts'; +import type { command_execute_options } from '../mod.ts'; + +export async function status(opts: command_execute_options): Promise { + const bridge = await opts.lightning.data.get_bridge_by_channel( + opts.channel, + ); + + if (!bridge) return create_message(`You are not in a bridge`); + + let str = `*Bridge status:*\n\n`; + str += `**Name:** ${bridge.name}\n`; + str += `**Channels:**\n`; + + for (const [i, value] of bridge.channels.entries()) { + str += `${i + 1}. \`${value.id}\` on \`${value.plugin}\`\n`; + } + + str += `\n**Settings:**\n`; + + for (const [key, value] of Object.entries(bridge.settings)) { + str += `\`${key}: ${value}\`\n`; + } + + return create_message(str); +} \ No newline at end of file diff --git a/packages/lightning/src/commands_v2/mod.ts b/packages/lightning/src/commands_v2/mod.ts new file mode 100644 index 0000000..4bdd45a --- /dev/null +++ b/packages/lightning/src/commands_v2/mod.ts @@ -0,0 +1,54 @@ +import { bridge_command } from './bridge/mod.ts'; +import type { lightning } from '../lightning.ts'; +import { create_message, type message } from '../messages.ts'; + +export interface command_execute_options { + channel: string; + plugin: string; + timestamp: Temporal.Instant; + arguments: Record; + lightning: lightning; + id: string; +} + +export interface command { + name: string; + description: string; + arguments?: { + name: string; + description: string; + required: boolean; + }[]; + subcommands?: command[]; + // TODO(jersey): message | string | Promise | Promise + execute: (opts: command_execute_options) => Promise | message; +} + +export const default_commands = [ + ['help', { + name: 'help', + description: 'get help with the bot', + execute: () => + create_message( + 'check out [the docs](https://williamhorning.eu.org/bolt/) for help.', + ), + }], + ['ping', { + name: 'ping', + description: 'check if the bot is alive', + execute: ({ timestamp }) => { + const diff = Temporal.Now.instant().since(timestamp).total( + 'milliseconds', + ); + return create_message(`Pong! 🏓 ${diff}ms`); + }, + }], + ['version', { + name: 'version', + description: 'get the bots version', + execute: () => create_message('hello from v0.8.0!'), + }], + ['bridge', bridge_command], +] as [string, command][]; + +// TODO(jersey): make command runners diff --git a/packages/lightning/src/lightning.ts b/packages/lightning/src/lightning.ts index d7623fb..8bc50a4 100644 --- a/packages/lightning/src/lightning.ts +++ b/packages/lightning/src/lightning.ts @@ -1,16 +1,12 @@ import type { ClientOptions } from '@db/postgres'; import { type command, - type command_arguments, default_commands, -} from './commands/mod.ts'; +} from './commands_v2/mod.ts'; import type { create_plugin, plugin } from './plugins.ts'; import { bridge_data } from './bridge/data.ts'; import { handle_message } from './bridge/msg.ts'; -import { run_command } from './commands/run.ts'; -import { handle_command_message } from './commands/run.ts'; import type { message } from './messages.ts'; -import { bridge_command } from './bridge/cmd.ts'; /** configuration options for lightning */ export interface config { @@ -38,7 +34,6 @@ export class lightning { constructor(bridge_data: bridge_data, config: config) { this.data = bridge_data; this.config = config; - this.commands.set('bridge', bridge_command); this.plugins = new Map>(); for (const p of this.config.plugins || []) { @@ -57,19 +52,13 @@ export class lightning { if (sessionStorage.getItem(`${value[0].plugin}-${value[0].id}`)) continue; if (name === 'run_command') { - run_command({ - ...(value[0] as Omit< - command_arguments, - 'lightning' - >), - lightning: this, - }); + // TODO(jersey): migrate over to commands_v2 continue; } if (name === 'create_message') { - handle_command_message(value[0] as message, this); + // TODO(jersey): migrate over to commands_v2 } handle_message( diff --git a/packages/lightning/src/mod.ts b/packages/lightning/src/mod.ts index 57180e6..6162908 100644 --- a/packages/lightning/src/mod.ts +++ b/packages/lightning/src/mod.ts @@ -1,8 +1,4 @@ -export type { - command, - command_arguments, - command_options, -} from './commands/mod.ts'; +// TODO(jersey): add exports from commands_v2 export { log_error } from './errors.ts'; export { type config, lightning } from './lightning.ts'; export * from './messages.ts'; diff --git a/packages/lightning/src/plugins.ts b/packages/lightning/src/plugins.ts index 775aa12..8b421e3 100644 --- a/packages/lightning/src/plugins.ts +++ b/packages/lightning/src/plugins.ts @@ -1,5 +1,4 @@ import { EventEmitter } from '@denosaurs/event'; -import type { command_arguments } from './commands/mod.ts'; import type { lightning } from './lightning.ts'; import type { deleted_message, @@ -7,6 +6,7 @@ import type { message_options, process_result, } from './messages.ts'; +import type { command_execute_options } from './commands_v2/mod.ts'; /** the way to make a plugin */ export interface create_plugin< @@ -29,7 +29,7 @@ export type plugin_events = { /** when a message is deleted */ delete_message: [deleted_message]; /** when a command is run */ - run_command: [Omit]; + run_command: [Omit]; }; /** a plugin for lightning */ From 06257719b4f6b329ec56901d94d0761f48810e98 Mon Sep 17 00:00:00 2001 From: Jersey Date: Thu, 28 Nov 2024 17:18:28 -0500 Subject: [PATCH 11/23] commands v2 --- .../{commands_v2 => commands}/bridge/add.ts | 62 ++++------ .../{commands_v2 => commands}/bridge/mod.ts | 4 +- .../bridge/modify.ts | 23 ++-- .../bridge/status.ts | 15 +-- packages/lightning/src/commands/default.ts | 26 ++++ packages/lightning/src/commands/mod.ts | 113 ++++++++++++++++++ packages/lightning/src/commands/run.ts | 48 -------- packages/lightning/src/commands_v2/mod.ts | 54 --------- packages/lightning/src/lightning.ts | 20 +++- packages/lightning/src/plugins.ts | 4 +- 10 files changed, 198 insertions(+), 171 deletions(-) rename packages/lightning/src/{commands_v2 => commands}/bridge/add.ts (54%) rename packages/lightning/src/{commands_v2 => commands}/bridge/mod.ts (90%) rename packages/lightning/src/{commands_v2 => commands}/bridge/modify.ts (70%) rename packages/lightning/src/{commands_v2 => commands}/bridge/status.ts (54%) create mode 100644 packages/lightning/src/commands/default.ts create mode 100644 packages/lightning/src/commands/mod.ts delete mode 100644 packages/lightning/src/commands/run.ts delete mode 100644 packages/lightning/src/commands_v2/mod.ts diff --git a/packages/lightning/src/commands_v2/bridge/add.ts b/packages/lightning/src/commands/bridge/add.ts similarity index 54% rename from packages/lightning/src/commands_v2/bridge/add.ts rename to packages/lightning/src/commands/bridge/add.ts index 6fde26a..3f2faaa 100644 --- a/packages/lightning/src/commands_v2/bridge/add.ts +++ b/packages/lightning/src/commands/bridge/add.ts @@ -1,12 +1,13 @@ import type { bridge_channel } from '../../bridge/data.ts'; import { log_error } from '../../errors.ts'; -import { create_message, type message } from '../../messages.ts'; import type { command_execute_options } from '../mod.ts'; -export async function create(opts: command_execute_options): Promise { - const result = await _lightning_bridge_add_common(opts, 'name'); +export async function create( + opts: command_execute_options, +): Promise { + const result = await _lightning_bridge_add_common(opts); - if (!('data' in result)) return result; + if (typeof result === 'string') return result; const bridge_data = { name: opts.arguments.name, @@ -20,81 +21,68 @@ export async function create(opts: command_execute_options): Promise { try { await opts.lightning.data.create_bridge(bridge_data); - return create_message( - `Bridge created successfully! You can now join it using \`${opts.lightning.config.cmd_prefix}join ${result.id}\`. Keep this id safe, don't share it with anyone, and delete this message.`, - ); + return `Bridge created successfully! You can now join it using \`${opts.lightning.config.cmd_prefix}join ${result.id}\`. Keep this id safe, don't share it with anyone, and delete this message.`; } catch (e) { - return (await log_error( + throw await log_error( new Error('Failed to insert bridge into database', { cause: e }), bridge_data, - )).message; + ); } } -export async function join(opts: command_execute_options): Promise { +export async function join( + opts: command_execute_options, +): Promise { + const result = await _lightning_bridge_add_common(opts); + + if (typeof result === 'string') return result; + const target_bridge = await opts.lightning.data.get_bridge_by_id( opts.arguments.id, ); if (!target_bridge) { - return create_message( - `Bridge with id \`${opts.arguments.id}\` not found. Make sure you have the correct id.`, - ); + return `Bridge with id \`${opts.arguments.id}\` not found. Make sure you have the correct id.`; } - const result = await _lightning_bridge_add_common(opts, 'id'); - - if (!('data' in result)) return result; - target_bridge.channels.push(result); try { await opts.lightning.data.edit_bridge(target_bridge); - return create_message( - `Bridge joined successfully!`, - ); + return `Bridge joined successfully!`; } catch (e) { - return (await log_error( + throw await log_error( new Error('Failed to update bridge in database', { cause: e, }), { bridge: target_bridge, }, - )).message; + ); } } async function _lightning_bridge_add_common( opts: command_execute_options, - option_name: 'name' | 'id', -): Promise { +): Promise { const existing_bridge = await opts.lightning.data.get_bridge_by_channel( opts.channel, ); if (existing_bridge) { - return create_message( - `You are already in a bridge called \`${existing_bridge.name}\`. You must leave it before being in another bridge. Try using \`${opts.lightning.config.cmd_prefix}leave\` or \`${opts.lightning.config.cmd_prefix}help\` commands.`, - ); - } - - if (!opts.arguments[option_name]) { - return create_message( - `Please provide the \`${option_name}\` argument. Try using \`${opts.lightning.config.cmd_prefix}help\` command.`, - ); + return `You are already in a bridge called \`${existing_bridge.name}\`. You must leave it before being in another bridge. Try using \`${opts.lightning.config.cmd_prefix}leave\` or \`${opts.lightning.config.cmd_prefix}help\` commands.` } const plugin = opts.lightning.plugins.get(opts.plugin); if (!plugin) { - return (await log_error( + throw await log_error( new Error('Internal error: platform support not found'), { plugin: opts.plugin, }, - )).message; + ); } let bridge_data; @@ -102,13 +90,13 @@ async function _lightning_bridge_add_common( try { bridge_data = await plugin.create_bridge(opts.channel); } catch (e) { - return (await log_error( + throw await log_error( new Error('Failed to create bridge using plugin', { cause: e }), { channel: opts.channel, plugin_name: opts.plugin, }, - )).message; + ); } return { diff --git a/packages/lightning/src/commands_v2/bridge/mod.ts b/packages/lightning/src/commands/bridge/mod.ts similarity index 90% rename from packages/lightning/src/commands_v2/bridge/mod.ts rename to packages/lightning/src/commands/bridge/mod.ts index b3fec42..fd9fba7 100644 --- a/packages/lightning/src/commands_v2/bridge/mod.ts +++ b/packages/lightning/src/commands/bridge/mod.ts @@ -2,13 +2,11 @@ import { create, join } from './add.ts'; import { leave, toggle } from './modify.ts'; import { status } from './status.ts'; import type { command } from '../mod.ts'; -import { create_message } from '../../messages.ts'; export const bridge_command = { name: 'bridge', description: 'bridge commands', - execute: () => - create_message('take a look at the subcommands of this command'), + execute: () => 'take a look at the subcommands of this command', subcommands: [ { name: 'create', diff --git a/packages/lightning/src/commands_v2/bridge/modify.ts b/packages/lightning/src/commands/bridge/modify.ts similarity index 70% rename from packages/lightning/src/commands_v2/bridge/modify.ts rename to packages/lightning/src/commands/bridge/modify.ts index e51844e..2d6c4b8 100644 --- a/packages/lightning/src/commands_v2/bridge/modify.ts +++ b/packages/lightning/src/commands/bridge/modify.ts @@ -1,13 +1,12 @@ import { log_error } from '../../errors.ts'; -import { create_message, type message } from '../../messages.ts'; import type { command_execute_options } from '../mod.ts'; -export async function leave(opts: command_execute_options): Promise { +export async function leave(opts: command_execute_options): Promise { const bridge = await opts.lightning.data.get_bridge_by_channel( opts.channel, ); - if (!bridge) return create_message(`You are not in a bridge`); + if (!bridge) return `You are not in a bridge`; bridge.channels = bridge.channels.filter(( ch, @@ -17,28 +16,28 @@ export async function leave(opts: command_execute_options): Promise { await opts.lightning.data.edit_bridge( bridge, ); - return create_message(`Bridge left successfully`); + return `Bridge left successfully`; } catch (e) { - return (await log_error( + throw await log_error( new Error('Error updating bridge', { cause: e }), { bridge, }, - )).message; + ); } } const settings = ['allow_editing', 'allow_everyone', 'use_rawname']; -export async function toggle(opts: command_execute_options): Promise { +export async function toggle(opts: command_execute_options): Promise { const bridge = await opts.lightning.data.get_bridge_by_channel( opts.channel, ); - if (!bridge) return create_message(`You are not in a bridge`); + if (!bridge) return `You are not in a bridge`; if (!settings.includes(opts.arguments.setting)) { - return create_message(`That setting does not exist`); + return `That setting does not exist`; } const key = opts.arguments.setting as keyof typeof bridge.settings; @@ -49,13 +48,13 @@ export async function toggle(opts: command_execute_options): Promise { await opts.lightning.data.edit_bridge( bridge, ); - return create_message(`Bridge settings updated successfully`); + return `Bridge settings updated successfully`; } catch (e) { - return (await log_error( + throw await log_error( new Error('Error updating bridge', { cause: e }), { bridge, }, - )).message; + ); } } diff --git a/packages/lightning/src/commands_v2/bridge/status.ts b/packages/lightning/src/commands/bridge/status.ts similarity index 54% rename from packages/lightning/src/commands_v2/bridge/status.ts rename to packages/lightning/src/commands/bridge/status.ts index c700b55..27fefc0 100644 --- a/packages/lightning/src/commands_v2/bridge/status.ts +++ b/packages/lightning/src/commands/bridge/status.ts @@ -1,26 +1,23 @@ -import { create_message, type message } from '../../messages.ts'; import type { command_execute_options } from '../mod.ts'; -export async function status(opts: command_execute_options): Promise { +export async function status(opts: command_execute_options): Promise { const bridge = await opts.lightning.data.get_bridge_by_channel( opts.channel, ); - if (!bridge) return create_message(`You are not in a bridge`); + if (!bridge) return `You are not in a bridge`; - let str = `*Bridge status:*\n\n`; - str += `**Name:** ${bridge.name}\n`; - str += `**Channels:**\n`; + let str = `Name: \`${bridge.name}\`\n\nChannels:\n`; for (const [i, value] of bridge.channels.entries()) { str += `${i + 1}. \`${value.id}\` on \`${value.plugin}\`\n`; } - str += `\n**Settings:**\n`; + str += `\nSettings:\n`; for (const [key, value] of Object.entries(bridge.settings)) { - str += `\`${key}: ${value}\`\n`; + str += `- \`${key}\` ${value ? "✔" : "❌"}\n`; } - return create_message(str); + return str; } \ No newline at end of file diff --git a/packages/lightning/src/commands/default.ts b/packages/lightning/src/commands/default.ts new file mode 100644 index 0000000..1405876 --- /dev/null +++ b/packages/lightning/src/commands/default.ts @@ -0,0 +1,26 @@ +import { bridge_command } from './bridge/mod.ts'; +import type { command } from './mod.ts'; + +export const default_commands = new Map([ + ['help', { + name: 'help', + description: 'get help with the bot', + execute: () => + 'check out [the docs](https://williamhorning.eu.org/bolt/) for help.', + }], + ['ping', { + name: 'ping', + description: 'check if the bot is alive', + execute: ({ timestamp }) => + `Pong! 🏓 ${ + Temporal.Now.instant().since(timestamp).round('millisecond') + .total('milliseconds') + }ms`, + }], + ['version', { + name: 'version', + description: 'get the bots version', + execute: () => 'hello from v0.8.0!', + }], + ['bridge', bridge_command], +]) as Map; diff --git a/packages/lightning/src/commands/mod.ts b/packages/lightning/src/commands/mod.ts new file mode 100644 index 0000000..23d9be9 --- /dev/null +++ b/packages/lightning/src/commands/mod.ts @@ -0,0 +1,113 @@ +import type { lightning } from '../lightning.ts'; +import { create_message, type message } from '../messages.ts'; +import { parseArgs } from '@std/cli/parse-args'; +import { type err, log_error } from '../errors.ts'; + +export interface command_execute_options { + channel: string; + plugin: string; + timestamp: Temporal.Instant; + arguments: Record; + lightning: lightning; + id: string; +} + +export interface command { + name: string; + description: string; + arguments?: { + name: string; + description: string; + required: boolean; + }[]; + subcommands?: Omit[]; + execute: ( + opts: command_execute_options, + ) => Promise | string | err; +} + +export async function execute_text_command(msg: message, lightning: lightning) { + if (!msg.content?.startsWith(lightning.config.cmd_prefix)) return; + + const { + _: [cmd, ...rest], + ...args + } = parseArgs( + msg.content.replace(lightning.config.cmd_prefix, '').split(' '), + ); + + return await run_command({ + ...msg, + lightning, + command: cmd as string, + rest: rest as string[], + args, + }); +} + +export interface run_command_options + extends Omit { + command: string; + subcommand?: string; + args?: Record; + rest?: string[]; + reply: message['reply']; +} + +export async function run_command( + opts: run_command_options, +) { + let command = opts.lightning.commands.get(opts.command) ?? + opts.lightning.commands.get('help')!; + + const subcommand_name = opts.subcommand ?? opts.rest?.shift(); + + if (command.subcommands && subcommand_name) { + const subcommand = command.subcommands.find((i) => + i.name === subcommand_name + ); + + if (subcommand) command = subcommand; + } + + if (!opts.args) opts.args = {}; + + for (const arg of command.arguments || []) { + if (!opts.args[arg.name]) { + opts.args[arg.name] = opts.rest?.shift() as string; + } + + if (!opts.args[arg.name]) { + return opts.reply( + create_message( + `Please provide the \`${arg.name}\` argument. Try using the \`${opts.lightning.config.cmd_prefix}help\` command.`, + ), + false, + ); + } + } + + let resp: string | err; + + try { + resp = await command.execute({ + ...opts, + arguments: opts.args, + }); + } catch (e) { + // TODO(jersey): we should have err be a class that extends Error so checking this is easier + if (typeof e === 'object' && e !== null && 'cause' in e) { + resp = e as err; + } else { + resp = await log_error(e, { ...opts, reply: undefined }); + } + } + + try { + if (typeof resp === 'string') { + await opts.reply(create_message(resp), false); + } else await opts.reply(resp.message, false); + } catch (e) { + await log_error(e, { ...opts, reply: undefined }); + } +} diff --git a/packages/lightning/src/commands/run.ts b/packages/lightning/src/commands/run.ts deleted file mode 100644 index f803b07..0000000 --- a/packages/lightning/src/commands/run.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { command_arguments } from './mod.ts'; -import { create_message, type message } from '../messages.ts'; -import { log_error } from '../errors.ts'; -import type { lightning } from '../lightning.ts'; -import { parseArgs } from '@std/cli/parse-args'; - -// TODO(jersey): migrate over to commands_v2 - -export function handle_command_message(m: message, l: lightning) { - if (!m.content?.startsWith(l.config.cmd_prefix)) return; - - const { - _: [cmd, subcmd], - ...opts - } = parseArgs(m.content.replace(l.config.cmd_prefix, '').split(' ')); - - run_command({ - lightning: l, - cmd: cmd as string, - subcmd: subcmd as string, - opts, - ...m, - }); -} - -export async function run_command(args: command_arguments) { - let reply; - - try { - const cmd = args.lightning.commands.get(args.cmd) || - args.lightning.commands.get('help')!; - - const exec = cmd.options?.subcommands?.find((i) => - i.name === args.subcmd - )?.execute || - cmd.execute; - - reply = create_message(await exec(args)); - } catch (e) { - reply = (await log_error(e, { ...args, reply: undefined })).message; - } - - try { - await args.reply(reply, false); - } catch (e) { - await log_error(e, { ...args, reply: undefined }); - } -} diff --git a/packages/lightning/src/commands_v2/mod.ts b/packages/lightning/src/commands_v2/mod.ts deleted file mode 100644 index 4bdd45a..0000000 --- a/packages/lightning/src/commands_v2/mod.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { bridge_command } from './bridge/mod.ts'; -import type { lightning } from '../lightning.ts'; -import { create_message, type message } from '../messages.ts'; - -export interface command_execute_options { - channel: string; - plugin: string; - timestamp: Temporal.Instant; - arguments: Record; - lightning: lightning; - id: string; -} - -export interface command { - name: string; - description: string; - arguments?: { - name: string; - description: string; - required: boolean; - }[]; - subcommands?: command[]; - // TODO(jersey): message | string | Promise | Promise - execute: (opts: command_execute_options) => Promise | message; -} - -export const default_commands = [ - ['help', { - name: 'help', - description: 'get help with the bot', - execute: () => - create_message( - 'check out [the docs](https://williamhorning.eu.org/bolt/) for help.', - ), - }], - ['ping', { - name: 'ping', - description: 'check if the bot is alive', - execute: ({ timestamp }) => { - const diff = Temporal.Now.instant().since(timestamp).total( - 'milliseconds', - ); - return create_message(`Pong! 🏓 ${diff}ms`); - }, - }], - ['version', { - name: 'version', - description: 'get the bots version', - execute: () => create_message('hello from v0.8.0!'), - }], - ['bridge', bridge_command], -] as [string, command][]; - -// TODO(jersey): make command runners diff --git a/packages/lightning/src/lightning.ts b/packages/lightning/src/lightning.ts index 8bc50a4..44fa2ce 100644 --- a/packages/lightning/src/lightning.ts +++ b/packages/lightning/src/lightning.ts @@ -1,12 +1,15 @@ import type { ClientOptions } from '@db/postgres'; import { type command, - default_commands, -} from './commands_v2/mod.ts'; + execute_text_command, + run_command, + type run_command_options, +} from './commands/mod.ts'; import type { create_plugin, plugin } from './plugins.ts'; import { bridge_data } from './bridge/data.ts'; import { handle_message } from './bridge/msg.ts'; import type { message } from './messages.ts'; +import { default_commands } from './commands/default.ts'; /** configuration options for lightning */ export interface config { @@ -24,7 +27,7 @@ export class lightning { /** bridge data handling */ data: bridge_data; /** the commands registered */ - commands: Map = new Map(default_commands); + commands: Map = default_commands; /** the config used */ config: config; /** the plugins loaded */ @@ -49,16 +52,21 @@ export class lightning { for await (const { name, value } of plugin) { await new Promise((res) => setTimeout(res, 150)); - if (sessionStorage.getItem(`${value[0].plugin}-${value[0].id}`)) continue; + if (sessionStorage.getItem(`${value[0].plugin}-${value[0].id}`)) { + continue; + } if (name === 'run_command') { - // TODO(jersey): migrate over to commands_v2 + run_command({ + ...value[0], + lightning: this, + } as run_command_options); continue; } if (name === 'create_message') { - // TODO(jersey): migrate over to commands_v2 + execute_text_command(value[0] as message, this); } handle_message( diff --git a/packages/lightning/src/plugins.ts b/packages/lightning/src/plugins.ts index 8b421e3..19c6d76 100644 --- a/packages/lightning/src/plugins.ts +++ b/packages/lightning/src/plugins.ts @@ -6,7 +6,7 @@ import type { message_options, process_result, } from './messages.ts'; -import type { command_execute_options } from './commands_v2/mod.ts'; +import type { run_command_options } from './commands/mod.ts'; /** the way to make a plugin */ export interface create_plugin< @@ -29,7 +29,7 @@ export type plugin_events = { /** when a message is deleted */ delete_message: [deleted_message]; /** when a command is run */ - run_command: [Omit]; + run_command: [Omit]; }; /** a plugin for lightning */ From 7ba9ec31f8183564706c836e2e13c7de7df8c62b Mon Sep 17 00:00:00 2001 From: Jersey Date: Thu, 28 Nov 2024 18:40:28 -0500 Subject: [PATCH 12/23] replace log_error in most places --- packages/lightning/src/cli/mod.ts | 15 +- packages/lightning/src/commands/bridge/add.ts | 42 ++---- .../lightning/src/commands/bridge/modify.ts | 22 ++- packages/lightning/src/commands/mod.ts | 33 ++--- packages/lightning/src/errors.ts | 137 ++++++++++++------ packages/lightning/src/mod.ts | 2 +- 6 files changed, 137 insertions(+), 114 deletions(-) diff --git a/packages/lightning/src/cli/mod.ts b/packages/lightning/src/cli/mod.ts index 02ba47e..8d1560a 100644 --- a/packages/lightning/src/cli/mod.ts +++ b/packages/lightning/src/cli/mod.ts @@ -1,6 +1,6 @@ import { parseArgs } from '@std/cli/parse-args'; import { join, toFileUrl } from '@std/path'; -import { log_error } from '../errors.ts'; +import { logError } from '../errors.ts'; import { type config, lightning } from '../lightning.ts'; const version = '0.8.0'; @@ -16,21 +16,18 @@ if (_.v || _.version) { const config: config = (await import(toFileUrl(_.config).toString())).default; - addEventListener('error', async (ev) => { - await log_error(ev.error, { type: 'global error' }); - Deno.exit(1); + addEventListener('error', (ev) => { + logError(ev.error, { extra: { type: 'global error' } }); }); - addEventListener('unhandledrejection', async (ev) => { - await log_error(ev.reason, { type: 'global rejection' }); - Deno.exit(1); + addEventListener('unhandledrejection', (ev) => { + logError(ev.reason, { extra: { type: 'global rejection' } }); }); try { await lightning.create(config); } catch (e) { - await log_error(e, { type: 'global class error' }); - Deno.exit(1); + logError(e, { extra: { type: 'global class error' } }); } } else if (_._[0] === 'migrations') { // TODO(jersey): implement migrations diff --git a/packages/lightning/src/commands/bridge/add.ts b/packages/lightning/src/commands/bridge/add.ts index 3f2faaa..9b7c14d 100644 --- a/packages/lightning/src/commands/bridge/add.ts +++ b/packages/lightning/src/commands/bridge/add.ts @@ -1,5 +1,5 @@ import type { bridge_channel } from '../../bridge/data.ts'; -import { log_error } from '../../errors.ts'; +import { logError } from '../../errors.ts'; import type { command_execute_options } from '../mod.ts'; export async function create( @@ -23,10 +23,10 @@ export async function create( await opts.lightning.data.create_bridge(bridge_data); return `Bridge created successfully! You can now join it using \`${opts.lightning.config.cmd_prefix}join ${result.id}\`. Keep this id safe, don't share it with anyone, and delete this message.`; } catch (e) { - throw await log_error( - new Error('Failed to insert bridge into database', { cause: e }), - bridge_data, - ); + logError(e, { + message: 'Failed to insert bridge into database', + extra: bridge_data + }); } } @@ -52,14 +52,10 @@ export async function join( return `Bridge joined successfully!`; } catch (e) { - throw await log_error( - new Error('Failed to update bridge in database', { - cause: e, - }), - { - bridge: target_bridge, - }, - ); + logError(e, { + message: 'Failed to update bridge in database', + extra: { target_bridge } + }) } } @@ -77,12 +73,9 @@ async function _lightning_bridge_add_common( const plugin = opts.lightning.plugins.get(opts.plugin); if (!plugin) { - throw await log_error( - new Error('Internal error: platform support not found'), - { - plugin: opts.plugin, - }, - ); + logError('Internal error: platform support not found', { + extra: { plugin: opts.plugin } + }) } let bridge_data; @@ -90,13 +83,10 @@ async function _lightning_bridge_add_common( try { bridge_data = await plugin.create_bridge(opts.channel); } catch (e) { - throw await log_error( - new Error('Failed to create bridge using plugin', { cause: e }), - { - channel: opts.channel, - plugin_name: opts.plugin, - }, - ); + logError(e, { + message: 'Failed to create bridge using plugin', + extra: { channel: opts.channel, plugin_name: opts.plugin } + }) } return { diff --git a/packages/lightning/src/commands/bridge/modify.ts b/packages/lightning/src/commands/bridge/modify.ts index 2d6c4b8..8cd33d6 100644 --- a/packages/lightning/src/commands/bridge/modify.ts +++ b/packages/lightning/src/commands/bridge/modify.ts @@ -1,4 +1,4 @@ -import { log_error } from '../../errors.ts'; +import { logError } from '../../errors.ts'; import type { command_execute_options } from '../mod.ts'; export async function leave(opts: command_execute_options): Promise { @@ -18,12 +18,10 @@ export async function leave(opts: command_execute_options): Promise { ); return `Bridge left successfully`; } catch (e) { - throw await log_error( - new Error('Error updating bridge', { cause: e }), - { - bridge, - }, - ); + logError(e, { + message: 'Failed to update bridge in database', + extra: { bridge }, + }) } } @@ -50,11 +48,9 @@ export async function toggle(opts: command_execute_options): Promise { ); return `Bridge settings updated successfully`; } catch (e) { - throw await log_error( - new Error('Error updating bridge', { cause: e }), - { - bridge, - }, - ); + logError(e, { + message: 'Failed to update bridge in database', + extra: { bridge }, + }) } } diff --git a/packages/lightning/src/commands/mod.ts b/packages/lightning/src/commands/mod.ts index 23d9be9..1fd4152 100644 --- a/packages/lightning/src/commands/mod.ts +++ b/packages/lightning/src/commands/mod.ts @@ -1,7 +1,6 @@ import type { lightning } from '../lightning.ts'; import { create_message, type message } from '../messages.ts'; -import { parseArgs } from '@std/cli/parse-args'; -import { type err, log_error } from '../errors.ts'; +import { LightningError } from '../errors.ts'; export interface command_execute_options { channel: string; @@ -23,25 +22,19 @@ export interface command { subcommands?: Omit[]; execute: ( opts: command_execute_options, - ) => Promise | string | err; + ) => Promise | string; } export async function execute_text_command(msg: message, lightning: lightning) { if (!msg.content?.startsWith(lightning.config.cmd_prefix)) return; - const { - _: [cmd, ...rest], - ...args - } = parseArgs( - msg.content.replace(lightning.config.cmd_prefix, '').split(' '), - ); + const [cmd, ...rest] = msg.content.replace(lightning.config.cmd_prefix, '').split(' '); return await run_command({ ...msg, lightning, command: cmd as string, rest: rest as string[], - args, }); } @@ -87,7 +80,7 @@ export async function run_command( } } - let resp: string | err; + let resp: string | LightningError; try { resp = await command.execute({ @@ -95,19 +88,21 @@ export async function run_command( arguments: opts.args, }); } catch (e) { - // TODO(jersey): we should have err be a class that extends Error so checking this is easier - if (typeof e === 'object' && e !== null && 'cause' in e) { - resp = e as err; - } else { - resp = await log_error(e, { ...opts, reply: undefined }); - } + if (e instanceof LightningError) resp = e; + else resp = new LightningError(e, { + message: 'An error occurred while executing the command', + extra: { command: command.name }, + }) } try { if (typeof resp === 'string') { await opts.reply(create_message(resp), false); - } else await opts.reply(resp.message, false); + } else await opts.reply(resp.msg, false); } catch (e) { - await log_error(e, { ...opts, reply: undefined }); + new LightningError(e, { + message: 'An error occurred while sending the command response', + extra: { command: command.name }, + }) } } diff --git a/packages/lightning/src/errors.ts b/packages/lightning/src/errors.ts index 2a76423..78aafd7 100644 --- a/packages/lightning/src/errors.ts +++ b/packages/lightning/src/errors.ts @@ -1,5 +1,90 @@ import { create_message, type message } from './messages.ts'; +export interface LightningErrorOptions { + /** the user-facing message of the error */ + message?: string; + /** the extra data to log */ + extra?: Record; +} + +export class LightningError extends Error { + id: string; + override cause: Error; + extra: Record; + msg: message; + + constructor(e: unknown, options?: LightningErrorOptions) { + if (e instanceof LightningError) { + super(e.message, { cause: e.cause }); + this.id = e.id; + this.cause = e.cause; + this.extra = e.extra; + this.msg = e.msg; + return; + } + + const cause = e instanceof Error + ? e + : e instanceof Object + ? new Error(JSON.stringify(e)) + : new Error(String(e)); + + super(options?.message ?? cause.message, { cause }); + + this.name = 'LightningError'; + this.id = crypto.randomUUID(); + this.cause = cause; + this.extra = options?.extra ?? {}; + this.msg = create_message( + `Something went wrong! Take a look at [the docs](https://williamhorning.eu.org/lightning).\n\`\`\`\n${this.message}\n${this.id}\n\`\`\`` + ); + this.log(); + } + + log() { + console.error(`%clightning error ${this.id}`, 'color: red'); + console.error(this.cause, this.extra); + + const webhook = Deno.env.get('LIGHTNING_ERROR_WEBHOOK'); + + for (const key in this.extra) { + if (key === 'lightning') { + delete this.extra[key]; + } + + if (typeof this.extra[key] === 'object' && this.extra[key] !== null) { + if ('lightning' in this.extra[key]) { + delete this.extra[key].lightning; + } + } + } + + if (webhook && webhook.length > 0) { + let json_str = `\`\`\`json\n${JSON.stringify(this.extra, null, 2)}\n\`\`\``; + + if (json_str.length > 2000) json_str = '*see console*'; + + fetch(webhook, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + content: `# ${this.cause.message}\n*${this.id}*`, + embeds: [ + { + title: 'extra', + description: json_str, + }, + ], + }), + }); + } + } +} + +export function logError(e: unknown, options?: LightningErrorOptions): never { + throw new LightningError(e, options); +} + /** the error returned from log_error */ export interface err { /** id of the error */ @@ -21,52 +106,12 @@ export async function log_error( e: unknown, extra: Record = {}, ): Promise { - const id = crypto.randomUUID(); - const webhook = Deno.env.get('LIGHTNING_ERROR_WEBHOOK'); - const cause = e instanceof Error - ? e - : e instanceof Object - ? new Error(JSON.stringify(e)) - : new Error(String(e)); - const user_facing_text = - `Something went wrong! Take a look at [the docs](https://williamhorning.eu.org/lightning).\n\`\`\`\n${cause.message}\n${id}\n\`\`\``; - - for (const key in extra) { - if (key === 'lightning') { - delete extra[key]; - } + const error = new LightningError(e, { extra }); - if (typeof extra[key] === 'object' && extra[key] !== null) { - if ('lightning' in extra[key]) { - delete extra[key].lightning; - } - } - } - - // TODO(jersey): this is a really bad way of error handling-especially given it doesn't do a lot of stuff that would help debug errors-but it'll be replaced - - console.error(`%clightning error ${id}`, 'color: red'); - console.error(cause, extra); - - if (webhook && webhook.length > 0) { - let json_str = `\`\`\`json\n${JSON.stringify(extra, null, 2)}\n\`\`\``; - - if (json_str.length > 2000) json_str = '*see console*'; - - await fetch(webhook, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - content: `# ${cause.message}\n*${id}*`, - embeds: [ - { - title: 'extra', - description: json_str, - }, - ], - }), - }); + return { + id: error.id, + cause: error.cause, + extra: error.extra, + message: error.msg, } - - return { id, cause, extra, message: create_message(user_facing_text) }; } diff --git a/packages/lightning/src/mod.ts b/packages/lightning/src/mod.ts index 6162908..0d4a02f 100644 --- a/packages/lightning/src/mod.ts +++ b/packages/lightning/src/mod.ts @@ -1,5 +1,5 @@ // TODO(jersey): add exports from commands_v2 -export { log_error } from './errors.ts'; +export { LightningError, log_error } from './errors.ts'; export { type config, lightning } from './lightning.ts'; export * from './messages.ts'; export * from './plugins.ts'; From aa323de716899152ef728681fdb6efdcb22ba4df Mon Sep 17 00:00:00 2001 From: Jersey Date: Thu, 28 Nov 2024 19:10:44 -0500 Subject: [PATCH 13/23] discord commands_v2 --- .../lightning-plugin-discord/src/commands.ts | 76 +++++++++---------- packages/lightning-plugin-discord/src/mod.ts | 2 +- packages/lightning/src/lightning.ts | 6 +- packages/lightning/src/mod.ts | 2 +- packages/lightning/src/plugins.ts | 2 +- 5 files changed, 40 insertions(+), 48 deletions(-) diff --git a/packages/lightning-plugin-discord/src/commands.ts b/packages/lightning-plugin-discord/src/commands.ts index 8b6e24f..267b007 100644 --- a/packages/lightning-plugin-discord/src/commands.ts +++ b/packages/lightning-plugin-discord/src/commands.ts @@ -1,15 +1,13 @@ import type { API } from '@discordjs/core'; -import type { command, command_arguments } from '@jersey/lightning'; +import type { command, run_command_options, lightning } from '@jersey/lightning'; import type { APIInteraction } from 'discord-api-types'; import { to_discord } from './discord.ts'; import { instant } from './lightning.ts'; -// TODO(jersey): migrate over to commands_v2 - -export function to_command(interaction: { api: API; data: APIInteraction }) { +export function to_command(interaction: { api: API; data: APIInteraction }, lightning: lightning) { if (interaction.data.type !== 2 || interaction.data.data.type !== 1) return; const opts = {} as Record; - let subcmd = ''; + let subcmd; for (const opt of interaction.data.data.options || []) { if (opt.type === 1) subcmd = opt.name; @@ -17,8 +15,13 @@ export function to_command(interaction: { api: API; data: APIInteraction }) { } return { - cmd: interaction.data.data.name, - subcmd, + command: interaction.data.data.name, + subcommand: subcmd, + channel: interaction.data.channel.id, + id: interaction.data.id, + timestamp: instant(interaction.data.id), + lightning, + plugin: 'bolt-discord', reply: async (msg) => { await interaction.api.interactions.reply( interaction.data.id, @@ -26,45 +29,38 @@ export function to_command(interaction: { api: API; data: APIInteraction }) { await to_discord(msg), ); }, - channel: interaction.data.channel.id, - plugin: 'bolt-discord', - opts, - timestamp: instant(interaction.data.id), - } as command_arguments; + args: opts, + } as run_command_options; } -export function to_intent_opts({ options }: command) { +export function to_intent_opts({ arguments: args, subcommands }: command) { const opts = []; - if (options?.argument_name) { - opts.push({ - name: options.argument_name, - description: 'option to pass to this command', - type: 3, - required: options.argument_required, - }); + if (args) { + for (const arg of args) { + opts.push({ + name: arg.name, + description: arg.description, + type: 3, + required: arg.required, + }); + } } - if (options?.subcommands) { - opts.push( - ...options.subcommands.map((i) => { - return { - name: i.name, - description: i.description || i.name, - type: 1, - 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 (subcommands) { + for (const sub of subcommands) { + opts.push({ + name: sub.name, + description: sub.description, + type: 1, + options: sub.arguments?.map((opt) => ({ + name: opt.name, + description: opt.description, + type: 3, + required: opt.required, + })), + }); + } } return opts; diff --git a/packages/lightning-plugin-discord/src/mod.ts b/packages/lightning-plugin-discord/src/mod.ts index 74d24b9..75e5da9 100644 --- a/packages/lightning-plugin-discord/src/mod.ts +++ b/packages/lightning-plugin-discord/src/mod.ts @@ -69,7 +69,7 @@ export class discord_plugin extends plugin { }); this.bot.on(GatewayDispatchEvents.InteractionCreate, (interaction) => { - const cmd = to_command(interaction); + const cmd = to_command(interaction, this.lightning); if (cmd) this.emit('run_command', cmd); }); } diff --git a/packages/lightning/src/lightning.ts b/packages/lightning/src/lightning.ts index 44fa2ce..cf75c43 100644 --- a/packages/lightning/src/lightning.ts +++ b/packages/lightning/src/lightning.ts @@ -57,11 +57,7 @@ export class lightning { } if (name === 'run_command') { - run_command({ - ...value[0], - lightning: this, - } as run_command_options); - + run_command(value[0] as run_command_options); continue; } diff --git a/packages/lightning/src/mod.ts b/packages/lightning/src/mod.ts index 0d4a02f..48cb71e 100644 --- a/packages/lightning/src/mod.ts +++ b/packages/lightning/src/mod.ts @@ -1,4 +1,4 @@ -// TODO(jersey): add exports from commands_v2 +export { type command, type run_command_options } from './commands/mod.ts' export { LightningError, log_error } from './errors.ts'; export { type config, lightning } from './lightning.ts'; export * from './messages.ts'; diff --git a/packages/lightning/src/plugins.ts b/packages/lightning/src/plugins.ts index 19c6d76..a9a2aec 100644 --- a/packages/lightning/src/plugins.ts +++ b/packages/lightning/src/plugins.ts @@ -29,7 +29,7 @@ export type plugin_events = { /** when a message is deleted */ delete_message: [deleted_message]; /** when a command is run */ - run_command: [Omit]; + run_command: [run_command_options]; }; /** a plugin for lightning */ From 845d7a00d1d075f875097c2a6e495836ff52769d Mon Sep 17 00:00:00 2001 From: Jersey Date: Thu, 28 Nov 2024 20:35:59 -0500 Subject: [PATCH 14/23] errors v2 and pluginz v8 --- packages/lightning/src/bridge/data.ts | 8 +- packages/lightning/src/bridge/msg.ts | 334 +++++++++--------- packages/lightning/src/commands/bridge/add.ts | 2 +- packages/lightning/src/errors.ts | 40 +-- packages/lightning/src/lightning.ts | 8 +- packages/lightning/src/messages.ts | 76 +--- packages/lightning/src/mod.ts | 2 +- packages/lightning/src/plugins.ts | 30 +- 8 files changed, 214 insertions(+), 286 deletions(-) diff --git a/packages/lightning/src/bridge/data.ts b/packages/lightning/src/bridge/data.ts index fdd9af8..12b123a 100644 --- a/packages/lightning/src/bridge/data.ts +++ b/packages/lightning/src/bridge/data.ts @@ -115,13 +115,13 @@ export class bridge_data { return res.rows[0]; } - async create_bridge_message(msg: bridge_message): Promise { + async create_message(msg: bridge_message): Promise { await this.pg.queryArray`INSERT INTO bridge_messages (id, bridge_id, channels, messages, settings) VALUES (${msg.id}, ${msg.bridge_id}, ${JSON.stringify(msg.channels)}, ${JSON.stringify(msg.messages)}, ${JSON.stringify(msg.settings)})`; } - async edit_bridge_message(msg: bridge_message): Promise { + async edit_message(msg: bridge_message): Promise { await this.pg.queryArray` UPDATE bridge_messages SET messages = ${JSON.stringify(msg.messages)}, channels = ${JSON.stringify(msg.channels)}, settings = ${JSON.stringify(msg.settings)} @@ -129,13 +129,13 @@ export class bridge_data { `; } - async delete_bridge_message({ id }: bridge_message): Promise { + async delete_message({ id }: bridge_message): Promise { await this.pg.queryArray` DELETE FROM bridge_messages WHERE id = ${id} `; } - async get_bridge_message(id: string): Promise { + async get_message(id: string): Promise { const res = await this.pg.queryObject(` SELECT * FROM bridge_messages WHERE id = '${id}' OR EXISTS ( diff --git a/packages/lightning/src/bridge/msg.ts b/packages/lightning/src/bridge/msg.ts index 47525d3..56f366e 100644 --- a/packages/lightning/src/bridge/msg.ts +++ b/packages/lightning/src/bridge/msg.ts @@ -1,178 +1,176 @@ -import type { lightning } from '../lightning.ts'; -import { log_error } from '../errors.ts'; +import type { deleted_message, message } from '../messages.ts'; +import { type lightning, LightningError } from '../mod.ts'; import type { - deleted_message, - message, - unprocessed_message, -} from '../messages.ts'; -import type { - bridge, - bridge_channel, - bridge_message, - bridged_message, + bridge, + bridge_channel, + bridge_message, + bridged_message, } from './data.ts'; export async function handle_message( - core: lightning, - msg: message | deleted_message, - type: 'create' | 'edit' | 'delete', -): Promise { - const br = type === 'create' - ? await core.data.get_bridge_by_channel(msg.channel) - : await core.data.get_bridge_message(msg.id); - - if (!br) return; - - if (type !== 'create' && br.settings.allow_editing !== true) return; - - if ( - br.channels.find((i) => - i.id === msg.channel && i.plugin === msg.plugin && i.disabled - ) - ) return; - - const channels = br.channels.filter( - (i) => i.id !== msg.channel || i.plugin !== msg.plugin, - ); - - if (channels.length < 1) return; - - const messages = [] as bridged_message[]; - - for (const ch of channels) { - if (!ch.data || ch.disabled) continue; - - const bridged_id = (br as Partial).messages?.find((i) => - i.channel === ch.id && i.plugin === ch.plugin - ); - - if ((type !== 'create' && !bridged_id)) { - continue; - } - - const plugin = core.plugins.get(ch.plugin); - - if (!plugin) { - await disable_channel( - ch, - br, - core, - (await log_error( - new Error(`plugin ${ch.plugin} doesn't exist`), - { channel: ch, bridged_id }, - )).cause, - ); - - continue; - } - - const reply_id = await get_reply_id(core, msg as message, ch); - - let res; - - try { - res = await plugin.process_message({ - action: type as 'edit', - channel: ch, - message: msg as message, - edit_id: bridged_id?.id as string[], - reply_id, - }); - - if (res.error) throw res.error; - } catch (e) { - if (type === 'delete') continue; - - if ((res as unprocessed_message).disable) { - await disable_channel(ch, br, core, e); - - continue; - } - - const err = await log_error(e, { - channel: ch, - bridged_id, - message: msg, - }); - - try { - res = await plugin.process_message({ - action: type as 'edit', - channel: ch, - message: err.message as message, - edit_id: bridged_id?.id as string[], - reply_id, - }); - - if (res.error) throw res.error; - } catch (e) { - await log_error( - new Error(`failed to log error`, { cause: e }), - { channel: ch, bridged_id, original_id: err.id }, - ); - - continue; - } - } - - for (const id of res.id) { - sessionStorage.setItem(`${ch.plugin}-${id}`, '1'); - } - - messages.push({ - id: res.id, - channel: ch.id, - plugin: ch.plugin, - }); - } - - await core.data[`${type}_bridge_message`]({ - ...br, - id: msg.id, - messages, - bridge_id: br.id, - }); -} - -async function disable_channel( - channel: bridge_channel, - bridge: bridge | bridge_message, - core: lightning, - error: unknown, -): Promise { - await log_error(error, { channel, bridge }); - - await core.data.edit_bridge({ - id: "bridge_id" in bridge ? bridge.bridge_id : bridge.id, - channels: bridge.channels.map((i) => - i.id === channel.id && i.plugin === channel.plugin - ? { ...i, disabled: true, data: error } - : i - ), - settings: bridge.settings - }); + lightning: lightning, + event: 'create_message' | 'edit_message' | 'delete_message', + data: message | deleted_message, +) { + // get the bridge and return if it doesn't exist + let bridge; + + if (event === 'create_message') { + bridge = await lightning.data.get_bridge_by_channel(data.channel); + } else { + bridge = await lightning.data.get_message(data.id); + } + + if (!bridge) return; + + // if editing isn't allowed, return + if (event !== 'create_message' && bridge.settings.allow_editing !== true) { + return; + } + + // if the channel this event is from is disabled, return + if ( + bridge.channels.find((channel) => + channel.id === data.channel && channel.plugin === data.plugin && + channel.disabled + ) + ) return; + + // filter out the channel this event is from and any disabled channels + const channels = bridge.channels.filter( + (i) => i.id !== data.channel || i.plugin !== data.plugin, + ).filter((i) => !i.disabled || !i.data); + + // if there are no more channels, return + if (channels.length < 1) return; + + const messages = [] as bridged_message[]; + + for (const channel of channels) { + let prior_bridged_ids; + + if (event !== 'create_message') { + prior_bridged_ids = (bridge as bridge_message).messages?.find((i) => + i.channel === channel.id && i.plugin === channel.plugin + ); + + if (!prior_bridged_ids) continue; // the message wasn't bridged previously + } + + const plugin = lightning.plugins.get(channel.plugin); + + if (!plugin) { + await disable_channel( + channel, + bridge, + new LightningError(`plugin ${channel.plugin} doesn't exist`), + lightning, + ); + continue; + } + + const reply_id = await get_reply_id(lightning, data, channel); + + let result_ids: string[]; + + try { + result_ids = await plugin[event]({ + channel, + reply_id, + edit_ids: prior_bridged_ids?.id as string[], + msg: data as message, + }); + } catch (e) { + if (e instanceof LightningError && e.disable_channel) { + await disable_channel(channel, bridge, e, lightning); + continue; + } + + // try sending an error message + + const err = e instanceof LightningError + ? e + : new LightningError(e, { + message: + `An error occurred while processing a message in the bridge.`, + }); + + try { + result_ids = await plugin[event]({ + channel, + reply_id, + edit_ids: prior_bridged_ids?.id as string[], + msg: err.msg, + }); + } catch (e) { + new LightningError(e, { + message: `Failed to log error message in bridge`, + extra: { channel, original_error: err.id }, + }); + + continue; + } + } + + for (const result_id of result_ids) { + sessionStorage.setItem(`${channel.plugin}-${result_id}`, '1'); + } + + messages.push({ + id: result_ids, + channel: channel.id, + plugin: channel.plugin, + }); + } + + await lightning.data[event]({ + ...bridge, + id: data.id, + messages, + bridge_id: bridge.id, + }); } async function get_reply_id( - core: lightning, - msg: message, - channel: bridge_channel, + core: lightning, + msg: message | deleted_message, + channel: bridge_channel, ): Promise { - if (msg.reply_id) { - try { - const bridged = await core.data.get_bridge_message(msg.reply_id); - - if (!bridged) return; - - const br_ch = bridged.channels.find((i) => - i.id === channel.id && i.plugin === channel.plugin - ); - - if (!br_ch) return; + if ('reply_id' in msg && msg.reply_id) { + try { + const bridged = await core.data.get_message(msg.reply_id); + + const bridged_message = bridged?.messages?.find((i) => + i.channel === channel.id && i.plugin === channel.plugin + ); + + return bridged_message?.id[0]; + } catch { + return; + } + } +} - return br_ch.id; - } catch { - return; - } - } +async function disable_channel( + channel: bridge_channel, + bridge: bridge | bridge_message, + error: LightningError, + lightning: lightning, +) { + new LightningError( + `disabling channel ${channel.id} in bridge ${bridge.id}`, + { + extra: { original_error: error.id }, + }, + ); + + await lightning.data.edit_bridge({ + id: 'bridge_id' in bridge ? bridge.bridge_id : bridge.id, + channels: bridge.channels.map((i) => + i.id === channel.id && i.plugin === channel.plugin + ? { ...i, disabled: true, data: error } + : i + ), + settings: bridge.settings, + }); } diff --git a/packages/lightning/src/commands/bridge/add.ts b/packages/lightning/src/commands/bridge/add.ts index 9b7c14d..dba9a4b 100644 --- a/packages/lightning/src/commands/bridge/add.ts +++ b/packages/lightning/src/commands/bridge/add.ts @@ -81,7 +81,7 @@ async function _lightning_bridge_add_common( let bridge_data; try { - bridge_data = await plugin.create_bridge(opts.channel); + bridge_data = await plugin.setup_channel(opts.channel); } catch (e) { logError(e, { message: 'Failed to create bridge using plugin', diff --git a/packages/lightning/src/errors.ts b/packages/lightning/src/errors.ts index 78aafd7..485de6c 100644 --- a/packages/lightning/src/errors.ts +++ b/packages/lightning/src/errors.ts @@ -5,6 +5,8 @@ export interface LightningErrorOptions { message?: string; /** the extra data to log */ extra?: Record; + /** whether to disable the channel */ + disable?: boolean; } export class LightningError extends Error { @@ -12,14 +14,16 @@ export class LightningError extends Error { override cause: Error; extra: Record; msg: message; + disable_channel?: boolean; - constructor(e: unknown, options?: LightningErrorOptions) { + constructor(e: unknown, public options?: LightningErrorOptions) { if (e instanceof LightningError) { super(e.message, { cause: e.cause }); this.id = e.id; this.cause = e.cause; this.extra = e.extra; this.msg = e.msg; + this.disable_channel = e.disable_channel; return; } @@ -35,6 +39,7 @@ export class LightningError extends Error { this.id = crypto.randomUUID(); this.cause = cause; this.extra = options?.extra ?? {}; + this.disable_channel = options?.disable; this.msg = create_message( `Something went wrong! Take a look at [the docs](https://williamhorning.eu.org/lightning).\n\`\`\`\n${this.message}\n${this.id}\n\`\`\`` ); @@ -43,7 +48,7 @@ export class LightningError extends Error { log() { console.error(`%clightning error ${this.id}`, 'color: red'); - console.error(this.cause, this.extra); + console.error(this.cause, this.options); const webhook = Deno.env.get('LIGHTNING_ERROR_WEBHOOK'); @@ -84,34 +89,3 @@ export class LightningError extends Error { export function logError(e: unknown, options?: LightningErrorOptions): never { throw new LightningError(e, options); } - -/** the error returned from log_error */ -export interface err { - /** id of the error */ - id: string; - /** the original error */ - cause: Error; - /** extra information about the error */ - extra: Record; - /** the message associated with the error */ - message: message; -} - -/** - * logs an error and returns a unique id and a message for users - * @param e the error to log - * @param extra any extra data to log - */ -export async function log_error( - e: unknown, - extra: Record = {}, -): Promise { - const error = new LightningError(e, { extra }); - - return { - id: error.id, - cause: error.cause, - extra: error.extra, - message: error.msg, - } -} diff --git a/packages/lightning/src/lightning.ts b/packages/lightning/src/lightning.ts index cf75c43..d2c9309 100644 --- a/packages/lightning/src/lightning.ts +++ b/packages/lightning/src/lightning.ts @@ -40,7 +40,7 @@ export class lightning { this.plugins = new Map>(); for (const p of this.config.plugins || []) { - if (p.support.some((v) => ['0.7.3'].includes(v))) { + if (p.support.includes('0.8.0')) { const plugin = new p.type(this, p.config); this.plugins.set(plugin.name, plugin); this._handle_events(plugin); @@ -65,11 +65,7 @@ export class lightning { execute_text_command(value[0] as message, this); } - handle_message( - this, - value[0] as message, - name.split('_')[0] as 'create', - ); + handle_message(this, name, value[0]); } } diff --git a/packages/lightning/src/messages.ts b/packages/lightning/src/messages.ts index c8a6ba9..d2c1f6d 100644 --- a/packages/lightning/src/messages.ts +++ b/packages/lightning/src/messages.ts @@ -132,72 +132,24 @@ export interface message extends deleted_message { reply_id?: string; } -/** the options given to plugins when a message needs to be sent */ +/** a message to be bridged */ export interface create_message_opts { - /** the action to take */ - action: 'create'; - /** the channel to send the message to */ - channel: bridge_channel; - /** the message to send */ - message: message; - /** the id of the message to reply to */ - reply_id?: string; + msg: message, + channel: bridge_channel, + reply_id?: string, } -/** the options given to plugins when a message needs to be edited */ +/** a message to be edited */ export interface edit_message_opts { - /** the action to take */ - action: 'edit'; - /** the channel to send the message to */ - channel: bridge_channel; - /** the message to send */ - message: message; - /** the id of the message to edit */ - edit_id: string[]; - /** the id of the message to reply to */ - reply_id?: string; + msg: message, + channel: bridge_channel, + reply_id?: string, + edit_ids: string[], } -/** the options given to plugins when a message needs to be deleted */ +/** a message to be deleted */ export interface delete_message_opts { - /** the action to take */ - action: 'delete'; - /** the channel to send the message to */ - channel: bridge_channel; - /** the message to send */ - message: deleted_message; - /** the id of the message to delete */ - edit_id: string[]; - /** the id of the message to reply to */ - reply_id?: string; -} - -/** the options given to plugins when a message needs to be processed */ -export type message_options = - | create_message_opts - | edit_message_opts - | delete_message_opts; - -/** successfully processed message */ -export interface processed_message { - /** whether there was an error */ - error?: undefined; - /** the message that was processed */ - id: string[]; - /** the channel the message was sent to */ - channel: bridge_channel; -} - -/** messages not processed */ -export interface unprocessed_message { - /** the channel the message was to be sent to */ - channel: bridge_channel; - /** whether the channel should be disabled */ - disable?: boolean; - /** the error causing this */ - // TODO(jersey): make this unknown ideally - error: Error; -} - -/** process result */ -export type process_result = processed_message | unprocessed_message; + msg: deleted_message, + channel: bridge_channel, + edit_ids: string[], +} \ No newline at end of file diff --git a/packages/lightning/src/mod.ts b/packages/lightning/src/mod.ts index 48cb71e..76a566c 100644 --- a/packages/lightning/src/mod.ts +++ b/packages/lightning/src/mod.ts @@ -1,5 +1,5 @@ export { type command, type run_command_options } from './commands/mod.ts' -export { LightningError, log_error } from './errors.ts'; +export { LightningError, logError } from './errors.ts'; export { type config, lightning } from './lightning.ts'; export * from './messages.ts'; export * from './plugins.ts'; diff --git a/packages/lightning/src/plugins.ts b/packages/lightning/src/plugins.ts index a9a2aec..2be6c0a 100644 --- a/packages/lightning/src/plugins.ts +++ b/packages/lightning/src/plugins.ts @@ -1,10 +1,11 @@ import { EventEmitter } from '@denosaurs/event'; import type { lightning } from './lightning.ts'; import type { + create_message_opts, + delete_message_opts, deleted_message, + edit_message_opts, message, - message_options, - process_result, } from './messages.ts'; import type { run_command_options } from './commands/mod.ts'; @@ -40,24 +41,31 @@ export abstract class plugin extends EventEmitter { config: cfg; /** the name of your plugin */ abstract name: string; - /** create a new plugin instance */ static new>( this: new (l: lightning, config: T['config']) => T, config: T['config'], ): create_plugin { - return { type: this, config, support: ['0.7.3'] }; + return { type: this, config, support: ['0.8.0'] }; } - + /** initialize a plugin with the given lightning instance and config */ constructor(l: lightning, config: cfg) { super(); this.lightning = l; this.config = config; } - - /** this should return the data you need to send to the channel given */ - abstract create_bridge(channel: string): Promise | unknown; - - /** processes a message and returns information */ - abstract process_message(opts: message_options): Promise; + /** setup a channel to be used in a bridge */ + abstract setup_channel(channel: string): Promise | unknown; + /** send a message to a given channel */ + abstract create_message( + opts: create_message_opts, + ): Promise; + /** edit a message in a given channel */ + abstract edit_message( + opts: edit_message_opts, + ): Promise; + /** delete a message in a given channel */ + abstract delete_message( + opts: delete_message_opts, + ): Promise; } From 460b9c041c5bd7beefcd0bfa6381f66b6a56ebde Mon Sep 17 00:00:00 2001 From: Jersey Date: Fri, 29 Nov 2024 00:19:50 -0500 Subject: [PATCH 15/23] change structure a bit --- .gitignore | 1 - deno.jsonc | 3 +- packages/lightning/{readme.md => README.md} | 5 +- packages/lightning/deno.jsonc | 5 +- packages/lightning/src/bridge.ts | 175 +++++++++++++++++ packages/lightning/src/bridge/msg.ts | 176 ------------------ packages/lightning/src/{cli/mod.ts => cli.ts} | 16 +- .../src/commands/bridge/_internal.ts | 40 ++++ packages/lightning/src/commands/bridge/add.ts | 98 ---------- .../lightning/src/commands/bridge/create.ts | 31 +++ .../lightning/src/commands/bridge/join.ts | 32 ++++ .../lightning/src/commands/bridge/leave.ts | 26 +++ packages/lightning/src/commands/bridge/mod.ts | 52 ------ .../lightning/src/commands/bridge/modify.ts | 56 ------ .../lightning/src/commands/bridge/status.ts | 32 ++-- .../lightning/src/commands/bridge/toggle.ts | 31 +++ packages/lightning/src/commands/default.ts | 96 +++++++--- packages/lightning/src/commands/mod.ts | 108 ----------- packages/lightning/src/commands/runners.ts | 81 ++++++++ .../src/{bridge/data.ts => database.ts} | 69 +++---- packages/lightning/src/errors.ts | 91 --------- packages/lightning/src/lightning.ts | 35 ++-- packages/lightning/src/messages.ts | 155 --------------- packages/lightning/src/mod.ts | 11 +- packages/lightning/src/structures/bridge.ts | 100 ++++++++++ packages/lightning/src/structures/commands.ts | 41 ++++ packages/lightning/src/structures/errors.ts | 107 +++++++++++ packages/lightning/src/structures/events.ts | 30 +++ packages/lightning/src/structures/media.ts | 68 +++++++ packages/lightning/src/structures/messages.ts | 67 +++++++ packages/lightning/src/structures/mod.ts | 7 + .../lightning/src/{ => structures}/plugins.ts | 20 +- 32 files changed, 989 insertions(+), 876 deletions(-) rename packages/lightning/{readme.md => README.md} (71%) create mode 100644 packages/lightning/src/bridge.ts delete mode 100644 packages/lightning/src/bridge/msg.ts rename packages/lightning/src/{cli/mod.ts => cli.ts} (71%) create mode 100644 packages/lightning/src/commands/bridge/_internal.ts delete mode 100644 packages/lightning/src/commands/bridge/add.ts create mode 100644 packages/lightning/src/commands/bridge/create.ts create mode 100644 packages/lightning/src/commands/bridge/join.ts create mode 100644 packages/lightning/src/commands/bridge/leave.ts delete mode 100644 packages/lightning/src/commands/bridge/mod.ts delete mode 100644 packages/lightning/src/commands/bridge/modify.ts create mode 100644 packages/lightning/src/commands/bridge/toggle.ts delete mode 100644 packages/lightning/src/commands/mod.ts create mode 100644 packages/lightning/src/commands/runners.ts rename packages/lightning/src/{bridge/data.ts => database.ts} (58%) delete mode 100644 packages/lightning/src/errors.ts delete mode 100644 packages/lightning/src/messages.ts create mode 100644 packages/lightning/src/structures/bridge.ts create mode 100644 packages/lightning/src/structures/commands.ts create mode 100644 packages/lightning/src/structures/errors.ts create mode 100644 packages/lightning/src/structures/events.ts create mode 100644 packages/lightning/src/structures/media.ts create mode 100644 packages/lightning/src/structures/messages.ts create mode 100644 packages/lightning/src/structures/mod.ts rename packages/lightning/src/{ => structures}/plugins.ts (77%) diff --git a/.gitignore b/.gitignore index c3da77e..031f26e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ /.env /config /config.ts -packages/lightning-old packages/postgres \ No newline at end of file diff --git a/deno.jsonc b/deno.jsonc index b375ef0..1cf437e 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -21,8 +21,7 @@ }, "workspace": [ "./packages/lightning", - // TODO(jersey): remove these two - "./packages/lightning-old", + // TODO(jersey): contribute upstream "./packages/postgres", "./packages/lightning-plugin-telegram", "./packages/lightning-plugin-revolt", diff --git a/packages/lightning/readme.md b/packages/lightning/README.md similarity index 71% rename from packages/lightning/readme.md rename to packages/lightning/README.md index cc12d72..a61f60c 100644 --- a/packages/lightning/readme.md +++ b/packages/lightning/README.md @@ -7,4 +7,7 @@ apps via plugins ## [docs](https://williamhorning.eu.org/bolt) - +```ts +import {} from "@jersey/lightning"; +// TODO(jersey): add example +``` \ No newline at end of file diff --git a/packages/lightning/deno.jsonc b/packages/lightning/deno.jsonc index 3c2c21f..4169f01 100644 --- a/packages/lightning/deno.jsonc +++ b/packages/lightning/deno.jsonc @@ -1,10 +1,7 @@ { "name": "@jersey/lightning", "version": "0.8.0", - "exports": { - ".": "./src/mod.ts", - "./cli": "./src/cli/mod.ts" - }, + "exports": "./src/mod.ts", "imports": { "@db/postgres": "jsr:@db/postgres@^0.19.4", "@denosaurs/event": "jsr:@denosaurs/event@^2.0.2", diff --git a/packages/lightning/src/bridge.ts b/packages/lightning/src/bridge.ts new file mode 100644 index 0000000..3cca33d --- /dev/null +++ b/packages/lightning/src/bridge.ts @@ -0,0 +1,175 @@ +import type { lightning } from './lightning.ts'; +import { LightningError } from './structures/errors.ts'; +import type { + bridge, + bridge_channel, + bridge_message, + bridged_message, + deleted_message, + message, +} from './structures/mod.ts'; + +export async function bridge_message( + lightning: lightning, + event: 'create_message' | 'edit_message' | 'delete_message', + data: message | deleted_message, +) { + // get the bridge and return if it doesn't exist + let bridge; + + if (event === 'create_message') { + bridge = await lightning.data.get_bridge_by_channel(data.channel); + } else { + bridge = await lightning.data.get_message(data.id); + } + + if (!bridge) return; + + // if editing isn't allowed, return + if (event !== 'create_message' && bridge.settings.allow_editing !== true) { + return; + } + + // if the channel this event is from is disabled, return + if ( + bridge.channels.find((channel) => + channel.id === data.channel && channel.plugin === data.plugin && + channel.disabled + ) + ) return; + + // filter out the channel this event is from and any disabled channels + const channels = bridge.channels.filter( + (i) => i.id !== data.channel || i.plugin !== data.plugin, + ).filter((i) => !i.disabled || !i.data); + + // if there are no more channels, return + if (channels.length < 1) return; + + const messages = [] as bridged_message[]; + + for (const channel of channels) { + let prior_bridged_ids; + + if (event !== 'create_message') { + prior_bridged_ids = (bridge as bridge_message).messages?.find((i) => + i.channel === channel.id && i.plugin === channel.plugin + ); + + if (!prior_bridged_ids) continue; // the message wasn't bridged previously + } + + const plugin = lightning.plugins.get(channel.plugin); + + if (!plugin) { + await disable_channel( + channel, + bridge, + new LightningError(`plugin ${channel.plugin} doesn't exist`), + lightning, + ); + continue; + } + + const reply_id = await get_reply_id(lightning, data, channel); + + let result_ids: string[]; + + try { + result_ids = await plugin[event]({ + channel, + reply_id, + edit_ids: prior_bridged_ids?.id as string[], + msg: data as message, + }); + } catch (e) { + if (e instanceof LightningError && e.disable_channel) { + await disable_channel(channel, bridge, e, lightning); + continue; + } + + // try sending an error message + + const err = e instanceof LightningError ? e : new LightningError(e, { + message: `An error occurred while processing a message in the bridge.`, + }); + + try { + result_ids = await plugin[event]({ + channel, + reply_id, + edit_ids: prior_bridged_ids?.id as string[], + msg: err.msg, + }); + } catch (e) { + new LightningError(e, { + message: `Failed to log error message in bridge`, + extra: { channel, original_error: err.id }, + }); + + continue; + } + } + + for (const result_id of result_ids) { + sessionStorage.setItem(`${channel.plugin}-${result_id}`, '1'); + } + + messages.push({ + id: result_ids, + channel: channel.id, + plugin: channel.plugin, + }); + } + + await lightning.data[event]({ + ...bridge, + id: data.id, + messages, + bridge_id: bridge.id, + }); +} + +async function get_reply_id( + core: lightning, + msg: message | deleted_message, + channel: bridge_channel, +): Promise { + if ('reply_id' in msg && msg.reply_id) { + try { + const bridged = await core.data.get_message(msg.reply_id); + + const bridged_message = bridged?.messages?.find((i) => + i.channel === channel.id && i.plugin === channel.plugin + ); + + return bridged_message?.id[0]; + } catch { + return; + } + } +} + +async function disable_channel( + channel: bridge_channel, + bridge: bridge | bridge_message, + error: LightningError, + lightning: lightning, +) { + new LightningError( + `disabling channel ${channel.id} in bridge ${bridge.id}`, + { + extra: { original_error: error.id }, + }, + ); + + await lightning.data.edit_bridge({ + id: 'bridge_id' in bridge ? bridge.bridge_id : bridge.id, + channels: bridge.channels.map((i) => + i.id === channel.id && i.plugin === channel.plugin + ? { ...i, disabled: true, data: error } + : i + ), + settings: bridge.settings, + }); +} diff --git a/packages/lightning/src/bridge/msg.ts b/packages/lightning/src/bridge/msg.ts deleted file mode 100644 index 56f366e..0000000 --- a/packages/lightning/src/bridge/msg.ts +++ /dev/null @@ -1,176 +0,0 @@ -import type { deleted_message, message } from '../messages.ts'; -import { type lightning, LightningError } from '../mod.ts'; -import type { - bridge, - bridge_channel, - bridge_message, - bridged_message, -} from './data.ts'; - -export async function handle_message( - lightning: lightning, - event: 'create_message' | 'edit_message' | 'delete_message', - data: message | deleted_message, -) { - // get the bridge and return if it doesn't exist - let bridge; - - if (event === 'create_message') { - bridge = await lightning.data.get_bridge_by_channel(data.channel); - } else { - bridge = await lightning.data.get_message(data.id); - } - - if (!bridge) return; - - // if editing isn't allowed, return - if (event !== 'create_message' && bridge.settings.allow_editing !== true) { - return; - } - - // if the channel this event is from is disabled, return - if ( - bridge.channels.find((channel) => - channel.id === data.channel && channel.plugin === data.plugin && - channel.disabled - ) - ) return; - - // filter out the channel this event is from and any disabled channels - const channels = bridge.channels.filter( - (i) => i.id !== data.channel || i.plugin !== data.plugin, - ).filter((i) => !i.disabled || !i.data); - - // if there are no more channels, return - if (channels.length < 1) return; - - const messages = [] as bridged_message[]; - - for (const channel of channels) { - let prior_bridged_ids; - - if (event !== 'create_message') { - prior_bridged_ids = (bridge as bridge_message).messages?.find((i) => - i.channel === channel.id && i.plugin === channel.plugin - ); - - if (!prior_bridged_ids) continue; // the message wasn't bridged previously - } - - const plugin = lightning.plugins.get(channel.plugin); - - if (!plugin) { - await disable_channel( - channel, - bridge, - new LightningError(`plugin ${channel.plugin} doesn't exist`), - lightning, - ); - continue; - } - - const reply_id = await get_reply_id(lightning, data, channel); - - let result_ids: string[]; - - try { - result_ids = await plugin[event]({ - channel, - reply_id, - edit_ids: prior_bridged_ids?.id as string[], - msg: data as message, - }); - } catch (e) { - if (e instanceof LightningError && e.disable_channel) { - await disable_channel(channel, bridge, e, lightning); - continue; - } - - // try sending an error message - - const err = e instanceof LightningError - ? e - : new LightningError(e, { - message: - `An error occurred while processing a message in the bridge.`, - }); - - try { - result_ids = await plugin[event]({ - channel, - reply_id, - edit_ids: prior_bridged_ids?.id as string[], - msg: err.msg, - }); - } catch (e) { - new LightningError(e, { - message: `Failed to log error message in bridge`, - extra: { channel, original_error: err.id }, - }); - - continue; - } - } - - for (const result_id of result_ids) { - sessionStorage.setItem(`${channel.plugin}-${result_id}`, '1'); - } - - messages.push({ - id: result_ids, - channel: channel.id, - plugin: channel.plugin, - }); - } - - await lightning.data[event]({ - ...bridge, - id: data.id, - messages, - bridge_id: bridge.id, - }); -} - -async function get_reply_id( - core: lightning, - msg: message | deleted_message, - channel: bridge_channel, -): Promise { - if ('reply_id' in msg && msg.reply_id) { - try { - const bridged = await core.data.get_message(msg.reply_id); - - const bridged_message = bridged?.messages?.find((i) => - i.channel === channel.id && i.plugin === channel.plugin - ); - - return bridged_message?.id[0]; - } catch { - return; - } - } -} - -async function disable_channel( - channel: bridge_channel, - bridge: bridge | bridge_message, - error: LightningError, - lightning: lightning, -) { - new LightningError( - `disabling channel ${channel.id} in bridge ${bridge.id}`, - { - extra: { original_error: error.id }, - }, - ); - - await lightning.data.edit_bridge({ - id: 'bridge_id' in bridge ? bridge.bridge_id : bridge.id, - channels: bridge.channels.map((i) => - i.id === channel.id && i.plugin === channel.plugin - ? { ...i, disabled: true, data: error } - : i - ), - settings: bridge.settings, - }); -} diff --git a/packages/lightning/src/cli/mod.ts b/packages/lightning/src/cli.ts similarity index 71% rename from packages/lightning/src/cli/mod.ts rename to packages/lightning/src/cli.ts index 8d1560a..f89d0a7 100644 --- a/packages/lightning/src/cli/mod.ts +++ b/packages/lightning/src/cli.ts @@ -1,7 +1,7 @@ import { parseArgs } from '@std/cli/parse-args'; import { join, toFileUrl } from '@std/path'; -import { logError } from '../errors.ts'; -import { type config, lightning } from '../lightning.ts'; +import { lightning, type config } from './lightning.ts'; +import { log_error } from './structures/errors.ts'; const version = '0.8.0'; const _ = parseArgs(Deno.args); @@ -14,23 +14,25 @@ if (_.v || _.version) { // TODO(jersey): this is somewhat broken when acting as a JSR package if (!_.config) _.config = join(Deno.cwd(), 'config.ts'); - const config: config = (await import(toFileUrl(_.config).toString())).default; + const config = (await import(toFileUrl(_.config).toString())).default as config; + + if (config?.error_url) Deno.env.set('LIGHTNING_ERROR_WEBHOOK', config.error_url); addEventListener('error', (ev) => { - logError(ev.error, { extra: { type: 'global error' } }); + log_error(ev.error, { extra: { type: 'global error' } }); }); addEventListener('unhandledrejection', (ev) => { - logError(ev.reason, { extra: { type: 'global rejection' } }); + log_error(ev.reason, { extra: { type: 'global rejection' } }); }); try { await lightning.create(config); } catch (e) { - logError(e, { extra: { type: 'global class error' } }); + log_error(e, { extra: { type: 'global class error' } }); } } else if (_._[0] === 'migrations') { - // TODO(jersey): implement migrations + // TODO(jersey): implement migrations (separate module?) } else { console.log('[lightning] command not found, showing help'); run_help(); diff --git a/packages/lightning/src/commands/bridge/_internal.ts b/packages/lightning/src/commands/bridge/_internal.ts new file mode 100644 index 0000000..df3c656 --- /dev/null +++ b/packages/lightning/src/commands/bridge/_internal.ts @@ -0,0 +1,40 @@ +import { log_error } from '../../structures/errors.ts'; +import type { bridge_channel, command_opts } from '../../structures/mod.ts'; + +export async function bridge_add_common( + opts: command_opts, +): Promise { + const existing_bridge = await opts.lightning.data.get_bridge_by_channel( + opts.channel, + ); + + if (existing_bridge) { + return `You are already in a bridge called \`${existing_bridge.name}\`. You must leave it before being in another bridge. Try using \`${opts.lightning.config.prefix}leave\` or \`${opts.lightning.config.prefix}help\` commands.`; + } + + const plugin = opts.lightning.plugins.get(opts.plugin); + + if (!plugin) { + log_error('Internal error: platform support not found', { + extra: { plugin: opts.plugin }, + }); + } + + let bridge_data; + + try { + bridge_data = await plugin.setup_channel(opts.channel); + } catch (e) { + log_error(e, { + message: 'Failed to create bridge using plugin', + extra: { channel: opts.channel, plugin_name: opts.plugin }, + }); + } + + return { + id: opts.channel, + data: bridge_data, + disabled: false, + plugin: opts.plugin, + }; +} diff --git a/packages/lightning/src/commands/bridge/add.ts b/packages/lightning/src/commands/bridge/add.ts deleted file mode 100644 index dba9a4b..0000000 --- a/packages/lightning/src/commands/bridge/add.ts +++ /dev/null @@ -1,98 +0,0 @@ -import type { bridge_channel } from '../../bridge/data.ts'; -import { logError } from '../../errors.ts'; -import type { command_execute_options } from '../mod.ts'; - -export async function create( - opts: command_execute_options, -): Promise { - const result = await _lightning_bridge_add_common(opts); - - if (typeof result === 'string') return result; - - const bridge_data = { - name: opts.arguments.name, - channels: [result], - settings: { - allow_editing: true, - allow_everyone: false, - use_rawname: false, - }, - }; - - try { - await opts.lightning.data.create_bridge(bridge_data); - return `Bridge created successfully! You can now join it using \`${opts.lightning.config.cmd_prefix}join ${result.id}\`. Keep this id safe, don't share it with anyone, and delete this message.`; - } catch (e) { - logError(e, { - message: 'Failed to insert bridge into database', - extra: bridge_data - }); - } -} - -export async function join( - opts: command_execute_options, -): Promise { - const result = await _lightning_bridge_add_common(opts); - - if (typeof result === 'string') return result; - - const target_bridge = await opts.lightning.data.get_bridge_by_id( - opts.arguments.id, - ); - - if (!target_bridge) { - return `Bridge with id \`${opts.arguments.id}\` not found. Make sure you have the correct id.`; - } - - target_bridge.channels.push(result); - - try { - await opts.lightning.data.edit_bridge(target_bridge); - - return `Bridge joined successfully!`; - } catch (e) { - logError(e, { - message: 'Failed to update bridge in database', - extra: { target_bridge } - }) - } -} - -async function _lightning_bridge_add_common( - opts: command_execute_options, -): Promise { - const existing_bridge = await opts.lightning.data.get_bridge_by_channel( - opts.channel, - ); - - if (existing_bridge) { - return `You are already in a bridge called \`${existing_bridge.name}\`. You must leave it before being in another bridge. Try using \`${opts.lightning.config.cmd_prefix}leave\` or \`${opts.lightning.config.cmd_prefix}help\` commands.` - } - - const plugin = opts.lightning.plugins.get(opts.plugin); - - if (!plugin) { - logError('Internal error: platform support not found', { - extra: { plugin: opts.plugin } - }) - } - - let bridge_data; - - try { - bridge_data = await plugin.setup_channel(opts.channel); - } catch (e) { - logError(e, { - message: 'Failed to create bridge using plugin', - extra: { channel: opts.channel, plugin_name: opts.plugin } - }) - } - - return { - id: opts.channel, - data: bridge_data, - disabled: false, - plugin: opts.plugin, - }; -} diff --git a/packages/lightning/src/commands/bridge/create.ts b/packages/lightning/src/commands/bridge/create.ts new file mode 100644 index 0000000..62b1ed0 --- /dev/null +++ b/packages/lightning/src/commands/bridge/create.ts @@ -0,0 +1,31 @@ +import type { command_opts } from '../../structures/commands.ts'; +import { log_error } from '../../structures/errors.ts'; +import { bridge_add_common } from './_internal.ts'; + +export async function create( + opts: command_opts, +): Promise { + const result = await bridge_add_common(opts); + + if (typeof result === 'string') return result; + + const bridge_data = { + name: opts.args.name, + channels: [result], + settings: { + allow_editing: true, + allow_everyone: false, + use_rawname: false, + }, + }; + + try { + await opts.lightning.data.create_bridge(bridge_data); + return `Bridge created successfully! You can now join it using \`${opts.lightning.config.prefix}join ${result.id}\`. Keep this id safe, don't share it with anyone, and delete this message.`; + } catch (e) { + log_error(e, { + message: 'Failed to insert bridge into database', + extra: bridge_data, + }); + } +} diff --git a/packages/lightning/src/commands/bridge/join.ts b/packages/lightning/src/commands/bridge/join.ts new file mode 100644 index 0000000..9cf02bb --- /dev/null +++ b/packages/lightning/src/commands/bridge/join.ts @@ -0,0 +1,32 @@ +import type { command_opts } from '../../structures/commands.ts'; +import { log_error } from '../../structures/errors.ts'; +import { bridge_add_common } from './_internal.ts'; + +export async function join( + opts: command_opts, +): Promise { + const result = await bridge_add_common(opts); + + if (typeof result === 'string') return result; + + const target_bridge = await opts.lightning.data.get_bridge_by_id( + opts.args.id, + ); + + if (!target_bridge) { + return `Bridge with id \`${opts.args.id}\` not found. Make sure you have the correct id.`; + } + + target_bridge.channels.push(result); + + try { + await opts.lightning.data.edit_bridge(target_bridge); + + return `Bridge joined successfully!`; + } catch (e) { + log_error(e, { + message: 'Failed to update bridge in database', + extra: { target_bridge }, + }); + } +} diff --git a/packages/lightning/src/commands/bridge/leave.ts b/packages/lightning/src/commands/bridge/leave.ts new file mode 100644 index 0000000..327c069 --- /dev/null +++ b/packages/lightning/src/commands/bridge/leave.ts @@ -0,0 +1,26 @@ +import type { command_opts } from '../../structures/commands.ts'; +import { log_error } from '../../structures/errors.ts'; + +export async function leave(opts: command_opts): Promise { + const bridge = await opts.lightning.data.get_bridge_by_channel( + opts.channel, + ); + + if (!bridge) return `You are not in a bridge`; + + bridge.channels = bridge.channels.filter(( + ch, + ) => ch.id !== opts.channel); + + try { + await opts.lightning.data.edit_bridge( + bridge, + ); + return `Bridge left successfully`; + } catch (e) { + log_error(e, { + message: 'Failed to update bridge in database', + extra: { bridge }, + }); + } +} diff --git a/packages/lightning/src/commands/bridge/mod.ts b/packages/lightning/src/commands/bridge/mod.ts deleted file mode 100644 index fd9fba7..0000000 --- a/packages/lightning/src/commands/bridge/mod.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { create, join } from './add.ts'; -import { leave, toggle } from './modify.ts'; -import { status } from './status.ts'; -import type { command } from '../mod.ts'; - -export const bridge_command = { - name: 'bridge', - description: 'bridge commands', - execute: () => 'take a look at the subcommands of this command', - subcommands: [ - { - name: 'create', - description: 'create a new bridge', - arguments: [{ - name: 'name', - description: 'name of the bridge', - required: true, - }], - execute: create, - }, - { - name: 'join', - description: 'join an existing bridge', - arguments: [{ - name: 'id', - description: 'id of the bridge', - required: true, - }], - execute: join, - }, - { - name: 'leave', - description: 'leave the current bridge', - execute: leave, - }, - { - name: 'toggle', - description: 'toggle a setting on the current bridge', - arguments: [{ - name: 'setting', - description: 'setting to toggle', - required: true, - }], - execute: toggle, - }, - { - name: 'status', - description: 'get the status of the current bridge', - execute: status, - }, - ], -} as command; diff --git a/packages/lightning/src/commands/bridge/modify.ts b/packages/lightning/src/commands/bridge/modify.ts deleted file mode 100644 index 8cd33d6..0000000 --- a/packages/lightning/src/commands/bridge/modify.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { logError } from '../../errors.ts'; -import type { command_execute_options } from '../mod.ts'; - -export async function leave(opts: command_execute_options): Promise { - const bridge = await opts.lightning.data.get_bridge_by_channel( - opts.channel, - ); - - if (!bridge) return `You are not in a bridge`; - - bridge.channels = bridge.channels.filter(( - ch, - ) => ch.id !== opts.channel); - - try { - await opts.lightning.data.edit_bridge( - bridge, - ); - return `Bridge left successfully`; - } catch (e) { - logError(e, { - message: 'Failed to update bridge in database', - extra: { bridge }, - }) - } -} - -const settings = ['allow_editing', 'allow_everyone', 'use_rawname']; - -export async function toggle(opts: command_execute_options): Promise { - const bridge = await opts.lightning.data.get_bridge_by_channel( - opts.channel, - ); - - if (!bridge) return `You are not in a bridge`; - - if (!settings.includes(opts.arguments.setting)) { - return `That setting does not exist`; - } - - const key = opts.arguments.setting as keyof typeof bridge.settings; - - bridge.settings[key] = !bridge.settings[key]; - - try { - await opts.lightning.data.edit_bridge( - bridge, - ); - return `Bridge settings updated successfully`; - } catch (e) { - logError(e, { - message: 'Failed to update bridge in database', - extra: { bridge }, - }) - } -} diff --git a/packages/lightning/src/commands/bridge/status.ts b/packages/lightning/src/commands/bridge/status.ts index 27fefc0..9db79f8 100644 --- a/packages/lightning/src/commands/bridge/status.ts +++ b/packages/lightning/src/commands/bridge/status.ts @@ -1,23 +1,23 @@ -import type { command_execute_options } from '../mod.ts'; +import type { command_opts } from '../../structures/commands.ts'; -export async function status(opts: command_execute_options): Promise { - const bridge = await opts.lightning.data.get_bridge_by_channel( - opts.channel, - ); +export async function status(opts: command_opts): Promise { + const bridge = await opts.lightning.data.get_bridge_by_channel( + opts.channel, + ); - if (!bridge) return `You are not in a bridge`; + if (!bridge) return `You are not in a bridge`; - let str = `Name: \`${bridge.name}\`\n\nChannels:\n`; + let str = `Name: \`${bridge.name}\`\n\nChannels:\n`; - for (const [i, value] of bridge.channels.entries()) { - str += `${i + 1}. \`${value.id}\` on \`${value.plugin}\`\n`; - } + for (const [i, value] of bridge.channels.entries()) { + str += `${i + 1}. \`${value.id}\` on \`${value.plugin}\`\n`; + } - str += `\nSettings:\n`; + str += `\nSettings:\n`; - for (const [key, value] of Object.entries(bridge.settings)) { - str += `- \`${key}\` ${value ? "✔" : "❌"}\n`; - } + for (const [key, value] of Object.entries(bridge.settings)) { + str += `- \`${key}\` ${value ? '✔' : '❌'}\n`; + } - return str; -} \ No newline at end of file + return str; +} diff --git a/packages/lightning/src/commands/bridge/toggle.ts b/packages/lightning/src/commands/bridge/toggle.ts new file mode 100644 index 0000000..5c8944b --- /dev/null +++ b/packages/lightning/src/commands/bridge/toggle.ts @@ -0,0 +1,31 @@ +import type { command_opts } from '../../structures/commands.ts'; +import { log_error } from '../../structures/errors.ts'; +import { bridge_settings_list } from '../../structures/bridge.ts'; + +export async function toggle(opts: command_opts): Promise { + const bridge = await opts.lightning.data.get_bridge_by_channel( + opts.channel, + ); + + if (!bridge) return `You are not in a bridge`; + + if (!bridge_settings_list.includes(opts.args.setting)) { + return `That setting does not exist`; + } + + const key = opts.args.setting as keyof typeof bridge.settings; + + bridge.settings[key] = !bridge.settings[key]; + + try { + await opts.lightning.data.edit_bridge( + bridge, + ); + return `Bridge settings updated successfully`; + } catch (e) { + log_error(e, { + message: 'Failed to update bridge in database', + extra: { bridge }, + }); + } +} diff --git a/packages/lightning/src/commands/default.ts b/packages/lightning/src/commands/default.ts index 1405876..2c741b5 100644 --- a/packages/lightning/src/commands/default.ts +++ b/packages/lightning/src/commands/default.ts @@ -1,26 +1,76 @@ -import { bridge_command } from './bridge/mod.ts'; -import type { command } from './mod.ts'; +import type { command, command_opts } from '../structures/commands.ts'; +import { create } from './bridge/create.ts'; +import { join } from './bridge/join.ts'; +import { leave } from './bridge/leave.ts'; +import { status } from './bridge/status.ts'; +import { toggle } from './bridge/toggle.ts'; export const default_commands = new Map([ - ['help', { - name: 'help', - description: 'get help with the bot', - execute: () => - 'check out [the docs](https://williamhorning.eu.org/bolt/) for help.', - }], - ['ping', { - name: 'ping', - description: 'check if the bot is alive', - execute: ({ timestamp }) => - `Pong! 🏓 ${ - Temporal.Now.instant().since(timestamp).round('millisecond') - .total('milliseconds') - }ms`, - }], - ['version', { - name: 'version', - description: 'get the bots version', - execute: () => 'hello from v0.8.0!', - }], - ['bridge', bridge_command], + ['help', { + name: 'help', + description: 'get help with the bot', + execute: () => + 'check out [the docs](https://williamhorning.eu.org/bolt/) for help.', + }], + ['ping', { + name: 'ping', + description: 'check if the bot is alive', + execute: ({ timestamp }: command_opts) => + `Pong! 🏓 ${ + Temporal.Now.instant().since(timestamp).round('millisecond') + .total('milliseconds') + }ms`, + }], + ['version', { + name: 'version', + description: 'get the bots version', + execute: () => 'hello from v0.8.0!', + }], + ['bridge', { + name: 'bridge', + description: 'bridge commands', + execute: () => 'take a look at the subcommands of this command', + subcommands: [ + { + name: 'create', + description: 'create a new bridge', + arguments: [{ + name: 'name', + description: 'name of the bridge', + required: true, + }], + execute: create, + }, + { + name: 'join', + description: 'join an existing bridge', + arguments: [{ + name: 'id', + description: 'id of the bridge', + required: true, + }], + execute: join, + }, + { + name: 'leave', + description: 'leave the current bridge', + execute: leave, + }, + { + name: 'toggle', + description: 'toggle a setting on the current bridge', + arguments: [{ + name: 'setting', + description: 'setting to toggle', + required: true, + }], + execute: toggle, + }, + { + name: 'status', + description: 'get the status of the current bridge', + execute: status, + }, + ], + }], ]) as Map; diff --git a/packages/lightning/src/commands/mod.ts b/packages/lightning/src/commands/mod.ts deleted file mode 100644 index 1fd4152..0000000 --- a/packages/lightning/src/commands/mod.ts +++ /dev/null @@ -1,108 +0,0 @@ -import type { lightning } from '../lightning.ts'; -import { create_message, type message } from '../messages.ts'; -import { LightningError } from '../errors.ts'; - -export interface command_execute_options { - channel: string; - plugin: string; - timestamp: Temporal.Instant; - arguments: Record; - lightning: lightning; - id: string; -} - -export interface command { - name: string; - description: string; - arguments?: { - name: string; - description: string; - required: boolean; - }[]; - subcommands?: Omit[]; - execute: ( - opts: command_execute_options, - ) => Promise | string; -} - -export async function execute_text_command(msg: message, lightning: lightning) { - if (!msg.content?.startsWith(lightning.config.cmd_prefix)) return; - - const [cmd, ...rest] = msg.content.replace(lightning.config.cmd_prefix, '').split(' '); - - return await run_command({ - ...msg, - lightning, - command: cmd as string, - rest: rest as string[], - }); -} - -export interface run_command_options - extends Omit { - command: string; - subcommand?: string; - args?: Record; - rest?: string[]; - reply: message['reply']; -} - -export async function run_command( - opts: run_command_options, -) { - let command = opts.lightning.commands.get(opts.command) ?? - opts.lightning.commands.get('help')!; - - const subcommand_name = opts.subcommand ?? opts.rest?.shift(); - - if (command.subcommands && subcommand_name) { - const subcommand = command.subcommands.find((i) => - i.name === subcommand_name - ); - - if (subcommand) command = subcommand; - } - - if (!opts.args) opts.args = {}; - - for (const arg of command.arguments || []) { - if (!opts.args[arg.name]) { - opts.args[arg.name] = opts.rest?.shift() as string; - } - - if (!opts.args[arg.name]) { - return opts.reply( - create_message( - `Please provide the \`${arg.name}\` argument. Try using the \`${opts.lightning.config.cmd_prefix}help\` command.`, - ), - false, - ); - } - } - - let resp: string | LightningError; - - try { - resp = await command.execute({ - ...opts, - arguments: opts.args, - }); - } catch (e) { - if (e instanceof LightningError) resp = e; - else resp = new LightningError(e, { - message: 'An error occurred while executing the command', - extra: { command: command.name }, - }) - } - - try { - if (typeof resp === 'string') { - await opts.reply(create_message(resp), false); - } else await opts.reply(resp.msg, false); - } catch (e) { - new LightningError(e, { - message: 'An error occurred while sending the command response', - extra: { command: command.name }, - }) - } -} diff --git a/packages/lightning/src/commands/runners.ts b/packages/lightning/src/commands/runners.ts new file mode 100644 index 0000000..d37290a --- /dev/null +++ b/packages/lightning/src/commands/runners.ts @@ -0,0 +1,81 @@ +import type { lightning } from '../lightning.ts'; +import { + type create_command, + create_message, + LightningError, + type message, +} from '../structures/mod.ts'; + +export async function execute_text_command(msg: message, lightning: lightning) { + if (!msg.content?.startsWith(lightning.config.prefix)) return; + + const [cmd, ...rest] = msg.content.replace(lightning.config.prefix, '') + .split(' '); + + return await run_command({ + ...msg, + lightning, + command: cmd as string, + rest: rest as string[], + }); +} + +export async function run_command( + opts: create_command, +) { + let command = opts.lightning.commands.get(opts.command) ?? + opts.lightning.commands.get('help')!; + + const subcommand_name = opts.subcommand ?? opts.rest?.shift(); + + if (command.subcommands && subcommand_name) { + const subcommand = command.subcommands.find((i) => + i.name === subcommand_name + ); + + if (subcommand) command = subcommand; + } + + if (!opts.args) opts.args = {}; + + for (const arg of command.arguments || []) { + if (!opts.args[arg.name]) { + opts.args[arg.name] = opts.rest?.shift() as string; + } + + if (!opts.args[arg.name]) { + return opts.reply( + create_message( + `Please provide the \`${arg.name}\` argument. Try using the \`${opts.lightning.config.prefix}help\` command.`, + ), + false, + ); + } + } + + let resp: string | LightningError; + + try { + resp = await command.execute({ + ...opts, + args: opts.args, + }); + } catch (e) { + if (e instanceof LightningError) resp = e; + else {resp = new LightningError(e, { + message: 'An error occurred while executing the command', + extra: { command: command.name }, + });} + } + + try { + if (typeof resp === 'string') { + await opts.reply(create_message(resp), false); + } else await opts.reply(resp.msg, false); + } catch (e) { + new LightningError(e, { + message: 'An error occurred while sending the command response', + extra: { command: command.name }, + }); + } +} diff --git a/packages/lightning/src/bridge/data.ts b/packages/lightning/src/database.ts similarity index 58% rename from packages/lightning/src/bridge/data.ts rename to packages/lightning/src/database.ts index 12b123a..32bfa35 100644 --- a/packages/lightning/src/bridge/data.ts +++ b/packages/lightning/src/database.ts @@ -1,39 +1,6 @@ import { Client, type ClientOptions } from '@db/postgres'; import { ulid } from '@std/ulid'; - -export interface bridge { - id: string; /* ulid */ - name: string; /* name of the bridge */ - channels: bridge_channel[]; /* channels bridged */ - settings: bridge_settings; /* settings for the bridge */ -} - -export interface bridge_channel { - id: string; /* from the platform */ - data: unknown; /* data needed to bridge this channel */ - disabled: boolean; /* whether the channel is disabled */ - plugin: string; /* the plugin used to bridge this channel */ -} - -export interface bridge_settings { - allow_editing: boolean; /* allow editing/deletion */ - allow_everyone: boolean; /* @everyone/@here/@room */ - use_rawname: boolean; /* rawname = username */ -} - -export interface bridge_message { - id: string; /* original message id */ - bridge_id: string; /* bridge id */ - channels: bridge_channel[]; /* channels bridged */ - messages: bridged_message[]; /* bridged messages */ - settings: bridge_settings; /* settings for the bridge */ -} - -export interface bridged_message { - id: string[]; /* message id */ - channel: string; /* channel id */ - plugin: string; /* plugin id */ -} +import type { bridge, bridge_message } from './structures/bridge.ts'; export class bridge_data { private pg: Client; @@ -41,15 +8,15 @@ export class bridge_data { static async create(pg_options: ClientOptions): Promise { const pg = new Client(pg_options); await pg.connect(); - await bridge_data.create_table(pg); - return new bridge_data(pg); } private static async create_table(pg: Client) { - const exists = (await pg.queryArray`SELECT relname FROM pg_class - WHERE relname = 'bridges'`).rows.length > 0; + const exists = (await pg.queryArray` + SELECT relname FROM pg_class + WHERE relname = 'bridges' + `).rows.length > 0; if (exists) return; @@ -75,29 +42,31 @@ export class bridge_data { this.pg = pg_client; } - async create_bridge(br: Omit): Promise { + async create_bridge(br: Omit): Promise { const id = ulid(); await this.pg.queryArray` INSERT INTO bridges (id, name, channels, settings) - VALUES (${id}, ${br.name}, ${JSON.stringify(br.channels)}, ${JSON.stringify(br.settings)}) + VALUES (${id}, ${br.name}, ${JSON.stringify(br.channels)}, ${ + JSON.stringify(br.settings) + }) `; return { id, ...br }; } - async edit_bridge(br: Omit): Promise { + async edit_bridge(br: Omit): Promise { await this.pg.queryArray` UPDATE bridges - SET channels = ${JSON.stringify(br.channels)}, settings = ${JSON.stringify(br.settings)} + SET channels = ${JSON.stringify(br.channels)}, + settings = ${JSON.stringify(br.settings)} WHERE id = ${br.id} `; } async get_bridge_by_id(id: string): Promise { const res = await this.pg.queryObject` - SELECT * FROM bridges - WHERE id = ${id} + SELECT * FROM bridges WHERE id = ${id} `; return res.rows[0]; @@ -105,8 +74,7 @@ export class bridge_data { async get_bridge_by_channel(ch: string): Promise { const res = await this.pg.queryObject(` - SELECT * FROM bridges - WHERE EXISTS ( + SELECT * FROM bridges WHERE EXISTS ( SELECT 1 FROM jsonb_array_elements(channels) AS ch WHERE ch->>'id' = '${ch}' ) @@ -118,13 +86,18 @@ export class bridge_data { async create_message(msg: bridge_message): Promise { await this.pg.queryArray`INSERT INTO bridge_messages (id, bridge_id, channels, messages, settings) VALUES - (${msg.id}, ${msg.bridge_id}, ${JSON.stringify(msg.channels)}, ${JSON.stringify(msg.messages)}, ${JSON.stringify(msg.settings)})`; + (${msg.id}, ${msg.bridge_id}, ${JSON.stringify(msg.channels)}, ${ + JSON.stringify(msg.messages) + }, ${JSON.stringify(msg.settings)}) + `; } async edit_message(msg: bridge_message): Promise { await this.pg.queryArray` UPDATE bridge_messages - SET messages = ${JSON.stringify(msg.messages)}, channels = ${JSON.stringify(msg.channels)}, settings = ${JSON.stringify(msg.settings)} + SET messages = ${JSON.stringify(msg.messages)}, + channels = ${JSON.stringify(msg.channels)}, + settings = ${JSON.stringify(msg.settings)} WHERE id = ${msg.id} `; } diff --git a/packages/lightning/src/errors.ts b/packages/lightning/src/errors.ts deleted file mode 100644 index 485de6c..0000000 --- a/packages/lightning/src/errors.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { create_message, type message } from './messages.ts'; - -export interface LightningErrorOptions { - /** the user-facing message of the error */ - message?: string; - /** the extra data to log */ - extra?: Record; - /** whether to disable the channel */ - disable?: boolean; -} - -export class LightningError extends Error { - id: string; - override cause: Error; - extra: Record; - msg: message; - disable_channel?: boolean; - - constructor(e: unknown, public options?: LightningErrorOptions) { - if (e instanceof LightningError) { - super(e.message, { cause: e.cause }); - this.id = e.id; - this.cause = e.cause; - this.extra = e.extra; - this.msg = e.msg; - this.disable_channel = e.disable_channel; - return; - } - - const cause = e instanceof Error - ? e - : e instanceof Object - ? new Error(JSON.stringify(e)) - : new Error(String(e)); - - super(options?.message ?? cause.message, { cause }); - - this.name = 'LightningError'; - this.id = crypto.randomUUID(); - this.cause = cause; - this.extra = options?.extra ?? {}; - this.disable_channel = options?.disable; - this.msg = create_message( - `Something went wrong! Take a look at [the docs](https://williamhorning.eu.org/lightning).\n\`\`\`\n${this.message}\n${this.id}\n\`\`\`` - ); - this.log(); - } - - log() { - console.error(`%clightning error ${this.id}`, 'color: red'); - console.error(this.cause, this.options); - - const webhook = Deno.env.get('LIGHTNING_ERROR_WEBHOOK'); - - for (const key in this.extra) { - if (key === 'lightning') { - delete this.extra[key]; - } - - if (typeof this.extra[key] === 'object' && this.extra[key] !== null) { - if ('lightning' in this.extra[key]) { - delete this.extra[key].lightning; - } - } - } - - if (webhook && webhook.length > 0) { - let json_str = `\`\`\`json\n${JSON.stringify(this.extra, null, 2)}\n\`\`\``; - - if (json_str.length > 2000) json_str = '*see console*'; - - fetch(webhook, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - content: `# ${this.cause.message}\n*${this.id}*`, - embeds: [ - { - title: 'extra', - description: json_str, - }, - ], - }), - }); - } - } -} - -export function logError(e: unknown, options?: LightningErrorOptions): never { - throw new LightningError(e, options); -} diff --git a/packages/lightning/src/lightning.ts b/packages/lightning/src/lightning.ts index d2c9309..3717859 100644 --- a/packages/lightning/src/lightning.ts +++ b/packages/lightning/src/lightning.ts @@ -1,25 +1,27 @@ import type { ClientOptions } from '@db/postgres'; -import { - type command, - execute_text_command, - run_command, - type run_command_options, -} from './commands/mod.ts'; -import type { create_plugin, plugin } from './plugins.ts'; -import { bridge_data } from './bridge/data.ts'; -import { handle_message } from './bridge/msg.ts'; -import type { message } from './messages.ts'; +import { bridge_message } from './bridge.ts'; import { default_commands } from './commands/default.ts'; +import { execute_text_command, run_command } from './commands/runners.ts'; +import { bridge_data } from './database.ts'; +import type { + command, + create_command, + create_plugin, + message, + plugin, +} from './structures/mod.ts'; /** configuration options for lightning */ export interface config { + /** error URL */ + error_url?: string; /** database options */ - postgres_options: ClientOptions; + postgres: ClientOptions; /** a list of plugins */ // deno-lint-ignore no-explicit-any plugins?: create_plugin[]; /** the prefix used for commands */ - cmd_prefix: string; + prefix: string; } /** an instance of lightning */ @@ -48,6 +50,7 @@ export class lightning { } } + /** event handler */ private async _handle_events(plugin: plugin) { for await (const { name, value } of plugin) { await new Promise((res) => setTimeout(res, 150)); @@ -56,8 +59,8 @@ export class lightning { continue; } - if (name === 'run_command') { - run_command(value[0] as run_command_options); + if (name === 'create_command') { + run_command(value[0] as create_command); continue; } @@ -65,13 +68,13 @@ export class lightning { execute_text_command(value[0] as message, this); } - handle_message(this, name, value[0]); + bridge_message(this, name, value[0]); } } /** create a new instance of lightning */ static async create(config: config): Promise { - const data = await bridge_data.create(config.postgres_options); + const data = await bridge_data.create(config.postgres); return new lightning(data, config); } diff --git a/packages/lightning/src/messages.ts b/packages/lightning/src/messages.ts deleted file mode 100644 index d2c1f6d..0000000 --- a/packages/lightning/src/messages.ts +++ /dev/null @@ -1,155 +0,0 @@ -import type { bridge_channel } from './bridge/data.ts'; - -/** - * creates a message that can be sent using lightning - * @param text the text of the message (can be markdown) - */ -export function create_message(text: string): message { - const data = { - author: { - username: 'lightning', - profile: 'https://williamhorning.eu.org/assets/lightning.png', - rawname: 'lightning', - id: 'lightning', - }, - content: text, - channel: '', - id: '', - reply: async () => {}, - timestamp: Temporal.Now.instant(), - plugin: 'lightning', - }; - return data; -} - -/** attachments within a message */ -export interface attachment { - /** alt text for images */ - alt?: string; - /** a URL pointing to the file */ - file: string; - /** the file's name */ - name?: string; - /** whether or not the file has a spoiler */ - spoiler?: boolean; - /** file size */ - size: number; -} - -/** a representation of a message that has been deleted */ -export interface deleted_message { - /** the message's id */ - id: string; - /** the channel the message was sent in */ - channel: string; - /** the plugin that recieved the message */ - plugin: string; - /** the time the message was sent/edited as a temporal instant */ - timestamp: Temporal.Instant; -} - -/** a discord-style embed */ -export interface embed { - /** the author of the embed */ - author?: { - /** the name of the author */ - name: string; - /** the url of the author */ - url?: string; - /** the icon of the author */ - icon_url?: string; - }; - /** the color of the embed */ - color?: number; - /** the text in an embed */ - description?: string; - /** fields within the embed */ - fields?: { - /** the name of the field */ - name: string; - /** the value of the field */ - value: string; - /** whether or not the field is inline */ - inline?: boolean; - }[]; - /** a footer shown in the embed */ - footer?: { - /** the footer text */ - text: string; - /** the icon of the footer */ - icon_url?: string; - }; - /** an image shown in the embed */ - image?: media; - /** a thumbnail shown in the embed */ - thumbnail?: media; - /** the time (in epoch ms) shown in the embed */ - timestamp?: number; - /** the title of the embed */ - title?: string; - /** a site linked to by the embed */ - url?: string; - /** a video inside of the embed */ - video?: media; -} - -/** media inside of an embed */ -export interface media { - /** the height of the media */ - height?: number; - /** the url of the media */ - url: string; - /** the width of the media */ - width?: number; -} - -/** a message recieved by a plugin */ -export interface message extends deleted_message { - /** the attachments sent with the message */ - attachments?: attachment[]; - /** the author of the message */ - author: { - /** the nickname of the author */ - username: string; - /** the author's username */ - rawname: string; - /** a url pointing to the authors profile picture */ - profile?: string; - /** a url pointing to the authors banner */ - banner?: string; - /** the author's id */ - id: string; - /** the color of an author */ - color?: string; - }; - /** message content (can be markdown) */ - content?: string; - /** discord-style embeds */ - embeds?: embed[]; - /** a function to reply to a message */ - reply: (message: message, optional?: unknown) => Promise; - /** the id of the message replied to */ - reply_id?: string; -} - -/** a message to be bridged */ -export interface create_message_opts { - msg: message, - channel: bridge_channel, - reply_id?: string, -} - -/** a message to be edited */ -export interface edit_message_opts { - msg: message, - channel: bridge_channel, - reply_id?: string, - edit_ids: string[], -} - -/** a message to be deleted */ -export interface delete_message_opts { - msg: deleted_message, - channel: bridge_channel, - edit_ids: string[], -} \ No newline at end of file diff --git a/packages/lightning/src/mod.ts b/packages/lightning/src/mod.ts index 76a566c..4cb23fc 100644 --- a/packages/lightning/src/mod.ts +++ b/packages/lightning/src/mod.ts @@ -1,5 +1,6 @@ -export { type command, type run_command_options } from './commands/mod.ts' -export { LightningError, logError } from './errors.ts'; -export { type config, lightning } from './lightning.ts'; -export * from './messages.ts'; -export * from './plugins.ts'; +if (import.meta.main) { + await import('./cli.ts'); +} + +export * from './lightning.ts'; +export * from './structures/mod.ts'; diff --git a/packages/lightning/src/structures/bridge.ts b/packages/lightning/src/structures/bridge.ts new file mode 100644 index 0000000..d5d20cd --- /dev/null +++ b/packages/lightning/src/structures/bridge.ts @@ -0,0 +1,100 @@ +import type { deleted_message, message } from './messages.ts'; + +/** representation of a bridge */ +export interface bridge { + /** ulid secret used as primary key */ + id: string; + /** user-facing name of the bridge */ + name: string; + /** channels in the bridge */ + channels: bridge_channel[]; + /** settings for the bridge */ + settings: bridge_settings; +} + +/** a channel within a bridge */ +export interface bridge_channel { + /** from the platform */ + id: string; + /** data needed to bridge this channel */ + data: unknown; + /** whether the channel is disabled */ + disabled: boolean; + /** the plugin used to bridge this channel */ + plugin: string; +} + +// TODO(jersey): implement allow_everyone and use_rawname settings + +/** possible settings for a bridge */ +export interface bridge_settings { + /** allow editing/deletion */ + allow_editing: boolean; + /** @everyone/@here/@room */ + allow_everyone: boolean; + /** rawname = username */ + use_rawname: boolean; +} + +/** list of settings for a bridge */ +export const bridge_settings_list = [ + 'allow_editing', + 'allow_everyone', + 'use_rawname', +]; + +/** representation of a bridged message collection */ +export interface bridge_message { + /** original message id */ + id: string; + /** original bridge id */ + bridge_id: string; + /** channels in the bridge */ + channels: bridge_channel[]; + /** messages bridged */ + messages: bridged_message[]; + /** settings for the bridge */ + settings: bridge_settings; +} + +/** representation of an individual bridged message */ +export interface bridged_message { + /** ids of the message */ + id: string[]; + /** the channel id sent to */ + channel: string; + /** the plugin used */ + plugin: string; +} + +/** a message to be bridged */ +export interface create_message_opts { + /** the actual message */ + msg: message; + /** the channel to use */ + channel: bridge_channel; + /** message to reply to, if any */ + reply_id?: string; +} + +/** a message to be edited */ +export interface edit_message_opts { + /** the actual message */ + msg: message; + /** the channel to use */ + channel: bridge_channel; + /** message to reply to, if any */ + reply_id?: string; + /** ids of messages to edit */ + edit_ids: string[]; +} + +/** a message to be deleted */ +export interface delete_message_opts { + /** the actual deleted message */ + msg: deleted_message; + /** the channel to use */ + channel: bridge_channel; + /** ids of messages to delete */ + edit_ids: string[]; +} diff --git a/packages/lightning/src/structures/commands.ts b/packages/lightning/src/structures/commands.ts new file mode 100644 index 0000000..f6258b3 --- /dev/null +++ b/packages/lightning/src/structures/commands.ts @@ -0,0 +1,41 @@ +import type { lightning } from '../lightning.ts'; + +/** representation of a command */ +export interface command { + /** user-facing command name */ + name: string; + /** user-facing command description */ + description: string; + /** possible arguments */ + arguments?: command_argument[]; + /** possible subcommands (use `${prefix}${cmd} ${subcmd}` if run as text command) */ + subcommands?: Omit[]; + /** the functionality of the command, returning text */ + execute: ( + opts: command_opts, + ) => Promise | string; +} + +/** argument for a command */ +export interface command_argument { + /** user-facing name for the argument */ + name: string; + /** description of the argument */ + description: string; + /** whether the argument is required */ + required: boolean; +} + +/** options passed to command#execute */ +export interface command_opts { + /** the channel the command was run in */ + channel: string; + /** the plugin the command was run with */ + plugin: string; + /** the time the command was sent */ + timestamp: Temporal.Instant; + /** arguments for the command */ + args: Record; + /** a lightning instance */ + lightning: lightning; +} diff --git a/packages/lightning/src/structures/errors.ts b/packages/lightning/src/structures/errors.ts new file mode 100644 index 0000000..b49c418 --- /dev/null +++ b/packages/lightning/src/structures/errors.ts @@ -0,0 +1,107 @@ +import { create_message, type message } from './messages.ts'; + +/** options used to create an error */ +export interface error_options { + /** the user-facing message of the error */ + message?: string; + /** the extra data to log */ + extra?: Record; + /** whether to disable the associated channel (when bridging) */ + disable?: boolean; +} + +/** logs an error */ +export function log_error(e: unknown, options?: error_options): never { + throw new LightningError(e, options); +} + +/** lightning error */ +export class LightningError extends Error { + /** the id associated with the error */ + id: string; + /** the cause of the error */ + override cause: Error; + /** extra information associated with the error */ + extra: Record; + /** the user-facing error message */ + msg: message; + /** whether to disable the associated channel (when bridging) */ + disable_channel?: boolean; + + /** create and log an error */ + constructor(e: unknown, public options?: error_options) { + if (e instanceof LightningError) { + super(e.message, { cause: e.cause }); + this.id = e.id; + this.cause = e.cause; + this.extra = e.extra; + this.msg = e.msg; + this.disable_channel = e.disable_channel; + return; + } + + const cause = e instanceof Error + ? e + : e instanceof Object + ? new Error(JSON.stringify(e)) + : new Error(String(e)); + + const id = crypto.randomUUID(); + + super(options?.message ?? cause.message, { cause }); + + this.name = 'LightningError'; + this.id = id; + this.cause = cause; + this.extra = options?.extra ?? {}; + this.disable_channel = options?.disable; + this.msg = create_message( + `Something went wrong! Take a look at [the docs](https://williamhorning.eu.org/lightning).\n\`\`\`\n${this.message}\n${this.id}\n\`\`\``, + ); + + // the error-logging async fun + (async () => { + console.error(`%clightning error ${id}`, 'color: red'); + console.error(cause, this.options); + + const webhook = Deno.env.get('LIGHTNING_ERROR_WEBHOOK'); + + for (const key in this.options?.extra) { + if (key === 'lightning') { + delete this.options.extra[key]; + } + + if ( + typeof this.options.extra[key] === 'object' && + this.options.extra[key] !== null + ) { + if ('lightning' in this.options.extra[key]) { + delete this.options.extra[key].lightning; + } + } + } + + if (webhook && webhook.length > 0) { + let json_str = `\`\`\`json\n${ + JSON.stringify(this.options?.extra, null, 2) + }\n\`\`\``; + + if (json_str.length > 2000) json_str = '*see console*'; + + await fetch(webhook, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + content: `# ${cause.message}\n*${id}*`, + embeds: [ + { + title: 'extra', + description: json_str, + }, + ], + }), + }); + } + })(); + } +} diff --git a/packages/lightning/src/structures/events.ts b/packages/lightning/src/structures/events.ts new file mode 100644 index 0000000..6f433eb --- /dev/null +++ b/packages/lightning/src/structures/events.ts @@ -0,0 +1,30 @@ +import type { command_opts } from './commands.ts'; +import type { deleted_message, message } from './messages.ts'; + +/** command execution event */ +export interface create_command extends Omit { + /** the command to run */ + command: string; + /** the subcommand, if any, to use */ + subcommand?: string; + /** arguments, if any, to use */ + args?: Record; + /** extra string options */ + rest?: string[]; + /** event reply function */ + reply: message['reply']; + /** id of the associated event */ + id: string; +} + +/** the events emitted by a plugin */ +export type plugin_events = { + /** when a message is created */ + create_message: [message]; + /** when a message is edited */ + edit_message: [message]; + /** when a message is deleted */ + delete_message: [deleted_message]; + /** when a command is run */ + create_command: [create_command]; +}; diff --git a/packages/lightning/src/structures/media.ts b/packages/lightning/src/structures/media.ts new file mode 100644 index 0000000..e4ef5c7 --- /dev/null +++ b/packages/lightning/src/structures/media.ts @@ -0,0 +1,68 @@ +/** attachments within a message */ +export interface attachment { + /** alt text for images */ + alt?: string; + /** a URL pointing to the file */ + file: string; + /** the file's name */ + name?: string; + /** whether or not the file has a spoiler */ + spoiler?: boolean; + /** file size */ + size: number; +} + +/** a discord-style embed */ +export interface embed { + /** the author of the embed */ + author?: { + /** the name of the author */ + name: string; + /** the url of the author */ + url?: string; + /** the icon of the author */ + icon_url?: string; + }; + /** the color of the embed */ + color?: number; + /** the text in an embed */ + description?: string; + /** fields within the embed */ + fields?: { + /** the name of the field */ + name: string; + /** the value of the field */ + value: string; + /** whether or not the field is inline */ + inline?: boolean; + }[]; + /** a footer shown in the embed */ + footer?: { + /** the footer text */ + text: string; + /** the icon of the footer */ + icon_url?: string; + }; + /** an image shown in the embed */ + image?: media; + /** a thumbnail shown in the embed */ + thumbnail?: media; + /** the time (in epoch ms) shown in the embed */ + timestamp?: number; + /** the title of the embed */ + title?: string; + /** a site linked to by the embed */ + url?: string; + /** a video inside of the embed */ + video?: media; +} + +/** media inside of an embed */ +export interface media { + /** the height of the media */ + height?: number; + /** the url of the media */ + url: string; + /** the width of the media */ + width?: number; +} diff --git a/packages/lightning/src/structures/messages.ts b/packages/lightning/src/structures/messages.ts new file mode 100644 index 0000000..20eaf84 --- /dev/null +++ b/packages/lightning/src/structures/messages.ts @@ -0,0 +1,67 @@ +import type { attachment, embed } from './media.ts'; + +/** + * creates a message that can be sent using lightning + * @param text the text of the message (can be markdown) + */ +export function create_message(text: string): message { + const data = { + author: { + username: 'lightning', + profile: 'https://williamhorning.eu.org/assets/lightning.png', + rawname: 'lightning', + id: 'lightning', + }, + content: text, + channel: '', + id: '', + reply: async () => {}, + timestamp: Temporal.Now.instant(), + plugin: 'lightning', + }; + return data; +} + +/** a representation of a message that has been deleted */ +export interface deleted_message { + /** the message's id */ + id: string; + /** the channel the message was sent in */ + channel: string; + /** the plugin that recieved the message */ + plugin: string; + /** the time the message was sent/edited as a temporal instant */ + timestamp: Temporal.Instant; +} + +/** a message recieved by a plugin */ +export interface message extends deleted_message { + /** the attachments sent with the message */ + attachments?: attachment[]; + /** the author of the message */ + author: message_author; + /** message content (can be markdown) */ + content?: string; + /** discord-style embeds */ + embeds?: embed[]; + /** a function to reply to a message */ + reply: (message: message, optional?: unknown) => Promise; + /** the id of the message replied to */ + reply_id?: string; +} + +/** an author of a message */ +export interface message_author { + /** the nickname of the author */ + username: string; + /** the author's username */ + rawname: string; + /** a url pointing to the authors profile picture */ + profile?: string; + /** a url pointing to the authors banner */ + banner?: string; + /** the author's id */ + id: string; + /** the color of an author */ + color?: string; +} diff --git a/packages/lightning/src/structures/mod.ts b/packages/lightning/src/structures/mod.ts new file mode 100644 index 0000000..89145a1 --- /dev/null +++ b/packages/lightning/src/structures/mod.ts @@ -0,0 +1,7 @@ +export * from './bridge.ts'; +export * from './commands.ts'; +export * from './errors.ts'; +export * from './events.ts'; +export * from './media.ts'; +export * from './messages.ts'; +export * from './plugins.ts'; diff --git a/packages/lightning/src/plugins.ts b/packages/lightning/src/structures/plugins.ts similarity index 77% rename from packages/lightning/src/plugins.ts rename to packages/lightning/src/structures/plugins.ts index 2be6c0a..b8540b6 100644 --- a/packages/lightning/src/plugins.ts +++ b/packages/lightning/src/structures/plugins.ts @@ -1,13 +1,11 @@ import { EventEmitter } from '@denosaurs/event'; -import type { lightning } from './lightning.ts'; +import type { lightning } from '../lightning.ts'; import type { create_message_opts, delete_message_opts, - deleted_message, edit_message_opts, - message, -} from './messages.ts'; -import type { run_command_options } from './commands/mod.ts'; +} from './bridge.ts'; +import type { plugin_events } from './events.ts'; /** the way to make a plugin */ export interface create_plugin< @@ -21,18 +19,6 @@ export interface create_plugin< support: string[]; } -/** the events emitted by a plugin */ -export type plugin_events = { - /** when a message is created */ - create_message: [message]; - /** when a message is edited */ - edit_message: [message]; - /** when a message is deleted */ - delete_message: [deleted_message]; - /** when a command is run */ - run_command: [run_command_options]; -}; - /** a plugin for lightning */ export abstract class plugin extends EventEmitter { /** access the instance of lightning you're connected to */ From ae7fd398fba6861b5ee0ad85a2b1e77ee2d45218 Mon Sep 17 00:00:00 2001 From: Jersey Date: Fri, 29 Nov 2024 21:54:24 -0500 Subject: [PATCH 16/23] discord plugin to v8 --- packages/lightning-plugin-discord/deno.json | 8 +- .../src/bridge_to_discord.ts | 86 ++++++++ .../lightning-plugin-discord/src/commands.ts | 67 ------- .../lightning-plugin-discord/src/discord.ts | 83 -------- .../src/discord_message/files.ts | 31 +++ .../src/discord_message/get_author.ts | 23 +++ .../src/discord_message/mod.ts | 52 +++++ .../src/discord_message/reply_embed.ts | 24 +++ .../src/error_handler.ts | 38 ++++ .../lightning-plugin-discord/src/lightning.ts | 94 --------- packages/lightning-plugin-discord/src/mod.ts | 187 ++++++++---------- .../src/process_message.ts | 73 ------- .../src/slash_commands.ts | 58 ++++++ .../src/to_lightning/command.ts | 38 ++++ .../src/to_lightning/deleted.ts | 13 ++ .../src/to_lightning/message.ts | 91 +++++++++ packages/lightning/src/structures/media.ts | 2 +- 17 files changed, 544 insertions(+), 424 deletions(-) create mode 100644 packages/lightning-plugin-discord/src/bridge_to_discord.ts delete mode 100644 packages/lightning-plugin-discord/src/commands.ts delete mode 100644 packages/lightning-plugin-discord/src/discord.ts create mode 100644 packages/lightning-plugin-discord/src/discord_message/files.ts create mode 100644 packages/lightning-plugin-discord/src/discord_message/get_author.ts create mode 100644 packages/lightning-plugin-discord/src/discord_message/mod.ts create mode 100644 packages/lightning-plugin-discord/src/discord_message/reply_embed.ts create mode 100644 packages/lightning-plugin-discord/src/error_handler.ts delete mode 100644 packages/lightning-plugin-discord/src/lightning.ts delete mode 100644 packages/lightning-plugin-discord/src/process_message.ts create mode 100644 packages/lightning-plugin-discord/src/slash_commands.ts create mode 100644 packages/lightning-plugin-discord/src/to_lightning/command.ts create mode 100644 packages/lightning-plugin-discord/src/to_lightning/deleted.ts create mode 100644 packages/lightning-plugin-discord/src/to_lightning/message.ts diff --git a/packages/lightning-plugin-discord/deno.json b/packages/lightning-plugin-discord/deno.json index b22054a..57c0760 100644 --- a/packages/lightning-plugin-discord/deno.json +++ b/packages/lightning-plugin-discord/deno.json @@ -4,9 +4,9 @@ "exports": "./src/mod.ts", "imports": { "@jersey/lightning": "jsr:@jersey/lightning@^0.8.0", - "@discordjs/core": "npm:@discordjs/core@^1.2.0", - "@discordjs/rest": "npm:@discordjs/rest@^2.3.0", - "@discordjs/ws": "npm:@discordjs/ws@^1.1.1", - "discord-api-types": "npm:discord-api-types@0.37.83/v10" + "@discordjs/core": "npm:@discordjs/core@^2.0.0", + "@discordjs/rest": "npm:@discordjs/rest@^2.4.0", + "@discordjs/ws": "npm:@discordjs/ws@^2.0.0", + "discord-api-types": "npm:discord-api-types@^0.37.110/v10" } } diff --git a/packages/lightning-plugin-discord/src/bridge_to_discord.ts b/packages/lightning-plugin-discord/src/bridge_to_discord.ts new file mode 100644 index 0000000..c3b6ba3 --- /dev/null +++ b/packages/lightning-plugin-discord/src/bridge_to_discord.ts @@ -0,0 +1,86 @@ +import type { + create_message_opts, + delete_message_opts, + edit_message_opts, +} from '@jersey/lightning'; +import { message_to_discord } from './discord_message/mod.ts'; +import { error_handler } from './error_handler.ts'; +import type { API } from '@discordjs/core'; + +type data = { id: string; token: string }; + +export async function setup_bridge(api: API, channel: string) { + try { + const { id, token } = await api.channels.createWebhook( + channel, + { + name: 'lightning bridge', + }, + ); + + return { id, token }; + } catch (e) { + return error_handler(e, channel, 'setting up channel'); + } +} + +export async function create_message(api: API, opts: create_message_opts) { + const data = opts.channel.data as data; + const transformed = await message_to_discord( + opts.msg, + api, + opts.channel.id, + opts.reply_id, + ); + + try { + const res = await api.webhooks.execute( + data.id, + data.token, + transformed, + ); + + return [res.id]; + } catch (e) { + return error_handler(e, opts.channel.id, 'creating message'); + } +} + +export async function edit_message(api: API, opts: edit_message_opts) { + const data = opts.channel.data as data; + const transformed = await message_to_discord( + opts.msg, + api, + opts.channel.id, + opts.reply_id, + ); + + try { + await api.webhooks.editMessage( + data.id, + data.token, + opts.edit_ids[0], + transformed, + ); + + return opts.edit_ids; + } catch (e) { + return error_handler(e, opts.channel.id, 'editing message'); + } +} + +export async function delete_message(api: API, opts: delete_message_opts) { + const data = opts.channel.data as data; + + try { + await api.webhooks.deleteMessage( + data.id, + data.token, + opts.edit_ids[0], + ); + + return opts.edit_ids; + } catch (e) { + return error_handler(e, opts.channel.id, 'editing message'); + } +} diff --git a/packages/lightning-plugin-discord/src/commands.ts b/packages/lightning-plugin-discord/src/commands.ts deleted file mode 100644 index 267b007..0000000 --- a/packages/lightning-plugin-discord/src/commands.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type { API } from '@discordjs/core'; -import type { command, run_command_options, lightning } from '@jersey/lightning'; -import type { APIInteraction } from 'discord-api-types'; -import { to_discord } from './discord.ts'; -import { instant } from './lightning.ts'; - -export function to_command(interaction: { api: API; data: APIInteraction }, lightning: lightning) { - 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; - } - - return { - command: interaction.data.data.name, - subcommand: subcmd, - channel: interaction.data.channel.id, - id: interaction.data.id, - timestamp: instant(interaction.data.id), - lightning, - plugin: 'bolt-discord', - reply: async (msg) => { - await interaction.api.interactions.reply( - interaction.data.id, - interaction.data.token, - await to_discord(msg), - ); - }, - args: opts, - } as run_command_options; -} - -export function to_intent_opts({ arguments: args, subcommands }: command) { - const opts = []; - - if (args) { - for (const arg of args) { - opts.push({ - name: arg.name, - description: arg.description, - type: 3, - required: arg.required, - }); - } - } - - if (subcommands) { - for (const sub of subcommands) { - opts.push({ - name: sub.name, - description: sub.description, - type: 1, - options: sub.arguments?.map((opt) => ({ - name: opt.name, - description: opt.description, - type: 3, - required: opt.required, - })), - }); - } - } - - return opts; -} diff --git a/packages/lightning-plugin-discord/src/discord.ts b/packages/lightning-plugin-discord/src/discord.ts deleted file mode 100644 index 9fe95ad..0000000 --- a/packages/lightning-plugin-discord/src/discord.ts +++ /dev/null @@ -1,83 +0,0 @@ -import type { RawFile } from '@discordjs/rest'; -import type { message } from '@jersey/lightning'; -import type { - GatewayMessageUpdateDispatchData as update_data, - RESTPostAPIWebhookWithTokenJSONBody as wh_token, - RESTPostAPIWebhookWithTokenQuery as wh_query, -} from 'discord-api-types'; - -async function async_flat(arr: A[], f: (a: A) => Promise) { - return (await Promise.all(arr.map(f))).flat(); -} - -export type discord_message = Omit; - -type webhook_message = wh_query & wh_token & { files?: RawFile[]; wait: true }; - -export async function to_discord( - message: message, - replied_message?: discord_message, -): Promise { - if (message.reply_id && replied_message) { - if (!message.embeds) message.embeds = []; - message.embeds.push( - { - author: { - name: `replying to ${ - replied_message.member?.nick || - replied_message.author?.global_name || - replied_message.author?.username || - 'a user' - }`, - icon_url: - `https://cdn.discordapp.com/avatars/${replied_message.author?.id}/${replied_message.author?.avatar}.png`, - }, - description: replied_message.content, - }, - ...(replied_message.embeds || []).map( - (i: Exclude[0]) => { - return { - ...i, - timestamp: i.timestamp ? Number(i.timestamp) : undefined, - video: i.video ? { ...i.video, url: i.video.url || '' } : undefined, - }; - }, - ), - ); - } - - if ((!message.content || message.content.length < 1) && !message.embeds) { - message.content = '*empty message*'; - } - - if (!message.author.username || message.author.username.length < 1) { - message.author.username = message.author.id; - } - - return { - avatar_url: message.author.profile, - content: message.content, - embeds: message.embeds?.map((i) => { - return { - ...i, - timestamp: i.timestamp ? String(i.timestamp) : undefined, - }; - }), - files: message.attachments - ? 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: new Uint8Array( - await (await fetch(a.file)).arrayBuffer(), - ), - }, - ]; - }) - : undefined, - username: message.author.username, - wait: true, - }; -} diff --git a/packages/lightning-plugin-discord/src/discord_message/files.ts b/packages/lightning-plugin-discord/src/discord_message/files.ts new file mode 100644 index 0000000..daf4d30 --- /dev/null +++ b/packages/lightning-plugin-discord/src/discord_message/files.ts @@ -0,0 +1,31 @@ +import type { attachment } from '@jersey/lightning'; +import type { RawFile } from '@discordjs/rest'; + +export async function files_up_to_25MiB(attachments: attachment[] | undefined) { + if (!attachments) return; + + const files: RawFile[] = []; + const total_size = 0; + + for (const attachment of attachments) { + if (attachment.size >= 25) continue; + if (total_size + attachment.size >= 25) break; + + try { + const data = new Uint8Array( + await (await fetch(attachment.file, { + signal: AbortSignal.timeout(5000), + })).arrayBuffer(), + ); + + files.push({ + name: attachment.name ?? attachment.file.split('/').pop()!, + data, + }); + } catch { + continue; + } + } + + return files; +} diff --git a/packages/lightning-plugin-discord/src/discord_message/get_author.ts b/packages/lightning-plugin-discord/src/discord_message/get_author.ts new file mode 100644 index 0000000..31bdff9 --- /dev/null +++ b/packages/lightning-plugin-discord/src/discord_message/get_author.ts @@ -0,0 +1,23 @@ +import type { APIMessage } from 'discord-api-types'; +import type { API } from '@discordjs/core'; +import { calculateUserDefaultAvatarIndex } from '@discordjs/rest'; + +export async function get_author(api: API, message: APIMessage) { + let name = message.author.global_name || message.author.username; + let avatar = message.author.avatar ? `https://cdn.discordapp.com/avatars/${message.author.id}/${message.author.avatar}.png` : `https://cdn.discordapp.com/embed/avatars/${calculateUserDefaultAvatarIndex(message.author.id)}.png`; + + const channel = await api.channels.get(message.channel_id); + + if ("guild_id" in channel) { + try { + const member = await api.guilds.getMember(channel.guild_id, message.author.id); + + name ??= member.nick; + avatar = member.avatar ? `https://cdn.discordapp.com/guilds/${channel.guild_id}/users/${message.author.id}/avatars/${member.avatar}.png` : avatar; + } catch { + // safe to ignore + } + } + + return { name, avatar } +} diff --git a/packages/lightning-plugin-discord/src/discord_message/mod.ts b/packages/lightning-plugin-discord/src/discord_message/mod.ts new file mode 100644 index 0000000..eb28b44 --- /dev/null +++ b/packages/lightning-plugin-discord/src/discord_message/mod.ts @@ -0,0 +1,52 @@ +import type { message } from '@jersey/lightning'; +import type { API } from '@discordjs/core'; +import type { + RESTPostAPIWebhookWithTokenJSONBody, + RESTPostAPIWebhookWithTokenQuery, +} from 'discord-api-types'; +import type { RawFile } from '@discordjs/rest'; +import { reply_embed } from './reply_embed.ts'; +import { files_up_to_25MiB } from './files.ts'; + +export interface discord_message_send + extends + RESTPostAPIWebhookWithTokenJSONBody, + RESTPostAPIWebhookWithTokenQuery { + files?: RawFile[]; + wait: true; +} + +export async function message_to_discord( + msg: message, + api?: API, + channel?: string, + reply_id?: string, +): Promise { + const discord: discord_message_send = { + avatar_url: msg.author.profile, + content: (msg.content?.length || 0) > 2000 + ? `${msg.content?.substring(0, 1997)}...` + : msg.content, + embeds: msg.embeds?.map((e) => { + return { ...e, timestamp: e.timestamp?.toString() }; + }), + username: msg.author.username, + wait: true, + }; + + if (api && channel && reply_id) { + const embed = await reply_embed(api, channel, reply_id); + if (embed) { + if (!discord.embeds) discord.embeds = []; + discord.embeds.push(embed); + } + } + + discord.files = await files_up_to_25MiB(msg.attachments); + + if (!discord.content && (!discord.embeds || discord.embeds.length === 0)) { + discord.content = '*empty message*'; + } + + return discord; +} diff --git a/packages/lightning-plugin-discord/src/discord_message/reply_embed.ts b/packages/lightning-plugin-discord/src/discord_message/reply_embed.ts new file mode 100644 index 0000000..39d48cf --- /dev/null +++ b/packages/lightning-plugin-discord/src/discord_message/reply_embed.ts @@ -0,0 +1,24 @@ +import type { API } from '@discordjs/core'; +import type { APIMessage } from 'discord-api-types'; +import { get_author } from './get_author.ts'; + +export async function reply_embed(api: API, channel: string, id: string) { + try { + const message = await api.channels.getMessage( + channel, + id, + ) as APIMessage; + + const { name, avatar } = await get_author(api, message); + + return { + author: { + name: `replying to ${name}`, + icon_url: avatar, + }, + description: message.content, + }; + } catch { + return; + } +} diff --git a/packages/lightning-plugin-discord/src/error_handler.ts b/packages/lightning-plugin-discord/src/error_handler.ts new file mode 100644 index 0000000..0486ff0 --- /dev/null +++ b/packages/lightning-plugin-discord/src/error_handler.ts @@ -0,0 +1,38 @@ +import { DiscordAPIError } from '@discordjs/rest'; +import { log_error } from '@jersey/lightning'; + +export function error_handler(e: unknown, channel_id: string, action: string) { + if (e instanceof DiscordAPIError) { + if (e.code === 30007 || e.code === 30058) { + log_error(e, { + message: + 'too many webhooks in channel/guild. try deleting some', + extra: { channel_id }, + }); + } else if (e.code === 50013) { + log_error(e, { + message: + 'missing permissions to create webhook. check bot permissions', + extra: { channel_id }, + }); + } else if (e.code === 10003 || e.code === 10015 || e.code === 50027) { + log_error(e, { + disable: true, + message: `disabling channel due to error code ${e.code}`, + extra: { channel_id }, + }); + } else if (action === 'editing message' && e.code === 10008) { + return []; // message already deleted or non-existent + } else { + log_error(e, { + message: `unknown error ${action}`, + extra: { channel_id, code: e.code }, + }); + } + } else { + log_error(e, { + message: `unknown error ${action}`, + extra: { channel_id }, + }); + } +} diff --git a/packages/lightning-plugin-discord/src/lightning.ts b/packages/lightning-plugin-discord/src/lightning.ts deleted file mode 100644 index feeaa82..0000000 --- a/packages/lightning-plugin-discord/src/lightning.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type { API } from '@discordjs/core'; -import type { message } from '@jersey/lightning'; -import { type discord_message, to_discord } from './discord.ts'; - -export function instant(id: string) { - return Temporal.Instant.fromEpochMilliseconds( - Number(BigInt(id) >> 22n) + 1420070400000, - ); -} - -export async function to_message( - api: API, - message: discord_message, -): Promise { - if (message.flags && message.flags & 128) message.content = 'Loading...'; - - 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*'; - } - } - } - - const data = { - author: { - profile: - `https://cdn.discordapp.com/avatars/${message.author?.id}/${message.author?.avatar}.png`, - username: 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: '#5865F2', - }, - channel: message.channel_id, - content: (message.content?.length || 0) > 2000 - ? `${message.content?.substring(0, 1997)}...` - : message.content, - id: message.id, - timestamp: instant(message.id), - embeds: message.embeds?.map( - (i: Exclude[0]) => { - return { - ...i, - timestamp: i.timestamp ? Number(i.timestamp) : undefined, - }; - }, - ), - reply: async (msg: message) => { - if (!data.author.id || data.author.id === '') return; - await api.channels.createMessage(message.channel_id, { - ...(await to_discord(msg)), - message_reference: { - message_id: message.id, - }, - }); - }, - plugin: 'bolt-discord', - attachments: message.attachments?.map( - (i: Exclude[0]) => { - return { - file: i.url, - alt: i.description, - name: i.filename, - size: i.size / 1000000, - }; - }, - ), - reply_id: message.referenced_message?.id, - }; - - return data as message; -} diff --git a/packages/lightning-plugin-discord/src/mod.ts b/packages/lightning-plugin-discord/src/mod.ts index 75e5da9..3a301e5 100644 --- a/packages/lightning-plugin-discord/src/mod.ts +++ b/packages/lightning-plugin-discord/src/mod.ts @@ -2,110 +2,93 @@ import { Client } from '@discordjs/core'; import { REST } from '@discordjs/rest'; import { WebSocketManager } from '@discordjs/ws'; import { - type lightning, - type message_options, - plugin, - type process_result, + type create_message_opts, + type delete_message_opts, + type edit_message_opts, + type lightning, + plugin, } from '@jersey/lightning'; import { GatewayDispatchEvents } from 'discord-api-types'; -import { to_command, to_intent_opts } from './commands.ts'; -import { to_message } from './lightning.ts'; -import { process_message } from './process_message.ts'; - -/** options for the discord plugin */ -export type discord_config = { - /** your bot's application id */ - app_id: string; - /** the token for your bot */ - token: string; - /** whether or not to enable slash commands */ - slash_cmds?: boolean; -}; +import * as bridge from './bridge_to_discord.ts'; +import { setup_slash_commands } from './slash_commands.ts'; +import { command_to } from './to_lightning/command.ts'; +import { deleted } from './to_lightning/deleted.ts'; +import { message } from './to_lightning/message.ts'; + +/** configuration for the discord plugin */ +export interface discord_config { + /** the discord bot token */ + token: string; + /** whether to enable slash commands */ + slash_commands: boolean; + /** discord application id */ + application_id: string; +} -/** the plugin to use */ export class discord_plugin extends plugin { - bot: Client; - name = 'bolt-discord'; - - /** setup the plugin */ - constructor(l: lightning, config: discord_config) { - super(l, config); - this.config = config; - this.bot = this.setup_client(); - this.setup_events(); - this.setup_commands(); - } - - private setup_client() { - const rest = new REST({ - version: '10', - /* @ts-ignore this works */ - makeRequest: fetch, - }).setToken(this.config.token); - - const gateway = new WebSocketManager({ - rest, - token: this.config.token, - intents: 0 | 33281, - }); - - gateway.connect(); - - // @ts-ignore this works? - return new Client({ rest, gateway }); - } - - private setup_events() { - this.bot.on(GatewayDispatchEvents.MessageCreate, async (msg) => { - this.emit('create_message', await to_message(msg.api, msg.data)); - }); - - this.bot.on(GatewayDispatchEvents.MessageUpdate, async (msg) => { - this.emit('edit_message', await to_message(msg.api, msg.data)); - }); - - this.bot.on(GatewayDispatchEvents.MessageDelete, async (msg) => { - this.emit('delete_message', await to_message(msg.api, msg.data)); - }); - - this.bot.on(GatewayDispatchEvents.InteractionCreate, (interaction) => { - const cmd = to_command(interaction, this.lightning); - if (cmd) this.emit('run_command', cmd); - }); - } - - private setup_commands() { - if (!this.config.slash_cmds) return; - - this.bot.api.applicationCommands.bulkOverwriteGlobalCommands( - this.config.app_id, - [...this.lightning.commands.values()].map((command) => { - return { - name: command.name, - type: 1, - description: command.description || 'a command', - options: to_intent_opts(command), - }; - }), - ); - } - - /** creates a webhook in the channel for a bridge */ - async create_bridge( - channel: string, - ): Promise<{ id: string; token?: string }> { - const { id, token } = await this.bot.api.channels.createWebhook( - channel, - { - name: 'lightning bridge', - }, - ); - - return { id, token }; - } - - /** process a message event */ - async process_message(opts: message_options): Promise { - return await process_message(this.bot.api, opts); - } + name = 'bolt-discord'; + private api: Client['api']; + private client: Client; + + constructor(l: lightning, config: discord_config) { + super(l, config); + // @ts-ignore their type for makeRequest is funky + const rest = new REST({ version: '10', makeRequest: fetch }).setToken( + config.token, + ); + const gateway = new WebSocketManager({ + token: config.token, + intents: 0 | 33281, + rest, + }); + // @ts-ignore Deno is wrong here. + this.client = new Client({ rest, gateway }); + this.api = this.client.api; + + setup_slash_commands(this.api, config, l); + this.setup_events(); + gateway.connect(); + } + + private setup_events() { + // @ts-ignore I'm going to file an issue against Deno because this is so annoying + this.client.once(GatewayDispatchEvents.Ready, (ev) => { + console.log( + `bolt-discord: ready as ${ev.user.username}#${ev.user.discriminator} in ${ev.guilds.length} guilds`, + ); + }); + // @ts-ignore see above + this.client.on(GatewayDispatchEvents.MessageCreate, async (msg) => { + this.emit('create_message', await message(msg.api, msg.data)); + }); + // @ts-ignore see above + this.client.on(GatewayDispatchEvents.MessageUpdate, async (msg) => { + this.emit('edit_message', await message(msg.api, msg.data)); + }); + // @ts-ignore see above + this.client.on(GatewayDispatchEvents.MessageDelete, (msg) => { + this.emit('delete_message', deleted(msg.data)); + }); + // @ts-ignore see above + this.client.on(GatewayDispatchEvents.InteractionCreate, (cmd) => { + const command = command_to(cmd, this.lightning); + if (command) this.emit('create_command', command); + }); + } + + async setup_channel(channel: string) { + return await bridge.setup_bridge(this.api, channel); + } + + async create_message(opts: create_message_opts) { + return await bridge.create_message(this.api, opts); + } + + async edit_message(opts: edit_message_opts) { + return await bridge.edit_message(this.api, opts); + } + + async delete_message(opts: delete_message_opts) { + return await bridge.delete_message(this.api, opts); + } } diff --git a/packages/lightning-plugin-discord/src/process_message.ts b/packages/lightning-plugin-discord/src/process_message.ts deleted file mode 100644 index 04327ca..0000000 --- a/packages/lightning-plugin-discord/src/process_message.ts +++ /dev/null @@ -1,73 +0,0 @@ -import type { API } from '@discordjs/core'; -import type { message_options } from '@jersey/lightning'; -import { to_discord } from './discord.ts'; - -export async function process_message(api: API, opts: message_options) { - const data = opts.channel.data as { token: string; id: string }; - - if (opts.action !== 'delete') { - let replied_message; - - if (opts.reply_id) { - try { - replied_message = await api.channels - .getMessage(opts.channel.id, opts.reply_id); - } catch { - // safe to ignore - } - } - - const msg = await to_discord( - opts.message, - replied_message, - ); - - try { - let wh; - - if (opts.action === 'edit') { - wh = await api.webhooks.editMessage( - data.id, - data.token, - opts.edit_id[0], - msg, - ); - } else { - wh = await api.webhooks.execute( - data.id, - data.token, - msg, - ); - } - - return { - id: [wh.id], - channel: opts.channel, - }; - } catch (e) { - if ( - (e as { status: number }).status === 404 && - opts.action !== 'edit' - ) { - return { - channel: opts.channel, - error: e as Error, - disable: true, - }; - } else { - throw e; - } - } - } else { - await api.webhooks.deleteMessage( - data.id, - data.token, - opts.edit_id[0], - ); - - return { - id: opts.edit_id, - channel: opts.channel, - }; - } -} diff --git a/packages/lightning-plugin-discord/src/slash_commands.ts b/packages/lightning-plugin-discord/src/slash_commands.ts new file mode 100644 index 0000000..bdbffe4 --- /dev/null +++ b/packages/lightning-plugin-discord/src/slash_commands.ts @@ -0,0 +1,58 @@ +import type { command, lightning } from '@jersey/lightning'; +import type { API } from '@discordjs/core'; +import type { discord_config } from './mod.ts'; + +export async function setup_slash_commands( + api: API, + config: discord_config, + lightning: lightning, +) { + if (!config.slash_commands) return; + + const commands = lightning.commands.values().toArray(); + + await api.applicationCommands.bulkOverwriteGlobalCommands( + config.application_id, + commands_to_discord(commands) + ); +} + +function commands_to_discord(commands: command[]) { + return commands.map((command) => { + const opts = []; + + if (command.arguments) { + for (const argument of command.arguments) { + opts.push({ + name: argument.name, + description: argument.description, + type: 3, + required: argument.required, + }); + } + } + + if (command.subcommands) { + for (const subcommand of command.subcommands) { + opts.push({ + name: subcommand.name, + description: subcommand.description, + type: 1, + options: subcommand.arguments?.map((opt) => ({ + name: opt.name, + description: opt.description, + type: 3, + required: opt.required, + })), + }); + } + } + + return { + name: command.name, + type: 1, + description: command.description, + options: opts, + }; + }); +} diff --git a/packages/lightning-plugin-discord/src/to_lightning/command.ts b/packages/lightning-plugin-discord/src/to_lightning/command.ts new file mode 100644 index 0000000..24dc971 --- /dev/null +++ b/packages/lightning-plugin-discord/src/to_lightning/command.ts @@ -0,0 +1,38 @@ +import type { API } from '@discordjs/core'; +import type { APIInteraction } from 'discord-api-types'; +import type { create_command, lightning } from '@jersey/lightning'; +import { message_to_discord } from '../discord_message/mod.ts'; + +export function command_to( + interaction: { api: API; data: APIInteraction }, + lightning: lightning, +) { + 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; + } + + return { + command: interaction.data.data.name, + subcommand: subcmd, + channel: interaction.data.channel.id, + id: interaction.data.id, + timestamp: Temporal.Instant.fromEpochMilliseconds( + Number(BigInt(interaction.data.id) >> 22n) + 1420070400000, + ), + lightning, + plugin: 'bolt-discord', + reply: async (msg) => { + await interaction.api.interactions.reply( + interaction.data.id, + interaction.data.token, + await message_to_discord(msg), + ); + }, + args: opts, + } as create_command; +} diff --git a/packages/lightning-plugin-discord/src/to_lightning/deleted.ts b/packages/lightning-plugin-discord/src/to_lightning/deleted.ts new file mode 100644 index 0000000..93059c2 --- /dev/null +++ b/packages/lightning-plugin-discord/src/to_lightning/deleted.ts @@ -0,0 +1,13 @@ +import type { GatewayMessageDeleteDispatchData } from 'discord-api-types'; +import type { deleted_message } from '@jersey/lightning'; + +export function deleted( + message: GatewayMessageDeleteDispatchData, +): deleted_message { + return { + channel: message.channel_id, + id: message.id, + plugin: 'bolt-discord', + timestamp: Temporal.Now.instant(), + } +} diff --git a/packages/lightning-plugin-discord/src/to_lightning/message.ts b/packages/lightning-plugin-discord/src/to_lightning/message.ts new file mode 100644 index 0000000..370d116 --- /dev/null +++ b/packages/lightning-plugin-discord/src/to_lightning/message.ts @@ -0,0 +1,91 @@ +import type { API } from '@discordjs/core'; +import type { APIMessage } from 'discord-api-types'; +import { get_author } from '../discord_message/get_author.ts'; +import { message_to_discord } from '../discord_message/mod.ts'; +import type { message } from '@jersey/lightning'; + +export async function message( + api: API, + message: APIMessage, +): Promise { + if (message.flags && message.flags & 128) message.content = 'Loading...'; + + 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*'; + } + } + } + + const { name, avatar } = await get_author(api, message); + + const data = { + author: { + profile: avatar, + username: name, + rawname: message.author.username, + id: message.author.id, + color: '#5865F2', + }, + channel: message.channel_id, + content: (message.content?.length || 0) > 2000 + ? `${message.content?.substring(0, 1997)}...` + : message.content, + id: message.id, + timestamp: Temporal.Instant.fromEpochMilliseconds( + Number(BigInt(message.id) >> 22n) + 1420070400000, + ), + embeds: message.embeds?.map( + (i: Exclude[0]) => { + return { + ...i, + timestamp: i.timestamp ? Number(i.timestamp) : undefined, + }; + }, + ), + reply: async (msg: message) => { + if (!data.author.id || data.author.id === '') return; + await api.channels.createMessage(message.channel_id, { + ...(await message_to_discord(msg)), + message_reference: { + message_id: message.id, + }, + }); + }, + plugin: 'bolt-discord', + attachments: message.attachments?.map( + (i: Exclude[0]) => { + return { + file: i.url, + alt: i.description, + name: i.filename, + size: i.size / 1048576, // bytes -> MiB + }; + }, + ), + reply_id: message.referenced_message?.id, + }; + + return data as message; +} diff --git a/packages/lightning/src/structures/media.ts b/packages/lightning/src/structures/media.ts index e4ef5c7..f3564d4 100644 --- a/packages/lightning/src/structures/media.ts +++ b/packages/lightning/src/structures/media.ts @@ -8,7 +8,7 @@ export interface attachment { name?: string; /** whether or not the file has a spoiler */ spoiler?: boolean; - /** file size */ + /** file size in MiB */ size: number; } From 17f2a95a27a7b8883658141ac5461d7ec62982f0 Mon Sep 17 00:00:00 2001 From: Jersey Date: Fri, 29 Nov 2024 21:58:55 -0500 Subject: [PATCH 17/23] quick fix but otherwise it works i think --- packages/lightning-plugin-discord/src/mod.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/lightning-plugin-discord/src/mod.ts b/packages/lightning-plugin-discord/src/mod.ts index 3a301e5..c29be96 100644 --- a/packages/lightning-plugin-discord/src/mod.ts +++ b/packages/lightning-plugin-discord/src/mod.ts @@ -41,7 +41,7 @@ export class discord_plugin extends plugin { intents: 0 | 33281, rest, }); - // @ts-ignore Deno is wrong here. + this.client = new Client({ rest, gateway }); this.api = this.client.api; @@ -51,25 +51,20 @@ export class discord_plugin extends plugin { } private setup_events() { - // @ts-ignore I'm going to file an issue against Deno because this is so annoying - this.client.once(GatewayDispatchEvents.Ready, (ev) => { + this.client.once(GatewayDispatchEvents.Ready, ({data}) => { console.log( - `bolt-discord: ready as ${ev.user.username}#${ev.user.discriminator} in ${ev.guilds.length} guilds`, + `bolt-discord: ready as ${data.user.username}#${data.user.discriminator} in ${data.guilds.length} guilds`, ); }); - // @ts-ignore see above this.client.on(GatewayDispatchEvents.MessageCreate, async (msg) => { this.emit('create_message', await message(msg.api, msg.data)); }); - // @ts-ignore see above this.client.on(GatewayDispatchEvents.MessageUpdate, async (msg) => { this.emit('edit_message', await message(msg.api, msg.data)); }); - // @ts-ignore see above this.client.on(GatewayDispatchEvents.MessageDelete, (msg) => { this.emit('delete_message', deleted(msg.data)); }); - // @ts-ignore see above this.client.on(GatewayDispatchEvents.InteractionCreate, (cmd) => { const command = command_to(cmd, this.lightning); if (command) this.emit('create_command', command); From 7701d516b14f1b228b4d1708ed7294359205733a Mon Sep 17 00:00:00 2001 From: Jersey Date: Fri, 29 Nov 2024 22:28:50 -0500 Subject: [PATCH 18/23] telegram plugin v8 --- packages/lightning-plugin-telegram/README.md | 13 +-- packages/lightning-plugin-telegram/deno.json | 3 +- packages/lightning-plugin-telegram/src/mod.ts | 103 +++++++++--------- 3 files changed, 55 insertions(+), 64 deletions(-) diff --git a/packages/lightning-plugin-telegram/README.md b/packages/lightning-plugin-telegram/README.md index 02f12aa..fedede1 100644 --- a/packages/lightning-plugin-telegram/README.md +++ b/packages/lightning-plugin-telegram/README.md @@ -2,17 +2,15 @@ lightning-plugin-telegram is a plugin for [lightning](https://williamhroning.eu.org/lightning) that adds support for -telegram +telegram (including attachments via the included file proxy) ## example config ```ts -import type { config } from 'jsr:@jersey/lightning@0.7.4'; -import { telegram_plugin } from 'jsr:@jersey/lightning-plugin-telegram@0.7.4'; +import type { config } from 'jsr:@jersey/lightning@0.8.0'; +import { telegram_plugin } from 'jsr:@jersey/lightning-plugin-telegram@0.8.0'; export default { - redis_host: 'localhost', - redis_port: 6379, plugins: [ telegram_plugin.new({ bot_token: 'your_token', @@ -22,8 +20,3 @@ export default { ], } as config; ``` - -## notes - -this plugin has a telegram file proxy, which should be publically accessible so -that you don't leak your bot token when bridging attachments or profile pictures diff --git a/packages/lightning-plugin-telegram/deno.json b/packages/lightning-plugin-telegram/deno.json index 82137b3..ceb7cdb 100644 --- a/packages/lightning-plugin-telegram/deno.json +++ b/packages/lightning-plugin-telegram/deno.json @@ -5,7 +5,6 @@ "imports": { "@jersey/lightning": "jsr:@jersey/lightning@^0.8.0", "telegramify-markdown": "npm:telegramify-markdown@^1.2.2", - "grammy": "npm:grammy@^1.28.0", - "grammy/types": "npm:grammy@^1.28.0/types" + "grammy": "npm:grammy@^1.32.0" } } diff --git a/packages/lightning-plugin-telegram/src/mod.ts b/packages/lightning-plugin-telegram/src/mod.ts index b461071..b742911 100644 --- a/packages/lightning-plugin-telegram/src/mod.ts +++ b/packages/lightning-plugin-telegram/src/mod.ts @@ -1,8 +1,9 @@ import { + type create_message_opts, + type delete_message_opts, + type edit_message_opts, type lightning, - type message_options, plugin, - type process_result, } from '@jersey/lightning'; import { Bot } from 'grammy'; import { from_lightning, from_telegram } from './messages.ts'; @@ -20,7 +21,7 @@ export type telegram_config = { /** the plugin to use */ export class telegram_plugin extends plugin { name = 'bolt-telegram'; - bot: Bot; + private bot: Bot; constructor(l: lightning, cfg: telegram_config) { super(l, cfg); @@ -41,7 +42,15 @@ export class telegram_plugin extends plugin { } private serve_proxy() { - Deno.serve({ port: this.config.plugin_port }, (req: Request) => { + Deno.serve({ + port: this.config.plugin_port, + onListen: (addr) => { + console.log( + `bolt-telegram: file proxy listening on http://localhost:${addr.port}`, + `bolt-telegram: also available at: ${this.config.plugin_url}`, + ); + }, + }, (req: Request) => { const { pathname } = new URL(req.url); return fetch( `https://api.telegram.org/file/bot${this.bot.token}/${ @@ -52,67 +61,57 @@ export class telegram_plugin extends plugin { } /** create a bridge */ - create_bridge(channel: string): string { + setup_channel(channel: string) { return channel; } - /** process a message event */ - async process_message(opts: message_options): Promise { - if (opts.action === 'delete') { - for (const id of opts.edit_id) { - await this.bot.api.deleteMessage( - opts.channel.id, - Number(id), - ); - } - - return { - id: opts.edit_id, - channel: opts.channel, - }; - } else if (opts.action === 'edit') { - const content = from_lightning(opts.message)[0]; + async create_message(opts: create_message_opts) { + const content = from_lightning(opts.msg); + const messages = []; - await this.bot.api.editMessageText( + for (const msg of content) { + const result = await this.bot.api[msg.function]( opts.channel.id, - Number(opts.edit_id[0]), - content.value, + msg.value, { + reply_parameters: opts.reply_id + ? { + message_id: Number(opts.reply_id), + } + : undefined, parse_mode: 'MarkdownV2', }, ); - return { - id: opts.edit_id, - channel: opts.channel, - }; - } else if (opts.action === 'create') { - const content = from_lightning(opts.message); - const messages = []; + messages.push(String(result.message_id)); + } - for (const msg of content) { - const result = await this.bot.api[msg.function]( - opts.channel.id, - msg.value, - { - reply_parameters: opts.reply_id - ? { - message_id: Number(opts.reply_id), - } - : undefined, - parse_mode: 'MarkdownV2', - }, - ); + return messages; + } - messages.push(String(result.message_id)); - } + async edit_message(opts: edit_message_opts) { + const content = from_lightning(opts.msg)[0]; - return { - id: messages, - channel: opts.channel, - }; - } else { - throw new Error('unknown action'); + await this.bot.api.editMessageText( + opts.channel.id, + Number(opts.edit_ids[0]), + content.value, + { + parse_mode: 'MarkdownV2', + }, + ); + + return opts.edit_ids; + } + + async delete_message(opts: delete_message_opts) { + for (const id of opts.edit_ids) { + await this.bot.api.deleteMessage( + opts.channel.id, + Number(id), + ); } + + return opts.edit_ids; } } From f2e26a3b975e595a913ad2382d8f0d18be2a5de7 Mon Sep 17 00:00:00 2001 From: Jersey Date: Fri, 29 Nov 2024 22:37:27 -0500 Subject: [PATCH 19/23] rename thingies --- packages/lightning-plugin-discord/deno.json | 2 +- .../src/bridge_to_discord.ts | 12 ++++++------ .../src/discord_message/get_author.ts | 4 ++-- packages/lightning-plugin-discord/src/mod.ts | 12 ++++++------ packages/lightning-plugin-telegram/src/mod.ts | 12 ++++++------ packages/lightning/src/structures/bridge.ts | 6 +++--- packages/lightning/src/structures/plugins.ts | 12 ++++-------- 7 files changed, 28 insertions(+), 32 deletions(-) diff --git a/packages/lightning-plugin-discord/deno.json b/packages/lightning-plugin-discord/deno.json index 57c0760..e38c181 100644 --- a/packages/lightning-plugin-discord/deno.json +++ b/packages/lightning-plugin-discord/deno.json @@ -7,6 +7,6 @@ "@discordjs/core": "npm:@discordjs/core@^2.0.0", "@discordjs/rest": "npm:@discordjs/rest@^2.4.0", "@discordjs/ws": "npm:@discordjs/ws@^2.0.0", - "discord-api-types": "npm:discord-api-types@^0.37.110/v10" + "discord-api-types": "npm:discord-api-types@0.37.97/v10" } } diff --git a/packages/lightning-plugin-discord/src/bridge_to_discord.ts b/packages/lightning-plugin-discord/src/bridge_to_discord.ts index c3b6ba3..f91ae21 100644 --- a/packages/lightning-plugin-discord/src/bridge_to_discord.ts +++ b/packages/lightning-plugin-discord/src/bridge_to_discord.ts @@ -1,7 +1,7 @@ import type { - create_message_opts, - delete_message_opts, - edit_message_opts, + create_opts, + delete_opts, + edit_opts, } from '@jersey/lightning'; import { message_to_discord } from './discord_message/mod.ts'; import { error_handler } from './error_handler.ts'; @@ -24,7 +24,7 @@ export async function setup_bridge(api: API, channel: string) { } } -export async function create_message(api: API, opts: create_message_opts) { +export async function create_message(api: API, opts: create_opts) { const data = opts.channel.data as data; const transformed = await message_to_discord( opts.msg, @@ -46,7 +46,7 @@ export async function create_message(api: API, opts: create_message_opts) { } } -export async function edit_message(api: API, opts: edit_message_opts) { +export async function edit_message(api: API, opts: edit_opts) { const data = opts.channel.data as data; const transformed = await message_to_discord( opts.msg, @@ -69,7 +69,7 @@ export async function edit_message(api: API, opts: edit_message_opts) { } } -export async function delete_message(api: API, opts: delete_message_opts) { +export async function delete_message(api: API, opts: delete_opts) { const data = opts.channel.data as data; try { diff --git a/packages/lightning-plugin-discord/src/discord_message/get_author.ts b/packages/lightning-plugin-discord/src/discord_message/get_author.ts index 31bdff9..1ecab81 100644 --- a/packages/lightning-plugin-discord/src/discord_message/get_author.ts +++ b/packages/lightning-plugin-discord/src/discord_message/get_author.ts @@ -8,11 +8,11 @@ export async function get_author(api: API, message: APIMessage) { const channel = await api.channels.get(message.channel_id); - if ("guild_id" in channel) { + if ("guild_id" in channel && channel.guild_id) { try { const member = await api.guilds.getMember(channel.guild_id, message.author.id); - name ??= member.nick; + if (member.nick !== null && member.nick !== undefined) name = member.nick; avatar = member.avatar ? `https://cdn.discordapp.com/guilds/${channel.guild_id}/users/${message.author.id}/avatars/${member.avatar}.png` : avatar; } catch { // safe to ignore diff --git a/packages/lightning-plugin-discord/src/mod.ts b/packages/lightning-plugin-discord/src/mod.ts index c29be96..c4446c8 100644 --- a/packages/lightning-plugin-discord/src/mod.ts +++ b/packages/lightning-plugin-discord/src/mod.ts @@ -2,9 +2,9 @@ import { Client } from '@discordjs/core'; import { REST } from '@discordjs/rest'; import { WebSocketManager } from '@discordjs/ws'; import { - type create_message_opts, - type delete_message_opts, - type edit_message_opts, + type create_opts, + type delete_opts, + type edit_opts, type lightning, plugin, } from '@jersey/lightning'; @@ -75,15 +75,15 @@ export class discord_plugin extends plugin { return await bridge.setup_bridge(this.api, channel); } - async create_message(opts: create_message_opts) { + async create_message(opts: create_opts) { return await bridge.create_message(this.api, opts); } - async edit_message(opts: edit_message_opts) { + async edit_message(opts: edit_opts) { return await bridge.edit_message(this.api, opts); } - async delete_message(opts: delete_message_opts) { + async delete_message(opts: delete_opts) { return await bridge.delete_message(this.api, opts); } } diff --git a/packages/lightning-plugin-telegram/src/mod.ts b/packages/lightning-plugin-telegram/src/mod.ts index b742911..d73763e 100644 --- a/packages/lightning-plugin-telegram/src/mod.ts +++ b/packages/lightning-plugin-telegram/src/mod.ts @@ -1,7 +1,7 @@ import { - type create_message_opts, - type delete_message_opts, - type edit_message_opts, + type create_opts, + type delete_opts, + type edit_opts, type lightning, plugin, } from '@jersey/lightning'; @@ -65,7 +65,7 @@ export class telegram_plugin extends plugin { return channel; } - async create_message(opts: create_message_opts) { + async create_message(opts: create_opts) { const content = from_lightning(opts.msg); const messages = []; @@ -89,7 +89,7 @@ export class telegram_plugin extends plugin { return messages; } - async edit_message(opts: edit_message_opts) { + async edit_message(opts: edit_opts) { const content = from_lightning(opts.msg)[0]; await this.bot.api.editMessageText( @@ -104,7 +104,7 @@ export class telegram_plugin extends plugin { return opts.edit_ids; } - async delete_message(opts: delete_message_opts) { + async delete_message(opts: delete_opts) { for (const id of opts.edit_ids) { await this.bot.api.deleteMessage( opts.channel.id, diff --git a/packages/lightning/src/structures/bridge.ts b/packages/lightning/src/structures/bridge.ts index d5d20cd..d02b59a 100644 --- a/packages/lightning/src/structures/bridge.ts +++ b/packages/lightning/src/structures/bridge.ts @@ -68,7 +68,7 @@ export interface bridged_message { } /** a message to be bridged */ -export interface create_message_opts { +export interface create_opts { /** the actual message */ msg: message; /** the channel to use */ @@ -78,7 +78,7 @@ export interface create_message_opts { } /** a message to be edited */ -export interface edit_message_opts { +export interface edit_opts { /** the actual message */ msg: message; /** the channel to use */ @@ -90,7 +90,7 @@ export interface edit_message_opts { } /** a message to be deleted */ -export interface delete_message_opts { +export interface delete_opts { /** the actual deleted message */ msg: deleted_message; /** the channel to use */ diff --git a/packages/lightning/src/structures/plugins.ts b/packages/lightning/src/structures/plugins.ts index b8540b6..61557d0 100644 --- a/packages/lightning/src/structures/plugins.ts +++ b/packages/lightning/src/structures/plugins.ts @@ -1,10 +1,6 @@ import { EventEmitter } from '@denosaurs/event'; import type { lightning } from '../lightning.ts'; -import type { - create_message_opts, - delete_message_opts, - edit_message_opts, -} from './bridge.ts'; +import type { create_opts, delete_opts, edit_opts } from './bridge.ts'; import type { plugin_events } from './events.ts'; /** the way to make a plugin */ @@ -44,14 +40,14 @@ export abstract class plugin extends EventEmitter { abstract setup_channel(channel: string): Promise | unknown; /** send a message to a given channel */ abstract create_message( - opts: create_message_opts, + opts: create_opts, ): Promise; /** edit a message in a given channel */ abstract edit_message( - opts: edit_message_opts, + opts: edit_opts, ): Promise; /** delete a message in a given channel */ abstract delete_message( - opts: delete_message_opts, + opts: delete_opts, ): Promise; } From 0a156867684953426995dcfa7e7eaae2132ac9f0 Mon Sep 17 00:00:00 2001 From: Jersey Date: Fri, 29 Nov 2024 22:38:31 -0500 Subject: [PATCH 20/23] formatting --- .../src/bridge_to_discord.ts | 120 +++++++------- .../src/discord_message/files.ts | 42 ++--- .../src/discord_message/get_author.ts | 37 +++-- .../src/discord_message/mod.ts | 68 ++++---- .../src/discord_message/reply_embed.ts | 32 ++-- .../src/error_handler.ts | 64 ++++---- packages/lightning-plugin-discord/src/mod.ts | 128 +++++++-------- .../src/slash_commands.ts | 86 +++++----- .../src/to_lightning/command.ts | 56 +++---- .../src/to_lightning/deleted.ts | 14 +- .../src/to_lightning/message.ts | 153 +++++++++--------- packages/lightning/README.md | 4 +- packages/lightning/src/cli.ts | 9 +- packages/lightning/src/mod.ts | 2 +- 14 files changed, 410 insertions(+), 405 deletions(-) diff --git a/packages/lightning-plugin-discord/src/bridge_to_discord.ts b/packages/lightning-plugin-discord/src/bridge_to_discord.ts index f91ae21..d9668d9 100644 --- a/packages/lightning-plugin-discord/src/bridge_to_discord.ts +++ b/packages/lightning-plugin-discord/src/bridge_to_discord.ts @@ -1,8 +1,4 @@ -import type { - create_opts, - delete_opts, - edit_opts, -} from '@jersey/lightning'; +import type { create_opts, delete_opts, edit_opts } from '@jersey/lightning'; import { message_to_discord } from './discord_message/mod.ts'; import { error_handler } from './error_handler.ts'; import type { API } from '@discordjs/core'; @@ -10,77 +6,77 @@ import type { API } from '@discordjs/core'; type data = { id: string; token: string }; export async function setup_bridge(api: API, channel: string) { - try { - const { id, token } = await api.channels.createWebhook( - channel, - { - name: 'lightning bridge', - }, - ); + try { + const { id, token } = await api.channels.createWebhook( + channel, + { + name: 'lightning bridge', + }, + ); - return { id, token }; - } catch (e) { - return error_handler(e, channel, 'setting up channel'); - } + return { id, token }; + } catch (e) { + return error_handler(e, channel, 'setting up channel'); + } } export async function create_message(api: API, opts: create_opts) { - const data = opts.channel.data as data; - const transformed = await message_to_discord( - opts.msg, - api, - opts.channel.id, - opts.reply_id, - ); + const data = opts.channel.data as data; + const transformed = await message_to_discord( + opts.msg, + api, + opts.channel.id, + opts.reply_id, + ); - try { - const res = await api.webhooks.execute( - data.id, - data.token, - transformed, - ); + try { + const res = await api.webhooks.execute( + data.id, + data.token, + transformed, + ); - return [res.id]; - } catch (e) { - return error_handler(e, opts.channel.id, 'creating message'); - } + return [res.id]; + } catch (e) { + return error_handler(e, opts.channel.id, 'creating message'); + } } export async function edit_message(api: API, opts: edit_opts) { - const data = opts.channel.data as data; - const transformed = await message_to_discord( - opts.msg, - api, - opts.channel.id, - opts.reply_id, - ); + const data = opts.channel.data as data; + const transformed = await message_to_discord( + opts.msg, + api, + opts.channel.id, + opts.reply_id, + ); - try { - await api.webhooks.editMessage( - data.id, - data.token, - opts.edit_ids[0], - transformed, - ); + try { + await api.webhooks.editMessage( + data.id, + data.token, + opts.edit_ids[0], + transformed, + ); - return opts.edit_ids; - } catch (e) { - return error_handler(e, opts.channel.id, 'editing message'); - } + return opts.edit_ids; + } catch (e) { + return error_handler(e, opts.channel.id, 'editing message'); + } } export async function delete_message(api: API, opts: delete_opts) { - const data = opts.channel.data as data; + const data = opts.channel.data as data; - try { - await api.webhooks.deleteMessage( - data.id, - data.token, - opts.edit_ids[0], - ); + try { + await api.webhooks.deleteMessage( + data.id, + data.token, + opts.edit_ids[0], + ); - return opts.edit_ids; - } catch (e) { - return error_handler(e, opts.channel.id, 'editing message'); - } + return opts.edit_ids; + } catch (e) { + return error_handler(e, opts.channel.id, 'editing message'); + } } diff --git a/packages/lightning-plugin-discord/src/discord_message/files.ts b/packages/lightning-plugin-discord/src/discord_message/files.ts index daf4d30..b5e5e2b 100644 --- a/packages/lightning-plugin-discord/src/discord_message/files.ts +++ b/packages/lightning-plugin-discord/src/discord_message/files.ts @@ -2,30 +2,30 @@ import type { attachment } from '@jersey/lightning'; import type { RawFile } from '@discordjs/rest'; export async function files_up_to_25MiB(attachments: attachment[] | undefined) { - if (!attachments) return; + if (!attachments) return; - const files: RawFile[] = []; - const total_size = 0; + const files: RawFile[] = []; + const total_size = 0; - for (const attachment of attachments) { - if (attachment.size >= 25) continue; - if (total_size + attachment.size >= 25) break; + for (const attachment of attachments) { + if (attachment.size >= 25) continue; + if (total_size + attachment.size >= 25) break; - try { - const data = new Uint8Array( - await (await fetch(attachment.file, { - signal: AbortSignal.timeout(5000), - })).arrayBuffer(), - ); + try { + const data = new Uint8Array( + await (await fetch(attachment.file, { + signal: AbortSignal.timeout(5000), + })).arrayBuffer(), + ); - files.push({ - name: attachment.name ?? attachment.file.split('/').pop()!, - data, - }); - } catch { - continue; - } - } + files.push({ + name: attachment.name ?? attachment.file.split('/').pop()!, + data, + }); + } catch { + continue; + } + } - return files; + return files; } diff --git a/packages/lightning-plugin-discord/src/discord_message/get_author.ts b/packages/lightning-plugin-discord/src/discord_message/get_author.ts index 1ecab81..1b805bd 100644 --- a/packages/lightning-plugin-discord/src/discord_message/get_author.ts +++ b/packages/lightning-plugin-discord/src/discord_message/get_author.ts @@ -3,21 +3,30 @@ import type { API } from '@discordjs/core'; import { calculateUserDefaultAvatarIndex } from '@discordjs/rest'; export async function get_author(api: API, message: APIMessage) { - let name = message.author.global_name || message.author.username; - let avatar = message.author.avatar ? `https://cdn.discordapp.com/avatars/${message.author.id}/${message.author.avatar}.png` : `https://cdn.discordapp.com/embed/avatars/${calculateUserDefaultAvatarIndex(message.author.id)}.png`; + let name = message.author.global_name || message.author.username; + let avatar = message.author.avatar + ? `https://cdn.discordapp.com/avatars/${message.author.id}/${message.author.avatar}.png` + : `https://cdn.discordapp.com/embed/avatars/${ + calculateUserDefaultAvatarIndex(message.author.id) + }.png`; - const channel = await api.channels.get(message.channel_id); - - if ("guild_id" in channel && channel.guild_id) { - try { - const member = await api.guilds.getMember(channel.guild_id, message.author.id); + const channel = await api.channels.get(message.channel_id); - if (member.nick !== null && member.nick !== undefined) name = member.nick; - avatar = member.avatar ? `https://cdn.discordapp.com/guilds/${channel.guild_id}/users/${message.author.id}/avatars/${member.avatar}.png` : avatar; - } catch { - // safe to ignore - } - } + if ('guild_id' in channel && channel.guild_id) { + try { + const member = await api.guilds.getMember( + channel.guild_id, + message.author.id, + ); - return { name, avatar } + if (member.nick !== null && member.nick !== undefined) name = member.nick; + avatar = member.avatar + ? `https://cdn.discordapp.com/guilds/${channel.guild_id}/users/${message.author.id}/avatars/${member.avatar}.png` + : avatar; + } catch { + // safe to ignore + } + } + + return { name, avatar }; } diff --git a/packages/lightning-plugin-discord/src/discord_message/mod.ts b/packages/lightning-plugin-discord/src/discord_message/mod.ts index eb28b44..3931cda 100644 --- a/packages/lightning-plugin-discord/src/discord_message/mod.ts +++ b/packages/lightning-plugin-discord/src/discord_message/mod.ts @@ -1,52 +1,52 @@ import type { message } from '@jersey/lightning'; import type { API } from '@discordjs/core'; import type { - RESTPostAPIWebhookWithTokenJSONBody, - RESTPostAPIWebhookWithTokenQuery, + RESTPostAPIWebhookWithTokenJSONBody, + RESTPostAPIWebhookWithTokenQuery, } from 'discord-api-types'; import type { RawFile } from '@discordjs/rest'; import { reply_embed } from './reply_embed.ts'; import { files_up_to_25MiB } from './files.ts'; export interface discord_message_send - extends - RESTPostAPIWebhookWithTokenJSONBody, - RESTPostAPIWebhookWithTokenQuery { - files?: RawFile[]; - wait: true; + extends + RESTPostAPIWebhookWithTokenJSONBody, + RESTPostAPIWebhookWithTokenQuery { + files?: RawFile[]; + wait: true; } export async function message_to_discord( - msg: message, - api?: API, - channel?: string, - reply_id?: string, + msg: message, + api?: API, + channel?: string, + reply_id?: string, ): Promise { - const discord: discord_message_send = { - avatar_url: msg.author.profile, - content: (msg.content?.length || 0) > 2000 - ? `${msg.content?.substring(0, 1997)}...` - : msg.content, - embeds: msg.embeds?.map((e) => { - return { ...e, timestamp: e.timestamp?.toString() }; - }), - username: msg.author.username, - wait: true, - }; + const discord: discord_message_send = { + avatar_url: msg.author.profile, + content: (msg.content?.length || 0) > 2000 + ? `${msg.content?.substring(0, 1997)}...` + : msg.content, + embeds: msg.embeds?.map((e) => { + return { ...e, timestamp: e.timestamp?.toString() }; + }), + username: msg.author.username, + wait: true, + }; - if (api && channel && reply_id) { - const embed = await reply_embed(api, channel, reply_id); - if (embed) { - if (!discord.embeds) discord.embeds = []; - discord.embeds.push(embed); - } - } + if (api && channel && reply_id) { + const embed = await reply_embed(api, channel, reply_id); + if (embed) { + if (!discord.embeds) discord.embeds = []; + discord.embeds.push(embed); + } + } - discord.files = await files_up_to_25MiB(msg.attachments); + discord.files = await files_up_to_25MiB(msg.attachments); - if (!discord.content && (!discord.embeds || discord.embeds.length === 0)) { - discord.content = '*empty message*'; - } + if (!discord.content && (!discord.embeds || discord.embeds.length === 0)) { + discord.content = '*empty message*'; + } - return discord; + return discord; } diff --git a/packages/lightning-plugin-discord/src/discord_message/reply_embed.ts b/packages/lightning-plugin-discord/src/discord_message/reply_embed.ts index 39d48cf..debe824 100644 --- a/packages/lightning-plugin-discord/src/discord_message/reply_embed.ts +++ b/packages/lightning-plugin-discord/src/discord_message/reply_embed.ts @@ -3,22 +3,22 @@ import type { APIMessage } from 'discord-api-types'; import { get_author } from './get_author.ts'; export async function reply_embed(api: API, channel: string, id: string) { - try { - const message = await api.channels.getMessage( - channel, - id, - ) as APIMessage; + try { + const message = await api.channels.getMessage( + channel, + id, + ) as APIMessage; - const { name, avatar } = await get_author(api, message); + const { name, avatar } = await get_author(api, message); - return { - author: { - name: `replying to ${name}`, - icon_url: avatar, - }, - description: message.content, - }; - } catch { - return; - } + return { + author: { + name: `replying to ${name}`, + icon_url: avatar, + }, + description: message.content, + }; + } catch { + return; + } } diff --git a/packages/lightning-plugin-discord/src/error_handler.ts b/packages/lightning-plugin-discord/src/error_handler.ts index 0486ff0..6f07d12 100644 --- a/packages/lightning-plugin-discord/src/error_handler.ts +++ b/packages/lightning-plugin-discord/src/error_handler.ts @@ -2,37 +2,35 @@ import { DiscordAPIError } from '@discordjs/rest'; import { log_error } from '@jersey/lightning'; export function error_handler(e: unknown, channel_id: string, action: string) { - if (e instanceof DiscordAPIError) { - if (e.code === 30007 || e.code === 30058) { - log_error(e, { - message: - 'too many webhooks in channel/guild. try deleting some', - extra: { channel_id }, - }); - } else if (e.code === 50013) { - log_error(e, { - message: - 'missing permissions to create webhook. check bot permissions', - extra: { channel_id }, - }); - } else if (e.code === 10003 || e.code === 10015 || e.code === 50027) { - log_error(e, { - disable: true, - message: `disabling channel due to error code ${e.code}`, - extra: { channel_id }, - }); - } else if (action === 'editing message' && e.code === 10008) { - return []; // message already deleted or non-existent - } else { - log_error(e, { - message: `unknown error ${action}`, - extra: { channel_id, code: e.code }, - }); - } - } else { - log_error(e, { - message: `unknown error ${action}`, - extra: { channel_id }, - }); - } + if (e instanceof DiscordAPIError) { + if (e.code === 30007 || e.code === 30058) { + log_error(e, { + message: 'too many webhooks in channel/guild. try deleting some', + extra: { channel_id }, + }); + } else if (e.code === 50013) { + log_error(e, { + message: 'missing permissions to create webhook. check bot permissions', + extra: { channel_id }, + }); + } else if (e.code === 10003 || e.code === 10015 || e.code === 50027) { + log_error(e, { + disable: true, + message: `disabling channel due to error code ${e.code}`, + extra: { channel_id }, + }); + } else if (action === 'editing message' && e.code === 10008) { + return []; // message already deleted or non-existent + } else { + log_error(e, { + message: `unknown error ${action}`, + extra: { channel_id, code: e.code }, + }); + } + } else { + log_error(e, { + message: `unknown error ${action}`, + extra: { channel_id }, + }); + } } diff --git a/packages/lightning-plugin-discord/src/mod.ts b/packages/lightning-plugin-discord/src/mod.ts index c4446c8..2f1471b 100644 --- a/packages/lightning-plugin-discord/src/mod.ts +++ b/packages/lightning-plugin-discord/src/mod.ts @@ -2,11 +2,11 @@ import { Client } from '@discordjs/core'; import { REST } from '@discordjs/rest'; import { WebSocketManager } from '@discordjs/ws'; import { - type create_opts, - type delete_opts, - type edit_opts, - type lightning, - plugin, + type create_opts, + type delete_opts, + type edit_opts, + type lightning, + plugin, } from '@jersey/lightning'; import { GatewayDispatchEvents } from 'discord-api-types'; import * as bridge from './bridge_to_discord.ts'; @@ -17,73 +17,73 @@ import { message } from './to_lightning/message.ts'; /** configuration for the discord plugin */ export interface discord_config { - /** the discord bot token */ - token: string; - /** whether to enable slash commands */ - slash_commands: boolean; - /** discord application id */ - application_id: string; + /** the discord bot token */ + token: string; + /** whether to enable slash commands */ + slash_commands: boolean; + /** discord application id */ + application_id: string; } export class discord_plugin extends plugin { - name = 'bolt-discord'; - private api: Client['api']; - private client: Client; + name = 'bolt-discord'; + private api: Client['api']; + private client: Client; - constructor(l: lightning, config: discord_config) { - super(l, config); - // @ts-ignore their type for makeRequest is funky - const rest = new REST({ version: '10', makeRequest: fetch }).setToken( - config.token, - ); - const gateway = new WebSocketManager({ - token: config.token, - intents: 0 | 33281, - rest, - }); - - this.client = new Client({ rest, gateway }); - this.api = this.client.api; + constructor(l: lightning, config: discord_config) { + super(l, config); + // @ts-ignore their type for makeRequest is funky + const rest = new REST({ version: '10', makeRequest: fetch }).setToken( + config.token, + ); + const gateway = new WebSocketManager({ + token: config.token, + intents: 0 | 33281, + rest, + }); - setup_slash_commands(this.api, config, l); - this.setup_events(); - gateway.connect(); - } + this.client = new Client({ rest, gateway }); + this.api = this.client.api; - private setup_events() { - this.client.once(GatewayDispatchEvents.Ready, ({data}) => { - console.log( - `bolt-discord: ready as ${data.user.username}#${data.user.discriminator} in ${data.guilds.length} guilds`, - ); - }); - this.client.on(GatewayDispatchEvents.MessageCreate, async (msg) => { - this.emit('create_message', await message(msg.api, msg.data)); - }); - this.client.on(GatewayDispatchEvents.MessageUpdate, async (msg) => { - this.emit('edit_message', await message(msg.api, msg.data)); - }); - this.client.on(GatewayDispatchEvents.MessageDelete, (msg) => { - this.emit('delete_message', deleted(msg.data)); - }); - this.client.on(GatewayDispatchEvents.InteractionCreate, (cmd) => { - const command = command_to(cmd, this.lightning); - if (command) this.emit('create_command', command); - }); - } + setup_slash_commands(this.api, config, l); + this.setup_events(); + gateway.connect(); + } - async setup_channel(channel: string) { - return await bridge.setup_bridge(this.api, channel); - } + private setup_events() { + this.client.once(GatewayDispatchEvents.Ready, ({ data }) => { + console.log( + `bolt-discord: ready as ${data.user.username}#${data.user.discriminator} in ${data.guilds.length} guilds`, + ); + }); + this.client.on(GatewayDispatchEvents.MessageCreate, async (msg) => { + this.emit('create_message', await message(msg.api, msg.data)); + }); + this.client.on(GatewayDispatchEvents.MessageUpdate, async (msg) => { + this.emit('edit_message', await message(msg.api, msg.data)); + }); + this.client.on(GatewayDispatchEvents.MessageDelete, (msg) => { + this.emit('delete_message', deleted(msg.data)); + }); + this.client.on(GatewayDispatchEvents.InteractionCreate, (cmd) => { + const command = command_to(cmd, this.lightning); + if (command) this.emit('create_command', command); + }); + } - async create_message(opts: create_opts) { - return await bridge.create_message(this.api, opts); - } + async setup_channel(channel: string) { + return await bridge.setup_bridge(this.api, channel); + } - async edit_message(opts: edit_opts) { - return await bridge.edit_message(this.api, opts); - } + async create_message(opts: create_opts) { + return await bridge.create_message(this.api, opts); + } - async delete_message(opts: delete_opts) { - return await bridge.delete_message(this.api, opts); - } + async edit_message(opts: edit_opts) { + return await bridge.edit_message(this.api, opts); + } + + async delete_message(opts: delete_opts) { + return await bridge.delete_message(this.api, opts); + } } diff --git a/packages/lightning-plugin-discord/src/slash_commands.ts b/packages/lightning-plugin-discord/src/slash_commands.ts index bdbffe4..caf52c4 100644 --- a/packages/lightning-plugin-discord/src/slash_commands.ts +++ b/packages/lightning-plugin-discord/src/slash_commands.ts @@ -3,56 +3,56 @@ import type { API } from '@discordjs/core'; import type { discord_config } from './mod.ts'; export async function setup_slash_commands( - api: API, - config: discord_config, - lightning: lightning, + api: API, + config: discord_config, + lightning: lightning, ) { - if (!config.slash_commands) return; + if (!config.slash_commands) return; - const commands = lightning.commands.values().toArray(); + const commands = lightning.commands.values().toArray(); - await api.applicationCommands.bulkOverwriteGlobalCommands( - config.application_id, - commands_to_discord(commands) - ); + await api.applicationCommands.bulkOverwriteGlobalCommands( + config.application_id, + commands_to_discord(commands), + ); } function commands_to_discord(commands: command[]) { - return commands.map((command) => { - const opts = []; + return commands.map((command) => { + const opts = []; - if (command.arguments) { - for (const argument of command.arguments) { - opts.push({ - name: argument.name, - description: argument.description, - type: 3, - required: argument.required, - }); - } - } + if (command.arguments) { + for (const argument of command.arguments) { + opts.push({ + name: argument.name, + description: argument.description, + type: 3, + required: argument.required, + }); + } + } - if (command.subcommands) { - for (const subcommand of command.subcommands) { - opts.push({ - name: subcommand.name, - description: subcommand.description, - type: 1, - options: subcommand.arguments?.map((opt) => ({ - name: opt.name, - description: opt.description, - type: 3, - required: opt.required, - })), - }); - } - } + if (command.subcommands) { + for (const subcommand of command.subcommands) { + opts.push({ + name: subcommand.name, + description: subcommand.description, + type: 1, + options: subcommand.arguments?.map((opt) => ({ + name: opt.name, + description: opt.description, + type: 3, + required: opt.required, + })), + }); + } + } - return { - name: command.name, - type: 1, - description: command.description, - options: opts, - }; - }); + return { + name: command.name, + type: 1, + description: command.description, + options: opts, + }; + }); } diff --git a/packages/lightning-plugin-discord/src/to_lightning/command.ts b/packages/lightning-plugin-discord/src/to_lightning/command.ts index 24dc971..aaf56e5 100644 --- a/packages/lightning-plugin-discord/src/to_lightning/command.ts +++ b/packages/lightning-plugin-discord/src/to_lightning/command.ts @@ -4,35 +4,35 @@ import type { create_command, lightning } from '@jersey/lightning'; import { message_to_discord } from '../discord_message/mod.ts'; export function command_to( - interaction: { api: API; data: APIInteraction }, - lightning: lightning, + interaction: { api: API; data: APIInteraction }, + lightning: lightning, ) { - if (interaction.data.type !== 2 || interaction.data.data.type !== 1) return; - const opts = {} as Record; - let subcmd; + 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; - } + for (const opt of interaction.data.data.options || []) { + if (opt.type === 1) subcmd = opt.name; + if (opt.type === 3) opts[opt.name] = opt.value; + } - return { - command: interaction.data.data.name, - subcommand: subcmd, - channel: interaction.data.channel.id, - id: interaction.data.id, - timestamp: Temporal.Instant.fromEpochMilliseconds( - Number(BigInt(interaction.data.id) >> 22n) + 1420070400000, - ), - lightning, - plugin: 'bolt-discord', - reply: async (msg) => { - await interaction.api.interactions.reply( - interaction.data.id, - interaction.data.token, - await message_to_discord(msg), - ); - }, - args: opts, - } as create_command; + return { + command: interaction.data.data.name, + subcommand: subcmd, + channel: interaction.data.channel.id, + id: interaction.data.id, + timestamp: Temporal.Instant.fromEpochMilliseconds( + Number(BigInt(interaction.data.id) >> 22n) + 1420070400000, + ), + lightning, + plugin: 'bolt-discord', + reply: async (msg) => { + await interaction.api.interactions.reply( + interaction.data.id, + interaction.data.token, + await message_to_discord(msg), + ); + }, + args: opts, + } as create_command; } diff --git a/packages/lightning-plugin-discord/src/to_lightning/deleted.ts b/packages/lightning-plugin-discord/src/to_lightning/deleted.ts index 93059c2..30fe17c 100644 --- a/packages/lightning-plugin-discord/src/to_lightning/deleted.ts +++ b/packages/lightning-plugin-discord/src/to_lightning/deleted.ts @@ -2,12 +2,12 @@ import type { GatewayMessageDeleteDispatchData } from 'discord-api-types'; import type { deleted_message } from '@jersey/lightning'; export function deleted( - message: GatewayMessageDeleteDispatchData, + message: GatewayMessageDeleteDispatchData, ): deleted_message { - return { - channel: message.channel_id, - id: message.id, - plugin: 'bolt-discord', - timestamp: Temporal.Now.instant(), - } + return { + channel: message.channel_id, + id: message.id, + plugin: 'bolt-discord', + timestamp: Temporal.Now.instant(), + }; } diff --git a/packages/lightning-plugin-discord/src/to_lightning/message.ts b/packages/lightning-plugin-discord/src/to_lightning/message.ts index 370d116..62d551b 100644 --- a/packages/lightning-plugin-discord/src/to_lightning/message.ts +++ b/packages/lightning-plugin-discord/src/to_lightning/message.ts @@ -5,87 +5,86 @@ import { message_to_discord } from '../discord_message/mod.ts'; import type { message } from '@jersey/lightning'; export async function message( - api: API, - message: APIMessage, + api: API, + message: APIMessage, ): Promise { - if (message.flags && message.flags & 128) message.content = 'Loading...'; + if (message.flags && message.flags & 128) message.content = 'Loading...'; - if (message.type === 7) message.content = '*joined on discord*'; + 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*'; - } - } - } + 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*'; + } + } + } - const { name, avatar } = await get_author(api, message); + const { name, avatar } = await get_author(api, message); - const data = { - author: { - profile: avatar, - username: name, - rawname: message.author.username, - id: message.author.id, - color: '#5865F2', - }, - channel: message.channel_id, - content: (message.content?.length || 0) > 2000 - ? `${message.content?.substring(0, 1997)}...` - : message.content, - id: message.id, - timestamp: Temporal.Instant.fromEpochMilliseconds( - Number(BigInt(message.id) >> 22n) + 1420070400000, - ), - embeds: message.embeds?.map( - (i: Exclude[0]) => { - return { - ...i, - timestamp: i.timestamp ? Number(i.timestamp) : undefined, - }; - }, - ), - reply: async (msg: message) => { - if (!data.author.id || data.author.id === '') return; - await api.channels.createMessage(message.channel_id, { - ...(await message_to_discord(msg)), - message_reference: { - message_id: message.id, - }, - }); - }, - plugin: 'bolt-discord', - attachments: message.attachments?.map( - (i: Exclude[0]) => { - return { - file: i.url, - alt: i.description, - name: i.filename, - size: i.size / 1048576, // bytes -> MiB - }; - }, - ), - reply_id: message.referenced_message?.id, - }; + const data = { + author: { + profile: avatar, + username: name, + rawname: message.author.username, + id: message.author.id, + color: '#5865F2', + }, + channel: message.channel_id, + content: (message.content?.length || 0) > 2000 + ? `${message.content?.substring(0, 1997)}...` + : message.content, + id: message.id, + timestamp: Temporal.Instant.fromEpochMilliseconds( + Number(BigInt(message.id) >> 22n) + 1420070400000, + ), + embeds: message.embeds?.map( + (i: Exclude[0]) => { + return { + ...i, + timestamp: i.timestamp ? Number(i.timestamp) : undefined, + }; + }, + ), + reply: async (msg: message) => { + if (!data.author.id || data.author.id === '') return; + await api.channels.createMessage(message.channel_id, { + ...(await message_to_discord(msg)), + message_reference: { + message_id: message.id, + }, + }); + }, + plugin: 'bolt-discord', + attachments: message.attachments?.map( + (i: Exclude[0]) => { + return { + file: i.url, + alt: i.description, + name: i.filename, + size: i.size / 1048576, // bytes -> MiB + }; + }, + ), + reply_id: message.referenced_message?.id, + }; - return data as message; + return data as message; } diff --git a/packages/lightning/README.md b/packages/lightning/README.md index a61f60c..5778ea7 100644 --- a/packages/lightning/README.md +++ b/packages/lightning/README.md @@ -8,6 +8,6 @@ apps via plugins ## [docs](https://williamhorning.eu.org/bolt) ```ts -import {} from "@jersey/lightning"; +import {} from '@jersey/lightning'; // TODO(jersey): add example -``` \ No newline at end of file +``` diff --git a/packages/lightning/src/cli.ts b/packages/lightning/src/cli.ts index f89d0a7..69381d9 100644 --- a/packages/lightning/src/cli.ts +++ b/packages/lightning/src/cli.ts @@ -1,6 +1,6 @@ import { parseArgs } from '@std/cli/parse-args'; import { join, toFileUrl } from '@std/path'; -import { lightning, type config } from './lightning.ts'; +import { type config, lightning } from './lightning.ts'; import { log_error } from './structures/errors.ts'; const version = '0.8.0'; @@ -14,9 +14,12 @@ if (_.v || _.version) { // TODO(jersey): this is somewhat broken when acting as a JSR package if (!_.config) _.config = join(Deno.cwd(), 'config.ts'); - const config = (await import(toFileUrl(_.config).toString())).default as config; + const config = (await import(toFileUrl(_.config).toString())) + .default as config; - if (config?.error_url) Deno.env.set('LIGHTNING_ERROR_WEBHOOK', config.error_url); + if (config?.error_url) { + Deno.env.set('LIGHTNING_ERROR_WEBHOOK', config.error_url); + } addEventListener('error', (ev) => { log_error(ev.error, { extra: { type: 'global error' } }); diff --git a/packages/lightning/src/mod.ts b/packages/lightning/src/mod.ts index 4cb23fc..e95da6e 100644 --- a/packages/lightning/src/mod.ts +++ b/packages/lightning/src/mod.ts @@ -1,5 +1,5 @@ if (import.meta.main) { - await import('./cli.ts'); + await import('./cli.ts'); } export * from './lightning.ts'; From aae813579fc8a9f80e81128bef519ad88f5e0603 Mon Sep 17 00:00:00 2001 From: Jersey Date: Fri, 29 Nov 2024 23:22:41 -0500 Subject: [PATCH 21/23] split file proxy --- .../src/file_proxy.ts | 20 ++++++++++++++++ packages/lightning-plugin-telegram/src/mod.ts | 24 +++---------------- 2 files changed, 23 insertions(+), 21 deletions(-) create mode 100644 packages/lightning-plugin-telegram/src/file_proxy.ts diff --git a/packages/lightning-plugin-telegram/src/file_proxy.ts b/packages/lightning-plugin-telegram/src/file_proxy.ts new file mode 100644 index 0000000..f33c82e --- /dev/null +++ b/packages/lightning-plugin-telegram/src/file_proxy.ts @@ -0,0 +1,20 @@ +import type { telegram_config } from './mod.ts'; + +export function file_proxy(config: telegram_config) { + Deno.serve({ + port: config.plugin_port, + onListen: (addr) => { + console.log( + `bolt-telegram: file proxy listening on http://localhost:${addr.port}`, + `\nbolt-telegram: also available at: ${config.plugin_url}`, + ); + }, + }, (req: Request) => { + const { pathname } = new URL(req.url); + return fetch( + `https://api.telegram.org/file/bot${config.bot_token}/${ + pathname.replace('/telegram/', '') + }`, + ); + }); +} \ No newline at end of file diff --git a/packages/lightning-plugin-telegram/src/mod.ts b/packages/lightning-plugin-telegram/src/mod.ts index d73763e..8e0aed5 100644 --- a/packages/lightning-plugin-telegram/src/mod.ts +++ b/packages/lightning-plugin-telegram/src/mod.ts @@ -7,6 +7,7 @@ import { } from '@jersey/lightning'; import { Bot } from 'grammy'; import { from_lightning, from_telegram } from './messages.ts'; +import { file_proxy } from './file_proxy.ts'; /** options for the telegram plugin */ export type telegram_config = { @@ -21,7 +22,7 @@ export type telegram_config = { /** the plugin to use */ export class telegram_plugin extends plugin { name = 'bolt-telegram'; - private bot: Bot; + bot: Bot; constructor(l: lightning, cfg: telegram_config) { super(l, cfg); @@ -37,29 +38,10 @@ export class telegram_plugin extends plugin { this.emit('edit_message', msg); }); // turns out it's impossible to deal with messages being deleted due to tdlib/telegram-bot-api#286 - this.serve_proxy(); + file_proxy(cfg); this.bot.start(); } - private serve_proxy() { - Deno.serve({ - port: this.config.plugin_port, - onListen: (addr) => { - console.log( - `bolt-telegram: file proxy listening on http://localhost:${addr.port}`, - `bolt-telegram: also available at: ${this.config.plugin_url}`, - ); - }, - }, (req: Request) => { - const { pathname } = new URL(req.url); - return fetch( - `https://api.telegram.org/file/bot${this.bot.token}/${ - pathname.replace('/telegram/', '') - }`, - ); - }); - } - /** create a bridge */ setup_channel(channel: string) { return channel; From c1e27e5a061ab46dc865bc3e1397ce45180a6c91 Mon Sep 17 00:00:00 2001 From: Jersey Date: Fri, 29 Nov 2024 23:26:24 -0500 Subject: [PATCH 22/23] fixes and stuff --- .../src/discord_message/get_author.ts | 22 +++++++++++++------ .../src/to_lightning/message.ts | 8 +++---- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/lightning-plugin-discord/src/discord_message/get_author.ts b/packages/lightning-plugin-discord/src/discord_message/get_author.ts index 1b805bd..fbc262c 100644 --- a/packages/lightning-plugin-discord/src/discord_message/get_author.ts +++ b/packages/lightning-plugin-discord/src/discord_message/get_author.ts @@ -1,25 +1,33 @@ -import type { APIMessage } from 'discord-api-types'; +import type { GatewayMessageUpdateDispatchData } from 'discord-api-types'; import type { API } from '@discordjs/core'; import { calculateUserDefaultAvatarIndex } from '@discordjs/rest'; -export async function get_author(api: API, message: APIMessage) { - let name = message.author.global_name || message.author.username; - let avatar = message.author.avatar +export async function get_author( + api: API, + message: GatewayMessageUpdateDispatchData, +) { + let name = message.author?.global_name || message.author?.username || + 'discord user'; + let avatar = message.author?.avatar ? `https://cdn.discordapp.com/avatars/${message.author.id}/${message.author.avatar}.png` : `https://cdn.discordapp.com/embed/avatars/${ - calculateUserDefaultAvatarIndex(message.author.id) + calculateUserDefaultAvatarIndex( + message.author?.id || '360005875697582081', + ) }.png`; const channel = await api.channels.get(message.channel_id); - if ('guild_id' in channel && channel.guild_id) { + if ('guild_id' in channel && channel.guild_id && message.author) { try { const member = await api.guilds.getMember( channel.guild_id, message.author.id, ); - if (member.nick !== null && member.nick !== undefined) name = member.nick; + if (member.nick !== null && member.nick !== undefined) { + name = member.nick; + } avatar = member.avatar ? `https://cdn.discordapp.com/guilds/${channel.guild_id}/users/${message.author.id}/avatars/${member.avatar}.png` : avatar; diff --git a/packages/lightning-plugin-discord/src/to_lightning/message.ts b/packages/lightning-plugin-discord/src/to_lightning/message.ts index 62d551b..b038058 100644 --- a/packages/lightning-plugin-discord/src/to_lightning/message.ts +++ b/packages/lightning-plugin-discord/src/to_lightning/message.ts @@ -1,12 +1,12 @@ import type { API } from '@discordjs/core'; -import type { APIMessage } from 'discord-api-types'; +import type { GatewayMessageUpdateDispatchData } from 'discord-api-types'; import { get_author } from '../discord_message/get_author.ts'; import { message_to_discord } from '../discord_message/mod.ts'; import type { message } from '@jersey/lightning'; export async function message( api: API, - message: APIMessage, + message: GatewayMessageUpdateDispatchData, ): Promise { if (message.flags && message.flags & 128) message.content = 'Loading...'; @@ -43,8 +43,8 @@ export async function message( author: { profile: avatar, username: name, - rawname: message.author.username, - id: message.author.id, + rawname: message.author?.username || 'discord user', + id: message.author?.id || message.webhook_id || '', color: '#5865F2', }, channel: message.channel_id, From 71741ac2f8eec62e2746f99c735650ccc13be5de Mon Sep 17 00:00:00 2001 From: Jersey Date: Sun, 1 Dec 2024 13:32:41 -0500 Subject: [PATCH 23/23] use_rawname --- packages/lightning/src/bridge.ts | 6 +++++- packages/lightning/src/structures/bridge.ts | 2 -- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/lightning/src/bridge.ts b/packages/lightning/src/bridge.ts index 3cca33d..605380a 100644 --- a/packages/lightning/src/bridge.ts +++ b/packages/lightning/src/bridge.ts @@ -25,11 +25,15 @@ export async function bridge_message( if (!bridge) return; - // if editing isn't allowed, return + // handle bridge settings if (event !== 'create_message' && bridge.settings.allow_editing !== true) { return; } + if (bridge.settings.use_rawname && "author" in data) data.author.username = data.author.rawname; + + // TODO(jersey): implement allow_everyone here + // if the channel this event is from is disabled, return if ( bridge.channels.find((channel) => diff --git a/packages/lightning/src/structures/bridge.ts b/packages/lightning/src/structures/bridge.ts index d02b59a..4f847f3 100644 --- a/packages/lightning/src/structures/bridge.ts +++ b/packages/lightning/src/structures/bridge.ts @@ -24,8 +24,6 @@ export interface bridge_channel { plugin: string; } -// TODO(jersey): implement allow_everyone and use_rawname settings - /** possible settings for a bridge */ export interface bridge_settings { /** allow editing/deletion */