Skip to content

Commit

Permalink
0.6 stuff (#58)
Browse files Browse the repository at this point in the history
* move to JSR dependencies
* move to r2d2 from deno_redis and use redis json (fix #57)
* redo a lot of the core to move stuff (fix #38) 
* clean up and document public api
* change stuff to lightning from bolt
* redo plugin config
* publish to JSR

---------

Signed-off-by: Jersey <wgyt735yt@gmail.com>
williamhorning authored Mar 26, 2024
1 parent edd5945 commit 4537e02
Showing 53 changed files with 1,174 additions and 1,069 deletions.
1 change: 0 additions & 1 deletion .github/security.md

This file was deleted.

30 changes: 23 additions & 7 deletions .github/workflows/docker.yml → .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -9,29 +9,45 @@ 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
uses: docker/login-action@v1
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: .
2 changes: 1 addition & 1 deletion docker-compose.example.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
version: '2'

services:
bolt:
lightning:
build: .
volumes:
- ./config/data:/app/data
15 changes: 8 additions & 7 deletions dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
6 changes: 3 additions & 3 deletions packages/bolt-discord/_deps.ts
Original file line number Diff line number Diff line change
@@ -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';
10 changes: 5 additions & 5 deletions packages/bolt-discord/commands.ts
Original file line number Diff line number Diff line change
@@ -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
};
});
14 changes: 7 additions & 7 deletions packages/bolt-discord/mod.ts
Original file line number Diff line number Diff line change
@@ -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<discord_config> {
export class discord_plugin extends plugin<discord_config> {
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<discord_config> {
});
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();
}

6 changes: 3 additions & 3 deletions packages/bolt-guilded/_deps.ts
Original file line number Diff line number Diff line change
@@ -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';
18 changes: 10 additions & 8 deletions packages/bolt-guilded/legacybridging.ts
Original file line number Diff line number Diff line change
@@ -25,23 +25,25 @@ 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.`
)
)
);
}
}
}

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);
}
}
}
17 changes: 9 additions & 8 deletions packages/bolt-guilded/mod.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import {
Bolt,
lightning,
Client,
WebhookPayload,
WebhookClient,
bolt_plugin,
plugin,
bridge_platform,
deleted_message,
message
} from './_deps.ts';
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
})
}
6 changes: 3 additions & 3 deletions packages/bolt-revolt/deps.ts
Original file line number Diff line number Diff line change
@@ -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';
12 changes: 6 additions & 6 deletions packages/bolt-revolt/mod.ts
Original file line number Diff line number Diff line change
@@ -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;
9 changes: 0 additions & 9 deletions packages/bolt/_deps.ts

This file was deleted.

81 changes: 0 additions & 81 deletions packages/bolt/bolt.ts

This file was deleted.

10 changes: 0 additions & 10 deletions packages/bolt/bridges/_deps.ts

This file was deleted.

200 changes: 0 additions & 200 deletions packages/bolt/bridges/mod.ts

This file was deleted.

15 changes: 0 additions & 15 deletions packages/bolt/bridges/types.ts

This file was deleted.

40 changes: 0 additions & 40 deletions packages/bolt/cmds/_default.ts

This file was deleted.

3 changes: 0 additions & 3 deletions packages/bolt/cmds/_deps.ts

This file was deleted.

56 changes: 0 additions & 56 deletions packages/bolt/cmds/mod.ts

This file was deleted.

25 changes: 0 additions & 25 deletions packages/bolt/cmds/types.ts

This file was deleted.

14 changes: 0 additions & 14 deletions packages/bolt/deno.jsonc

This file was deleted.

26 changes: 0 additions & 26 deletions packages/bolt/migrations/_utils.ts

This file was deleted.

40 changes: 0 additions & 40 deletions packages/bolt/migrations/fourbetatofive.ts

This file was deleted.

53 changes: 0 additions & 53 deletions packages/bolt/migrations/fourtofourbeta.ts

This file was deleted.

26 changes: 0 additions & 26 deletions packages/bolt/migrations/mod.ts

This file was deleted.

21 changes: 0 additions & 21 deletions packages/bolt/mod.ts

This file was deleted.

5 changes: 0 additions & 5 deletions packages/bolt/utils/_deps.ts

This file was deleted.

22 changes: 0 additions & 22 deletions packages/bolt/utils/config.ts

This file was deleted.

77 changes: 0 additions & 77 deletions packages/bolt/utils/errors.ts

This file was deleted.

84 changes: 0 additions & 84 deletions packages/bolt/utils/messages.ts

This file was deleted.

4 changes: 0 additions & 4 deletions packages/bolt/utils/mod.ts

This file was deleted.

65 changes: 0 additions & 65 deletions packages/bolt/utils/plugins.ts

This file was deleted.

37 changes: 17 additions & 20 deletions packages/bolt/_testdata.ts → packages/lightning/_testdata.ts
Original file line number Diff line number Diff line change
@@ -3,19 +3,18 @@ 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: '',
id: '',
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,27 +90,26 @@ 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: '',
id: '',
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,28 +127,27 @@ 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'
}
}
};

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'
52 changes: 20 additions & 32 deletions packages/bolt/_tests.ts → packages/lightning/_tests.ts
Original file line number Diff line number Diff line change
@@ -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<unknown>) => 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;

Original file line number Diff line number Diff line change
@@ -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,31 +45,30 @@ 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
)
});

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}`
})
)
};
}
Original file line number Diff line number Diff line change
@@ -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
}
]
}
144 changes: 144 additions & 0 deletions packages/lightning/bridges/_internal.ts
Original file line number Diff line number Diff line change
@@ -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<string, boolean>();

constructor(bridge: bridges, l: lightning) {
this.l = l;
this.bridges = bridge;
}

is_bridged_internal(msg: deleted_message<unknown>): boolean {
return Boolean(this.bridged_message_id_map.get(msg.id));
}

async handle_message(
msg: message<unknown> | deleted_message<unknown>,
action: 'create_message' | 'edit_message' | 'delete_message'
): Promise<void> {
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<unknown>,
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<unknown> | deleted_message<unknown>,
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<unknown> | deleted_message<unknown>,
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<unknown>;
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<unknown>;
platform: bridge_platform & { id: string };
};
}
}
86 changes: 86 additions & 0 deletions packages/lightning/bridges/mod.ts
Original file line number Diff line number Diff line change
@@ -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<bridge_document>;
/** 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<bridge_platform[] | null> {
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<unknown>): 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<bridge_document | undefined> {
const query = {} as Record<string, string>;

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<void> {
await this.bridge_collection.replaceOne({ _id: bridge._id }, bridge, {
upsert: true
});
}
}
29 changes: 29 additions & 0 deletions packages/lightning/bridges/types.ts
Original file line number Diff line number Diff line change
@@ -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;
}
62 changes: 40 additions & 22 deletions packages/bolt/cli.ts → packages/lightning/cli.ts
Original file line number Diff line number Diff line change
@@ -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 <string>: 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();
}
26 changes: 26 additions & 0 deletions packages/lightning/deno.jsonc
Original file line number Diff line number Diff line change
@@ -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"]
}
61 changes: 61 additions & 0 deletions packages/lightning/lightning.ts
Original file line number Diff line number Diff line change
@@ -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<plugin_events> {
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<string, plugin<unknown>> = new Map<string, plugin<unknown>>();

/** 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<plugin<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 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);
}
})();
}
}
}
}
19 changes: 19 additions & 0 deletions packages/lightning/mod.ts
Original file line number Diff line number Diff line change
@@ -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';
38 changes: 38 additions & 0 deletions packages/lightning/utils/_fourbetafive.ts
Original file line number Diff line number Diff line change
@@ -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';
}
63 changes: 63 additions & 0 deletions packages/lightning/utils/_fourfourbeta.ts
Original file line number Diff line number Diff line change
@@ -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;
}
135 changes: 135 additions & 0 deletions packages/lightning/utils/commands.ts
Original file line number Diff line number Diff line change
@@ -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<string, command> {
// 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<string, string>,
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<command_arguments, 'commands'>) {
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<string, string>;
platform: string;
replyfn: message<unknown>['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<unknown>> | message<unknown>;
}
29 changes: 29 additions & 0 deletions packages/lightning/utils/config.ts
Original file line number Diff line number Diff line change
@@ -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>): 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<any>[];
/** 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;
}
71 changes: 71 additions & 0 deletions packages/lightning/utils/errors.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown> = {},
_id: () => string = crypto.randomUUID
): Promise<{
e: Error;
extra: Record<string, unknown>;
uuid: string;
message: message<unknown>;
}> {
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;
};
}
112 changes: 112 additions & 0 deletions packages/lightning/utils/messages.ts
Original file line number Diff line number Diff line change
@@ -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<undefined> {
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<t> {
/** 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<embed_media, 'url'> & { url?: string };
}

export interface message<t> extends deleted_message<t> {
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<unknown>, optional?: unknown) => Promise<void>;
/** the id of the message replied to */
replytoid?: string;
}

export interface deleted_message<t> {
/** the message's id */
id: string;
/** the channel the message was sent in */
channel: string;
/** the platform the message was sent on */
platform: platform<t>;
/**
* the time the message was sent/edited as a temporal instant
* @see https://tc39.es/proposal-temporal/docs/instant.html
*/
timestamp: Temporal.Instant;
}
44 changes: 44 additions & 0 deletions packages/lightning/utils/migrations.ts
Original file line number Diff line number Diff line change
@@ -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);
}
24 changes: 24 additions & 0 deletions packages/lightning/utils/mod.ts
Original file line number Diff line number Diff line change
@@ -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';
86 changes: 86 additions & 0 deletions packages/lightning/utils/plugins.ts
Original file line number Diff line number Diff line change
@@ -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<cfg> extends EventEmitter<plugin_events> {
/**
* 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<T extends plugin<unknown>>(
this: new (l: lightning, config: T['config']) => T,
config: T['config']
): create_plugin<T> {
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<unknown>;

/** 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<unknown>): boolean | 'query';

/** this is used to bridge a NEW message */
abstract create_message(
message: message<unknown>,
bridge: bridge_platform
): Promise<bridge_platform>;

/** this is used to bridge an EDITED message */
abstract edit_message(
new_message: message<unknown>,
bridge: bridge_platform & { id: string }
): Promise<bridge_platform>;

/** this is used to bridge a DELETED message */
abstract delete_message(
message: deleted_message<unknown>,
bridge: bridge_platform & { id: string }
): Promise<bridge_platform>;
}

export type plugin_events = {
/** when a message is created */
create_message: [message<unknown>];
/** when a command is run (not a text command) */
create_command: [Omit<command_arguments, 'commands'>];
/** when a message isn't already bridged (don't emit outside of core) */
create_nonbridged_message: [message<unknown>];
/** when a message is edited */
edit_message: [message<unknown>];
/** when a message is deleted */
delete_message: [deleted_message<unknown>];
/** when your plugin is ready */
ready: [];
};

/** the constructor for a plugin */
export interface create_plugin<T extends plugin<T['config']>> {
type: new (l: lightning, config: T['config']) => T;
config: T['config'];
}

0 comments on commit 4537e02

Please sign in to comment.