From 24ec4d6ad69ef8792856b026b57b2f26f7bd8f2a Mon Sep 17 00:00:00 2001 From: Jacob Nguyen <76754747+jacoobes@users.noreply.github.com> Date: Mon, 10 Mar 2025 00:39:37 -0500 Subject: [PATCH] testbot --- bot/.gitignore | 4 + bot/README.md | 6 + bot/assets/locals/en-US.json | 12 + bot/assets/locals/es-ES.json | 12 + bot/assets/test.txt | 1 + bot/package-lock.json | 418 ++++++++++++++++++ bot/package.json | 30 ++ bot/rm.py | 11 + bot/sern.config.json | 13 + bot/src/commands/!plugins.ts | 0 bot/src/commands/add.ts | 37 ++ bot/src/commands/admin/!plugins.ts | 8 + bot/src/commands/admin/admin.ts | 11 + bot/src/commands/btn.ts | 14 + bot/src/commands/channelselect.ts | 9 + bot/src/commands/chicken.ts | 25 ++ bot/src/commands/collectors.ts | 103 +++++ bot/src/commands/dmMe.ts | 6 + bot/src/commands/flat-autocmp.ts | 24 + bot/src/commands/nested.ts | 52 +++ bot/src/commands/ping-ctx-msg.ts | 8 + bot/src/commands/ping-ctx.ts | 9 + bot/src/commands/ping.ts | 84 ++++ bot/src/commands/roleselect.ts | 9 + bot/src/commands/sernoptions.ts | 12 + bot/src/commands/subcommandoption.ts | 62 +++ bot/src/commands/test-ctx.ts | 40 ++ bot/src/commands/testing-neo.ts | 33 ++ bot/src/commands/userselect.ts | 8 + bot/src/constants.ts | 1 + bot/src/dependencies.d.ts | 18 + bot/src/events/error.ts | 10 + bot/src/events/messageCreate.ts | 10 + bot/src/events/module.activate.ts | 10 + bot/src/events/modulesLoaded.ts | 9 + bot/src/events/threadCreate.ts | 8 + bot/src/index.ts | 41 ++ bot/src/plugins/args.ts | 156 +++++++ bot/src/plugins/assertFields.ts | 57 +++ bot/src/plugins/channelType.ts | 39 ++ bot/src/plugins/confirmation.ts | 106 +++++ bot/src/plugins/correctFile.ts | 6 + bot/src/plugins/disable.ts | 38 ++ bot/src/plugins/dmOnly.ts | 29 ++ bot/src/plugins/filter.ts | 637 +++++++++++++++++++++++++++ bot/src/plugins/filterA.ts | 40 ++ bot/src/plugins/fromCallback.ts | 36 ++ bot/src/plugins/json-params.ts | 5 + bot/src/plugins/nsfwOnly.ts | 48 ++ bot/src/plugins/ownerOnly.ts | 30 ++ bot/src/plugins/permCheck.ts | 39 ++ bot/src/plugins/publish.ts | 215 +++++++++ bot/src/plugins/requirePermission.ts | 95 ++++ bot/src/plugins/serverOnly.ts | 38 ++ bot/src/presence.ts | 34 ++ bot/src/tasks/dbbackup.ts | 9 + bot/tsconfig.json | 3 + 57 files changed, 2828 insertions(+) create mode 100644 bot/.gitignore create mode 100644 bot/README.md create mode 100644 bot/assets/locals/en-US.json create mode 100644 bot/assets/locals/es-ES.json create mode 100644 bot/assets/test.txt create mode 100644 bot/package-lock.json create mode 100644 bot/package.json create mode 100644 bot/rm.py create mode 100644 bot/sern.config.json create mode 100644 bot/src/commands/!plugins.ts create mode 100644 bot/src/commands/add.ts create mode 100644 bot/src/commands/admin/!plugins.ts create mode 100644 bot/src/commands/admin/admin.ts create mode 100644 bot/src/commands/btn.ts create mode 100644 bot/src/commands/channelselect.ts create mode 100644 bot/src/commands/chicken.ts create mode 100644 bot/src/commands/collectors.ts create mode 100644 bot/src/commands/dmMe.ts create mode 100644 bot/src/commands/flat-autocmp.ts create mode 100644 bot/src/commands/nested.ts create mode 100644 bot/src/commands/ping-ctx-msg.ts create mode 100644 bot/src/commands/ping-ctx.ts create mode 100644 bot/src/commands/ping.ts create mode 100644 bot/src/commands/roleselect.ts create mode 100644 bot/src/commands/sernoptions.ts create mode 100644 bot/src/commands/subcommandoption.ts create mode 100644 bot/src/commands/test-ctx.ts create mode 100644 bot/src/commands/testing-neo.ts create mode 100644 bot/src/commands/userselect.ts create mode 100644 bot/src/constants.ts create mode 100644 bot/src/dependencies.d.ts create mode 100644 bot/src/events/error.ts create mode 100644 bot/src/events/messageCreate.ts create mode 100644 bot/src/events/module.activate.ts create mode 100644 bot/src/events/modulesLoaded.ts create mode 100644 bot/src/events/threadCreate.ts create mode 100644 bot/src/index.ts create mode 100644 bot/src/plugins/args.ts create mode 100644 bot/src/plugins/assertFields.ts create mode 100644 bot/src/plugins/channelType.ts create mode 100644 bot/src/plugins/confirmation.ts create mode 100644 bot/src/plugins/correctFile.ts create mode 100644 bot/src/plugins/disable.ts create mode 100644 bot/src/plugins/dmOnly.ts create mode 100644 bot/src/plugins/filter.ts create mode 100644 bot/src/plugins/filterA.ts create mode 100644 bot/src/plugins/fromCallback.ts create mode 100644 bot/src/plugins/json-params.ts create mode 100644 bot/src/plugins/nsfwOnly.ts create mode 100644 bot/src/plugins/ownerOnly.ts create mode 100644 bot/src/plugins/permCheck.ts create mode 100644 bot/src/plugins/publish.ts create mode 100644 bot/src/plugins/requirePermission.ts create mode 100644 bot/src/plugins/serverOnly.ts create mode 100644 bot/src/presence.ts create mode 100644 bot/src/tasks/dbbackup.ts create mode 100644 bot/tsconfig.json diff --git a/bot/.gitignore b/bot/.gitignore new file mode 100644 index 0000000..119c140 --- /dev/null +++ b/bot/.gitignore @@ -0,0 +1,4 @@ +/node_modules +/dist +.env +.sern diff --git a/bot/README.md b/bot/README.md new file mode 100644 index 0000000..192c90d --- /dev/null +++ b/bot/README.md @@ -0,0 +1,6 @@ +# Test bot + + +## add .env +DISCORD_TOKEN= +NODE_ENV= diff --git a/bot/assets/locals/en-US.json b/bot/assets/locals/en-US.json new file mode 100644 index 0000000..b54eb75 --- /dev/null +++ b/bot/assets/locals/en-US.json @@ -0,0 +1,12 @@ +{ + "command/ping": { + "name": "ping", + "description": "yeth", + "options": { + "asdfs": { + "name": "shidenglish", + "description": "yeah" + } + } + } +} diff --git a/bot/assets/locals/es-ES.json b/bot/assets/locals/es-ES.json new file mode 100644 index 0000000..fc71da0 --- /dev/null +++ b/bot/assets/locals/es-ES.json @@ -0,0 +1,12 @@ +{ + "command/ping": { + "name": "ping", + "description": "hola", + "options": { + "asdfs": { + "name": "shidspnaol", + "description": "si" + } + } + } +} diff --git a/bot/assets/test.txt b/bot/assets/test.txt new file mode 100644 index 0000000..9bea03a --- /dev/null +++ b/bot/assets/test.txt @@ -0,0 +1 @@ +{ "sdfasdfas": "asdf" } diff --git a/bot/package-lock.json b/bot/package-lock.json new file mode 100644 index 0000000..7cdce20 --- /dev/null +++ b/bot/package-lock.json @@ -0,0 +1,418 @@ +{ + "name": "plugtest", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "plugtest", + "version": "1.0.0", + "license": "UNLICENSED", + "dependencies": { + "@sern/handler": "file:../", + "@sern/localizer": "^1.1.3", + "@sern/publisher": "^1.1.2", + "discord.js": "^14.15.0", + "dotenv": "^16.4.5" + }, + "devDependencies": { + "@types/node": "^17.0.25", + "typescript": "latest" + } + }, + "..": { + "name": "@sern/handler", + "version": "4.2.4", + "license": "MIT", + "dependencies": { + "@sern/ioc": "^1.1.2", + "callsites": "^3.1.0", + "cron": "^3.1.7", + "deepmerge": "^4.3.1" + }, + "devDependencies": { + "@faker-js/faker": "^8.0.1", + "@types/node": "^20.0.0", + "@types/node-cron": "^3.0.11", + "@typescript-eslint/eslint-plugin": "5.58.0", + "@typescript-eslint/parser": "5.59.1", + "discord.js": "^14.14.1", + "eslint": "8.39.0", + "typescript": "5.0.2", + "vitest": "^1.6.0" + }, + "engines": { + "node": ">= 20.0.x" + } + }, + "../../tools/packages/builder": { + "name": "@sern/builder", + "version": "1.0.0-rc1", + "extraneous": true, + "license": "ISC", + "dependencies": { + "discord-api-types": "latest" + }, + "devDependencies": { + "@types/node": "^20.1.0" + } + }, + "../handler": { + "name": "@sern/handler", + "version": "4.2.3", + "extraneous": true, + "license": "MIT", + "dependencies": { + "@sern/ioc": "^1.1.2", + "callsites": "^3.1.0", + "cron": "^3.1.7", + "deepmerge": "^4.3.1" + }, + "devDependencies": { + "@faker-js/faker": "^8.0.1", + "@types/node": "^20.0.0", + "@types/node-cron": "^3.0.11", + "@typescript-eslint/eslint-plugin": "5.58.0", + "@typescript-eslint/parser": "5.59.1", + "discord.js": "^14.14.1", + "eslint": "8.39.0", + "typescript": "5.0.2", + "vitest": "^1.6.0" + }, + "engines": { + "node": ">= 20.0.x" + } + }, + "../tools/packages/builder": { + "name": "@sern/builder", + "version": "1.0.0-rc1", + "extraneous": true, + "license": "ISC", + "dependencies": { + "discord-api-types": "latest" + }, + "devDependencies": { + "@types/node": "^20.1.0" + } + }, + "../tools/packages/localizer": { + "name": "@sern/localizer", + "version": "1.1.1", + "extraneous": true, + "license": "ISC", + "dependencies": { + "shrimple-locales": "^0.2.1" + }, + "devDependencies": { + "@sern/handler": "^4.0.0", + "discord.js": "^14.15.3", + "vitest": "^1.2.2" + } + }, + "node_modules/@discordjs/builders": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.10.0.tgz", + "integrity": "sha512-ikVZsZP+3shmVJ5S1oM+7SveUCK3L9fTyfA8aJ7uD9cNQlTqF+3Irbk2Y22KXTb3C3RNUahRkSInClJMkHrINg==", + "dependencies": { + "@discordjs/formatters": "^0.6.0", + "@discordjs/util": "^1.1.1", + "@sapphire/shapeshift": "^4.0.0", + "discord-api-types": "^0.37.114", + "fast-deep-equal": "^3.1.3", + "ts-mixer": "^6.0.4", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/collection": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", + "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/formatters": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.0.tgz", + "integrity": "sha512-YIruKw4UILt/ivO4uISmrGq2GdMY6EkoTtD0oS0GvkJFRZbTSdPhzYiUILbJ/QslsvC9H9nTgGgnarnIl4jMfw==", + "dependencies": { + "discord-api-types": "^0.37.114" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.4.2.tgz", + "integrity": "sha512-9bOvXYLQd5IBg/kKGuEFq3cstVxAMJ6wMxO2U3wjrgO+lHv8oNCT+BBRpuzVQh7BoXKvk/gpajceGvQUiRoJ8g==", + "dependencies": { + "@discordjs/collection": "^2.1.1", + "@discordjs/util": "^1.1.1", + "@sapphire/async-queue": "^1.5.3", + "@sapphire/snowflake": "^3.5.3", + "@vladfrangu/async_event_emitter": "^2.4.6", + "discord-api-types": "^0.37.114", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.19.8" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/util": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz", + "integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.0.tgz", + "integrity": "sha512-QH5CAFe3wHDiedbO+EI3OOiyipwWd+Q6BdoFZUw/Wf2fw5Cv2fgU/9UEtJRmJa9RecI+TAhdGPadMaEIur5yJg==", + "dependencies": { + "@discordjs/collection": "^2.1.0", + "@discordjs/rest": "^2.4.1", + "@discordjs/util": "^1.1.0", + "@sapphire/async-queue": "^1.5.2", + "@types/ws": "^8.5.10", + "@vladfrangu/async_event_emitter": "^2.2.4", + "discord-api-types": "^0.37.114", + "tslib": "^2.6.2", + "ws": "^8.17.0" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@sapphire/async-queue": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", + "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/shapeshift": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz", + "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v16" + } + }, + "node_modules/@sapphire/snowflake": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz", + "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sern/handler": { + "resolved": "..", + "link": true + }, + "node_modules/@sern/localizer": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@sern/localizer/-/localizer-1.1.3.tgz", + "integrity": "sha512-hTn0DtiAzIWSuokqMsvnVuFqU+P776p/Yv5etlrq+CWDgw332Hwuj3geyqN1C0yEjwF+ceyXJE/kGu2/inkEyg==", + "dependencies": { + "shrimple-locales": "^0.2.1" + } + }, + "node_modules/@sern/publisher": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@sern/publisher/-/publisher-1.1.2.tgz", + "integrity": "sha512-1zh99JZykKUhqHhE75ZXfiLsBtf1WI+NnDCojv8UlpnGBEyzO8xyI1X7PNf6cPKRs4W9XqY3PqTJ+hrqzIsMkg==" + }, + "node_modules/@types/node": { + "version": "17.0.45", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", + "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==" + }, + "node_modules/@types/ws": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz", + "integrity": "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vladfrangu/async_event_emitter": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.6.tgz", + "integrity": "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/discord-api-types": { + "version": "0.37.119", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.119.tgz", + "integrity": "sha512-WasbGFXEB+VQWXlo6IpW3oUv73Yuau1Ig4AZF/m13tXcTKnMpc/mHjpztIlz4+BM9FG9BHQkEXiPto3bKduQUg==" + }, + "node_modules/discord.js": { + "version": "14.17.3", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.17.3.tgz", + "integrity": "sha512-8/j8udc3CU7dz3Eqch64UaSHoJtUT6IXK4da5ixjbav4NAXJicloWswD/iwn1ImZEMoAV3LscsdO0zhBh6H+0Q==", + "dependencies": { + "@discordjs/builders": "^1.10.0", + "@discordjs/collection": "1.5.3", + "@discordjs/formatters": "^0.6.0", + "@discordjs/rest": "^2.4.2", + "@discordjs/util": "^1.1.1", + "@discordjs/ws": "^1.2.0", + "@sapphire/snowflake": "3.5.3", + "discord-api-types": "^0.37.114", + "fast-deep-equal": "3.1.3", + "lodash.snakecase": "4.1.1", + "tslib": "^2.6.3", + "undici": "6.19.8" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==" + }, + "node_modules/magic-bytes.js": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.10.0.tgz", + "integrity": "sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ==" + }, + "node_modules/shrimple-locales": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/shrimple-locales/-/shrimple-locales-0.2.1.tgz", + "integrity": "sha512-j2vNBDXJgED3XqGXCD/vqXBSqwlDXP1iGkseVos8mCtZqHp3R+0FImx8xwtjeYufJcYfhjBMkaBTWgsBi8eJZw==" + }, + "node_modules/ts-mixer": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", + "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz", + "integrity": "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/bot/package.json b/bot/package.json new file mode 100644 index 0000000..f79f92f --- /dev/null +++ b/bot/package.json @@ -0,0 +1,30 @@ +{ + "name": "plugtest", + "version": "1.0.0", + "description": "a descriptiuon", + "main": "dist/index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build": "sern build", + "start": "node ./dist/index.js", + "run": "node ./dist/index.js" + }, + "keywords": [ + "typescript", + "sern", + "discord.js" + ], + "license": "UNLICENSED", + "dependencies": { + "@sern/handler": "file:../", + "@sern/localizer": "^1.1.3", + "@sern/publisher": "^1.1.2", + "discord.js": "^14.15.0", + "dotenv": "^16.4.5" + }, + "devDependencies": { + "@types/node": "^17.0.25", + "typescript": "latest" + }, + "type": "module" +} diff --git a/bot/rm.py b/bot/rm.py new file mode 100644 index 0000000..bffa111 --- /dev/null +++ b/bot/rm.py @@ -0,0 +1,11 @@ +import os + +for root, dirs, files in os.walk('.'): + for filename in files: + if filename.endswith('.js'): + file_path = os.path.join(root, filename) + try: + os.remove(file_path) + print(f'Successfully deleted: {file_path}') + except OSError as e: + print(f'Error deleting {file_path}: {e.strerror}') diff --git a/bot/sern.config.json b/bot/sern.config.json new file mode 100644 index 0000000..3e2315f --- /dev/null +++ b/bot/sern.config.json @@ -0,0 +1,13 @@ +{ + "language": "typescript", + "defaultPrefix": "!", + "paths": { + "base": "src", + "commands": "commands", + "events": "events" + }, + "app": { + "tags": ["Nice ass bot"], + "description": "A bot" + } +} diff --git a/bot/src/commands/!plugins.ts b/bot/src/commands/!plugins.ts new file mode 100644 index 0000000..e69de29 diff --git a/bot/src/commands/add.ts b/bot/src/commands/add.ts new file mode 100644 index 0000000..899d1be --- /dev/null +++ b/bot/src/commands/add.ts @@ -0,0 +1,37 @@ +import { CommandType, commandModule } from "@sern/handler"; +import { ApplicationCommandOptionType } from "discord.js"; + +export default commandModule({ + name: 'add', + type: CommandType.Slash, + description: 'Adds numbers together', + options: [ + { + type: ApplicationCommandOptionType.String, + name: 'numbers', + description: 'Numbers to add together separated by a space.', + required: true, + min_length: 3, + }, + ], + execute: async (ctx) => { + let numbers = ctx.options.getString('numbers')?.split(' ')!; + + numbers = numbers.filter((num) => num !== ''); + + if (!numbers.every((num) => !isNaN(parseFloat(num)))) { + return ctx.reply({ + content: 'You can only input numbers.', + ephemeral: true, + }); + } + + const sum = numbers.reduce((acc, num) => acc + parseFloat(num), 0); + + + return ctx.reply({ + content: `The sum is ${sum}`, + ephemeral: true, + }); + }, +}); diff --git a/bot/src/commands/admin/!plugins.ts b/bot/src/commands/admin/!plugins.ts new file mode 100644 index 0000000..1ff23de --- /dev/null +++ b/bot/src/commands/admin/!plugins.ts @@ -0,0 +1,8 @@ +import { filter, hasRole } from "../../plugins/filter.js"; +import { ownerOnly } from "../../plugins/ownerOnly.js"; +import { ADMIN } from '../../constants.js' + +export default [ + ownerOnly(), + filter({ condition: [hasRole(ADMIN)] }) +] diff --git a/bot/src/commands/admin/admin.ts b/bot/src/commands/admin/admin.ts new file mode 100644 index 0000000..2c7f873 --- /dev/null +++ b/bot/src/commands/admin/admin.ts @@ -0,0 +1,11 @@ +import { commandModule, CommandType } from '@sern/handler' + + + +export default commandModule({ + type: CommandType.Slash, + description: "A", + execute: (ctx, args) => { + + } +}) diff --git a/bot/src/commands/btn.ts b/bot/src/commands/btn.ts new file mode 100644 index 0000000..aa7b01f --- /dev/null +++ b/bot/src/commands/btn.ts @@ -0,0 +1,14 @@ +import { CommandType, commandModule } from "@sern/handler"; +import { json } from "../plugins/json-params.js"; + +export default commandModule({ + type: CommandType.Button, + plugins: [json], + execute(ctx, args) { + console.log(args.state['json/data']) + //@ts-ignore + ctx.reply(args.state['json/data'].uid) + } + + +}) diff --git a/bot/src/commands/channelselect.ts b/bot/src/commands/channelselect.ts new file mode 100644 index 0000000..8b7a069 --- /dev/null +++ b/bot/src/commands/channelselect.ts @@ -0,0 +1,9 @@ +import { CommandType, commandModule } from "@sern/handler"; + +export default commandModule({ + type: CommandType.ChannelSelect, + execute: (s) => { + s.reply('clicked channel'); + } + +}); diff --git a/bot/src/commands/chicken.ts b/bot/src/commands/chicken.ts new file mode 100644 index 0000000..cc3c518 --- /dev/null +++ b/bot/src/commands/chicken.ts @@ -0,0 +1,25 @@ +import { CommandType, commandModule } from "@sern/handler"; +import { publishConfig } from "@sern/publisher"; +import { PermissionFlagsBits } from "discord.js"; + +export default commandModule({ + type: CommandType.Slash, + plugins: [ + publishConfig({ + integrationTypes: ['User'], + contexts: [0,1,2], + defaultMemberPermissions: + PermissionFlagsBits.Speak + | PermissionFlagsBits.Connect + | PermissionFlagsBits.BanMembers + + + + }) + ], + description: "yo", + execute:(ctx) => { + ctx.reply("hello"); + } + +}) diff --git a/bot/src/commands/collectors.ts b/bot/src/commands/collectors.ts new file mode 100644 index 0000000..880b206 --- /dev/null +++ b/bot/src/commands/collectors.ts @@ -0,0 +1,103 @@ +import { CommandType, Context, commandModule } from "@sern/handler"; +import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; + +export default commandModule ({ + type: CommandType.Slash, + description: 'collectors', + execute: async (ctx) => { + //await close(ctx) + await testCollect(ctx) + } +}) + +const testCollect = async (ctx: Context) => { + const msgcmpt = ctx.interaction.channel?.createMessageComponentCollector() + const buttonRow = new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId("closeyes").setLabel("Yes").setStyle(ButtonStyle.Success), + new ButtonBuilder().setCustomId("closeno").setLabel("No").setStyle(ButtonStyle.Danger) + ); + ctx.reply({ components: [buttonRow] }) + msgcmpt?.on('collect', async button => { + + await button.deferUpdate(); + if (button.customId === "closeyes") { + try { + await button.editReply('closing') + } catch (e) { + await button.editReply({ content: "An error has occurred and I could not close the ticket...", components: [] }); + } + } else { + await button.editReply({ content: "This ticket will remain open.", components: [] }); + msgcmpt.stop(); + } + }); +} +//export const quiz = async(client: Client, ctx: Context) => { +// try { +// const pokemon = Math.round(Math.random() * 890) +// const question = `https://cdn.dagpi.xyz/wtp/pokemon/${pokemon}q.png`; +// const answer = `https://cdn.dagpi.xyz/wtp/pokemon/${pokemon}a.png`; +// +// const correctPokemon = await (await fetch(`https://pokeapi.co/api/v2/pokemon/${pokemon}`)).json(); +// const allPokemon = await (await fetch("https://pokeapi.co/api/v2/pokemon?limit=899")).json(); +// +// const options: string[] = []; +// //client.utils.log("WARNING", "INFO", `Correct answer is ${correctPokemon.name}`); +// +// while (options.length < 9) { +// let option = allPokemon.results[pokemon]; +// if (options.includes(option.name)) continue; +// options.push(option.name); +// } +// +// if (!options.includes(correctPokemon.name)) { +// options.splice(client.utils.randomRange(0, 10), 0, correctPokemon.name.toLowerCase()); +// } else { +// while (options.length < 10) { +// let option = allPokemon.results[client.utils.randomRange(1, 890)]; +// if (options.includes(option.name)) continue; +// options.push(option.name); +// } +// } +// +// const msgEmbed = (await client.utils.CustomEmbed({ userID: ctx.user.id })).setTitle("Who's that Pokémon?").setImage(question); +// const row = new ActionRowBuilder().addComponents( +// new StringSelectMenuBuilder().setCustomId("pokequiz").addOptions( +// options.sort().map((opt) => { +// return { label: client.utils.titleCase(opt), value: opt.toLowerCase() }; +// }) +// ) +// ); +// +// const msg = await client.utils.fetchReply(ctx.interaction, { embeds: [msgEmbed], components: [row] }); +// const filter = (i: StringSelectMenuInteraction) => i.user.id === ctx.user.id && i.message.id === msg.id; +// +// const collector = msg.createMessageComponentCollector({ filter, componentType: ComponentType.StringSelect, time: 1000 * 20 }); +// collector.on("collect", async (i) => { +// const guess = i.values[0].toLowerCase(); +// +// msgEmbed.setImage(answer).setTitle(`It's ${client.utils.titleCase(correctPokemon.name)}!`); +// +// if (guess === correctPokemon.name.toLowerCase()) msgEmbed.setColor("Green").setFooter({ text: "You're correct!" }); +// else msgEmbed.setColor("Red").setFooter({ text: `You guessed ${client.utils.titleCase(guess)}.` }); +// +// await i.update({ embeds: [msgEmbed], components: [] }); +// collector.stop("Guessed"); +// }); +// +// collector.on("end", async (i, reason) => { +// if (reason === "Guessed") return; +// +// msgEmbed +// .setImage(answer) +// .setTitle(`It's ${client.utils.titleCase(correctPokemon.name)}!`) +// .setColor("Red") +// .setFooter({ text: "You did not guess in time." }); +// +// await ctx.interaction.editReply({ embeds: [msgEmbed], components: [] }); +// }); +// } catch (e) { +// client.utils.log("ERROR", __filename, `${e}`); +// return ctx.interaction.reply({ content: "An error has occurred. Please try again.", ephemeral: true }); +// } +//}; diff --git a/bot/src/commands/dmMe.ts b/bot/src/commands/dmMe.ts new file mode 100644 index 0000000..bf113ce --- /dev/null +++ b/bot/src/commands/dmMe.ts @@ -0,0 +1,6 @@ +import { CommandType, commandModule } from "@sern/handler"; + +export default commandModule({ + type: CommandType.Modal, + execute: (modal) => modal.reply('thanks') +}); diff --git a/bot/src/commands/flat-autocmp.ts b/bot/src/commands/flat-autocmp.ts new file mode 100644 index 0000000..8eeb415 --- /dev/null +++ b/bot/src/commands/flat-autocmp.ts @@ -0,0 +1,24 @@ +import { commandModule, CommandType } from "@sern/handler"; +import { ApplicationCommandOptionType } from "discord.js"; + +export default commandModule({ + description: "testing", + type: CommandType.Slash, + options: [ + { + name: "option", + description: "option desc", + type: ApplicationCommandOptionType.String, + required: true, + autocomplete: true, + command: { + execute: (i) => { + i.respond([{ name: "rah", value: "rah" }]); + }, + }, + }, + ], + execute: (ctx) => { + return ctx.reply("rah"); + }, +}); diff --git a/bot/src/commands/nested.ts b/bot/src/commands/nested.ts new file mode 100644 index 0000000..a6d37d5 --- /dev/null +++ b/bot/src/commands/nested.ts @@ -0,0 +1,52 @@ +import { commandModule, CommandType } from "@sern/handler"; +import { ActionRowBuilder, ApplicationCommandOptionType, ModalBuilder, TextInputBuilder, TextInputStyle } from "discord.js"; + +export default commandModule({ + type: CommandType.Slash, + description : 'a ping command', + options: [ + { + name: "nest", + description: "testing nested", + type: ApplicationCommandOptionType.SubcommandGroup, + options : [ + { + name: "nest", + description: "testing nested", + type: ApplicationCommandOptionType.Subcommand, + options : [ + { + name: "sdfasd", + description: "testing autocomplete", + autocomplete: true, + type: ApplicationCommandOptionType.String, + command : { + onEvent : [], + async execute(autocmp, sdt) { + //console.log(autocmp) + const choices = ['butt', 'deez', 'lmao', 'lmfao', 'nuts', 'chicken']; + await autocmp.respond(choices.map((e,i) => ({ name : e, value: i.toString()}))); + } + } + } + ] + }, + ] + }, + ], + async execute ({ interaction }) { + const modal = new ModalBuilder() + .setCustomId('dmMe') + .setTitle('send something to my dm (nothing bad pls)'); + + const input = new TextInputBuilder() + .setCustomId('message') + .setLabel("Send something to me") + .setStyle(TextInputStyle.Short); + + const firstActionRow = new ActionRowBuilder().addComponents([input]); + modal.addComponents([firstActionRow]); + await interaction.showModal(modal); + } +}); + diff --git a/bot/src/commands/ping-ctx-msg.ts b/bot/src/commands/ping-ctx-msg.ts new file mode 100644 index 0000000..8fed221 --- /dev/null +++ b/bot/src/commands/ping-ctx-msg.ts @@ -0,0 +1,8 @@ +import { CommandType, commandModule } from '@sern/handler' + +export default commandModule({ + type: CommandType.CtxMsg, + execute: (i, sdt) => { + i.reply('pong msg') + } +}) diff --git a/bot/src/commands/ping-ctx.ts b/bot/src/commands/ping-ctx.ts new file mode 100644 index 0000000..e46082b --- /dev/null +++ b/bot/src/commands/ping-ctx.ts @@ -0,0 +1,9 @@ +import { CommandType, commandModule } from "@sern/handler"; + + +export default commandModule({ + type: CommandType.CtxUser, + execute: (i, sdt) => { + i.reply('pong') + } +}) diff --git a/bot/src/commands/ping.ts b/bot/src/commands/ping.ts new file mode 100644 index 0000000..0c0ab77 --- /dev/null +++ b/bot/src/commands/ping.ts @@ -0,0 +1,84 @@ +import {commandModule, CommandType, controller, CommandInitPlugin, CommandControlPlugin } from '@sern/handler'; +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + ChannelSelectMenuBuilder, + RoleSelectMenuBuilder, + UserSelectMenuBuilder, +} from "discord.js"; +import { localize } from '@sern/localizer'; + +const plugin = CommandControlPlugin(() => { + return controller.next({ a: 'from plugin1' }); +}); + +const plugin2 = CommandControlPlugin(() => { + return controller.next({ a: 'from plugin2' }); +}) + +const updateDescription = (description: string) => { + return CommandInitPlugin(() => { + if(description.length > 100) { + console.error("Description is invalid") + return controller.stop("From updateDescription: description is invalid"); + } + return controller.next({ description }); // continue to next plugin + }); +}; + +export default commandModule({ + type: CommandType.Slash, + plugins: [localize()], + description: 'A ping command I just updated', + options: [ + str(name("asdfs"), + description("sdfds")) + ], + execute: async (ctx, sdt) => { + ctx.interaction + const btn = new ButtonBuilder() + .setStyle(ButtonStyle.Link) + .setLabel("Click me") + .setURL('https://www.youtube.com/watch?v=dQw4w9WgXcQ&pp=ygUIcmlja3JvbGw%3D') + + const editButton = new ButtonBuilder({ + customId: `btn/{"uid":"1061421834341462036"}`, + label: "click me also", + emoji: "🛠", + style: ButtonStyle.Primary, + }); + + ctx.reply({ components: [ + new ActionRowBuilder().addComponents(btn, editButton), + new ActionRowBuilder({ + components: [ + new UserSelectMenuBuilder({ + custom_id: "userselect", + placeholder: "select channel", + minValues: 1, + }), + ], + }), + new ActionRowBuilder().addComponents( + new ChannelSelectMenuBuilder({ + custom_id: "channelselect", + placeholder: "select channel", + minValues: 1, + + }), + ), + new ActionRowBuilder({ + components: [ + new RoleSelectMenuBuilder({ + custom_id: "roleselect", + placeholder: "select role", + minValues: 1, + }), + ], + }) + ]}) + }, +}); + + diff --git a/bot/src/commands/roleselect.ts b/bot/src/commands/roleselect.ts new file mode 100644 index 0000000..ac1fa3c --- /dev/null +++ b/bot/src/commands/roleselect.ts @@ -0,0 +1,9 @@ +import { CommandType, commandModule } from "@sern/handler" + +export default commandModule( { + type: CommandType.RoleSelect, + execute: (s) => { + s.reply('selected role') + } + +}) diff --git a/bot/src/commands/sernoptions.ts b/bot/src/commands/sernoptions.ts new file mode 100644 index 0000000..d4b4469 --- /dev/null +++ b/bot/src/commands/sernoptions.ts @@ -0,0 +1,12 @@ +import { CommandType, commandModule } from "@sern/handler"; + + +export default commandModule({ + type: CommandType.Slash, + description: 'shid', + execute({ interaction }) { + interaction.reply('hello') + } + + +}) diff --git a/bot/src/commands/subcommandoption.ts b/bot/src/commands/subcommandoption.ts new file mode 100644 index 0000000..533fb1a --- /dev/null +++ b/bot/src/commands/subcommandoption.ts @@ -0,0 +1,62 @@ +import { commandModule, CommandType } from '@sern/handler'; +import { ApplicationCommandOptionType } from 'discord.js'; + +export default commandModule({ + type: CommandType.Slash, + description: 'A ping command', + options: [ + { + name: "art", + type: ApplicationCommandOptionType.Subcommand, + description: "Lists out information about an Animal Crossing artwork.", + options: [ + { + name: "name", + description: "The name of the artwork to lookup.", + type: ApplicationCommandOptionType.String, + autocomplete: true, + required: true, + command: { + async execute(ctx) { + await ctx.respond([{ name: 'art', value: 'first' }]) + }, + }, + }, + ], + }, + { + name: "villager", + type: ApplicationCommandOptionType.Subcommand, + description: "Lists out information about an Animal Crossing villager.", + options: [ + { + name: "name", + description: "The name of the villager to lookup.", + type: ApplicationCommandOptionType.String, + autocomplete: true, + required: true, + command: { + onEvent: [], + async execute(ctx) { + await ctx.respond([{ name: 'villager', value: 'second' } ]) + }, + }, + }, + ], + }, + ], + execute: async (ctx) => { + const command = ctx.options.getSubcommand(); + switch (command) { + case "art": { + + ctx.reply('art'); + break; + } + case "villager": { + ctx.reply('vil'); + break; + } + } + }, +}); diff --git a/bot/src/commands/test-ctx.ts b/bot/src/commands/test-ctx.ts new file mode 100644 index 0000000..07a7bfb --- /dev/null +++ b/bot/src/commands/test-ctx.ts @@ -0,0 +1,40 @@ +import { ApplicationCommandOptionType } from "discord.js"; +import { Service, commandModule, CommandType } from "@sern/handler"; + +export const config = { + guildIds: ['941002690211766332'] +} + + +export default commandModule({ + type: CommandType.Both, + description: 'tests context', + options: [ + { + type: ApplicationCommandOptionType.String, + name: "hello", + description: "wassup", + required: false, + } + ], + async execute(ctx) { + const logger = Service('@sern/logger'); + + if(ctx.isMessage()) { + logger?.info({ message : ctx.message.content }) + logger?.info({ message : ctx.prefix }) + } else { + logger?.info({ message : ctx.interaction.toString() }) + } + + logger?.info({ message: ctx.id }) + logger?.info({ message: ctx.channel?.toString()! }) + logger?.info({ message: ctx.user.toString()! }) + logger?.info({ message: ctx.createdTimestamp.toString() }) + logger?.info({ message: ctx.guild?.toString() }) + logger?.info({ message: ctx.member?.toString() }) + logger?.info({ message: ctx.client }) + logger?.info({ message: ctx.inGuild }) + await ctx.reply("guayin bodishivatta") + } +}) diff --git a/bot/src/commands/testing-neo.ts b/bot/src/commands/testing-neo.ts new file mode 100644 index 0000000..4529884 --- /dev/null +++ b/bot/src/commands/testing-neo.ts @@ -0,0 +1,33 @@ +import { CommandType, commandModule } from "@sern/handler"; +import { ActionRowBuilder, ModalActionRowComponentBuilder, ModalBuilder, TextInputBuilder, TextInputStyle } from "discord.js"; + +const informationRequestModal = new ModalBuilder() + .setCustomId("information-request") + .setTitle("More Information") + .addComponents( + new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId("command-name") + .setLabel("Command Name") + .setPlaceholder("The name of the command that this bug occurred on.") + .setStyle(TextInputStyle.Short) + .setMinLength(4) + .setMaxLength(20) + .setRequired(true))); + +export default commandModule({ + type: CommandType.Slash, + plugins: [], + description: "A random test command.", + execute: async (ctx) => { + await ctx.interaction.showModal(informationRequestModal); + + await ctx.interaction + .awaitModalSubmit({ time: 300_000 }) + .then(async (modal) => { + modal.reply("thanks brody") + }) + .catch(() => null); + }, +}); + diff --git a/bot/src/commands/userselect.ts b/bot/src/commands/userselect.ts new file mode 100644 index 0000000..afd8f40 --- /dev/null +++ b/bot/src/commands/userselect.ts @@ -0,0 +1,8 @@ +import { CommandType, commandModule } from "@sern/handler"; + +export default commandModule( { + type: CommandType.UserSelect, + execute: (s) => { + s.reply('selected user') + } +}) diff --git a/bot/src/constants.ts b/bot/src/constants.ts new file mode 100644 index 0000000..1f28797 --- /dev/null +++ b/bot/src/constants.ts @@ -0,0 +1 @@ +export const ADMIN = '983754333944434712' diff --git a/bot/src/dependencies.d.ts b/bot/src/dependencies.d.ts new file mode 100644 index 0000000..d22344f --- /dev/null +++ b/bot/src/dependencies.d.ts @@ -0,0 +1,18 @@ +import type { + Logging, + ErrorHandling, + CoreDependencies +} from '@sern/handler' +import type { Publisher } from '@sern/publisher'; +import type { Localizer } from '@sern/localizer'; +declare global { + interface Dependencies extends CoreDependencies { + localizer: Localizer; + publisher: Publisher + } +} + +export {} + + + diff --git a/bot/src/events/error.ts b/bot/src/events/error.ts new file mode 100644 index 0000000..3edba14 --- /dev/null +++ b/bot/src/events/error.ts @@ -0,0 +1,10 @@ +import { EventType, eventModule } from "@sern/handler"; + + +export default eventModule({ + name: 'error', + type: EventType.Sern, + execute: (e) => { + console.log(e) + } +}) diff --git a/bot/src/events/messageCreate.ts b/bot/src/events/messageCreate.ts new file mode 100644 index 0000000..72447a8 --- /dev/null +++ b/bot/src/events/messageCreate.ts @@ -0,0 +1,10 @@ +import { discordEvent } from "@sern/handler"; + +const execute = (...args: any[]) => { + console.log(args[0].content) +} +export default discordEvent({ + name: 'messageCreate', + once: true, + execute +}) diff --git a/bot/src/events/module.activate.ts b/bot/src/events/module.activate.ts new file mode 100644 index 0000000..a1674e9 --- /dev/null +++ b/bot/src/events/module.activate.ts @@ -0,0 +1,10 @@ +import {eventModule, EventType} from "@sern/handler"; + + +export default eventModule({ + type: EventType.Sern, + name: 'module.activate', + execute(args) { + + } +}) diff --git a/bot/src/events/modulesLoaded.ts b/bot/src/events/modulesLoaded.ts new file mode 100644 index 0000000..efd0a97 --- /dev/null +++ b/bot/src/events/modulesLoaded.ts @@ -0,0 +1,9 @@ +import { CommandType, EventType, Service, eventModule } from "@sern/handler"; + + +export default eventModule({ + type: EventType.Sern, + execute: async () => { + console.log('eventmodule: all loaded'); + } +}) diff --git a/bot/src/events/threadCreate.ts b/bot/src/events/threadCreate.ts new file mode 100644 index 0000000..2cfb9e3 --- /dev/null +++ b/bot/src/events/threadCreate.ts @@ -0,0 +1,8 @@ +import { discordEvent } from "@sern/handler"; + +export default discordEvent({ + name: 'threadCreate', + execute(thread) { + console.log(thread) + } +}) diff --git a/bot/src/index.ts b/bot/src/index.ts new file mode 100644 index 0000000..963c235 --- /dev/null +++ b/bot/src/index.ts @@ -0,0 +1,41 @@ +import 'dotenv/config'; +import { makeDependencies, Sern, Service } from '@sern/handler' +import { Client, GatewayIntentBits, Partials } from 'discord.js'; +import { Publisher } from '@sern/publisher' +import { Localization } from '@sern/localizer' + +__DEV__: console.log(1); + +const intents = GatewayIntentBits.Guilds | + GatewayIntentBits.GuildMembers | + GatewayIntentBits.GuildMessageReactions | + GatewayIntentBits.GuildMessages | + GatewayIntentBits.DirectMessages | + GatewayIntentBits.MessageContent; + +const partials = [ + Partials.Channel +]; + +async function init() { + await makeDependencies(({ add }) => { + add('@sern/client', new Client({ intents, partials })); + add('localizer', Localization()); + add('publisher', deps => { + return new Publisher(deps['@sern/modules'], + deps['@sern/emitter'], + deps['@sern/logger']!) + }) + }) + Sern.init({ + commands : "./dist/commands", + events: "./dist/events", + tasks: "./dist/tasks", + defaultPrefix: "!" + }) +} +init().then(() => { + Service('@sern/client').login() +}) +//View docs for all options + diff --git a/bot/src/plugins/args.ts b/bot/src/plugins/args.ts new file mode 100644 index 0000000..d8be300 --- /dev/null +++ b/bot/src/plugins/args.ts @@ -0,0 +1,156 @@ +/** + * @author HighArcs + * @version 1.0.0 + * @description converts array of argument strings to an object (and maps them) + * @license null + * @example + * ```ts + * import { parsedCommandModule, args } from "../plugins/args"; + * import { CommandType } from "@sern/handler"; + * + * interface Arg { + * value: number; + * } + * + * export default parsedCommandModule({ + * type : CommandType.Text + * plugins: [args({ value: Number })], + * execute: (ctx, args) => { + * console.log(ctx.args.value); + * } + * }) + */ + +import { + commandModule, + CommandType, + Context, ControlPlugin, + Plugin, CommandControlPlugin, controller +} from "@sern/handler"; +import type { Awaitable } from "discord.js"; + +type Converter = (value?: string) => Awaitable; +type Struct = Record; +type ConverterList = { + [K in keyof T]: Converter; +}; +type Ctx = Context & { _args: T }; + +interface Err { + key: string; + error: string; + given: string; + index: number; +} + +type OnError = (context: Ctx, error: Err) => any; + +type SpecialEvt = { + readonly "@@plugin": symbol +} & ControlPlugin<[Ctx, ]> + +async function convert( + args: Array, + struct: ConverterList +) { + const entries = Object.entries(struct); + const result = {} as T; + for (let i = 0; i < entries.length; i++) { + const value = args[i]; + const [key, converter] = entries[i]!; + try { + result[key as keyof T] = await converter(value); + } catch (error) { + throw { key, error: String(error), given: value, index: i }; + } + } + + return result; +} + +interface ParsedInputCommandModule { + name?: string; + description: string; + type: CommandType.Both | CommandType.Text | CommandType.Slash; + execute: (context: Ctx, args: Array) => any; + plugins: () => + | [SpecialEvt, ...Array] + | [] + | undefined; +} + +export const Structs = { + string: (value: string) => String(value), + number: (value: string) => Number(value), + boolean: (value: string) => value === "true" || value === "1", + date: (value: string) => new Date(value), + integer: (value: string) => Number.parseInt(value), +}; + +export function parsedCommandModule( + a: ParsedInputCommandModule +) { + const plugins = (a.plugins() ?? []); + return commandModule({ ...a, plugins } as never); +} + +export namespace Checks { + export function choices( + choices: K[], + value?: string + ): asserts value is K { + if (!choices.includes(value as unknown as K)) { + throw "value is not in choices"; + } + } + + export function required(value?: string): asserts value is string { + if (value === undefined) { + throw "value is required"; + } + } + + export function limit(min: number, max: number, value?: string) { + required(value); + const val = Structs.number(value); + if (val < min) { + throw `value must be higher than ${min}`; + } + + if (val > max) { + throw `value must be lower than ${max}`; + } + + return val; + } +} + +export function args( + struct: ConverterList, + onError?: OnError +): SpecialEvt { + const plugin = CommandControlPlugin(async (ctx, args) => { + switch(args.type) { + case "slash": { + let result: T; + } break; + case "text" : { + let result: T; + try { + result = await convert(args, struct) + } catch (e) { + if (onError) { + onError(ctx as Ctx, e as Err); + } + return controller.stop(); + } + //@warn - mutable assignment! + (ctx as Ctx)._args = result; + return controller.next(); + } + } + return controller.next() + }) + Object.defineProperty(plugin, "@@plugin", { value: Symbol("args") }) + return plugin as SpecialEvt; +} diff --git a/bot/src/plugins/assertFields.ts b/bot/src/plugins/assertFields.ts new file mode 100644 index 0000000..a3b9bab --- /dev/null +++ b/bot/src/plugins/assertFields.ts @@ -0,0 +1,57 @@ +/** + * This plugin checks the fields of a ModalSubmitInteraction + * with regex or a custom callback + * + * @author @jacoobes [<@182326315813306368>] + * @version 1.0.0 + * @example + * ```ts + * export default commandModule({ + * type: CommandType.Modal, + * plugins: [ + * assertFields({ + * fields: { + * // check the modal field "mcUsernameInput" with the regex /a+b+c/ + * mcUsernameInput: /a+b+c+/ + * }, + * failure: (errors, interaction) => { + * interaction.reply(errors.join("\n")) + * } + * }), + * ], + * execute: ctx => { + * ctx.reply("nice!") + * } + * }) + * ``` + */ +import { CommandControlPlugin, CommandType, controller } from "@sern/handler"; +import type { ModalSubmitInteraction } from "discord.js"; + +type Assertion = + | RegExp + | ((value : string) => boolean); + +export function assertFields(config: { + fields: Record, + failure: (errors: string[], interaction: ModalSubmitInteraction) => any +}) { + return CommandControlPlugin(modal => { + const pairs = Object.entries(config.fields); + const errors = []; + for(const [ field, assertion ] of pairs) { + // Keep in mind this doesn't check for typos! + // feel free to add more checks. + const input = modal.fields.getTextInputValue(field) + const resolvedAssertion = assertion instanceof RegExp ? (value: string) => assertion.test(value) : assertion; + if(!resolvedAssertion(input)) { + errors.push(input + " failed to pass assertion " + resolvedAssertion.toString() ) + } + } + if(errors.length > 0) { + config.failure(errors, modal); + return controller.stop(); + } + return controller.next(); + }) +} diff --git a/bot/src/plugins/channelType.ts b/bot/src/plugins/channelType.ts new file mode 100644 index 0000000..60db49c --- /dev/null +++ b/bot/src/plugins/channelType.ts @@ -0,0 +1,39 @@ +/** + * This plugin checks if a channel is the specified type + * + * @author @Benzo-Fury [<@762918086349029386>] + * @version 1.0.0 + * @example + * ```ts + * import { channelType } from "../plugins/channelType"; + * import { ChannelType } from "discord.js" + * import { commandModule } from "@sern/handler"; + * export default commandModule({ + * plugins: [ channelType([ChannelType.GuildText], 'This cannot be used here') ], + * execute: (ctx) => { + * //your code here + * } + * }) + * ``` + */ +import { ChannelType } from "discord.js"; +import {CommandControlPlugin, CommandType, controller } from "@sern/handler"; +export function channelType( + channelType: ChannelType[], + onFail?: string +){ + return CommandControlPlugin(async (ctx) => { + let channel = ctx.channel?.type; + //for some reason the dm channel type was returning undefined at some points + if (channel === undefined) { + channel = ChannelType.DM; + } + if (channelType.includes(channel)) { + return controller.next(); + } + if (onFail) { + await ctx.reply(onFail); + } + return controller.stop(); + }) +} diff --git a/bot/src/plugins/confirmation.ts b/bot/src/plugins/confirmation.ts new file mode 100644 index 0000000..aa5d4de --- /dev/null +++ b/bot/src/plugins/confirmation.ts @@ -0,0 +1,106 @@ +/** + * This is buttonConfirmation plugin, it runs confirmation prompt in the form of buttons. + * Note that you need to use edit/editReply in the command itself because we are already replying in the plugin! + * Credits to original plugin of confirmation using reactions and its author! + * + * @author @EvolutionX-10 [<@697795666373640213>] + * @version 1.0.0 + * @example + * ```ts + * import { buttonConfirmation } from "../plugins/buttonConfirmation"; + * import { commandModule } from "@sern/handler"; + * export default commandModule({ + * plugins: [ buttonConfirmation() ], + * execute: (ctx) => { + * //your code here + * } + * }) + * ``` + */ + +import {CommandControlPlugin, CommandType, controller} from "@sern/handler"; +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + ComponentType, +} from "discord.js"; + +export function confirmation( + options?: Partial +) { + return CommandControlPlugin(async (ctx, args) => { + options = { + content: "Do you want to proceed?", + denialMessage: "Cancelled", + labels: ["No", "Yes"], + time: 60_000, + wrongUserResponse: "Not for you!", + ...options, + }; + + const buttons = options.labels!.map((l, i) => { + return new ButtonBuilder() + .setCustomId(l) + .setLabel(l) + .setStyle( i === 0 ? ButtonStyle.Danger : ButtonStyle.Success + ); + }); + const sent = await ctx.reply({ + content: options.content, + components: [ + new ActionRowBuilder().setComponents( + buttons + ), + ], + }); + + const collector = sent.createMessageComponentCollector({ + componentType: ComponentType.Button, + filter: (i) => i.user.id === ctx.user.id, + time: options.time, + }); + + return new Promise((resolve) => { + collector.on("collect", async (i) => { + await i.update({ components: [] }); + collector.stop(); + if (i.customId === options!.labels![1]) { + resolve(controller.next()); + return; + } + await i.editReply({ + content: options?.denialMessage, + }); + resolve(controller.stop()); + }); + + collector.on("end", async (c) => { + if (c.size) return; + buttons.forEach((b) => b.setDisabled()); + await sent.edit({ + components: [ + new ActionRowBuilder().setComponents( + buttons + ), + ], + }); + }); + + collector.on("ignore", async (i) => { + await i.reply({ + content: options?.wrongUserResponse, + ephemeral: true, + }); + }); + }); + }); +} + +interface ConfirmationOptions { + content: string; + denialMessage: string; + time: number; + labels: [string, string]; + wrongUserResponse: string; +} diff --git a/bot/src/plugins/correctFile.ts b/bot/src/plugins/correctFile.ts new file mode 100644 index 0000000..aa6c5d3 --- /dev/null +++ b/bot/src/plugins/correctFile.ts @@ -0,0 +1,6 @@ +import {CommandInitPlugin, controller} from "@sern/handler"; + + +export const correctFile = CommandInitPlugin(() => { + return controller.stop() +}) diff --git a/bot/src/plugins/disable.ts b/bot/src/plugins/disable.ts new file mode 100644 index 0000000..5fc2f1f --- /dev/null +++ b/bot/src/plugins/disable.ts @@ -0,0 +1,38 @@ +// @ts-nocheck +/** + * Disables a command entirely, for whatever reasons you may need. + * + * @author @jacoobes [<@182326315813306368>] + * @version 1.0.0 + * @example + * ```ts + * import { disable } from "../plugins/disable"; + * import { commandModule } from "@sern/handler"; + * export default commandModule({ + * plugins: [ disable() ], + * execute: (ctx) => { + * //your code here + * } + * }) + * ``` + */ +import { CommandType, EventPlugin, PluginType } from "@sern/handler"; +import { InteractionReplyOptions, ReplyMessageOptions } from "discord.js"; + +export function disable( + onFail?: + | string + | Omit + | ReplyMessageOptions +): EventPlugin { + return { + type: PluginType.Event, + description: "Disables command from responding", + async execute([ctx], controller) { + if (onFail !== undefined) { + await ctx.reply(onFail); + } + return controller.stop(); + }, + }; +} diff --git a/bot/src/plugins/dmOnly.ts b/bot/src/plugins/dmOnly.ts new file mode 100644 index 0000000..397586d --- /dev/null +++ b/bot/src/plugins/dmOnly.ts @@ -0,0 +1,29 @@ +/** + * This is dmOnly plugin, it allows commands to be run only in DMs. + * For discord.js you should have the Partials.Channel and DirectMessages intent enabled. + * @author @EvolutionX-10 [<@697795666373640213>] + * @version 1.0.0 + * @example + * ```ts + * import { dmOnly } from "../plugins/dmOnly"; + * import { commandModule } from "@sern/handler"; + * export default commandModule({ + * plugins: [dmOnly()], + * execute: (ctx) => { + * //your code here + * } + * }) + * ``` + */ +import {CommandControlPlugin, CommandType, controller } from "@sern/handler"; +export function dmOnly( + content?: string, + ephemeral?: boolean +) { + return CommandControlPlugin(async (ctx, _) => { + if (ctx.channel?.isDMBased()) return controller.next(); + + if (content) await ctx.reply({ content, ephemeral }); // Change this if you want or remove it for silent deny + return controller.stop(); + }) +} diff --git a/bot/src/plugins/filter.ts b/bot/src/plugins/filter.ts new file mode 100644 index 0000000..83b295f --- /dev/null +++ b/bot/src/plugins/filter.ts @@ -0,0 +1,637 @@ +import { + CommandControlPlugin, + type CommandType, + type Context, + controller, +} from "@sern/handler"; +import { + GuildMember, + GuildMemberRoleManager, + PermissionResolvable, + PermissionsBitField, + User, +} from "discord.js"; + + +export type Test = (context: Context) => boolean; + +export class Criteria { + public constructor( + public readonly name: string, + public readonly execute: Test, + public readonly children: Array + ) {} + toString() { + return this.name + ' ' + this.children.map(c => c.name).join(', ') + } +} + +export const or = (...filters: Array): FilterImpl => { + function execute(context: Context): boolean { + let pass = false; + tests: for (const filter of filters) { + if (filter.test(context)) { + pass = true; + break tests; + } + } + + return pass; + } + + const children: Array = filters.map((x) => x.criteria); + + return new FilterImpl( + new Criteria("or", execute, children), + `or(${filters.map((x) => x.message).join(", ")})` + ); +} + + +export const and = (...filters: Array): FilterImpl => { + function execute(context: Context): boolean { + for (const filter of filters) { + if (!filter.test(context)) { + return false; + } + } + + return true; + } + + const children: Array = filters.map((x) => x.criteria); + + return new FilterImpl( + new Criteria("and", execute, children), + `and(${filters.map((x) => x.message).join(", ")})` + ); + +} + + +export const not = (filter: FilterImpl): FilterImpl => { + function execute(context: Context): boolean { + return !filter.test(context); + } + + return new FilterImpl( + new Criteria("not", execute, [filter.criteria]), + `not(${filter.criteria})` + ); + } +export const custom =(execute: Test, message?: string): FilterImpl => { + return new FilterImpl(new Criteria("custom", execute, []), message); +} + +export const withCustomMessage = ( + filter: FilterImpl, + message?: string +): FilterImpl => { + return new FilterImpl(filter.criteria, message); +} + +export const hasGuildPermission = ( + permission: PermissionResolvable +): FilterImpl => { + const b = PermissionsBitField.resolve(permission); + const field = Object.entries(PermissionsBitField.Flags).find( + ([, v]) => v === b + ); + + if (field === undefined) { + throw new Error( + `unknown permission \`${permission}\` in filter \`hasGuildPermission\`` + ); + } + + const [name] = field; + + function execute(context: Context): boolean { + if (context.member !== null) { + if (typeof context.member.permissions === "string") { + return new PermissionsBitField(BigInt(context.member.permissions)).has(b); + } + return context.member.permissions.has(b); + } + + return true; + } + + return new FilterImpl( + new Criteria("hasGuildPermission", execute, []), + `has guild permission: ${name}` + ); +} +export const hasChannelPermission = ( + permission: PermissionResolvable, + channelId?: string + ): FilterImpl => { + const b = PermissionsBitField.resolve(permission); + const field = Object.entries(PermissionsBitField.Flags).find( + ([, v]) => v === b + ); + + if (field === undefined) { + throw new Error( + `unknown permission \`${permission}\` in filter \`hasChannelPermission\`` + ); + } + + const [name] = field; + + function execute(context: Context): boolean { + if (context.member !== null) { + const channel = + channelId !== undefined + ? context.guild?.channels.cache.get(channelId) + : context.channel; + + // ? + if (channel == undefined || channel === null) { + return false; + } + + if (channel.isDMBased()) { + return true; + } + + const field2 = channel.permissionsFor(context.user); + + // assume we have no permission overrides + if (field2 === null) { + if (context.member !== null) { + if (typeof context.member.permissions === "string") { + return new PermissionsBitField( + BigInt(context.member.permissions) + ).has(b); + } + + return context.member.permissions.has(b); + } + + return false; + } + + return field2.has(b); + } + + return true; + } + + return new FilterImpl( + new Criteria("hasChannelPermission", execute, []), + channelId !== undefined + ? `has channel permission ${name} in <#${channelId}>` + : `has channel permission ${name}` + ); + } + +export const canAddReactions =(channelId?: string): FilterImpl => { + return hasChannelPermission("AddReactions", channelId); +} + +export const canAttachFiles =(channelId?: string): FilterImpl => { + return hasChannelPermission("AttachFiles", channelId); +} + +export const canBanMembers = (): FilterImpl => { + return hasGuildPermission("BanMembers"); +} + +export const canChangeNickname = (): FilterImpl => { + return hasGuildPermission("ChangeNickname"); +} + +export const canConnect = (channelId?: string): FilterImpl => { + return hasChannelPermission("Connect", channelId); +} + +export const canCreateInstantInvite =(channelId?: string): FilterImpl => { + return hasChannelPermission("CreateInstantInvite", channelId); + } + +export const canDeafenMembers =(channelId?: string): FilterImpl => { + return hasChannelPermission("DeafenMembers", channelId); + } + +export const canEmbedLinks =(channelId?: string): FilterImpl => { + return hasChannelPermission("EmbedLinks", channelId); + } + +export const canKickMembers =(): FilterImpl => { + return hasGuildPermission("KickMembers"); + } + +export const canManageChannelWebhooks =(channelId?: string): FilterImpl => { + return hasChannelPermission("ManageWebhooks", channelId); + } + +export const canManageChannels =(channelId?: string): FilterImpl => { + return hasChannelPermission("ManageChannels", channelId); +} + +export const canManageEmojisAndStickers =(): FilterImpl => { + return hasGuildPermission("ManageEmojisAndStickers"); +} + +export const canManageGuild =(): FilterImpl => { + return hasGuildPermission("ManageGuild"); +} + +export const canManageGuildWebhooks =(): FilterImpl => { + return hasGuildPermission("ManageWebhooks"); +} + +export const canManageMessages =(channelId?: string): FilterImpl => { + return hasChannelPermission("ManageMessages", channelId); +} + +export const canManageNicknames = (): FilterImpl => { + return hasGuildPermission("ManageNicknames"); +} + +export const canManageRoles = (): FilterImpl => { + return hasGuildPermission("ManageRoles"); +} + +export const canMentionEveryone = (channelId?: string): FilterImpl => { + return hasChannelPermission("MentionEveryone", channelId); + } + +export const canMoveMembers = (channelId?: string): FilterImpl => { + return hasChannelPermission("MoveMembers", channelId); + } + +export const canMuteMembers = (channelId?: string): FilterImpl => { + return hasChannelPermission("MuteMembers", channelId); + } + +export const canPrioritySpeaker = (channelId?: string): FilterImpl => { + return hasChannelPermission("PrioritySpeaker", channelId); + } + +export const canReadMessageHistory = (channelId?: string): FilterImpl => { + return hasChannelPermission("ReadMessageHistory", channelId); + } + +export const canViewChannel = (channelId: string): FilterImpl => { + return hasChannelPermission("ViewChannel", channelId); + } + +export const canSendMessages = (channelId: string): FilterImpl => { + return hasChannelPermission("SendMessages", channelId); + } + +export const canSendTtsMessages = (channelId?: string): FilterImpl => { + return hasChannelPermission("SendTTSMessages", channelId); + } + +export const canSpeak = (channelId?: string): FilterImpl => { + return hasChannelPermission("Speak", channelId); + } + +export const canStream = (channelId?: string): FilterImpl => { + return hasChannelPermission("Stream", channelId); + } + +export const canUseExternalEmojis = (channelId?: string): FilterImpl => { + return hasChannelPermission("UseExternalEmojis", channelId); + } + +export const canUseVoiceActivity = (channelId?: string): FilterImpl => { + return hasChannelPermission("UseVAD", channelId); + } + +export const canViewAuditLog = (): FilterImpl => { + return hasGuildPermission("ViewAuditLog"); + } + +export const canViewGuildInsights = (): FilterImpl => { + return hasGuildPermission("ViewGuildInsights"); + } + +export const channelIdIn = (channelIds: Array): FilterImpl => { + function execute(context: Context): boolean { + return channelIds.includes( + context.isMessage() + ? context.message.channelId + : context.interaction.channelId + ); + } + + return new FilterImpl( + new Criteria("channelIdIn", execute, []), + `channel is one of: ${channelIds.map((v) => `<#${v}>`).join(", ")}` + ); + } + +export const hasEveryRole = (roles: Array): FilterImpl => { + return withCustomMessage( + and(...roles.map((v) => hasRole(v))), + `has all of: ${roles.map((v) => `<@&${v}>`).join(", ")}` + ); + } + +export const hasMentionableRole = (): FilterImpl => { + function execute(context: Context): boolean { + if (context.member !== null) { + if (context.member.roles instanceof GuildMemberRoleManager) { + return ( + context.member.roles.cache.filter((x) => x.mentionable === true) + .size > 0 + ); + } + + if (context.guild === null) { + return false; + } + + return context.member.roles + .map((roleId) => context.guild!.roles.cache.get(roleId)) + .filter((x) => x !== undefined) + .some((x) => x!.mentionable); + } + + return false; + } + return new FilterImpl( + new Criteria("hasMentionableRole", execute, []), + "has a mentionable role" + ); + } + +export const hasNickname = (nickname?: string): FilterImpl => { + function execute(context: Context): boolean { + if (context.member !== null) { + if (context.member instanceof GuildMember) { + if (nickname !== null) { + return context.member.nickname === nickname; + } + + return context.member.nickname !== null; + } + + if (nickname !== null) { + return context.member.nick === nickname; + } + + return ( + context.member.nick !== null && context.member.nick !== undefined + ); + } + + // dm members can technically have nicknames but they're per-user, so this should never be true. + return false; + } + return new FilterImpl(new Criteria("hasNickname", execute, []), "has a nickname"); + } + +export const hasParentId = (parentId: string): FilterImpl => { + function execute(context: Context): boolean { + if (context.channel !== null) { + if (context.channel.isDMBased()) { + return false; + } + + return context.channel.parentId === parentId; + } + + return false; + } + + return new FilterImpl( + new Criteria("hasParentId", execute, []), + `has channel parent <#${parentId}>` + ); + } + +export const hasRole = (roleId: string): FilterImpl => { + function execute(context: Context): boolean { + if (context.member !== null) { + if (context.member.roles instanceof GuildMemberRoleManager) { + return context.member.roles.cache.has(roleId); + } + + if (context.guild === null) { + return false; + } + + return context.member.roles.includes(roleId); + } + + // assume dm members have every role ever. + return true; + } + + return new FilterImpl( + new Criteria("hasRole", execute, []), + `has role <@&${roleId}>` + ); + } + +export const hasSomeRole = (roles: Array): FilterImpl => { + return withCustomMessage( + or(...roles.map((role) => hasRole(role))), + `has any of: ${roles.map((v) => `<@&${v}>`).join(", ")}` + ); + } + +export const isAdministator = (): FilterImpl => { + return hasGuildPermission("Administrator"); + } + +export const isChannelId = (channelId: string): FilterImpl => { + function execute(context: Context): boolean { + if (context.isMessage()) { + return context.message.channelId === channelId; + } + + return context.interaction.channelId === channelId; + } + + return new FilterImpl( + new Criteria("isChannelId", execute, []), + `is channel <#${channelId}>` + ); + } + +export const isChannelNsfw = (): FilterImpl => { + function execute(context: Context): boolean { + if (context.channel !== null) { + if (context.channel.isDMBased() || context.channel.isThread()) { + return false; + } + + return context.channel.nsfw; + } + + return false; + } + return new FilterImpl( + new Criteria("isChannelNsfw", execute, []), + "channel marked as nsfw" + ); + } + +export const isGuildOwner = (): FilterImpl => { + function execute(context: Context): boolean { + if (context.guild !== null) { + return context.guild.ownerId === context.user.id; + } + + return true; + } + return new FilterImpl( + new Criteria("isGuildOwner", execute, []), + "is guild owner" + ); + } + +export const isBotOwner = (): FilterImpl => { + function execute(context: Context): boolean { + if (context.client.application !== null) { + if (context.client.application.owner !== null) { + if (context.client.application.owner instanceof User) { + return context.user.id === context.client.application.owner.id; + } + + return context.client.application.owner.members.has(context.user.id); + } + } + + // nope + return false; + } + return new FilterImpl(new Criteria("isBotOwner", execute, []), "is bot owner"); + } + +export const isUserId = (userId: string): FilterImpl => { + function execute(context: Context): boolean { + return context.user.id === userId; + } + return new FilterImpl( + new Criteria("isUserId", execute, []), + `is user: <@${userId}>` + ); + } + +export const parentIdIn = (parentIds: Array): FilterImpl => { + return withCustomMessage( + or(...parentIds.map((v) => hasParentId(v))), + `channel parent is one of: ${parentIds.map((v) => `<#${v}>`).join(", ")}` + ); + } + +export const userIdIn = (userIds: Array): FilterImpl => { + return withCustomMessage( + or(...userIds.map((v) => isUserId(v))), + `user is one of: ${userIds.map((v) => `<@${v}>`).join(", ")}` + ); + } + +export const isInGuild = (): FilterImpl => { + function execute(context: Context): boolean { + return context.guildId !== null; + } + + return new FilterImpl(new Criteria("isInGuild", execute, []), "is in guild"); + } + +export const isInDm = (): FilterImpl => { + const notInGuild = compose(not, isInGuild); + return withCustomMessage(notInGuild(), "is in dm"); +} + +export const never = (): FilterImpl => { + function execute(context: Context): boolean { + void context; + return false; + } + return new FilterImpl(new Criteria("never", execute, []), "never"); +} + +export const always = (): FilterImpl => { + function execute(context: Context): boolean { + void context; + return true; + } + return new FilterImpl(new Criteria("always", execute, []), "always"); +} +type CtxMap = (arg: T) => FilterImpl; + +/** + * Call FilterImpls in right to left order. + * @example + * import { compose, isUserId, not } from '../plugins/filter' + * const isNotUserId = compose(not, isUserId) + * + */ +export const compose = (...funcs: CtxMap[]): CtxMap => { + return (arg: T): FilterImpl => + //@ts-ignore + funcs.reduceRight((result, func) => func(result), arg); +} + + +export class FilterImpl { + public readonly test: Test; + + public constructor( + public readonly criteria: Criteria, + public message?: string + ) { + this.test = this.criteria.execute; + } +} + + +export type FilterOptions = { + condition: Array | FilterImpl, + onFailed?: (context: Context, filters: Array) => unknown +}; +/** + * Generalized `filter` plugin. revised by jacoobes, all credit to original author. + * Perform declarative conditionals as plugins. + * @author @trueharuu [<@504698587221852172>] + * @version 2.0.0 + * @example + * import { filter, not, isGuildOwner, canMentionEveryone } from '../plugins/filter'; + * import { commandModule } from '@sern/handler'; + * + * export default commandModule({ + * plugins: filter({ condition: [not(isGuildOwner()), canMentionEveryone()] }), + * async execute(context) { + * // your code here + * } + * }); + */ + +export const filter = + (options: FilterOptions) => { + return CommandControlPlugin(async (context) => { + const arrayifiedCondition = Array.isArray(options.condition) ? options.condition : [options.condition] + const value = and(...arrayifiedCondition).test(context); + + if (value) { + return controller.next(); + } + + if (options.onFailed !== undefined) { + await options.onFailed(context, arrayifiedCondition); + } else { + await context.reply({ + ephemeral: true, + content: `you do not match the criteria for this command:\n${arrayifiedCondition + .map((x) => x.message) + .filter((x) => x !== undefined) + .join("\n")}`, + allowedMentions: { + repliedUser: false, + parse: [], + }, + }); + } + + return controller.stop(); + }); + }; diff --git a/bot/src/plugins/filterA.ts b/bot/src/plugins/filterA.ts new file mode 100644 index 0000000..fa722c6 --- /dev/null +++ b/bot/src/plugins/filterA.ts @@ -0,0 +1,40 @@ +import { PluginType, makePlugin, controller, ControlPlugin } from "@sern/handler"; +import type { AutocompleteInteraction } from 'discord.js' + +/** + * @plugin + * filters autocomplete interaction that pass the criteria + * @author @jacoobes [<@182326315813306368>] + * @version 1.0.0 + * @example + * ```ts + * import { CommandType, commandModule } from "@sern/handler"; + * import { filterA } from '../plugins/filterA.js' + * export default commandModule({ + * type : CommandType.Slash, + * options: [ + * { + * autocomplete: true, + * command : { + * //only accept autocomplete interactions that include 'poo' in the text + * onEvent: [filterA(s => s.includes('poo'))], + * execute: (autocomplete) => { + * let data = [{ name: 'pooba', value: 'first' }, { name: 'pooga', value: 'second' }] + * autocomplete.respond(data) + * } + * } + * } + * ], + * execute: (ctx, args) => {} + * }) + * @end + */ + +export const filterA = (pred: (value: string) => boolean) => { + return makePlugin(PluginType.Control, (a: AutocompleteInteraction) => { + if(pred(a.options.getFocused())) { + return controller.next(); + } + return controller.stop(); + }) as ControlPlugin; +} diff --git a/bot/src/plugins/fromCallback.ts b/bot/src/plugins/fromCallback.ts new file mode 100644 index 0000000..f8d8b87 --- /dev/null +++ b/bot/src/plugins/fromCallback.ts @@ -0,0 +1,36 @@ +//@ts-nocheck +/** + * @plugin + * fromCallback turns a callback into a plugin result. + * if the callback returns truthy value, plugin continues. + * This control plugin works for every command type. The arguments of the callback + * mirror the execute method on the current module. + * @author @jacoobes [<@182326315813306368>] + * @version 1.0.0 + * @example + * ```ts + * const myServer = "941002690211766332"; + * export default commandModule({ + * type: CommandType.Both, + * plugins: [ + * fromCallback((ctx, args) => ctx.guildId == myServer) + * ], + * execute: ctx => { + * ctx.reply("I only respond in myServer!"); + * } + * }) + * ``` + * @end + */ + + +import { PluginType, makePlugin, controller } from "@sern/handler"; + +export const fromCallback = (cb: (...args: any[]) => boolean) => + makePlugin(PluginType.Control, (...args) => { + console.log(args) + if(cb.apply(null, args)) { + return controller.next(); + } + return controller.stop(); + }); diff --git a/bot/src/plugins/json-params.ts b/bot/src/plugins/json-params.ts new file mode 100644 index 0000000..65a1104 --- /dev/null +++ b/bot/src/plugins/json-params.ts @@ -0,0 +1,5 @@ +import { CommandControlPlugin, CommandType, controller } from "@sern/handler"; + +export const json = CommandControlPlugin((ctx, args) => { + return controller.next({ 'json/data': JSON.parse(args.params!) }); +}) diff --git a/bot/src/plugins/nsfwOnly.ts b/bot/src/plugins/nsfwOnly.ts new file mode 100644 index 0000000..f1a757f --- /dev/null +++ b/bot/src/plugins/nsfwOnly.ts @@ -0,0 +1,48 @@ +/** + * This plugin checks if the channel is nsfw and responds to user with a specified response if not nsfw + * + * @author @Benzo-Fury [<@762918086349029386>] + * @version 1.0.0 + * @example + * ```ts + * import { nsfwOnly } from "../plugins/nsfwOnly"; + * import { commandModule } from "@sern/handler"; + * export default commandModule({ + * plugins: [ nsfwOnly('response', true) ], + * execute: (ctx) => { + * //your code here + * } + * }) + * ``` + */ +import { + ChannelType, + GuildTextBasedChannel, + TextBasedChannel, + TextChannel, +} from "discord.js"; +import {CommandControlPlugin, CommandType, controller } from "@sern/handler"; +function isGuildText(channel: TextBasedChannel|null): channel is GuildTextBasedChannel { + return (channel?.type == ChannelType.GuildPublicThread || + channel?.type == ChannelType.GuildPrivateThread); +} +export function nsfwOnly(onFail: string, ephemeral: boolean) { + return CommandControlPlugin(async (ctx, _) => { + if (ctx.guild === null) { + await ctx.reply({ content: onFail, ephemeral }); + return controller.stop(); + } + //channel is thread (not supported by nsfw) + if (isGuildText(ctx.channel) == true) { + await ctx.reply({ content: onFail, ephemeral }); + return controller.stop(); + } + if (!(ctx.channel! as TextChannel).nsfw) { + //channel is not nsfw + await ctx.reply({ content: onFail, ephemeral }); + return controller.stop(); + } + //continues to command if nsfw + return controller.next(); + }); +} diff --git a/bot/src/plugins/ownerOnly.ts b/bot/src/plugins/ownerOnly.ts new file mode 100644 index 0000000..80e035b --- /dev/null +++ b/bot/src/plugins/ownerOnly.ts @@ -0,0 +1,30 @@ +// @ts-nocheck +/** + * This is OwnerOnly plugin, it allows only bot owners to run the command, like eval. + * + * @author @EvolutionX-10 [<@697795666373640213>] + * @version 1.2.0 + * @example + * ```ts + * import { ownerOnly } from "../plugins/ownerOnly"; + * import { commandModule } from "@sern/handler"; + * export default commandModule({ + * plugins: [ ownerOnly() ], // can also pass array of IDs to override default owner IDs + * execute: (ctx) => { + * //your code here + * } + * }) + * ``` + */ + +import { CommandType, CommandControlPlugin, controller } from "@sern/handler"; +const ownerIDs = ["182326315813306368"]; //! Fill your ID +export function ownerOnly(override?: string[]) { + return CommandControlPlugin((ctx) => { + if ((override ?? ownerIDs).includes(ctx.user.id)) + return controller.next(); + //* If you want to reply when the command fails due to user not being owner, you can use following + // await ctx.reply("Only owner can run it!!!"); + return controller.stop(); //! Important: It stops the execution of command! + }); +} diff --git a/bot/src/plugins/permCheck.ts b/bot/src/plugins/permCheck.ts new file mode 100644 index 0000000..7884997 --- /dev/null +++ b/bot/src/plugins/permCheck.ts @@ -0,0 +1,39 @@ +// @ts-nocheck +/** + * @plugin + * This is perm check, it allows users to parse the permission you want and let the plugin do the rest. (check user for that perm). + * + * @author @Benzo-Fury [<@762918086349029386>] + * @version 1.0.1 + * @example + * ```ts + * import { permCheck } from "../plugins/permCheck"; + * import { commandModule } from "@sern/handler"; + * export default commandModule({ + * plugins: [ permCheck('permission', 'No permission response') ], + * execute: (ctx) => { + * //your code here + * } + * }) + * ``` + * @end + */ + +import type { GuildMember, PermissionResolvable } from "discord.js"; +import { CommandControlPlugin, CommandType, controller } from "@sern/handler"; +export function permCheck(perm: PermissionResolvable, response: string) { + return CommandControlPlugin(async (ctx, args) => { + if (ctx.guild === null) { + await ctx.reply("This command cannot be used here"); + console.warn( + "PermCheck > A command stopped because we couldn't check a users permissions (was used in dms)", + ); //delete this line if you dont want to be notified when a command is used outside of a guild/server + return controller.stop(); + } + if (!(ctx.member! as GuildMember).permissions.has(perm)) { + await ctx.reply(response); + return controller.stop(); + } + return controller.next(); + }); +} diff --git a/bot/src/plugins/publish.ts b/bot/src/plugins/publish.ts new file mode 100644 index 0000000..7c43a7b --- /dev/null +++ b/bot/src/plugins/publish.ts @@ -0,0 +1,215 @@ +// @ts-nocheck +/** + * @plugin + * [DEPRECATED] It allows you to publish your application commands using the discord.js library with ease. + * + * @author @EvolutionX-10 [<@697795666373640213>] + * @version 2.0.0 + * @example + * ```ts + * import { publish } from "../plugins/publish"; + * import { commandModule } from "@sern/handler"; + * export default commandModule({ + * plugins: [ publish() ], // put an object containing permissions, ids for guild commands, boolean for dmPermission + * // plugins: [ publish({ guildIds: ['guildId'], defaultMemberPermissions: 'Administrator'})] + * execute: (ctx) => { + * //your code here + * } + * }) + * ``` + * @end + */ +import { + CommandInitPlugin, + CommandType, + controller, + SernOptionsData, + SlashCommand, + Service, +} from "@sern/handler"; +import { + ApplicationCommandData, + ApplicationCommandType, + ApplicationCommandOptionType, + PermissionResolvable, +} from "discord.js"; + +export const CommandTypeRaw = { + [CommandType.Both]: ApplicationCommandType.ChatInput, + [CommandType.CtxUser]: ApplicationCommandType.User, + [CommandType.CtxMsg]: ApplicationCommandType.Message, + [CommandType.Slash]: ApplicationCommandType.ChatInput, +} as const; + +export function publish< + T extends + | CommandType.Both + | CommandType.Slash + | CommandType.CtxMsg + | CommandType.CtxUser, +>(options?: PublishOptions) { + return CommandInitPlugin(async ({ module }) => { + // Users need to provide their own useContainer function. + let client; + try { + client = (await import("@sern/handler")).Service("@sern/client"); + } catch { + const { useContainer } = await import("../index.js"); + client = useContainer("@sern/client")[0]; + } + const defaultOptions = { + guildIds: [], + dmPermission: undefined, + defaultMemberPermissions: null, + }; + + options = { ...defaultOptions, ...options } as PublishOptions & + ValidPublishOptions; + let { defaultMemberPermissions, dmPermission, guildIds } = + options as unknown as ValidPublishOptions; + + function c(e: unknown) { + console.error("publish command didnt work for", module.name); + console.error(e); + } + + const log = + (...message: any[]) => + () => + console.log(...message); + const logged = (...message: any[]) => log(message); + /** + * a local function that returns either one value or the other, + * depending on {t}'s CommandType. If the commandtype of + * this module is CommandType.Both or CommandType.Text or CommandType.Slash, + * return 'is', else return 'els' + * @param t + * @returns S | T + */ + const appCmd = (t: V) => { + return (is: S, els: T) => ((t & CommandType.Both) !== 0 ? is : els); + }; + const curAppType = CommandTypeRaw[module.type]; + const createCommandData = () => { + const cmd = appCmd(module.type); + return { + name: module.name, + type: curAppType, + description: cmd(module.description, ""), + options: cmd( + optionsTransformer((module as SlashCommand).options ?? []), + [], + ), + defaultMemberPermissions, + dmPermission, + } as ApplicationCommandData; + }; + + try { + const commandData = createCommandData(); + + if (!guildIds.length) { + const cmd = (await client.application!.commands.fetch()).find( + (c) => c.name === module.name && c.type === curAppType, + ); + if (cmd) { + if (!cmd.equals(commandData, true)) { + logged( + `Found differences in global command ${module.name}`, + ); + cmd.edit(commandData).then( + log( + `${module.name} updated with new data successfully!`, + ), + ); + } + return controller.next(); + } + client + .application!.commands.create(commandData) + .then(log("Command created", module.name)) + .catch(c); + return controller.next(); + } + + for (const id of guildIds) { + const guild = await client.guilds.fetch(id).catch(c); + if (!guild) continue; + const guildCmd = (await guild.commands.fetch()).find( + (c) => c.name === module.name && c.type === curAppType, + ); + if (guildCmd) { + if (!guildCmd.equals(commandData, true)) { + logged(`Found differences in command ${module.name}`); + guildCmd + .edit(commandData) + .then( + log( + `${module.name} updated with new data successfully!`, + ), + ) + .catch(c); + continue; + } + continue; + } + guild.commands + .create(commandData) + .then(log("Guild Command created", module.name, guild.name)) + .catch(c); + } + return controller.next(); + } catch (e) { + logged("Command did not register" + module.name); + logged(e); + return controller.stop(); + } + }); +} + +export function optionsTransformer(ops: Array) { + return ops.map((el) => { + switch (el.type) { + case ApplicationCommandOptionType.String: + case ApplicationCommandOptionType.Number: + case ApplicationCommandOptionType.Integer: { + return el.autocomplete && "command" in el + ? (({ command, ...el }) => el)(el) + : el; + } + default: + return el; + } + }); +} + +export type NonEmptyArray = [T, ...T[]]; + +export interface ValidPublishOptions { + guildIds: string[]; + dmPermission: boolean; + defaultMemberPermissions: PermissionResolvable; +} + +interface GuildPublishOptions { + guildIds?: NonEmptyArray; + defaultMemberPermissions?: PermissionResolvable; + dmPermission?: never; +} + +interface GlobalPublishOptions { + defaultMemberPermissions?: PermissionResolvable; + dmPermission?: false; + guildIds?: never; +} + +type BasePublishOptions = GuildPublishOptions | GlobalPublishOptions; + +export type PublishOptions = BasePublishOptions & + ( + | Required> + | ( + | Required> + | Required> + ) + ); diff --git a/bot/src/plugins/requirePermission.ts b/bot/src/plugins/requirePermission.ts new file mode 100644 index 0000000..a50afa7 --- /dev/null +++ b/bot/src/plugins/requirePermission.ts @@ -0,0 +1,95 @@ +/** + * This is perm check, it allows users to parse the permission you want and let the plugin do the rest. (check bot or user for that perm). + * + * @author @Benzo-Fury [<@762918086349029386>] + * @author @needhamgary [<@342314924804014081>] + * @version 1.2.0 + * @example + * ```ts + * import { requirePermission } from "../plugins/myPermCheck"; + * import { commandModule, CommandType } from "@sern/handler"; + * export default commandModule({ + * plugins: [ requirePermission('target', 'permission', 'No response (optional)') ], + * execute: (ctx) => { + * //your code here + * } + * }) + * ``` + */ + +import type { GuildMember, PermissionResolvable } from "discord.js"; +import { + CommandType, CommandControlPlugin, controller, +} from "@sern/handler"; + +function payload(resp?: string) { + return { + fetchReply: true, + content: resp, + allowedMentions: { repliedUser: false }, + } as const; +} + +export function requirePermission( + target: "user" | "bot" | "both", + perm: PermissionResolvable[], + response?: string +) { + return CommandControlPlugin(async (ctx, _) => { + if (ctx.guild === null) { + ctx.reply(payload("This command cannot be used here")); + console.warn( + "PermCheck > A command stopped because we couldn't check a users permissions (was used in dms)" + ); //delete this line if you dont want to be notified when a command is used outside of a guild/server + return controller.stop(); + } + const bot = (await ctx.guild.members.fetchMe({ + cache: false, + })!) as GuildMember; const memm = ctx.member! as GuildMember; + switch (target) { + //*********************************************************************************************************************// + case "bot": + if (!bot.permissions.has(perm)) { + if (!response) + response = `I cannot use this command, please give me \`${perm.join( + ", " + )}\` permission(s).`; + await ctx.reply(payload(response)); + return controller.stop(); + } + return controller.next(); + //*********************************************************************************************************************// + case "user": + if (!memm.permissions.has(perm)) { + if (!response) + response = `You cannot use this command because you are missing \`${perm.join( + ", " + )}\` permission(s).`; + await ctx.reply(payload(response)); + return controller.stop(); + } + return controller.next(); + //*********************************************************************************************************************// + case "both": + if ( + !bot.permissions.has(perm) || + !memm.permissions.has(perm) + ) { + if (!response) + response = `Please ensure <@${bot.user.id}> and <@${ + memm.user.id + }> both have \`${perm.join(", ")}\` permission(s).`; + await ctx.reply(payload(response)); + return controller.stop(); + } + return controller.next(); + //*********************************************************************************************************************// + default: + console.warn( + "Perm Check >>> You didn't specify user or bot." + ); + ctx.reply(payload("User or Bot was not specified.")); + return controller.stop(); + } + }); +} diff --git a/bot/src/plugins/serverOnly.ts b/bot/src/plugins/serverOnly.ts new file mode 100644 index 0000000..844f331 --- /dev/null +++ b/bot/src/plugins/serverOnly.ts @@ -0,0 +1,38 @@ +/** + * Checks if a command is available in a specific server. + * + * @author @Peter-MJ-Parker [<@1017182455926624316>] + * @version 1.0.0 + * @example + * ```ts + * import { commandModule, CommandType } from "@sern/handler"; + * import { serverOnly } from "../plugins/serverOnly"; + * export default commandModule({ + * type: CommandType.Both, + * plugins: [serverOnly(["guildId"], failMessage)], // fail message is the message you will see when the command is ran in the wrong server. + * description: "command description", + * execute: async (ctx, args) => { + * // your code here + * }, + * }); + * ``` + */ + +import { CommandType, controller, CommandControlPlugin } from "@sern/handler"; + +export function serverOnly( + guildId: string[], + failMessage = "This command is not available in this guild. \nFor permission to use in your server, please contact my developer." +) { + return CommandControlPlugin(async (ctx, _) => { + if (!guildId.includes(ctx.guildId!)) { + ctx.reply(failMessage).then(async (m) => { + setTimeout(async () => { + await m.delete(); + }, 3000); + }); + return controller.stop(); + } + return controller.next(); + }) +} diff --git a/bot/src/presence.ts b/bot/src/presence.ts new file mode 100644 index 0000000..3c08591 --- /dev/null +++ b/bot/src/presence.ts @@ -0,0 +1,34 @@ +import { Presence } from '@sern/handler' +import { ActivityType, ClientPresenceStatus } from 'discord.js'; + +function shuffleArray(array: T[]) { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return [...array]; +} + +const statuses = [[ActivityType.Watching, "the sern community", "online"], + [ActivityType.Listening, "Evo", "dnd"], + [ActivityType.Playing, "with @sern/cli", "idle"], + [ActivityType.Watching, "sern bots", "dnd"], + [ActivityType.Watching, "github stars go brrr", "online"], + [ActivityType.Listening, "Spotify", "dnd"], + [ActivityType.Listening, "what's bofa", "idle"]] satisfies + [ActivityType, string, ClientPresenceStatus][]; + +export default Presence.module({ + execute: () => { + const [type, name, status] = statuses.at(0)!; + return Presence + .of({ activities: [ { type, name } ], status }) //start your presence with this. + .repeated(() => { + const [type, name, status] = [...shuffleArray(statuses)].shift()!; + return { + status, + activities: [{ type, name }] + }; + }, 60_000); //repeat and setPresence with returned result every minute + } +}) diff --git a/bot/src/tasks/dbbackup.ts b/bot/src/tasks/dbbackup.ts new file mode 100644 index 0000000..ff39e99 --- /dev/null +++ b/bot/src/tasks/dbbackup.ts @@ -0,0 +1,9 @@ +import { scheduledTask } from "@sern/handler"; + + +export default scheduledTask({ + trigger: "* * * * *", + execute: (args, { deps }) => { + console.log("hello") + } +}) diff --git a/bot/tsconfig.json b/bot/tsconfig.json new file mode 100644 index 0000000..8e3c033 --- /dev/null +++ b/bot/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./.sern/tsconfig.json" +}