diff --git a/.github/security.md b/.github/security.md deleted file mode 120000 index 4681017..0000000 --- a/.github/security.md +++ /dev/null @@ -1 +0,0 @@ -packages/bolt-dash/docs/content/security.md \ No newline at end of file diff --git a/.github/workflows/docker.yml b/.github/workflows/publish.yml similarity index 60% rename from .github/workflows/docker.yml rename to .github/workflows/publish.yml index 01c4e6c..36e83cd 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/publish.yml @@ -9,15 +9,31 @@ permissions: packages: write jobs: - build: + jsr: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # auth w/JSR + steps: + - name: checkout + uses: actions/checkout@v4 + - name: setup deno + uses: denoland/setup-deno@v1 + with: + deno-version: v1.41.3 + - name: publish to jsr + run: | + cd packages/lightning + deno publish + docker: runs-on: ubuntu-latest steps: # Get the repository's code - - name: Checkout - uses: actions/checkout@v2 - - name: Set up QEMU + - name: checkout + uses: actions/checkout@v4 + - name: set up QEMU uses: docker/setup-qemu-action@v1 - - name: Set up Docker Buildx + - name: set up buildx id: buildx uses: docker/setup-buildx-action@v1 - name: Login to Docker Hub @@ -25,13 +41,13 @@ jobs: with: username: williamfromnj password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Docker meta + - name: metadata id: yo uses: docker/metadata-action@v3 with: images: williamfromnj/bolt tags: type=ref,event=tag - - name: Build and push + - name: build and push uses: docker/build-push-action@v2 with: context: . diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 5bf9f8a..bf12648 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -1,7 +1,7 @@ version: '2' services: - bolt: + lightning: build: . volumes: - ./config/data:/app/data diff --git a/dockerfile b/dockerfile index 5f1a289..581a87f 100644 --- a/dockerfile +++ b/dockerfile @@ -1,12 +1,13 @@ -ARG DENO_VERSION=v1.40.4 +ARG DENO_VERSION=v1.41.3 -FROM docker.io/lukechannings/deno:${DENO_VERSION} +FROM docker.io/denoland/deno:${DENO_VERSION} -# add bolt to the image +# add lightning to the image WORKDIR /app -ADD ./packages/bolt /app -RUN deno install -A --unstable-temporal -n bolt /app/cli.ts +# TODO: change when repos split +ADD ./packages/lightning /app +RUN deno install -A --unstable-temporal -n lightning /app/cli.ts -# set bolt as the entrypoint and use the run command by default -ENTRYPOINT [ "bolt" ] +# set lightning as the entrypoint and use the run command by default +ENTRYPOINT [ "lightning" ] CMD [ "--run", "--config", "./data/config.ts"] diff --git a/packages/bolt-discord/_deps.ts b/packages/bolt-discord/_deps.ts index 3576edb..26ada80 100644 --- a/packages/bolt-discord/_deps.ts +++ b/packages/bolt-discord/_deps.ts @@ -10,9 +10,9 @@ export { export { REST as rest, type RawFile } from 'npm:@discordjs/rest@2.2.0'; export { WebSocketManager as socket } from 'npm:@discordjs/ws@1.0.2'; export { - Bolt, - bolt_plugin, + lightning, + plugin, type bridge_platform, type deleted_message, type message -} from '../bolt/mod.ts'; +} from '../lightning/mod.ts'; diff --git a/packages/bolt-discord/commands.ts b/packages/bolt-discord/commands.ts index 36d9ca1..13e8efe 100644 --- a/packages/bolt-discord/commands.ts +++ b/packages/bolt-discord/commands.ts @@ -1,14 +1,14 @@ -import { API, Bolt, cmd_body } from './_deps.ts'; +import { API, lightning, cmd_body } from './_deps.ts'; import { discord_config } from './mod.ts'; export async function register_commands( config: discord_config, api: API, - bolt: Bolt + l: lightning ) { if (!config.slash_cmds) return; - const data: cmd_body = [...bolt.cmds.values()].map(command => { + const data: cmd_body = [...l.cmds.values()].map(command => { const opts = []; if (command.options?.argument_name) { @@ -35,7 +35,7 @@ export async function register_commands( type: 3, required: i.options.argument_required || false } - ] + ] : undefined }; }) @@ -45,7 +45,7 @@ export async function register_commands( return { name: command.name, type: 1, - description: command.description || 'a bolt command', + description: command.description || 'a command', options: opts }; }); diff --git a/packages/bolt-discord/mod.ts b/packages/bolt-discord/mod.ts index d0e182d..01a240b 100644 --- a/packages/bolt-discord/mod.ts +++ b/packages/bolt-discord/mod.ts @@ -1,7 +1,7 @@ import { - Bolt, + lightning, Client, - bolt_plugin, + plugin, bridge_platform, deleted_message, message, @@ -18,14 +18,14 @@ export type discord_config = { slash_cmds?: boolean; }; -export class discord_plugin extends bolt_plugin { +export class discord_plugin extends plugin { bot: Client; name = 'bolt-discord'; - version = '0.5.8'; + version = '0.6.0'; support = ['0.5.5']; - constructor(bolt: Bolt, config: discord_config) { - super(bolt, config); + constructor(l: lightning, config: discord_config) { + super(l, config); this.config = config; const rest_client = new rest({ version: '10' }).setToken(config.token); const gateway = new socket({ @@ -35,7 +35,7 @@ export class discord_plugin extends bolt_plugin { }); this.bot = new Client({ rest: rest_client, gateway }); register_events(this); - register_commands(this.config, this.bot.api, bolt); + register_commands(this.config, this.bot.api, l); gateway.connect(); } diff --git a/packages/bolt-guilded/_deps.ts b/packages/bolt-guilded/_deps.ts index 4f9e0ef..be4f9d5 100644 --- a/packages/bolt-guilded/_deps.ts +++ b/packages/bolt-guilded/_deps.ts @@ -12,11 +12,11 @@ export { type WebhookPayload } from 'npm:guilded.js@0.23.7'; export { - Bolt, - bolt_plugin, + lightning, + plugin, create_message, type bridge_platform, type deleted_message, type embed, type message -} from '../bolt/mod.ts'; +} from '../lightning/mod.ts'; diff --git a/packages/bolt-guilded/legacybridging.ts b/packages/bolt-guilded/legacybridging.ts index 0652e20..0de0ee5 100644 --- a/packages/bolt-guilded/legacybridging.ts +++ b/packages/bolt-guilded/legacybridging.ts @@ -25,10 +25,10 @@ export async function bridge_legacy( await guilded.bot.messages.send( senddata, toguildedid( - create_message({ - text: `In the next major version of Bolt, embed-based bridges like this one won't be supported anymore. - See https://github.com/williamhorning/bolt/issues/36 for more information.` - }) + create_message( + `In the next major version of bolt-guilded, embed-based bridges like this one won't be supported anymore. + See https://github.com/williamhorning/bolt/issues/36 for more information.` + ) ) ); } @@ -36,12 +36,14 @@ export async function bridge_legacy( } async function migrate_bridge(channel: string, guilded: guilded_plugin) { - if (!guilded.bolt.db.redis.get(`guilded-embed-migration-${channel}`)) { - await guilded.bolt.db.redis.set( + if (!guilded.lightning.redis.get(`guilded-embed-migration-${channel}`)) { + await guilded.lightning.redis.set( `guilded-embed-migration-${channel}`, 'true' ); - const current = await guilded.bolt.bridge.get_bridge({ channel: channel }); + const current = await guilded.lightning.bridge.get_bridge({ + channel: channel + }); if (current) { current.platforms[ current.platforms.findIndex(i => i.channel === channel) @@ -50,7 +52,7 @@ async function migrate_bridge(channel: string, guilded: guilded_plugin) { plugin: 'bolt-guilded', senddata: await guilded.create_bridge(channel) }; - await guilded.bolt.bridge.update_bridge(current); + await guilded.lightning.bridge.update_bridge(current); } } } diff --git a/packages/bolt-guilded/mod.ts b/packages/bolt-guilded/mod.ts index 9266c59..6bc7599 100644 --- a/packages/bolt-guilded/mod.ts +++ b/packages/bolt-guilded/mod.ts @@ -1,9 +1,9 @@ import { - Bolt, + lightning, Client, WebhookPayload, WebhookClient, - bolt_plugin, + plugin, bridge_platform, deleted_message, message @@ -11,14 +11,14 @@ import { import { bridge_legacy } from './legacybridging.ts'; import { tocore, toguilded } from './messages.ts'; -export class guilded_plugin extends bolt_plugin<{ token: string }> { +export class guilded_plugin extends plugin<{ token: string }> { bot: Client; name = 'bolt-guilded'; - version = '0.5.8'; + version = '0.6.0'; support = ['0.5.5']; - constructor(bolt: Bolt, config: { token: string }) { - super(bolt, config); + constructor(l: lightning, config: { token: string }) { + super(l, config); this.bot = new Client(config); this.bot.on('ready', () => { this.emit('ready'); @@ -61,7 +61,8 @@ export class guilded_plugin extends bolt_plugin<{ token: string }> { }); const srvhooks = (await srvwhs.json()).webhooks; const found_wh = srvhooks.find((wh: WebhookPayload) => { - if (wh.name === 'Bolt Bridges' && wh.channelId === channel) return true; + if (wh.name === 'Lightning Bridges' && wh.channelId === channel) + return true; return false; }); if (found_wh && found_wh.token) @@ -75,7 +76,7 @@ export class guilded_plugin extends bolt_plugin<{ token: string }> { 'Content-Type': 'application/json' }, body: JSON.stringify({ - name: 'Bolt Bridges', + name: 'Lightning Bridges', channelId: channel }) } diff --git a/packages/bolt-revolt/deps.ts b/packages/bolt-revolt/deps.ts index 7ce2e50..239e015 100644 --- a/packages/bolt-revolt/deps.ts +++ b/packages/bolt-revolt/deps.ts @@ -10,8 +10,8 @@ export { UserSystemMessage } from 'npm:@williamhorning/revolt.js@7.0.0-beta.10'; export { - Bolt, - bolt_plugin, + lightning, + plugin, type bridge_platform, type message -} from '../bolt/mod.ts'; +} from '../lightning/mod.ts'; diff --git a/packages/bolt-revolt/mod.ts b/packages/bolt-revolt/mod.ts index e24bbf7..dbdd28a 100644 --- a/packages/bolt-revolt/mod.ts +++ b/packages/bolt-revolt/mod.ts @@ -1,21 +1,21 @@ import { - Bolt, + lightning, Client, Message, - bolt_plugin, + plugin, bridge_platform, message } from './deps.ts'; import { tocore, torevolt } from './messages.ts'; -export class revolt_plugin extends bolt_plugin<{ token: string }> { +export class revolt_plugin extends plugin<{ token: string }> { bot: Client; name = 'bolt-revolt'; - version = '0.5.8'; + version = '0.6.0'; support = ['0.5.5']; - constructor(bolt: Bolt, config: { token: string }) { - super(bolt, config); + constructor(l: lightning, config: { token: string }) { + super(l, config); this.bot = new Client(); this.bot.on('messageCreate', message => { if (message.systemMessage) return; diff --git a/packages/bolt/_deps.ts b/packages/bolt/_deps.ts deleted file mode 100644 index a29e1c0..0000000 --- a/packages/bolt/_deps.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { EventEmitter } from 'jsr:@denosaurs/event@^2.0.2'; -export { - MongoClient, - type Document, - type Collection -} from 'https://deno.land/x/mongo@v0.32.0/mod.ts'; -export { connect } from 'https://deno.land/x/redis@v0.32.0/mod.ts'; -export { parseArgs } from 'jsr:@std/cli@^0.219.1/parse_args'; -export { assertEquals } from 'jsr:@std/assert@^0.219.1/assert_equals'; diff --git a/packages/bolt/bolt.ts b/packages/bolt/bolt.ts deleted file mode 100644 index 7c03148..0000000 --- a/packages/bolt/bolt.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { bolt_bridges } from './bridges/mod.ts'; -import { bolt_commands } from './cmds/mod.ts'; -import { connect, EventEmitter, MongoClient } from './_deps.ts'; -import { - bolt_plugin, - config, - define_config, - log_error, - plugin_events, - create_plugin -} from './utils/mod.ts'; - -export class Bolt extends EventEmitter { - bridge: bolt_bridges; - cmds: bolt_commands = new bolt_commands(); - config: config; - db: { - mongo: MongoClient; - redis: Awaited>; - }; - plugins: Map> = new Map< - string, - bolt_plugin - >(); - - static async setup(cfg: Partial): Promise { - const config = define_config(cfg); - - Deno.env.set('BOLT_ERROR_HOOK', config.errorURL || ''); - - const mongo = new MongoClient(); - - let redis: Bolt['db']['redis'] | undefined; - - try { - await mongo.connect(config.mongo_uri); - redis = await connect({ - hostname: config.redis_host, - port: config.redis_port - }); - } catch (e) { - await log_error(e, { config }); - Deno.exit(1); - } - - return new Bolt(config, mongo, redis); - } - - private constructor( - config: config, - mongo: MongoClient, - redis: Bolt['db']['redis'] - ) { - super(); - this.config = config; - this.db = { mongo, redis }; - this.bridge = new bolt_bridges(this); - this.cmds.listen(this); - this.load(this.config.plugins); - } - - async load(plugins: { type: create_plugin; config: unknown }[]) { - for (const { type, config } of plugins) { - const plugin = new type(this, config); - if (!plugin.support.includes('0.5.5')) { - throw ( - await log_error( - new Error(`plugin '${plugin.name}' doesn't support bolt 0.5.5`) - ) - ).e; - } else { - this.plugins.set(plugin.name, plugin); - (async () => { - for await (const event of plugin) { - this.emit(event.name, ...event.value); - } - })(); - } - } - } -} diff --git a/packages/bolt/bridges/_deps.ts b/packages/bolt/bridges/_deps.ts deleted file mode 100644 index f01fcc6..0000000 --- a/packages/bolt/bridges/_deps.ts +++ /dev/null @@ -1,10 +0,0 @@ -export { type Collection } from '../_deps.ts'; -export { Bolt } from '../bolt.ts'; -export { type command, type command_arguments } from '../cmds/mod.ts'; -export { - bolt_plugin, - create_message, - log_error, - type message, - type deleted_message -} from '../utils/mod.ts'; diff --git a/packages/bolt/bridges/mod.ts b/packages/bolt/bridges/mod.ts deleted file mode 100644 index 6979f5f..0000000 --- a/packages/bolt/bridges/mod.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { bridge_commands } from './_commands.ts'; -import { - Bolt, - Collection, - message, - deleted_message, - log_error, - bolt_plugin -} from './_deps.ts'; -import { bridge_document, bridge_platform } from './types.ts'; - -export class bolt_bridges { - private bolt: Bolt; - private bridge_collection: Collection; - // TODO: find a better way to do this, maps work BUT don't't scale well - private bridged_message_id_map = new Map(); - - constructor(bolt: Bolt) { - this.bolt = bolt; - this.bridge_collection = bolt.db.mongo - .database(bolt.config.mongo_database) - .collection('bridges'); - this.bolt.on('create_message', async msg => { - await new Promise(res => setTimeout(res, 250)); - if (this.is_bridged(msg)) return; - bolt.emit('create_nonbridged_message', msg); - await this.handle_message(msg, 'create_message'); - }); - this.bolt.on('edit_message', async msg => { - await new Promise(res => setTimeout(res, 250)); - if (this.is_bridged(msg)) return; - await this.handle_message(msg, 'edit_message'); - }); - this.bolt.on('delete_message', async msg => { - await new Promise(res => setTimeout(res, 400)); - await this.handle_message(msg, 'delete_message'); - }); - this.bolt.cmds.set('bridge', bridge_commands(bolt)); - } - - async get_bridge_message(id: string): Promise { - const redis_data = await this.bolt.db.redis.get(`bolt-bridge-${id}`); - if (redis_data === null) return [] as bridge_platform[]; - return JSON.parse(redis_data) as bridge_platform[]; - } - - is_bridged(msg: deleted_message): boolean { - const platform = this.bolt.plugins.get(msg.platform.name); - if (!platform) return false; - const platsays = platform.is_bridged(msg); - if (platsays !== 'query') return platsays; - return Boolean(this.bridged_message_id_map.get(msg.id)); - } - - async get_bridge({ - _id, - channel - }: { - _id?: string; - channel?: string; - }): Promise { - const query = {} as Record; - - if (_id) { - query._id = _id; - } - if (channel) { - query['platforms.channel'] = channel; - } - return (await this.bridge_collection.findOne(query)) || undefined; - } - - async update_bridge(bridge: bridge_document): Promise { - await this.bridge_collection.replaceOne({ _id: bridge._id }, bridge, { - upsert: true - }); - } - - private async handle_message( - msg: deleted_message, - action: 'delete_message' - ): Promise; - private async handle_message( - msg: message, - action: 'create_message' | 'edit_message' - ): Promise; - private async handle_message( - msg: message | deleted_message, - action: 'create_message' | 'edit_message' | 'delete_message' - ): Promise { - const bridge_info = await this.get_platforms(msg, action); - if (!bridge_info) return; - - if (bridge_info.bridge.settings?.realnames === true) { - if ('author' in msg && msg.author) { - msg.author.username = msg.author.rawname; - } - } - - const data: (bridge_platform & { id: string })[] = []; - - for (const plat of bridge_info.platforms) { - const { plugin, platform } = await this.get_sane_plugin(plat, action); - if (!plugin || !platform) continue; - - let dat; - - try { - dat = await plugin[action]( - { - ...msg, - replytoid: await this.get_replytoid(msg, platform) - } as message, - platform - ); - } catch (e) { - if (action === 'delete_message') continue; - const err = await log_error(e, { platform, action }); - try { - dat = await plugin[action](err.message, platform); - } catch (e) { - await log_error( - new Error(`logging failed for ${err.uuid}`, { cause: e }) - ); - continue; - } - } - this.bridged_message_id_map.set(dat.id!, true); - data.push(dat as bridge_platform & { id: string }); - } - - for (const i of data) { - await this.bolt.db.redis.set(`bolt-bridge-${i.id}`, JSON.stringify(data)); - } - - await this.bolt.db.redis.set(`bolt-bridge-${msg.id}`, JSON.stringify(data)); - } - - private async get_platforms( - msg: message | deleted_message, - action: 'create_message' | 'edit_message' | 'delete_message' - ) { - const bridge = await this.get_bridge(msg); - if (!bridge) return; - if ( - action !== 'create_message' && - bridge.settings?.editing_allowed !== true - ) - return; - - const platforms = - action === 'create_message' - ? bridge.platforms.filter(i => i.channel !== msg.channel) - : await this.get_bridge_message(msg.id); - if (!platforms || platforms.length < 1) return; - return { platforms, bridge }; - } - - private async get_replytoid( - msg: message | deleted_message, - platform: bridge_platform - ) { - let replytoid; - if ('replytoid' in msg && msg.replytoid) { - try { - replytoid = (await this.get_bridge_message(msg.replytoid))?.find( - i => i.channel === platform.channel && i.plugin === platform.plugin - )?.id; - } catch { - replytoid = undefined; - } - } - return replytoid; - } - - private async get_sane_plugin( - platform: bridge_platform, - action: 'create_message' | 'edit_message' | 'delete_message' - ): Promise<{ - plugin?: bolt_plugin; - platform?: bridge_platform & { id: string }; - }> { - const plugin = this.bolt.plugins.get(platform.plugin); - - if (!plugin || !plugin[action]) { - await log_error(new Error(`plugin ${platform.plugin} has no ${action}`)); - return {}; - } - - if (!platform.senddata || (action !== 'create_message' && !platform.id)) - return {}; - - return { plugin, platform: platform } as { - plugin: bolt_plugin; - platform: bridge_platform & { id: string }; - }; - } -} - -export * from './types.ts'; diff --git a/packages/bolt/bridges/types.ts b/packages/bolt/bridges/types.ts deleted file mode 100644 index dceccc6..0000000 --- a/packages/bolt/bridges/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -export interface bridge_document { - _id: string; - platforms: bridge_platform[]; - settings?: { - realnames?: boolean; - editing_allowed?: boolean; - }; -} - -export interface bridge_platform { - channel: string; - plugin: string; - senddata: unknown; - id?: string; -} diff --git a/packages/bolt/cmds/_default.ts b/packages/bolt/cmds/_default.ts deleted file mode 100644 index d3d6648..0000000 --- a/packages/bolt/cmds/_default.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { create_message } from './_deps.ts'; -import { command } from './types.ts'; - -export const default_commands = [ - [ - 'help', - { - name: 'help', - description: 'get help', - execute: () => - create_message({ - text: 'check out [the docs](https://williamhorning.dev/bolt/) for help.' - }) - } - ], - [ - 'version', - { - name: 'version', - description: "get bolt's version", - execute: () => - create_message({ - text: 'hello from bolt 0.5.8!' - }) - } - ], - [ - 'ping', - { - name: 'ping', - description: 'pong', - execute: ({ timestamp }) => - create_message({ - text: `Pong! 🏓 ${Temporal.Now.instant() - .since(timestamp) - .total('milliseconds')}ms` - }) - } - ] -] as [string, command][]; diff --git a/packages/bolt/cmds/_deps.ts b/packages/bolt/cmds/_deps.ts deleted file mode 100644 index 596d413..0000000 --- a/packages/bolt/cmds/_deps.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { parseArgs } from '../_deps.ts'; -export { Bolt } from '../bolt.ts'; -export { create_message, log_error, type message } from '../utils/mod.ts'; diff --git a/packages/bolt/cmds/mod.ts b/packages/bolt/cmds/mod.ts deleted file mode 100644 index 8750d0b..0000000 --- a/packages/bolt/cmds/mod.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { default_commands } from './_default.ts'; -import { Bolt, log_error, parseArgs } from './_deps.ts'; -import { command, command_arguments } from './types.ts'; - -export class bolt_commands extends Map { - constructor() { - super(default_commands); - } - - listen(bolt: Bolt) { - bolt.on('create_nonbridged_message', msg => { - if (msg.content?.startsWith('!bolt')) { - const args = parseArgs(msg.content.split(' ')); - args._.shift(); - this.run({ - channel: msg.channel, - cmd: args._.shift() as string, - subcmd: args._.shift() as string, - opts: args as Record, - platform: msg.platform.name, - timestamp: msg.timestamp, - replyfn: msg.reply - }); - } - }); - - bolt.on('create_command', async cmd => { - await this.run(cmd); - }); - } - - async run(opts: command_arguments) { - const cmd = this.get(opts.cmd) || this.get('help')!; - const cmd_opts = { ...opts, commands: this }; - let reply; - try { - let execute; - if (cmd.options?.subcommands && opts.subcmd) { - execute = cmd.options.subcommands.find( - i => i.name === opts.subcmd - )?.execute; - } - if (!execute) execute = cmd.execute; - reply = await execute(cmd_opts); - } catch (e) { - reply = (await log_error(e, { ...opts, reply: undefined })).message; - } - try { - await opts.replyfn(reply, false); - } catch (e) { - await log_error(e, { ...opts, reply: undefined }); - } - } -} - -export * from './types.ts'; diff --git a/packages/bolt/cmds/types.ts b/packages/bolt/cmds/types.ts deleted file mode 100644 index 51127ac..0000000 --- a/packages/bolt/cmds/types.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { message } from './_deps.ts'; - -export type command_arguments = { - channel: string; - cmd: string; - opts: Record; - platform: string; - replyfn: message['reply']; - subcmd?: string; - timestamp: Temporal.Instant; -}; - -export type command = { - name: string; - description?: string; - options?: { - default?: boolean; - argument_name?: string; - argument_required?: boolean; - subcommands?: command[]; - }; - execute: ( - opts: command_arguments - ) => Promise> | message; -}; diff --git a/packages/bolt/deno.jsonc b/packages/bolt/deno.jsonc deleted file mode 100644 index e8127c0..0000000 --- a/packages/bolt/deno.jsonc +++ /dev/null @@ -1,14 +0,0 @@ -{ - "exports": "./mod.ts", - // TODO: move deps over here - "imports": { - - }, - // TODO: DO NOT PUBLISH UNDER THE BOLT NAME - "name": "@jersey/bolt", - "unstable": ["temporal"], - "test": { - "include": ["./_tests.ts"] - }, - "version": "0.5.9" -} diff --git a/packages/bolt/migrations/_utils.ts b/packages/bolt/migrations/_utils.ts deleted file mode 100644 index 72aedde..0000000 --- a/packages/bolt/migrations/_utils.ts +++ /dev/null @@ -1,26 +0,0 @@ -export function map_plugins(pluginname: string): string { - if (pluginname === 'discord') return 'bolt-discord'; - if (pluginname === 'guilded') return 'bolt-guilded'; - if (pluginname === 'revolt') return 'bolt-revolt'; - return 'unknown'; -} - -export function is_channel(channel: string): boolean { - if ( - channel.match( - /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i - ) - ) { - return true; - } - if (channel.match(/[0-7][0-9A-HJKMNP-TV-Z]{25}/gm)) return true; - if (!isNaN(Number(channel))) return true; - if ( - channel.startsWith('discord-') || - channel.startsWith('guilded-') || - channel.startsWith('revolt-') - ) { - return true; - } - return false; -} diff --git a/packages/bolt/migrations/fourbetatofive.ts b/packages/bolt/migrations/fourbetatofive.ts deleted file mode 100644 index 4ae630d..0000000 --- a/packages/bolt/migrations/fourbetatofive.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Document } from '../_deps.ts'; -import { map_plugins } from './_utils.ts'; - -export default { - from: '0.4-beta', - to: '0.5', - from_db: 'bridgev1', - to_db: 'bridges', - translate: ( - itemslist: ( - | Document - | { - _id: string; - value: { - bridges: { platform: string; channel: string; senddata: unknown }[]; - }; - } - )[] - ) => - itemslist.flatMap<{ - _id: string; - platforms: { plugin: string; channel: string; senddata: unknown }[]; - }>(({ _id, value }) => { - if (_id.startsWith('message-')) return []; - return [ - { - _id, - platforms: value.bridges.map( - (i: { platform: string; channel: string; senddata: unknown }) => { - return { - plugin: map_plugins(i.platform), - channel: i.channel, - senddata: i.senddata - }; - } - ) - } - ]; - }) as Document[] -}; diff --git a/packages/bolt/migrations/fourtofourbeta.ts b/packages/bolt/migrations/fourtofourbeta.ts deleted file mode 100644 index a322f64..0000000 --- a/packages/bolt/migrations/fourtofourbeta.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Document } from '../_deps.ts'; -import { is_channel } from './_utils.ts'; - -export default { - from: '0.4', - to: '0.4-beta', - from_db: 'bridge', - to_db: 'bridgev1', - translate: ( - items: (Document | { _id: string; value: string | unknown })[] - ): Document[] => { - const obj = {} as { - [key: string]: { - platform: string; - channel: string; - senddata: unknown; - }[]; - }; - - for (const item of items) { - const [platform, ...join] = item._id.split('-'); - const name = join.join('-'); - if (is_channel(name)) continue; - const _id = items.find(i => { - return i._id.startsWith(platform) && i.value === name; - })?._id; - if (!_id) continue; - if (!obj[name]) obj[name] = []; - obj[name].push({ - platform, - channel: _id.split('-').slice(1).join('-'), - senddata: item.value - }); - } - - const documents = []; - - for (const _id in obj) { - const value = obj[_id]; - if (!value) continue; - if (is_channel(_id)) continue; - if (value.length < 2) continue; - documents.push({ - _id, - value: { - bridges: value - } - }); - } - - return documents; - } -}; diff --git a/packages/bolt/migrations/mod.ts b/packages/bolt/migrations/mod.ts deleted file mode 100644 index cbc92f4..0000000 --- a/packages/bolt/migrations/mod.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Document } from '../_deps.ts'; -import BoltFourToFourBeta from './fourtofourbeta.ts'; -import BoltFourBetaToFive from './fourtofourbeta.ts'; - -const list_migrations = [BoltFourBetaToFive, BoltFourToFourBeta]; - -export type migration = (typeof list_migrations)[number]; - -export enum versions { - Four = '0.4', - FourBeta = '0.4-beta', - Five = '0.5' -} - -export function get_migrations(from: string, to: string): migration[] { - const indexoffrom = list_migrations.findIndex(i => i.from === from); - const indexofto = list_migrations.findLastIndex(i => i.to === to); - return list_migrations.slice(indexoffrom, indexofto); -} - -export function apply_migrations( - migrations: migration[], - data: Document[] -): Document[] { - return migrations.reduce((acc, migration) => migration.translate(acc), data); -} diff --git a/packages/bolt/mod.ts b/packages/bolt/mod.ts deleted file mode 100644 index 9316c54..0000000 --- a/packages/bolt/mod.ts +++ /dev/null @@ -1,21 +0,0 @@ -export { Bolt } from './bolt.ts'; -export { type bridge_platform } from './bridges/mod.ts'; -export { type command, type command_arguments } from './cmds/mod.ts'; -export { - apply_migrations, - get_migrations, - versions, - type migration -} from './migrations/mod.ts'; -export { - bolt_plugin, - create_message, - define_config, - log_error, - type config, - type deleted_message, - type embed, - type embed_media, - type message, - type plugin_events -} from './utils/mod.ts'; diff --git a/packages/bolt/utils/_deps.ts b/packages/bolt/utils/_deps.ts deleted file mode 100644 index 0b2a570..0000000 --- a/packages/bolt/utils/_deps.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { nanoid } from 'https://deno.land/x/nanoid@v3.0.0/mod.ts'; -export { Bolt } from '../bolt.ts'; -export { type bridge_platform } from '../bridges/mod.ts'; -export { type command_arguments } from '../cmds/mod.ts'; -export { EventEmitter } from '../_deps.ts'; diff --git a/packages/bolt/utils/config.ts b/packages/bolt/utils/config.ts deleted file mode 100644 index 5d05d23..0000000 --- a/packages/bolt/utils/config.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { create_plugin } from './plugins.ts'; - -export function define_config(config?: Partial): config { - if (!config) config = {}; - if (!config.prod) config.prod = false; - if (!config.plugins) config.plugins = []; - if (!config.mongo_uri) config.mongo_uri = 'mongodb://localhost:27017'; - if (!config.mongo_database) - config.mongo_database = config.prod ? 'bolt' : 'bolt-testing'; - if (!config.redis_host) config.redis_host = 'localhost'; - return config as config; -} - -export interface config { - prod: boolean; - plugins: { type: create_plugin; config: unknown }[]; - mongo_uri: string; - mongo_database: string; - redis_host: string; - redis_port?: number; - errorURL?: string; -} diff --git a/packages/bolt/utils/errors.ts b/packages/bolt/utils/errors.ts deleted file mode 100644 index 5af2973..0000000 --- a/packages/bolt/utils/errors.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { nanoid } from './_deps.ts'; -import { create_message, message } from './messages.ts'; - -function get_replacer() { - const seen = new WeakSet(); - return (_: string, value: unknown) => { - if (typeof value === 'object' && value !== null) { - if (seen.has(value)) { - return '[Circular]'; - } - seen.add(value); - } - if (typeof value === 'bigint') { - return value.toString(); - } - return value; - }; -} - -export async function log_error( - e: Error, - extra: Record = {}, - _id: () => string = nanoid -): Promise<{ - e: Error; - extra: Record; - uuid: string; - message: message & { uuid?: string }; -}> { - const uuid = _id(); - - const error_hook = Deno.env.get('BOLT_ERROR_HOOK'); - - if (error_hook && error_hook !== '') { - delete extra.msg; - - await ( - await fetch(error_hook, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify( - { - embeds: [ - { - title: e.message, - description: `\`\`\`${ - e.stack - }\`\`\`\n\`\`\`js\n${JSON.stringify( - { - ...extra, - uuid - }, - get_replacer(), - 2 - )}\`\`\`` - } - ] - }, - get_replacer() - ) - }) - ).text(); - } - - console.error(`\x1b[1;31mBolt Error - '${uuid}'\x1b[0m`); - console.error(e, extra); - - return { - e, - uuid, - extra, - message: create_message({ - text: `Something went wrong! Check [the docs](https://williamhorning.dev/bolt/docs/Using/) for help.\n\`\`\`\n${e.message}\n${uuid}\n\`\`\``, - uuid - }) - }; -} diff --git a/packages/bolt/utils/messages.ts b/packages/bolt/utils/messages.ts deleted file mode 100644 index 72e1d51..0000000 --- a/packages/bolt/utils/messages.ts +++ /dev/null @@ -1,84 +0,0 @@ -export function create_message({ - text, - uuid -}: { - text?: string; - uuid?: string; -}): message & { uuid?: string } { - const data = { - author: { - username: 'Bolt', - profile: - 'https://cdn.discordapp.com/icons/1011741670510968862/2d4ce9ff3f384c027d8781fa16a38b07.png?size=1024', - rawname: 'bolt', - id: 'bolt' - }, - content: text, - channel: '', - id: '', - reply: async () => {}, - timestamp: Temporal.Now.instant(), - platform: { - name: 'bolt', - message: undefined - }, - uuid - }; - return data; -} - -export type embed_media = { height?: number; url: string; width?: number }; - -export interface embed { - author?: { name: string; url?: string; icon_url?: string }; - color?: number; - description?: string; - fields?: { name: string; value: string; inline?: boolean }[]; - footer?: { text: string; icon_url?: string }; - image?: embed_media; - thumbnail?: embed_media; - timestamp?: number; - title?: string; - url?: string; - video?: Omit & { url?: string }; -} - -export interface message { - attachments?: { - alt?: string; - file: string; - name?: string; - spoiler?: boolean; - size: number; - }[]; - author: { - username: string; - rawname: string; - profile?: string; - banner?: string; - id: string; - color?: string; - }; - content?: string; - embeds?: embed[]; - reply: (message: message, optional?: unknown) => Promise; - replytoid?: string; - id: string; - platform: { - name: string; - message: t; - webhookid?: string; - }; - channel: string; - timestamp: Temporal.Instant; -} - -export interface deleted_message { - id: string; - channel: string; - platform: { - name: string; - message: t; - }; - timestamp: Temporal.Instant; -} diff --git a/packages/bolt/utils/mod.ts b/packages/bolt/utils/mod.ts deleted file mode 100644 index 0677158..0000000 --- a/packages/bolt/utils/mod.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './config.ts'; -export * from './messages.ts'; -export * from './plugins.ts'; -export * from './errors.ts'; diff --git a/packages/bolt/utils/plugins.ts b/packages/bolt/utils/plugins.ts deleted file mode 100644 index 1a2263a..0000000 --- a/packages/bolt/utils/plugins.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { - Bolt, - EventEmitter, - bridge_platform, - command_arguments -} from './_deps.ts'; -import { deleted_message, message } from './messages.ts'; - -export abstract class bolt_plugin extends EventEmitter { - bolt: Bolt; - config: t; - - /** the name of your plugin (like bolt-discord) */ - abstract name: string; - - /** the version of your plugin (like 0.0.1) */ - abstract version: string; - - /** the versions of bolt your plugin was made for (array of strings like `[0.5.0, 0.5.5]` that only includes breaking releases) */ - abstract support: string[]; - - /** constructor */ - constructor(bolt: Bolt, config: t) { - super(); - this.bolt = bolt; - this.config = config; - } - /** create data needed to bridge */ - abstract create_bridge(channel: string): Promise; - - /** checks if message is bridged */ - abstract is_bridged(message: deleted_message): boolean | 'query'; - - /** bridge a message */ - abstract create_message( - message: message, - bridge: bridge_platform - ): Promise; - - /** edit a bridged message */ - abstract edit_message( - new_message: message, - bridge: bridge_platform & { id: string } - ): Promise; - - /** delete a bridged message */ - abstract delete_message( - message: deleted_message, - bridge: bridge_platform & { id: string } - ): Promise; -} - -export type plugin_events = { - create_message: [message]; - create_command: [command_arguments]; - create_nonbridged_message: [message]; - edit_message: [message]; - delete_message: [deleted_message]; - ready: []; -}; - -export interface create_plugin { - new (bolt: Bolt, config: unknown): bolt_plugin; - readonly prototype: bolt_plugin; -} diff --git a/packages/bolt/_testdata.ts b/packages/lightning/_testdata.ts similarity index 85% rename from packages/bolt/_testdata.ts rename to packages/lightning/_testdata.ts index 803a507..7ef70e3 100644 --- a/packages/bolt/_testdata.ts +++ b/packages/lightning/_testdata.ts @@ -3,8 +3,8 @@ export const cmd_help_output = { username: 'Bolt', profile: 'https://cdn.discordapp.com/icons/1011741670510968862/2d4ce9ff3f384c027d8781fa16a38b07.png?size=1024', - rawname: 'bolt', - id: 'bolt' + rawname: 'lightning', + id: 'lightning' }, content: 'check out [the docs](https://williamhorning.dev/bolt/) for help.', channel: '', @@ -12,10 +12,9 @@ export const cmd_help_output = { reply: async () => {}, timestamp: Temporal.Instant.from('2021-01-01T00:00:00Z'), platform: { - name: 'bolt', + name: 'lightning', message: undefined - }, - uuid: undefined + } }; export const migrations_four_one = [ @@ -91,8 +90,8 @@ export const utils_msg = { username: 'Bolt', profile: 'https://cdn.discordapp.com/icons/1011741670510968862/2d4ce9ff3f384c027d8781fa16a38b07.png?size=1024', - rawname: 'bolt', - id: 'bolt' + rawname: 'lightning', + id: 'lightning' }, content: 'test', channel: '', @@ -100,18 +99,17 @@ export const utils_msg = { reply: async () => {}, timestamp: Temporal.Instant.from('2021-01-01T00:00:00Z'), platform: { - name: 'bolt', + name: 'lightning', message: undefined - }, - uuid: 'test' + } }; export const utils_cfg = { - prod: false, plugins: [], mongo_uri: 'mongodb://localhost:27017', - mongo_database: 'bolt-testing', - redis_host: 'localhost' + mongo_database: 'lightning', + redis_host: 'localhost', + redis_port: 6379 }; export const utils_err = new Error('test'); @@ -129,20 +127,19 @@ export const utils_err_return = { username: 'Bolt', profile: 'https://cdn.discordapp.com/icons/1011741670510968862/2d4ce9ff3f384c027d8781fa16a38b07.png?size=1024', - rawname: 'bolt', - id: 'bolt' + rawname: 'lightning', + id: 'lightning' }, content: - 'Something went wrong! Check [the docs](https://williamhorning.dev/bolt/docs/Using/) for help.\n```\ntest\ntest\n```', + 'Something went wrong! [Look here](https://williamhorning.dev/bolt) for help.\n```\ntest\ntest\n```', channel: '', id: '', reply: async () => {}, timestamp: Temporal.Instant.from('2021-01-01T00:00:00Z'), platform: { - name: 'bolt', + name: 'lightning', message: undefined - }, - uuid: 'test' + } } }; @@ -150,7 +147,7 @@ export const utils_err_hook = { embeds: [ { title: utils_err.message, - description: `\`\`\`${utils_err.stack}\`\`\`\n\`\`\`js\n${JSON.stringify( + description: `\`\`\`js\n${utils_err.stack}\n${JSON.stringify( { ...utils_extra, uuid: 'test' diff --git a/packages/bolt/_tests.ts b/packages/lightning/_tests.ts similarity index 70% rename from packages/bolt/_tests.ts rename to packages/lightning/_tests.ts index f65ca8b..6922058 100644 --- a/packages/bolt/_tests.ts +++ b/packages/lightning/_tests.ts @@ -1,6 +1,4 @@ -// the bolt test suite (incomplete) - -import { assertEquals } from './_deps.ts'; +import { assertEquals } from 'assert_eq'; import { cmd_help_output, migrations_five, @@ -15,17 +13,18 @@ import { utils_err_hook, utils_msg } from './_testdata.ts'; -import { bolt_commands } from './cmds/mod.ts'; import { + commands, message, apply_migrations, get_migrations, define_config, log_error, create_message -} from './mod.ts'; -import BoltFourToFourBeta from './migrations/fourtofourbeta.ts'; -import BoltFourBetaToFive from './migrations/fourbetatofive.ts'; +} from './utils/mod.ts'; +import fourfourbeta from './utils/_fourfourbeta.ts'; +import fourbetafive from './utils/_fourbetafive.ts'; +import { versions } from './utils/migrations.ts'; // override globals @@ -39,8 +38,8 @@ console.log = console.error = () => {}; // cmds -Deno.test('bolt/cmds', async t => { - const cmds = new bolt_commands(); +Deno.test('cmds', async t => { + const cmds = new commands(); await t.step('run help command', async () => { let res: (value: message) => void; @@ -53,7 +52,7 @@ Deno.test('bolt/cmds', async t => { channel: '', cmd: 'help', opts: {}, - platform: 'bolt', + platform: 'lightning', // deno-lint-ignore require-await replyfn: async msg => res(msg), timestamp: temporal_instant @@ -69,37 +68,27 @@ Deno.test('bolt/cmds', async t => { // migrations -Deno.test('bolt/migrations', async t => { +Deno.test('migrations', async t => { await t.step('get a migration', () => { - const migrations = get_migrations('0.4', '0.4-beta'); - assertEquals(migrations, [BoltFourToFourBeta]); + const migrations = get_migrations(versions.Four, versions.FourBeta); + assertEquals(migrations, [fourfourbeta]); }); await t.step('apply migrations', async t => { await t.step('0.4 => 0.4-beta (one platform)', () => { - const result = apply_migrations( - [BoltFourToFourBeta], - migrations_four_one - ); + const result = apply_migrations([fourfourbeta], migrations_four_one); assertEquals(result, []); }); await t.step('0.4 => 0.4-beta (two platforms)', () => { - const result = apply_migrations( - [BoltFourToFourBeta], - migrations_four_two - ); + const result = apply_migrations([fourfourbeta], migrations_four_two); assertEquals(result, migrations_fourbeta); }); await t.step('0.4-beta => 0.5', () => { - // TODO: fix - const result = apply_migrations( - [BoltFourBetaToFive], - migrations_fourbeta - ); + const result = apply_migrations([fourbetafive], migrations_fourbeta); assertEquals(result, migrations_five); }); @@ -108,13 +97,15 @@ Deno.test('bolt/migrations', async t => { // utils -Deno.test('bolt/utils', async t => { +Deno.test('utils', async t => { await t.step('config handling', () => { assertEquals(define_config(), utils_cfg); }); await t.step('error handling', async t => { await t.step('basic', async () => { + Deno.env.set('LIGHTNING_ERROR_HOOK', ''); + const result = await log_error(utils_err, utils_extra, utils_err_id); result.message.reply = utils_err_return.message.reply; @@ -123,7 +114,7 @@ Deno.test('bolt/utils', async t => { }); await t.step('webhooks', async () => { - Deno.env.set('BOLT_ERROR_HOOK', 'http://localhost:8000'); + Deno.env.set('LIGHTNING_ERROR_HOOK', 'http://localhost:8000'); let res: (value: unknown) => void; @@ -144,10 +135,7 @@ Deno.test('bolt/utils', async t => { }); await t.step('message creation', () => { - const result = create_message({ - text: 'test', - uuid: 'test' - }); + const result = create_message('test'); result.reply = utils_msg.reply; diff --git a/packages/bolt/bridges/_command_functions.ts b/packages/lightning/bridges/_command_functions.ts similarity index 50% rename from packages/bolt/bridges/_command_functions.ts rename to packages/lightning/bridges/_command_functions.ts index 1546b6b..242d1b8 100644 --- a/packages/bolt/bridges/_command_functions.ts +++ b/packages/lightning/bridges/_command_functions.ts @@ -1,21 +1,21 @@ -import { Bolt, command_arguments, create_message, log_error } from './_deps.ts'; +import { lightning } from '../lightning.ts'; +import { command_arguments, create_message, log_error } from '../utils/mod.ts'; -/** join a bridge */ export async function join( - { channel, platform, opts }: command_arguments, - bolt: Bolt + { channel, platform, opts, commands }: command_arguments, + l: lightning ) { const _idraw = opts.name?.split(' ')[0]; const _id = `bridge-${_idraw}`; - const current = await bolt.bridge.get_bridge({ channel }); + const current = await l.bridge.get_bridge({ channel }); const errorargs = { channel, platform, _id }; - const plugin = bolt.plugins.get(platform); + const plugin = l.plugins.get(platform); if (current || !_idraw) { return { - text: create_message({ - text: "to do this, you can't be in a bridge and need to name your bridge, see `!bolt help`" - }) + text: create_message( + `to do this, you can't be in a bridge and need to name your bridge, see \`${commands.prefix} help\`` + ) }; } else if (!plugin || !plugin.create_bridge) { return { @@ -24,7 +24,7 @@ export async function join( ).message }; } else { - const bridge = (await bolt.bridge.get_bridge({ _id })) || { + const bridge = (await l.bridge.get_bridge({ _id })) || { _id, platforms: [] }; @@ -34,9 +34,9 @@ export async function join( plugin: platform, senddata: await plugin.create_bridge(channel) }); - await bolt.bridge.update_bridge(bridge); + await l.bridge.update_bridge(bridge); return { - text: create_message({ text: 'Joined a bridge!' }), + text: create_message('Joined a bridge!'), ok: true }; } catch (e) { @@ -45,23 +45,22 @@ export async function join( } } -/** leave a bridge */ export async function leave( - { channel, platform }: command_arguments, - bolt: Bolt + { channel, platform, commands }: command_arguments, + l: lightning ) { - const current = await bolt.bridge.get_bridge({ channel }); + const current = await l.bridge.get_bridge({ channel }); if (!current) { return { - text: create_message({ - text: 'To run this command you need to be in a bridge. To learn more, run `!bolt help`.' - }), + text: create_message( + `To run this command you need to be in a bridge. To learn more, run \`${commands.prefix} help\`.` + ), ok: true }; } else { try { - await bolt.bridge.update_bridge({ + await l.bridge.update_bridge({ _id: current._id, platforms: current.platforms.filter( i => i.channel !== channel && i.plugin !== platform @@ -69,7 +68,7 @@ export async function leave( }); return { - text: create_message({ text: 'Left a bridge!' }), + text: create_message('Left a bridge!'), ok: true }; } catch (e) { @@ -80,44 +79,38 @@ export async function leave( } } -/** reset a bridge (leave then join) */ -export async function reset(args: command_arguments, bolt: Bolt) { +export async function reset(args: command_arguments, l: lightning) { if (!args.opts.name) { - const [_, ...rest] = ( - (await bolt.bridge.get_bridge(args))?._id || '' - ).split('bridge-'); + const [_, ...rest] = ((await l.bridge.get_bridge(args))?._id || '').split( + 'bridge-' + ); args.opts.name = rest.join('bridge-'); } - let result = await leave(args, bolt); + let result = await leave(args, l); if (!result.ok) return result; - result = await join(args, bolt); + result = await join(args, l); if (!result.ok) return result; - return { text: create_message({ text: 'Reset this bridge!' }) }; + return { text: create_message('Reset this bridge!') }; } -/** toggle a setting on a bridge */ -export async function toggle(args: command_arguments, bolt: Bolt) { - const current = await bolt.bridge.get_bridge(args); +export async function toggle(args: command_arguments, l: lightning) { + const current = await l.bridge.get_bridge(args); if (!current) { return { - text: create_message({ - text: 'You need to be in a bridge to toggle settings' - }) + text: create_message('You need to be in a bridge to toggle settings') }; } if (!args.opts.setting) { return { - text: create_message({ - text: 'You need to specify a setting to toggle' - }) + text: create_message('You need to specify a setting to toggle') }; } if (!['realnames', 'editing_allowed'].includes(args.opts.setting)) { return { - text: create_message({ text: "That setting doesn't exist" }) + text: create_message("That setting doesn't exist") }; } @@ -132,9 +125,9 @@ export async function toggle(args: command_arguments, bolt: Bolt) { }; try { - await bolt.bridge.update_bridge(bridge); + await l.bridge.update_bridge(bridge); return { - text: create_message({ text: 'Toggled that setting!' }) + text: create_message('Toggled that setting!') }; } catch (e) { return { @@ -143,14 +136,12 @@ export async function toggle(args: command_arguments, bolt: Bolt) { } } -export async function status(args: command_arguments, bolt: Bolt) { - const current = await bolt.bridge.get_bridge(args); +export async function status(args: command_arguments, l: lightning) { + const current = await l.bridge.get_bridge(args); if (!current) { return { - text: create_message({ - text: "You're not in any bridges right now." - }) + text: create_message("You're not in any bridges right now.") }; } @@ -164,10 +155,10 @@ export async function status(args: command_arguments, bolt: Bolt) { : 'as well as no settings'; return { - text: create_message({ - text: `This channel is connected to \`${current._id}\`, a bridge with ${ + text: create_message( + `This channel is connected to \`${current._id}\`, a bridge with ${ current.platforms.length - 1 } other channels connected to it, ${settings_text}` - }) + ) }; } diff --git a/packages/bolt/bridges/_commands.ts b/packages/lightning/bridges/_commands.ts similarity index 57% rename from packages/bolt/bridges/_commands.ts rename to packages/lightning/bridges/_commands.ts index 9442877..ce8810b 100644 --- a/packages/bolt/bridges/_commands.ts +++ b/packages/lightning/bridges/_commands.ts @@ -1,37 +1,38 @@ import { join, leave, reset, toggle, status } from './_command_functions.ts'; -import { Bolt, command, create_message } from './_deps.ts'; +import { command, create_message } from '../utils/mod.ts'; +import { lightning } from '../lightning.ts'; -export function bridge_commands(bolt: Bolt): command { +export function bridge_commands(l: lightning): command { return { name: 'bridge', description: 'bridge this channel to somewhere else', - execute: () => - create_message({ - text: 'Try running `!bolt help` for help with bridges' - }), + execute: ({ commands }) => + create_message( + `Try running \`${commands.prefix} help\` for help with bridges` + ), options: { subcommands: [ { name: 'join', description: 'join a bridge', - execute: async opts => (await join(opts, bolt)).text, + execute: async opts => (await join(opts, l)).text, options: { argument_name: 'name', argument_required: true } }, { name: 'leave', description: 'leave a bridge', - execute: async opts => (await leave(opts, bolt)).text + execute: async opts => (await leave(opts, l)).text }, { name: 'reset', description: 'reset a bridge', - execute: async opts => (await reset(opts, bolt)).text, + execute: async opts => (await reset(opts, l)).text, options: { argument_name: 'name' } }, { name: 'toggle', description: 'toggle a setting on a bridge', - execute: async opts => (await toggle(opts, bolt)).text, + execute: async opts => (await toggle(opts, l)).text, options: { argument_name: 'setting', argument_required: true @@ -40,7 +41,7 @@ export function bridge_commands(bolt: Bolt): command { { name: 'status', description: 'see what bridges you are in', - execute: async opts => (await status(opts, bolt)).text + execute: async opts => (await status(opts, l)).text } ] } diff --git a/packages/lightning/bridges/_internal.ts b/packages/lightning/bridges/_internal.ts new file mode 100644 index 0000000..db852cf --- /dev/null +++ b/packages/lightning/bridges/_internal.ts @@ -0,0 +1,144 @@ +import { bridges } from './mod.ts'; +import { message, deleted_message, log_error, plugin } from '../utils/mod.ts'; +import { bridge_platform } from './types.ts'; +import { lightning } from '../lightning.ts'; + +export class bridge_internals_dont_use_or_look_at { + private bridges: bridges; + private l: lightning; + // TODO: find a better way to do this, maps work BUT don't't scale well + private bridged_message_id_map = new Map(); + + constructor(bridge: bridges, l: lightning) { + this.l = l; + this.bridges = bridge; + } + + is_bridged_internal(msg: deleted_message): boolean { + return Boolean(this.bridged_message_id_map.get(msg.id)); + } + + async handle_message( + msg: message | deleted_message, + action: 'create_message' | 'edit_message' | 'delete_message' + ): Promise { + const bridge_info = await this.get_platforms(msg, action); + if (!bridge_info) return; + + if (bridge_info.bridge.settings?.realnames === true) { + if ('author' in msg && msg.author) { + msg.author.username = msg.author.rawname; + } + } + + const data: (bridge_platform & { id: string })[] = []; + + for (const plat of bridge_info.platforms) { + const { plugin, platform } = await this.get_sane_plugin(plat, action); + if (!plugin || !platform) continue; + + let dat; + + try { + dat = await plugin[action]( + { + ...msg, + replytoid: await this.get_replytoid(msg, platform) + } as message, + platform + ); + } catch (e) { + if (action === 'delete_message') continue; + const err = await log_error(e, { platform, action }); + try { + dat = await plugin[action](err.message, platform); + } catch (e) { + await log_error( + new Error(`logging failed for ${err.uuid}`, { cause: e }) + ); + continue; + } + } + this.bridged_message_id_map.set(dat.id!, true); + data.push(dat as bridge_platform & { id: string }); + } + + for (const i of data) { + await this.l.redis.sendCommand([ + 'JSON.SET', + `lightning-bridge-${i.id}`, + '$', + JSON.stringify(data) + ]); + } + + await this.l.redis.sendCommand([ + 'JSON.SET', + `lightning-bridge-${msg.id}`, + '$', + JSON.stringify(data) + ]); + } + + private async get_platforms( + msg: message | deleted_message, + action: 'create_message' | 'edit_message' | 'delete_message' + ) { + const bridge = await this.bridges.get_bridge(msg); + if (!bridge) return; + if ( + action !== 'create_message' && + bridge.settings?.editing_allowed !== true + ) + return; + + const platforms = + action === 'create_message' + ? bridge.platforms.filter(i => i.channel !== msg.channel) + : await this.bridges.get_bridge_message(msg.id); + if (!platforms || platforms.length < 1) return; + return { platforms, bridge }; + } + + private async get_replytoid( + msg: message | deleted_message, + platform: bridge_platform + ) { + let replytoid; + if ('replytoid' in msg && msg.replytoid) { + try { + replytoid = ( + await this.bridges.get_bridge_message(msg.replytoid) + )?.find( + i => i.channel === platform.channel && i.plugin === platform.plugin + )?.id; + } catch { + replytoid = undefined; + } + } + return replytoid; + } + + private async get_sane_plugin( + platform: bridge_platform, + action: 'create_message' | 'edit_message' | 'delete_message' + ): Promise<{ + plugin?: plugin; + platform?: bridge_platform & { id: string }; + }> { + const plugin = this.l.plugins.get(platform.plugin); + + if (!plugin || !plugin[action]) { + await log_error(new Error(`plugin ${platform.plugin} has no ${action}`)); + return {}; + } + + if (!platform.senddata || (action !== 'create_message' && !platform.id)) + return {}; + + return { plugin, platform: platform } as { + plugin: plugin; + platform: bridge_platform & { id: string }; + }; + } +} diff --git a/packages/lightning/bridges/mod.ts b/packages/lightning/bridges/mod.ts new file mode 100644 index 0000000..8c47f4e --- /dev/null +++ b/packages/lightning/bridges/mod.ts @@ -0,0 +1,86 @@ +import { bridge_commands } from './_commands.ts'; +import { lightning } from '../lightning.ts'; +import { Collection } from 'mongo'; +import { deleted_message } from '../utils/mod.ts'; +import { bridge_document, bridge_platform } from './types.ts'; +import { bridge_internals_dont_use_or_look_at } from './_internal.ts'; + +/** a thing that bridges messages between platforms defined by plugins */ +export class bridges { + /** the parent instance of lightning */ + private l: lightning; + /** the database collection containing all the bridges */ + private bridge_collection: Collection; + /** the scary internals that you never want to look at */ + private internals: bridge_internals_dont_use_or_look_at; + + /** create a bridge instance and attach to lightning */ + constructor(l: lightning) { + this.l = l; + this.internals = new bridge_internals_dont_use_or_look_at(this, l); + this.bridge_collection = l.mongo + .database(l.config.mongo_database) + .collection('bridges'); + l.on('create_message', async msg => { + await new Promise(res => setTimeout(res, 250)); + if (this.is_bridged(msg)) return; + l.emit('create_nonbridged_message', msg); + await this.internals.handle_message(msg, 'create_message'); + }); + l.on('edit_message', async msg => { + await new Promise(res => setTimeout(res, 250)); + if (this.is_bridged(msg)) return; + await this.internals.handle_message(msg, 'edit_message'); + }); + l.on('delete_message', async msg => { + await new Promise(res => setTimeout(res, 400)); + await this.internals.handle_message(msg, 'delete_message'); + }); + l.cmds.set('bridge', bridge_commands(l)); + } + + /** get all the platforms a message was bridged to */ + async get_bridge_message(id: string): Promise { + const rdata = await this.l.redis.sendCommand([ + 'JSON.GET', + `lightning-bridge-${id}` + ]); + if (!rdata) return [] as bridge_platform[]; + return JSON.parse(rdata as string) as bridge_platform[]; + } + + /** check if a message was bridged */ + is_bridged(msg: deleted_message): boolean { + const platform = this.l.plugins.get(msg.platform.name); + if (!platform) return false; + const platsays = platform.is_bridged(msg); + if (platsays !== 'query') return platsays; + return this.internals.is_bridged_internal(msg); + } + + /** get a bridge using the bridges name or a channel in it */ + async get_bridge({ + _id, + channel + }: { + _id?: string; + channel?: string; + }): Promise { + const query = {} as Record; + + if (_id) { + query._id = _id; + } + if (channel) { + query['platforms.channel'] = channel; + } + return (await this.bridge_collection.findOne(query)) || undefined; + } + + /** update a bridge in a database */ + async update_bridge(bridge: bridge_document): Promise { + await this.bridge_collection.replaceOne({ _id: bridge._id }, bridge, { + upsert: true + }); + } +} diff --git a/packages/lightning/bridges/types.ts b/packages/lightning/bridges/types.ts new file mode 100644 index 0000000..0e8696d --- /dev/null +++ b/packages/lightning/bridges/types.ts @@ -0,0 +1,29 @@ +/** the database's representation of a bridge */ +export interface bridge_document { + /** the bridge's id */ + _id: string; + /** each platform within the bridge */ + platforms: bridge_platform[]; + /** the settings for the bridge */ + settings?: bridge_settings; +} + +/** platform within a bridge */ +export interface bridge_platform { + /** the channel to be bridged */ + channel: string; + /** the plugin used for this platform */ + plugin: string; + /** the data needed for a message to be sent */ + senddata: unknown; + /** the id of a sent message */ + id?: string; +} + +/** bridge settings */ +export interface bridge_settings { + /** use an authors rawname instead of username */ + realnames?: boolean; + /** whether or not to allow editing to be bridged */ + editing_allowed?: boolean; +} diff --git a/packages/bolt/cli.ts b/packages/lightning/cli.ts similarity index 59% rename from packages/bolt/cli.ts rename to packages/lightning/cli.ts index 9ee4304..9794842 100644 --- a/packages/bolt/cli.ts +++ b/packages/lightning/cli.ts @@ -1,11 +1,14 @@ +import { lightning } from './lightning.ts'; +import { parseArgs } from 'std_args'; +import { MongoClient } from 'mongo'; import { apply_migrations, get_migrations, - versions -} from './migrations/mod.ts'; -import { Bolt } from './bolt.ts'; -import { MongoClient, parseArgs } from './_deps.ts'; -import { config } from './utils/mod.ts'; + versions, + config, + define_config, + log_error +} from './utils/mod.ts'; function log(text: string, color?: string, type?: 'error' | 'log') { console[type || 'log'](`%c${text}`, `color: ${color || 'white'}`); @@ -17,51 +20,64 @@ const f = parseArgs(Deno.args, { }); if (f.version) { - console.log('0.5.8'); + log('0.6.0'); Deno.exit(); } if (!f.run && !f.migrations) { - log('bolt v0.5.8 - cross-platform bot connecting communities', 'blue'); - log('Usage: bolt [options]', 'purple'); + log('lightning v0.6.0 - cross-platform bot connecting communities', 'blue'); + log('Usage: lightning [options]', 'purple'); log('Options:', 'green'); log('--help: show this'); log('--version: shows version'); log('--config : absolute path to config file'); - log('--run: run an of bolt using the settings in config.ts'); + log('--run: run an of lightning using the settings in config.ts'); log('--migrations: start interactive tool to migrate databases'); Deno.exit(); } try { - const cfg = (await import(f.config || `${Deno.cwd()}/config.ts`))?.default; - if (f.run) await Bolt.setup(cfg); - if (f.migrations) await migrations(cfg); + if (!Deno) throw new Error('not running on deno, exiting...'); + + const cfg = define_config( + (await import(f.config || `${Deno.cwd()}/config.ts`))?.default + ); + + Deno.env.set('LIGHTNING_ERROR_HOOK', cfg.errorURL || ''); + + const mongo = new MongoClient(); + await mongo.connect(cfg.mongo_uri); + + const redis = await Deno.connect({ + hostname: cfg.redis_host, + port: cfg.redis_port || 6379 + }); + + if (f.run) { + new lightning(cfg, mongo, redis); + } else if (f.migrations) { + await migrations(cfg, mongo); + } } catch (e) { - log('Something went wrong, exiting..', 'red', 'error'); - console.error(e); + await log_error(e); Deno.exit(1); } -async function migrations(cfg: config) { +async function migrations(cfg: config, mongo: MongoClient) { log(`Available versions are: ${Object.values(versions).join(', ')}`, 'blue'); const from = prompt('what version is the DB currently set up for?'); - const to = prompt('what version of bolt do you want to move to?'); + const to = prompt('what version of lightning do you want to move to?'); const is_invalid = (val: string) => !(Object.values(versions) as string[]).includes(val); - if (!from || !to || is_invalid(from) || is_invalid(to)) Deno.exit(1); + if (!from || !to || is_invalid(from) || is_invalid(to)) return Deno.exit(1); - const migrationlist = get_migrations(from, to); + const migrationlist = get_migrations(from as versions, to as versions); if (migrationlist.length < 1) Deno.exit(); - const mongo = new MongoClient(); - - await mongo.connect(cfg.mongo_uri); - const database = mongo.database(cfg.mongo_database); log('Migrating your data..', 'blue'); @@ -91,4 +107,6 @@ async function migrations(cfg: config) { ); log('Wrote data to the DB', 'green'); + + Deno.exit(); } diff --git a/packages/lightning/deno.jsonc b/packages/lightning/deno.jsonc new file mode 100644 index 0000000..b399064 --- /dev/null +++ b/packages/lightning/deno.jsonc @@ -0,0 +1,26 @@ +{ + "name": "@jersey/lightning", + "version": "0.6.0", + "imports": { + "assert_eq": "jsr:@std/assert@^0.219.1/assert_equals", + "event": "jsr:@denosaurs/event@^2.0.2", + "mongo": "jsr:@db/mongo@^0.33.0", + "r2d2": "jsr:@iuioiua/r2d2@2.1.1", + "std_args": "jsr:@std/cli@^0.219.1/parse_args" + }, + "exports": { + ".": "./mod.ts", + "./utils": "./utils/mod.ts" + }, + "publish": { + "exclude": ["./_testdata.ts", "./_tests.ts"] + }, + "test": { + "include": ["./_tests.ts"] + }, + "lint": { + "exclude": ["./_testdata.ts", "./_tests.ts"] + }, + "lock": false, + "unstable": ["temporal"] +} diff --git a/packages/lightning/lightning.ts b/packages/lightning/lightning.ts new file mode 100644 index 0000000..8be0771 --- /dev/null +++ b/packages/lightning/lightning.ts @@ -0,0 +1,61 @@ +import { EventEmitter } from 'event'; +import { MongoClient } from 'mongo'; +import { RedisClient } from 'r2d2'; +import { bridges } from './bridges/mod.ts'; +import { + commands, + plugin, + config, + create_plugin, + log_error, + plugin_events +} from './utils/mod.ts'; + +/** an instance of lightning */ +export class lightning extends EventEmitter { + bridge: bridges; + /** a command handler */ + cmds: commands = new commands(); + /** the config used */ + config: config; + /** a mongo client */ + mongo: MongoClient; + /** a redis client */ + redis: RedisClient; + /** the plugins loaded */ + plugins: Map> = new Map>(); + + /** setup an instance with the given config, mongo instance, and redis connection */ + constructor(config: config, mongo: MongoClient, redis_conn: Deno.TcpConn) { + super(); + this.config = config; + this.mongo = mongo; + this.redis = new RedisClient(redis_conn); + this.bridge = new bridges(this); + this.cmds.listen(this); + this.load(this.config.plugins); + } + + /** load plugins */ + async load(plugins: create_plugin>[]) { + for (const { type, config } of plugins) { + const plugin = new type(this, config); + if (!plugin.support.includes('0.5.5')) { + throw ( + await log_error( + new Error( + `plugin '${plugin.name}' doesn't support this version of lightning` + ) + ) + ).e; + } else { + this.plugins.set(plugin.name, plugin); + (async () => { + for await (const event of plugin) { + this.emit(event.name, ...event.value); + } + })(); + } + } + } +} diff --git a/packages/lightning/mod.ts b/packages/lightning/mod.ts new file mode 100644 index 0000000..cdb8228 --- /dev/null +++ b/packages/lightning/mod.ts @@ -0,0 +1,19 @@ +/** + * @module + */ + +export { bridges } from './bridges/mod.ts'; +export { + type bridge_document, + type bridge_platform, + type bridge_settings +} from './bridges/types.ts'; +export * from './utils/mod.ts'; +export { + lightning, + /** + * TODO: remove in 0.7.0 + * @deprecated will be removed in 0.7.0 + */ + lightning as Bolt +} from './lightning.ts'; diff --git a/packages/lightning/utils/_fourbetafive.ts b/packages/lightning/utils/_fourbetafive.ts new file mode 100644 index 0000000..88378e5 --- /dev/null +++ b/packages/lightning/utils/_fourbetafive.ts @@ -0,0 +1,38 @@ +import { Document } from 'mongo'; +import { versions } from './migrations.ts'; + +type doc = { + _id: string; + value: { + bridges: { platform: string; channel: string; senddata: unknown }[]; + }; +}; + +export default { + from: '0.4-beta' as versions, + to: '0.5' as versions, + from_db: 'bridgev1', + to_db: 'bridges', + translate: (itemslist: (doc | Document)[]) => + (itemslist as doc[]).flatMap(({ _id, value }) => { + if (_id.startsWith('message-')) return []; + return [ + { + _id, + platforms: value.bridges.map(({ platform, channel, senddata }) => ({ + plugin: map_plugins(platform), + channel, + senddata + })) + } + ]; + }) as Document[] +}; + +function map_plugins(pluginname: string): string { + // the use of bolt is intentional + if (pluginname === 'discord') return 'bolt-discord'; + if (pluginname === 'guilded') return 'bolt-guilded'; + if (pluginname === 'revolt') return 'bolt-revolt'; + return 'unknown'; +} diff --git a/packages/lightning/utils/_fourfourbeta.ts b/packages/lightning/utils/_fourfourbeta.ts new file mode 100644 index 0000000..690810e --- /dev/null +++ b/packages/lightning/utils/_fourfourbeta.ts @@ -0,0 +1,63 @@ +import { Document } from 'mongo'; +import { versions } from './migrations.ts'; + +export default { + from: '0.4' as versions, + to: '0.4-beta' as versions, + from_db: 'bridge', + to_db: 'bridgev1', + translate: ( + items: (Document | { _id: string; value: string | unknown })[] + ): Document[] => { + const obj = {} as { + [key: string]: { + platform: string; + channel: string; + senddata: unknown; + }[]; + }; + + for (const item of items) { + const [platform, ...join] = item._id.split('-'); + const name = join.join('-'); + if (is_channel(name)) continue; + const _id = items.find( + i => i._id.startsWith(platform) && i.value === name + )?._id; + if (!_id) continue; + if (!obj[name]) obj[name] = []; + obj[name].push({ + platform, + channel: _id.split('-').slice(1).join('-'), + senddata: item.value + }); + } + + return Object.entries(obj) + .filter(([key, value]) => !is_channel(key) && value.length >= 2) + .map(([key, value]) => ({ + _id: key, + value: { bridges: value } + })); + } +}; + +function is_channel(channel: string): boolean { + if ( + channel.match( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i + ) + ) { + return true; + } + if (channel.match(/[0-7][0-9A-HJKMNP-TV-Z]{25}/gm)) return true; + if (!isNaN(Number(channel))) return true; + if ( + channel.startsWith('discord-') || + channel.startsWith('guilded-') || + channel.startsWith('revolt-') + ) { + return true; + } + return false; +} diff --git a/packages/lightning/utils/commands.ts b/packages/lightning/utils/commands.ts new file mode 100644 index 0000000..66ff97f --- /dev/null +++ b/packages/lightning/utils/commands.ts @@ -0,0 +1,135 @@ +import { lightning } from '../lightning.ts'; +import { log_error } from './errors.ts'; +import { parseArgs } from 'std_args'; +import { create_message, message } from './messages.ts'; + +/** + * commands implements simple command handling for lightning that others may find useful + */ +export class commands extends Map { + // TODO: make this configurable + prefix = '!bolt'; + + /** + * creates a command handler instance with the given commands + * @param default_cmds - the commands to use by default, should include help as a fallback command + */ + constructor(default_cmds: [string, command][] = default_commands) { + super(default_cmds); + } + + /** + * listen for commands on the given lightning instance + */ + listen(l: lightning) { + l.on('create_nonbridged_message', msg => { + if (msg.content?.startsWith(this.prefix)) { + const args = parseArgs(msg.content.split(' ')); + args._.shift(); + this.run({ + channel: msg.channel, + cmd: args._.shift() as string, + subcmd: args._.shift() as string, + opts: args as Record, + platform: msg.platform.name, + timestamp: msg.timestamp, + replyfn: msg.reply + }); + } + }); + + l.on('create_command', async cmd => { + await this.run(cmd); + }); + } + + /** + * run a command given the options that would be passed to it + */ + async run(opts: Omit) { + let reply; + try { + const cmd = this.get(opts.cmd) || this.get('help')!; + const execute = + cmd.options?.subcommands && opts.subcmd + ? cmd.options.subcommands.find(i => i.name === opts.subcmd) + ?.execute || cmd.execute + : cmd.execute; + reply = await execute({ ...opts, commands: this }); + } catch (e) { + reply = (await log_error(e, { ...opts, reply: undefined })).message; + } + try { + await opts.replyfn(reply, false); + } catch (e) { + await log_error(e, { ...opts, reply: undefined }); + } + } +} + +// TODO: remove in 0.7.0 and make its own package + +const default_commands: [string, command][] = [ + [ + 'help', + { + name: 'help', + description: 'get help', + execute: () => + create_message( + 'check out [the docs](https://williamhorning.dev/bolt/) for help.' + ) + } + ], + [ + 'version', + { + name: 'version', + description: "get lightning's version", + execute: () => create_message('hello from lightning (bolt) v0.6.0!') + } + ], + [ + 'ping', + { + name: 'ping', + description: 'pong', + execute: ({ timestamp }) => + create_message( + `Pong! 🏓 ${Temporal.Now.instant() + .since(timestamp) + .total('milliseconds')}ms` + ) + } + ] +]; + +export interface command_arguments { + channel: string; + cmd: string; + opts: Record; + platform: string; + replyfn: message['reply']; + subcmd?: string; + timestamp: Temporal.Instant; + commands: commands; +} + +export interface command { + /** the name of the command */ + name: string; + /** an optional description */ + description?: string; + 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[]; + }; + /** a function that returns a message */ + execute: ( + options: command_arguments + ) => Promise> | message; +} diff --git a/packages/lightning/utils/config.ts b/packages/lightning/utils/config.ts new file mode 100644 index 0000000..1dab495 --- /dev/null +++ b/packages/lightning/utils/config.ts @@ -0,0 +1,29 @@ +import { create_plugin } from './plugins.ts'; + +/** a function that returns a config object when given a partial config object */ +export function define_config(config?: Partial): config { + return { + plugins: [], + mongo_uri: 'mongodb://localhost:27017', + mongo_database: 'lightning', + redis_host: 'localhost', + redis_port: 6379, + ...(config || {}) + }; +} + +export interface config { + /** a list of plugins */ + // deno-lint-ignore no-explicit-any + plugins: create_plugin[]; + /** the URI that points to your instance of mongodb */ + mongo_uri: string; + /** the database to use */ + mongo_database: string; + /** 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; +} diff --git a/packages/lightning/utils/errors.ts b/packages/lightning/utils/errors.ts new file mode 100644 index 0000000..5867722 --- /dev/null +++ b/packages/lightning/utils/errors.ts @@ -0,0 +1,71 @@ +import { create_message, message } from './messages.ts'; + +/** + * 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 + * @param _id a function that returns a unique id (used for testing) + */ +export async function log_error( + e: Error, + extra: Record = {}, + _id: () => string = crypto.randomUUID +): Promise<{ + e: Error; + extra: Record; + uuid: string; + message: message; +}> { + const uuid = _id(); + const error_hook = Deno.env.get('LIGHTNING_ERROR_HOOK') || ''; + + if (error_hook !== '') { + delete extra.msg; + + await ( + await fetch(error_hook, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + embeds: [ + { + title: e.message, + description: `\`\`\`js\n${e.stack}\n${JSON.stringify( + { ...extra, uuid }, + r(), + 2 + )}\`\`\`` + } + ] + }) + }) + ).text(); + } + + console.error(`%cLightning Error ${uuid}`, 'color: red', e, extra); + + return { + e, + uuid, + extra, + message: create_message( + `Something went wrong! [Look here](https://williamhorning.dev/bolt) for help.\n\`\`\`\n${e.message}\n${uuid}\n\`\`\`` + ) + }; +} + +function r() { + const seen = new WeakSet(); + return (_: string, value: unknown) => { + if (typeof value === 'object' && value !== null) { + if (seen.has(value)) { + return '[Circular]'; + } + seen.add(value); + } + if (typeof value === 'bigint') { + return value.toString(); + } + return value; + }; +} diff --git a/packages/lightning/utils/messages.ts b/packages/lightning/utils/messages.ts new file mode 100644 index 0000000..9beb69d --- /dev/null +++ b/packages/lightning/utils/messages.ts @@ -0,0 +1,112 @@ +/** + * 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: { + // TODO: make this configurable + username: 'Bolt', + profile: + 'https://cdn.discordapp.com/icons/1011741670510968862/2d4ce9ff3f384c027d8781fa16a38b07.png?size=1024', + rawname: 'lightning', + id: 'lightning' + }, + content: text, + channel: '', + id: '', + reply: async () => {}, + timestamp: Temporal.Now.instant(), + platform: { + name: 'lightning', + message: undefined + } + }; + return data; +} + +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 + * @deprecated will be removed in 0.7.0 + */ + size: number; +} + +export interface platform { + /** the name of a plugin */ + name: string; + /** the platforms representation of a message */ + message: t; + /** the webhook the message was sent with */ + webhookid?: string; +} + +export interface embed_media { + height?: number; + url: string; + width?: number; +} + +/** a discord-style embed */ +export interface embed { + author?: { name: string; url?: string; icon_url?: string }; + color?: number; + description?: string; + fields?: { name: string; value: string; inline?: boolean }[]; + footer?: { text: string; icon_url?: string }; + image?: embed_media; + thumbnail?: embed_media; + timestamp?: number; + title?: string; + url?: string; + video?: Omit & { url?: string }; +} + +export interface message extends deleted_message { + attachments?: attachment[]; + 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 on their platform */ + 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 */ + replytoid?: string; +} + +export interface deleted_message { + /** the message's id */ + id: string; + /** the channel the message was sent in */ + channel: string; + /** the platform the message was sent on */ + platform: platform; + /** + * the time the message was sent/edited as a temporal instant + * @see https://tc39.es/proposal-temporal/docs/instant.html + */ + timestamp: Temporal.Instant; +} diff --git a/packages/lightning/utils/migrations.ts b/packages/lightning/utils/migrations.ts new file mode 100644 index 0000000..ce5f435 --- /dev/null +++ b/packages/lightning/utils/migrations.ts @@ -0,0 +1,44 @@ +import { Document } from 'mongo'; +import fourfourbeta from './_fourfourbeta.ts'; +import fourbetafive from './_fourfourbeta.ts'; + +/** the type of a migration */ +export interface migration { + /** the version to translate from */ + from: versions; + /** the version to translate to */ + to: versions; + /** the database to translate from */ + from_db: string; + /** the database to translate to */ + to_db: string; + /** translate a document from one version to another */ + translate: (data: Document[]) => Document[]; +} + +/** all of the versions with migrations to/from them */ +export enum versions { + /** all versions below 0.5 */ + Four = '0.4', + /** versions after commit 7de1cf2 but below 0.5 */ + FourBeta = '0.4-beta', + /** versions 0.5 and above */ + Five = '0.5' +} + +const migrations: migration[] = [fourbetafive, fourfourbeta]; + +/** get migrations that can then be applied using apply_migrations */ +export function get_migrations(from: versions, to: versions): migration[] { + const indexoffrom = migrations.findIndex(i => i.from === from); + const indexofto = migrations.findLastIndex(i => i.to === to); + return migrations.slice(indexoffrom, indexofto); +} + +/** apply many migrations given mongodb documents */ +export function apply_migrations( + migrations: migration[], + data: Document[] +): Document[] { + return migrations.reduce((acc, migration) => migration.translate(acc), data); +} diff --git a/packages/lightning/utils/mod.ts b/packages/lightning/utils/mod.ts new file mode 100644 index 0000000..ed5c4da --- /dev/null +++ b/packages/lightning/utils/mod.ts @@ -0,0 +1,24 @@ +/** + * Various utilities for lightning + * @module + */ + +export { commands, type command, type command_arguments } from './commands.ts'; +export { type config, define_config } from './config.ts'; +export { log_error } from './errors.ts'; +export { + create_message, + type deleted_message, + type embed, + type embed_media, + type message, + type platform, + type attachment +} from './messages.ts'; +export { + apply_migrations, + get_migrations, + type migration, + versions +} from './migrations.ts'; +export { plugin, type create_plugin, type plugin_events } from './plugins.ts'; diff --git a/packages/lightning/utils/plugins.ts b/packages/lightning/utils/plugins.ts new file mode 100644 index 0000000..007f73b --- /dev/null +++ b/packages/lightning/utils/plugins.ts @@ -0,0 +1,86 @@ +import { EventEmitter } from 'event'; +import { lightning } from '../lightning.ts'; +import { bridge_platform } from '../bridges/types.ts'; +import { message, deleted_message } from './messages.ts'; +import { command_arguments } from './commands.ts'; + +/** + * a plugin for lightning + */ +export abstract class plugin extends EventEmitter { + /** + * access the instance of lightning you're connected to + * @deprecated use `l` instead, will be removed in 0.7.0 + */ + bolt: lightning; + /** access the instance of lightning you're connected to */ + lightning: lightning; + /** access the config passed to you by lightning */ + config: cfg; + + /** the name of your plugin */ + abstract name: string; + /** the version of your plugin */ + abstract version: string; + /** a list of major versions supported by your plugin, should include 0.5.5 */ + abstract support: 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 }; + } + constructor(l: lightning, config: cfg) { + super(); + this.bolt = l; + 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; + + /** this is used to check whether or not a message is bridged, return query if you don't know for sure */ + abstract is_bridged(message: deleted_message): boolean | 'query'; + + /** this is used to bridge a NEW message */ + abstract create_message( + message: message, + bridge: bridge_platform + ): Promise; + + /** this is used to bridge an EDITED message */ + abstract edit_message( + new_message: message, + bridge: bridge_platform & { id: string } + ): Promise; + + /** this is used to bridge a DELETED message */ + abstract delete_message( + message: deleted_message, + bridge: bridge_platform & { id: string } + ): Promise; +} + +export type plugin_events = { + /** when a message is created */ + create_message: [message]; + /** when a command is run (not a text command) */ + create_command: [Omit]; + /** 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 */ + delete_message: [deleted_message]; + /** when your plugin is ready */ + ready: []; +}; + +/** the constructor for a plugin */ +export interface create_plugin> { + type: new (l: lightning, config: T['config']) => T; + config: T['config']; +}