diff --git a/bots/discord/.drizzle/0000_schema.sql b/bots/discord/.drizzle/0000_schema.sql new file mode 100644 index 0000000..5b7a432 --- /dev/null +++ b/bots/discord/.drizzle/0000_schema.sql @@ -0,0 +1,18 @@ +CREATE TABLE `applied_presets` ( + `member` text NOT NULL, + `guild` text NOT NULL, + `roles` text DEFAULT '[]' NOT NULL, + `preset` text NOT NULL, + `until` integer +); +--> statement-breakpoint +CREATE UNIQUE INDEX `unique_composite` ON `applied_presets` (`member`,`preset`,`guild`);--> statement-breakpoint +CREATE TABLE `responses` ( + `reply` text PRIMARY KEY NOT NULL, + `channel` text NOT NULL, + `guild` text NOT NULL, + `ref` text NOT NULL, + `label` text NOT NULL, + `text` text NOT NULL, + `by` text +); diff --git a/bots/discord/.drizzle/0001_schema.sql b/bots/discord/.drizzle/0001_schema.sql new file mode 100644 index 0000000..b9d9b1f --- /dev/null +++ b/bots/discord/.drizzle/0001_schema.sql @@ -0,0 +1,12 @@ +CREATE TABLE `reminders` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `creator` text NOT NULL, + `target` text NOT NULL, + `guild` text NOT NULL, + `channel` text NOT NULL, + `message` text NOT NULL, + `created_at` integer NOT NULL, + `remind_at` integer NOT NULL, + `interval_seconds` integer NOT NULL, + `count` integer DEFAULT 0 NOT NULL +); diff --git a/bots/discord/.drizzle/0002_schema.sql b/bots/discord/.drizzle/0002_schema.sql new file mode 100644 index 0000000..1e3e8be --- /dev/null +++ b/bots/discord/.drizzle/0002_schema.sql @@ -0,0 +1,2 @@ +CREATE UNIQUE INDEX `reminders_remind_at_idx` ON `reminders` (`remind_at`);--> statement-breakpoint +CREATE UNIQUE INDEX `reminders_creator_guild_idx` ON `reminders` (`creator`,`guild`); \ No newline at end of file diff --git a/bots/discord/.drizzle/0003_schema.sql b/bots/discord/.drizzle/0003_schema.sql new file mode 100644 index 0000000..a8cc001 --- /dev/null +++ b/bots/discord/.drizzle/0003_schema.sql @@ -0,0 +1,4 @@ +DROP INDEX `reminders_remind_at_idx`;--> statement-breakpoint +DROP INDEX `reminders_creator_guild_idx`;--> statement-breakpoint +CREATE INDEX `reminders_remind_at_idx` ON `reminders` (`remind_at`);--> statement-breakpoint +CREATE INDEX `reminders_creator_guild_idx` ON `reminders` (`creator`,`guild`); \ No newline at end of file diff --git a/bots/discord/.drizzle/meta/0000_snapshot.json b/bots/discord/.drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..ea6460f --- /dev/null +++ b/bots/discord/.drizzle/meta/0000_snapshot.json @@ -0,0 +1,133 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "81c6e9da-4d03-4d2f-9934-1a6cf376dd6e", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "applied_presets": { + "name": "applied_presets", + "columns": { + "member": { + "name": "member", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "guild": { + "name": "guild", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "roles": { + "name": "roles", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "preset": { + "name": "preset", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "until": { + "name": "until", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "unique_composite": { + "name": "unique_composite", + "columns": [ + "member", + "preset", + "guild" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "responses": { + "name": "responses", + "columns": { + "reply": { + "name": "reply", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "guild": { + "name": "guild", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ref": { + "name": "ref", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "by": { + "name": "by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/bots/discord/.drizzle/meta/0001_snapshot.json b/bots/discord/.drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..bc1a1e7 --- /dev/null +++ b/bots/discord/.drizzle/meta/0001_snapshot.json @@ -0,0 +1,214 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "ea0c4e69-ffe0-45f8-81cc-aefc1c696751", + "prevId": "81c6e9da-4d03-4d2f-9934-1a6cf376dd6e", + "tables": { + "applied_presets": { + "name": "applied_presets", + "columns": { + "member": { + "name": "member", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "guild": { + "name": "guild", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "roles": { + "name": "roles", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "preset": { + "name": "preset", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "until": { + "name": "until", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "unique_composite": { + "name": "unique_composite", + "columns": [ + "member", + "preset", + "guild" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reminders": { + "name": "reminders", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "creator": { + "name": "creator", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target": { + "name": "target", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "guild": { + "name": "guild", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "remind_at": { + "name": "remind_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "responses": { + "name": "responses", + "columns": { + "reply": { + "name": "reply", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "guild": { + "name": "guild", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ref": { + "name": "ref", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "by": { + "name": "by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/bots/discord/.drizzle/meta/0002_snapshot.json b/bots/discord/.drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..b83835d --- /dev/null +++ b/bots/discord/.drizzle/meta/0002_snapshot.json @@ -0,0 +1,230 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "05335016-e3f2-40ac-96ae-ce775205f005", + "prevId": "ea0c4e69-ffe0-45f8-81cc-aefc1c696751", + "tables": { + "applied_presets": { + "name": "applied_presets", + "columns": { + "member": { + "name": "member", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "guild": { + "name": "guild", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "roles": { + "name": "roles", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "preset": { + "name": "preset", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "until": { + "name": "until", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "unique_composite": { + "name": "unique_composite", + "columns": [ + "member", + "preset", + "guild" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reminders": { + "name": "reminders", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "creator": { + "name": "creator", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target": { + "name": "target", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "guild": { + "name": "guild", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "remind_at": { + "name": "remind_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "reminders_remind_at_idx": { + "name": "reminders_remind_at_idx", + "columns": [ + "remind_at" + ], + "isUnique": true + }, + "reminders_creator_guild_idx": { + "name": "reminders_creator_guild_idx", + "columns": [ + "creator", + "guild" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "responses": { + "name": "responses", + "columns": { + "reply": { + "name": "reply", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "guild": { + "name": "guild", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ref": { + "name": "ref", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "by": { + "name": "by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/bots/discord/.drizzle/meta/0003_snapshot.json b/bots/discord/.drizzle/meta/0003_snapshot.json new file mode 100644 index 0000000..af49c5d --- /dev/null +++ b/bots/discord/.drizzle/meta/0003_snapshot.json @@ -0,0 +1,230 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "22c5d88c-ccb5-499c-adaa-199c09a74ca6", + "prevId": "05335016-e3f2-40ac-96ae-ce775205f005", + "tables": { + "applied_presets": { + "name": "applied_presets", + "columns": { + "member": { + "name": "member", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "guild": { + "name": "guild", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "roles": { + "name": "roles", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "preset": { + "name": "preset", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "until": { + "name": "until", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "unique_composite": { + "name": "unique_composite", + "columns": [ + "member", + "preset", + "guild" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reminders": { + "name": "reminders", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "creator": { + "name": "creator", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target": { + "name": "target", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "guild": { + "name": "guild", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "remind_at": { + "name": "remind_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "interval_seconds": { + "name": "interval_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "reminders_remind_at_idx": { + "name": "reminders_remind_at_idx", + "columns": [ + "remind_at" + ], + "isUnique": false + }, + "reminders_creator_guild_idx": { + "name": "reminders_creator_guild_idx", + "columns": [ + "creator", + "guild" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "responses": { + "name": "responses", + "columns": { + "reply": { + "name": "reply", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "guild": { + "name": "guild", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ref": { + "name": "ref", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "by": { + "name": "by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/bots/discord/.drizzle/meta/_journal.json b/bots/discord/.drizzle/meta/_journal.json new file mode 100644 index 0000000..49c5c98 --- /dev/null +++ b/bots/discord/.drizzle/meta/_journal.json @@ -0,0 +1,34 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1771768794695, + "tag": "0000_schema", + "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1771775341832, + "tag": "0001_schema", + "breakpoints": true + }, + { + "idx": 2, + "version": "6", + "when": 1771775646605, + "tag": "0002_schema", + "breakpoints": true + }, + { + "idx": 3, + "version": "6", + "when": 1772362642052, + "tag": "0003_schema", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/bots/discord/.gitignore b/bots/discord/.gitignore index 130f357..2e8d818 100644 --- a/bots/discord/.gitignore +++ b/bots/discord/.gitignore @@ -178,7 +178,6 @@ dist *.db *.sqlite *.sqlite3 -.drizzle # Auto-generated files src/commands/index.ts diff --git a/bots/discord/CHANGELOG.md b/bots/discord/CHANGELOG.md index 8ec7888..defa569 100644 --- a/bots/discord/CHANGELOG.md +++ b/bots/discord/CHANGELOG.md @@ -1,3 +1,34 @@ +# @revanced/discord-bot [1.6.0-dev.3](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.6.0-dev.2...@revanced/discord-bot@1.6.0-dev.3) (2026-03-01) + + +### Bug Fixes + +* **bots/discord/database/schemas:** index instead of unique index on reminders ([cafdbc0](https://github.com/revanced/revanced-bots/commit/cafdbc0c7bb43947afc7e7a262b2a0eb62791511)) +* **bots/discord:** add min max intervals in `remind` command ([1f1dd74](https://github.com/revanced/revanced-bots/commit/1f1dd7410204fb0b8ecddd655ee23fb107ec02cd)) +* **bots/discord:** require roles for using utility commands ([b519b56](https://github.com/revanced/revanced-bots/commit/b519b562f9bc1415bcd3a3cfa7bd73e18b2b21b2)) +* **bots/discord:** throw error if nothing inserted during adding reminders ([a94fd74](https://github.com/revanced/revanced-bots/commit/a94fd742a7f0b3cf8a7f31e716b88fe27f89f70b)) + +# @revanced/discord-bot [1.6.0-dev.2](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.6.0-dev.1...@revanced/discord-bot@1.6.0-dev.2) (2026-02-22) + + +### Bug Fixes + +* **bots/discord:** add missing database migration file ([f24a8fb](https://github.com/revanced/revanced-bots/commit/f24a8fbfdc968971c73715d0bf9768dec3341954)) +* **bots/discord:** off-by-one reminders query + log if query fails ([26680a9](https://github.com/revanced/revanced-bots/commit/26680a97fd15cc75250cd91a612132844567c54b)) + + +### Features + +* **bots/discord/database/schemas:** add indexes on reminders ([99c7422](https://github.com/revanced/revanced-bots/commit/99c74227c4185fa4d6731dac03230312d9b3106a)) + +# @revanced/discord-bot [1.6.0-dev.1](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.5.3...@revanced/discord-bot@1.6.0-dev.1) (2026-02-22) + + +### Features + +* **bots/discord:** add remind/unremind command ([#51](https://github.com/revanced/revanced-bots/issues/51)) ([18a119f](https://github.com/revanced/revanced-bots/commit/18a119fdad2af838e2d1fae5094ef9c5358e6d8c)) +* **bots/discord:** auto migrate database schema ([d34d3a5](https://github.com/revanced/revanced-bots/commit/d34d3a5abd5a87365bc1ca9ec2fe2f1fa8f8e58f)) + ## @revanced/discord-bot [1.5.3](https://github.com/revanced/revanced-bots/compare/@revanced/discord-bot@1.5.2...@revanced/discord-bot@1.5.3) (2026-01-17) diff --git a/bots/discord/config.js b/bots/discord/config.js index 1b04f24..ff5b829 100644 --- a/bots/discord/config.js +++ b/bots/discord/config.js @@ -35,6 +35,9 @@ export default { thread: 'THREAD_ID_HERE', }, }, + utilities: { + roles: ['ROLE_ID_HERE'], + }, rolePresets: { guilds: { GUILD_ID_HERE: { diff --git a/bots/discord/config.schema.ts b/bots/discord/config.schema.ts index 156d695..0eb456b 100644 --- a/bots/discord/config.schema.ts +++ b/bots/discord/config.schema.ts @@ -19,6 +19,9 @@ export type Config = { thread?: string } } + utilities?: { + roles?: string[] + } rolePresets?: { checkExpiredEvery: number guilds: Record> diff --git a/bots/discord/package.json b/bots/discord/package.json index db9f61e..ccde31f 100644 --- a/bots/discord/package.json +++ b/bots/discord/package.json @@ -2,7 +2,7 @@ "name": "@revanced/discord-bot", "type": "module", "private": true, - "version": "1.5.3", + "version": "1.6.0-dev.3", "description": "🤖 Discord bot assisting ReVanced", "main": "src/index.ts", "scripts": { diff --git a/bots/discord/scripts/build.ts b/bots/discord/scripts/build.ts index f382cfe..814575c 100644 --- a/bots/discord/scripts/build.ts +++ b/bots/discord/scripts/build.ts @@ -20,4 +20,3 @@ await cp('./config.js', './dist/config.js') logger.info('Copying database schema...') await cp('./.drizzle', './dist/.drizzle', { recursive: true }) -await rm('./.drizzle', { recursive: true }) diff --git a/bots/discord/src/commands/utilities/remind.ts b/bots/discord/src/commands/utilities/remind.ts new file mode 100644 index 0000000..bee3573 --- /dev/null +++ b/bots/discord/src/commands/utilities/remind.ts @@ -0,0 +1,137 @@ +import { EmbedBuilder, MessageFlags } from 'discord.js' +import { eq } from 'drizzle-orm' +import Command from '$/classes/Command' +import { config, database } from '$/context' +import { reminders } from '$/database/schemas' +import { applyCommonEmbedStyles } from '$/utils/discord/embeds' +import { durationToString, parseDuration } from '$/utils/duration' +import CommandError, { CommandErrorType } from '$/classes/CommandError' + +const MIN_DURATION = parseDuration('1m') +const MAX_DURATION = parseDuration('1y') + +export default new Command({ + name: 'remind', + description: 'Set a reminder or list your reminders', + type: Command.Type.ChatGuild, + requirements: { + roles: config.utilities?.roles, + }, + options: { + message: { + description: 'The reminder message', + required: false, + type: Command.OptionType.String, + maxLength: 1000, + }, + interval: { + description: 'When to remind (e.g., 1d, 2h30m, 1w). Default: 1 day. Min: 1 minute. Max: 1 year.', + required: false, + type: Command.OptionType.String, + }, + user: { + description: 'The user to remind (defaults to yourself)', + required: false, + type: Command.OptionType.User, + }, + }, + async execute({ logger }, interaction, { message, interval, user }) { + // If no message is provided, list all reminders + if (!message) { + const userReminders = await database.query.reminders.findMany({ + where: eq(reminders.creatorId, interaction.user.id), + }) + + if (userReminders.length === 0) { + const embed = applyCommonEmbedStyles( + new EmbedBuilder().setTitle('No Reminders').setDescription('You have no active reminders.'), + false, + true, + true, + ) + + await interaction.reply({ + embeds: [embed], + flags: MessageFlags.Ephemeral, + }) + return + } + + const reminderList = userReminders + .map(r => { + const targetStr = r.targetId === r.creatorId ? 'yourself' : `<@${r.targetId}>` + return ( + `**${r.id}.** ${r.message.substring(0, 50)}${r.message.length > 50 ? '...' : ''}\n` + + `-# For ${targetStr} • • Reminded ${r.count}x` + ) + }) + .join('\n\n') + + const embed = applyCommonEmbedStyles( + new EmbedBuilder().setTitle('Your Reminders').setDescription(reminderList), + false, + true, + true, + ) + + await interaction.reply({ + embeds: [embed], + flags: MessageFlags.Ephemeral, + }) + return + } + + // Create a new reminder + const targetUser = user ?? interaction.user + const durationMs = parseDuration(interval ?? '1d', 'd') + + if (durationMs < MIN_DURATION || durationMs > MAX_DURATION) + throw new CommandError(CommandErrorType.InvalidArgument, 'Interval must be between 1 minute and 1 year.') + + const now = Math.floor(Date.now() / 1000) + const remindAt = now + Math.floor(durationMs / 1000) + + const intervalSeconds = Math.floor(durationMs / 1000) + const [inserted] = await database + .insert(reminders) + .values({ + creatorId: interaction.user.id, + targetId: targetUser.id, + guildId: interaction.guildId!, + channelId: interaction.channelId, + message: message, + createdAt: now, + remindAt: remindAt, + intervalSeconds: intervalSeconds, + count: 0, + }) + .returning() + + const reminderId = inserted?.id + if (!reminderId) throw new CommandError(CommandErrorType.Generic, 'Failed to create reminder.') + + const targetStr = targetUser.id === interaction.user.id ? 'You' : targetUser.toString() + + const embed = applyCommonEmbedStyles( + new EmbedBuilder() + .setTitle('Reminder set') + .setDescription( + `${targetStr} will be reminded .\n\n` + + `**Message:** ${message}\n` + + `-# Reminder ID: ${reminderId}`, + ), + false, + true, + true, + ) + + await interaction.reply({ + embeds: [embed], + }) + + logger.info( + `User ${interaction.user.tag} (${interaction.user.id}) set reminder #${reminderId} ` + + `for ${targetUser.tag} (${targetUser.id}) in ${durationToString(durationMs)}`, + ) + }, +}) diff --git a/bots/discord/src/commands/utilities/unremind.ts b/bots/discord/src/commands/utilities/unremind.ts new file mode 100644 index 0000000..a30c022 --- /dev/null +++ b/bots/discord/src/commands/utilities/unremind.ts @@ -0,0 +1,62 @@ +import { EmbedBuilder, MessageFlags } from 'discord.js' +import { eq } from 'drizzle-orm' +import Command from '$/classes/Command' +import CommandError, { CommandErrorType } from '$/classes/CommandError' +import { config, database } from '$/context' +import { reminders } from '$/database/schemas' +import { applyCommonEmbedStyles } from '$/utils/discord/embeds' + +export default new Command({ + name: 'unremind', + description: 'Remove a reminder', + type: Command.Type.ChatGuild, + requirements: { + roles: config.utilities?.roles, + }, + options: { + id: { + description: 'The reminder ID to remove', + required: true, + type: Command.OptionType.Integer, + min: 1, + }, + }, + async execute({ logger }, interaction, { id }) { + const reminder = await database.query.reminders.findFirst({ + where: eq(reminders.id, id), + }) + + if (!reminder) { + throw new CommandError(CommandErrorType.InvalidArgument, `Reminder with ID **${id}** was not found.`) + } + + // Only the creator can remove the reminder + if (reminder.creatorId !== interaction.user.id) { + throw new CommandError( + CommandErrorType.RequirementsNotMet, + 'You can only remove reminders that you created.', + ) + } + + await database.delete(reminders).where(eq(reminders.id, id)) + + const embed = applyCommonEmbedStyles( + new EmbedBuilder() + .setTitle('Reminder removed') + .setDescription( + `Removed reminder **#${id}**.\n\n` + + `-# Message: ${reminder.message.substring(0, 100)}${reminder.message.length > 100 ? '...' : ''}`, + ), + false, + true, + true, + ) + + await interaction.reply({ + embeds: [embed], + flags: MessageFlags.Ephemeral, + }) + + logger.info(`User ${interaction.user.tag} (${interaction.user.id}) removed reminder #${id}`) + }, +}) diff --git a/bots/discord/src/context.ts b/bots/discord/src/context.ts index cfd7d07..445307d 100644 --- a/bots/discord/src/context.ts +++ b/bots/discord/src/context.ts @@ -3,7 +3,7 @@ import { Client as APIClient } from '@revanced/bot-api' import { createLogger } from '@revanced/bot-shared' import { Client as DiscordClient, type Message, Options, Partials } from 'discord.js' import { drizzle } from 'drizzle-orm/bun-sqlite' -import { existsSync, readdirSync, readFileSync } from 'fs' +import { migrate } from 'drizzle-orm/bun-sqlite/migrator' import { join } from 'path' import { __getConfig, config } from './config' import * as schemas from './database/schemas' @@ -30,37 +30,17 @@ export const api = { } const DatabasePath = process.env['DATABASE_PATH'] -const DatabaseSchemaDir = join(import.meta.dir, '..', '.drizzle') - -let dbSchemaFileName: string | undefined - -if (DatabasePath && !existsSync(DatabasePath)) { - logger.warn('Database file not found, trying to create from schema...') - - try { - const file = readdirSync(DatabaseSchemaDir, { withFileTypes: true }) - .filter(file => file.isFile() && file.name.endsWith('.sql')) - .sort() - .at(-1) - - if (!file) throw new Error('No schema file found') - - dbSchemaFileName = file.name - logger.debug(`Using schema file: ${dbSchemaFileName}`) - } catch (e) { - logger.fatal('Could not create database from schema, check if the schema file exists and is accessible') - logger.fatal(e) - process.exit(1) - } -} const db = new Database(DatabasePath, { readwrite: true, create: true }) -if (dbSchemaFileName) db.run(readFileSync(join(DatabaseSchemaDir, dbSchemaFileName)).toString()) -export const database = drizzle(db, { +const database = drizzle(db, { schema: schemas, }) +migrate(database, { migrationsFolder: join(import.meta.dir, '..', '.drizzle') }) + +export { database } + export const discord = { client: new DiscordClient({ intents: [ diff --git a/bots/discord/src/database/schemas.ts b/bots/discord/src/database/schemas.ts index 409c1f8..c679385 100644 --- a/bots/discord/src/database/schemas.ts +++ b/bots/discord/src/database/schemas.ts @@ -1,6 +1,28 @@ -import { integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core' +import { index, integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core' import type { InferSelectModel } from 'drizzle-orm' +export const reminders = sqliteTable( + 'reminders', + { + id: integer('id').primaryKey({ autoIncrement: true }), + creatorId: text('creator').notNull(), + targetId: text('target').notNull(), + guildId: text('guild').notNull(), + channelId: text('channel').notNull(), + message: text('message').notNull(), + createdAt: integer('created_at').notNull(), + remindAt: integer('remind_at').notNull(), + intervalSeconds: integer('interval_seconds').notNull(), + count: integer('count').notNull().default(0), + }, + table => [ + index('reminders_remind_at_idx').on(table.remindAt), + index('reminders_creator_guild_idx').on(table.creatorId, table.guildId), + ], +) + +export type Reminder = InferSelectModel + export const responses = sqliteTable('responses', { replyId: text('reply').primaryKey().notNull(), channelId: text('channel').notNull(), diff --git a/bots/discord/src/events/discord/ready/checkReminders.ts b/bots/discord/src/events/discord/ready/checkReminders.ts new file mode 100644 index 0000000..b5a47dc --- /dev/null +++ b/bots/discord/src/events/discord/ready/checkReminders.ts @@ -0,0 +1,78 @@ +import { type Client, EmbedBuilder } from 'discord.js' +import { eq, lte } from 'drizzle-orm' +import { database, logger } from '$/context' +import { reminders } from '$/database/schemas' +import { applyCommonEmbedStyles } from '$/utils/discord/embeds' +import { on, withContext } from '$/utils/discord/events' + +const REMINDER_CHECK_INTERVAL = 30_000 // Check every 30 seconds + +export default withContext(on, 'ready', async (_, client) => { + checkReminders(client).catch(e => logger.error('Error during initial reminder check:', e)) + setInterval( + () => checkReminders(client).catch(e => logger.error('Error in reminder check interval:', e)), + REMINDER_CHECK_INTERVAL, + ) +}) + +async function checkReminders(client: Client) { + logger.debug('Checking for due reminders...') + + const now = Math.floor(Date.now() / 1000) + const dueReminders = await database.query.reminders.findMany({ + where: lte(reminders.remindAt, now), + }) + + for (const reminder of dueReminders) { + try { + logger.debug(`Processing reminder #${reminder.id} for ${reminder.targetId}`) + + const guild = await client.guilds.fetch(reminder.guildId) + const channel = await guild.channels.fetch(reminder.channelId) + + if (!channel?.isTextBased()) { + logger.warn( + `Channel ${reminder.channelId} for reminder #${reminder.id} is not text-based or doesn't exist`, + ) + await database.delete(reminders).where(eq(reminders.id, reminder.id)) + continue + } + + const newCount = reminder.count + 1 + const creatorMention = reminder.creatorId === reminder.targetId ? '' : ` (set by <@${reminder.creatorId}>)` + + const embed = applyCommonEmbedStyles( + new EmbedBuilder() + .setTitle(`Reminder (#${newCount})`) + .setDescription(reminder.message) + .setFooter({ + text: `Set on ${new Date(reminder.createdAt * 1000).toLocaleDateString()}`, + }), + false, + false, + true, + ) + + await channel.send({ + content: `<@${reminder.targetId}>${creatorMention}`, + embeds: [embed], + }) + + logger.info( + `Sent reminder #${reminder.id} (count: ${newCount}) to ${reminder.targetId} in channel ${reminder.channelId}`, + ) + + // Update count and schedule next reminder + const nextRemindAt = now + reminder.intervalSeconds + await database + .update(reminders) + .set({ + count: newCount, + remindAt: nextRemindAt, + }) + .where(eq(reminders.id, reminder.id)) + } catch (e) { + logger.error(`Error while processing reminder #${reminder.id}:`, e) + } + } +}