diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 7923b70..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.gitignore b/.gitignore index c04c76a..8ef2274 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules config.jsonc +secrets.jsonc target *.0x/ docs/jsdoc/ diff --git a/Dockerfile b/Dockerfile index ab77418..fdc3315 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,4 +18,4 @@ COPY target/ ./target/ # Run it without compiling -CMD ["make", "run"] \ No newline at end of file +CMD ["make", "prod-run"] diff --git a/Makefile b/Makefile index f069edf..a449156 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,13 @@ start: build # instead of the compiled js node --enable-source-maps ./target/core/main.js +# Start the bot, without compiling, running using the production node profile +# (will start more slowly, but have better performance afterwords) +# https://nodejs.dev/en/learn/nodejs-the-difference-between-development-and-production/ +# (will only work in linux) +prod-run: + NODE_ENV=production node --enable-source-maps ./target/core/main.js + # Compile code and run unit tests https://nodejs.org/api/test.html#test-runner test: build node --test ./target/tests @@ -50,7 +57,7 @@ docker-run: # -d: run in daemon mode and start in the background # --mount type=bind...: take the config file in this directory, and mount it to the equivalent directory in the docker container # turingbot: the container to run - docker run --rm --name turingbot -d --mount type=bind,source=$(pwd)/config.jsonc,target=/usr/src/turingbot/config.jsonc turingbot + docker run --rm --name turingbot -d --mount type=bind,source=$(pwd)/config.jsonc,target=/usr/src/turingbot/config.jsonc --mount type=bind,source=$(pwd)/secrets.jsonc,target=/usr/src/turingbot/secrets.jsonc turingbot # Make a bot docker container and start it in daemon mode docker-start: docker-build diff --git a/config.default.jsonc b/config.default.jsonc index 4fc348f..02adbd9 100644 --- a/config.default.jsonc +++ b/config.default.jsonc @@ -1,25 +1,10 @@ { - /** - * https://discord.com/developers/docs/topics/oauth2 - * https://discord.com/developers - * create new application -> bot -> new bot user -> oath2 -> bot -> administrator -> copy link - * rememember to set your bot intents - */ - "authToken": "your-auth-token", - "applicationId": "", - // database configuration - "mongodb": { - // These are filled out using the settings from `k8s/dev.yaml`, but should probably - // be set to something different in prod - // If using Atlas, set the protocol to mongodb+srv:// - // Otherwise set it to mongodb:// - "protocol": "mongodb://", - "address": "mongodb.default.svc.cluster.local", - "username": "root", - "password": "root" - }, // Specifically event logging "logging": { + // restrict this command to people with the `administrator` perm + "permissions": { + "requiredPerms": ["administrator"] + }, /** * Different levels of verbose logging. * @@ -41,67 +26,15 @@ "userIds": [""], "verboseLevel": 1 }, - "loggingChannel": { "loggingChannelId": "", "verboseLevel": 3 } }, - "testing": { - // The user ID for the bot test user - "userID": "" - }, - // The key for each module should match the one specified in the root module initialization // if the `enabled` key is missing, the extension will be disabled by default, and a warning issued - - // --ALL IDS MUST BE STRINGS, NOT NUMBERS-- - - /* - Here's an example permissions config and boilerplate: - You can use this by setting the `permissions` key of each extension - to an object like this. It's not required for any of these keys to be set. - ``` - "permissions": { - // a list of all possible permission restrictions - "requiredPerms": ["kick", "ban", "timeout", "manage_roles", "administrator"], - "allowed": { - // list of user IDs - "users": [], - // list of role IDs (or names? needs to be discussed) - "roles": [], - // list of channel IDs - "channels": [], - // a list of category IDs - "categories": [], - }, - "denied": { - // a list of user IDs - "users": [], - // a list of role IDs - "roles": [], - // a list of channel IDs - "channels": [], - // a list of category IDs. - "categories": [], - } - // if you also want to restrict permissions per submodule, it can be done as below - "submodulePermissions": { - // each key in this object is the name of a submodule, so for a command like `/factoid all`, - // you could restrict it with a key named `"all"`. - "": { - // the same structure can be nested - "requiredPerms": [], - // you can also nest submodulePermissions to restrict a command 3 layers deep, like "/factoid all html" - "submodulePermissions: { - "": {} - } - } - } - } - ``` - */ + // permissions are explained in the production environment doc "modules": { "logging": { "enabled": true, @@ -112,9 +45,8 @@ // communication channel on the left, logging channel on the right "channelMap": { }, - // this map contains a list of channels that will not be logged. also populated by `logging populate` - "channelBlacklist": [ - ] + // this array contains a list of channels that will not be logged. also populated by `logging populate` + "channelBlacklist": [] }, "factoid": { "enabled": true, @@ -144,9 +76,7 @@ "roleIdOnApprove": "" }, "google": { - "enabled": true, - "ApiKey": "", - "CseId": "" + "enabled": true }, // API key is shared with google "youtube": { @@ -158,24 +88,12 @@ "autopaste": { "enabled": true, // Make sure the ids are all strings, not numbers - "immuneRoleIds": [], - "maxLength": 100, - "pasteFooterContent": "", - "pasteApi": "" - }, - "filter": { - "enabled": true, - "exemptRoles": [] - }, - "warn": { - "enabled": true, - "maxWarns": 3 - }, - "warns": { - "enabled": true - }, - "unwarn": { - "enabled": true + "immuneRoleIds": [], + "maxLength": 100, + // optionally, include a footer for autopasted messages + "pasteFooterContent": "Note: long messages are automatically pasted", + // the URL of a valid linx paste server + "pasteApi": "" } } } diff --git a/docs/guides/developing_modules.md b/docs/guides/developing_modules.md index a9d978a..1174f43 100644 --- a/docs/guides/developing_modules.md +++ b/docs/guides/developing_modules.md @@ -158,6 +158,70 @@ const conch = new util.RootModule( ); ``` +## Understanding Javascript Functions (and callbacks) +There are a lot of different ways to define what's functionally (no pun intended) the same thing in Javascript. In Javascript, functions can be treated as values, so it's possible to use them as arguments, re-assign them, and store them as variables. + +```typescript +// normal +function hiThere(args) { + //code you want to run goes here +} + +// while largely considered bad/dated practice, you can do this: +// this is assigning hiThere to a function. +const hiThere = function () { +} + +// "callback" or arrow syntax is a shorthand way to define functions, it's commonly used to pass functions as arguments eg: iWantToRunThisCode(() => {}) +const hiThere = (args) => { + // code you want to run goes here +}; + +// if you only have one arg, although this is less commonly used, you can do this, and exclude parentheses: +const hiThere = arg => { + // code you want to run goes here +}; + +// although a bit odd, a block and statement are usually interchangeable, so if you only want to run one statement, you can do that too +// the return value will be the result of arbitrary statement +const hiThere = arg => arbitraryStatement(); + +// there's also filter syntax, where you return the result of a comparison +const isHello = arg => arg === "hello"; + +// these can also all be async: +async function hiThere() {} +const hiThere = async () => {} +const hiThere = async arg => {} +// and so on +``` + +### Callbacks +It's possible to pass a function as an argument to another function. + +```typescript +// this function accepts a callback +function callAnotherFunction(functionToCall: () => {}) { + console.log('calling function'); + functionToCall(); +} + +// here, we can pass a function as an argument +callAnotherFunction(() => { + console.log('I got called!'); +}); + +// because functions can also be treated as values, we can define a function, then pass it in +function functionToCall() { + console.log('I got called, and passed as a variable!'); +} + +// excluding the parentheses means passing by value, not evaluating the function +callAnotherFunction(functionToCall) { + +} +``` + ## Using MongoDB MongoDB is the database of choice for this project, and it can be accessed by specifying it as a Dependency in the root module's constructor. This will disable your module if MongoDB is inaccessible. diff --git a/docs/guides/prod_environment.md b/docs/guides/prod_environment.md new file mode 100644 index 0000000..497017a --- /dev/null +++ b/docs/guides/prod_environment.md @@ -0,0 +1,172 @@ +# Setting up a production deployment of TuringBot +This guide will explain how to obtain all necessary API keys, populate the config, and deploy an instance of TuringBot, running in docker. TuringBot uses `.jsonc` files as the primary configuration format, they're [JSON](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/JSON) files, but with comments. + +## Creating a project directory +TuringBot relies on two separate files for functionality, `config.jsonc`, and `secrets.jsonc`. For convenience, these can be placed in a project directory. + +In this directory, create a file named `config.jsonc`, and open the repository, and copy the contents of `config.default.jsonc` into this `config.jsonc`. There's a few important keys that need to be addressed, but the defaults for most settings are usually fine. In the `logging` object, you can adjust the level at which messages will be logged, and specify a logging channel, and recipients for DM logging. `.logging.directMessageLogging.UserIds` contains an array of users that will receive DM logs, set `verboseLevel` to `0` if you do not want any users to receive logs. `.logging.loggingChannel.loggingChannelId` should be set to a channel you'd like to designate for bot events. If you do not want events logged to a channel, then set `verboseLevel` to `0`. + +> **All IDs should be set as a `"string"`, not a number.** + +- 0: No logging, no events will be logged + +- 1: Very minimalist logging, will log important errors and warnings. + +- 2: Slightly more verbose logging, a safe choice. + +- 3: The most verbose level of logging, includes all events. + +Scrolling down to the `modules` section, you can find a list of every module, and settings for each module. +### Module settings +- Logging:
+If the `loggingCategory` key is set to the ID of a category, you can use `/logging populate` to fill out the `channelMap` section of the config, and generate new logging channels if needed. Otherwise, you can manually define logging channels by setting a new key in `channelMap` to the ID of the actual channel you'd like to log, and the value to the channel you'd like to log messages to. Optionally, `channelBlacklist` can be filled out to contain a list of channel IDs that you *do not want* logged, and they'll be unselected by default when running `/logging populate`. + +- Application:
+Set the `channel` key to the channel you'd like applications to be sent to. `questions` contains a list of prompts that the user will be asked about. + +- Autopaste:
+ You can update `pasteFooterContent` to customize the autopaste embed footer to your liking. `pasteApi` should point to a valid linx paste server. + +### API keys +#### Discord +Go to https://discord.com/developers/applications, and sign in to your Discord account if necessary. + +Click on "New Application", enter a name, and click "Create". + +Under the left menu, select "Bot", and then select "Add Bot" + +Under the "Bot" menu, select "Reset Token", and then keep careful note of this token, you'll need it later, *do not share it*. + +Turn on "Privileged Gateway Intents", and toggle on "Guild", "MessageContent", and "Guild Messages". More may be needed as the bot receives updates, add them accordingly. + +To add the bot to a server, go to "OAuth2" -> "URL Generator"; Then select "bot" under "Scopes", and then toggle "Administrator". The generated link will be at the bottom of the page. + +#### MongoDB +MongoDB Atlas is the recommended database for a production environment. Running a local MongoDB instances is not (yet, as of 2023-09-08) supported in production, nor is it recommended to run a local DB instance. + +Please refer to the [guide](./mongodb_atlas_setup.md). + +#### Google/Youtube +Please refer to the [guide](./google_setup.md). + +## Permissions +Slash command permissions can be configured per module, under the `permissions` key. + +Restriction of slash commands via permission level can be set via the `permissions.requiredPerms` key. It accepts an array of permissions, where the user must have *all* listed permissions to execute the command. Valid options are `kick`, `ban`, `timeout`, `manage_roles`, and `administrator`. They must be specified in that *exact format*, as a string. + +Permissions can also be controlled with the `allowed` and `denied` keys. If a setting is made under the `allowed` key, then only execution contexts that *meet* that requirement will be able to execute. If a setting is made under the `denied` key, execution will be allowed unless that setting matches the execution context. Under the `denied` and `allowed` keys, you can specify `users`, `roles`, `channels`, or `categories`. + +```jsonc +{ + "users": [], + "roles": [], + "channels": [], + "categories": [], +} +``` + +Each key accepts a list of IDs, again, specified as a string. + +If you'd like to restrict subcommand permissions, scroll down to the example. + + + +### Examples +#### Restricting execution to users with the `administrator` permission. +```jsonc +"permissions": { + "requiredPerms": ["administrator"] +} +``` + +#### Restricting the command to a particular category +```jsonc +"permissions": { + "allowed": { + "categories": ["your_category_id_here"] + } +} +``` + +#### Preventing a specific user from executing a command +```jsonc +"permissions": { + "denied": { + "users": ["that_user_id_here"] + } +} +``` + +#### Restricting the permissions for a particular subcommand +> In this example, the subcommand is named `foo`, and we're restricting it to a particular channel. Replace that with the submodule you'd like to restrict. This can be set with the same sort of structure used for standard module permissions, so you can configure things like `requiredPerms`, `allowed`, and `denied`, the same way you'd configure base level command permissions. +```jsonc +"permissions": { + "submodulePermissions": { + "foo": { + "allowed": { + "channels": ["your_channel_id_here"] + } + } + } +} +``` + +#### Restricting a sub-sub-command +> In this example, we're restricting the sub-sub-command `bar` to a particular user, for the subcommand `foo`. The structure is similar to restricting a subcommand, it's just repeated. +```jsonc +"permissions": { + "submodulePermissions": { + "foo": { + "submodulePermissions": { + "allowed": { + "users": ["your_user_id_here"] + } + } + } + } +} +``` + + +```jsonc + "permissions": { + // a list of all possible permission restrictions + "requiredPerms": ["kick", "ban", "timeout", "manage_roles", "administrator"], + // **ALL IDs MUST BE STRINGS (not numbers)** + "allowed": { + // list of user IDs + "users": [], + // list of role IDs (or names? needs to be discussed) + "roles": [], + // list of channel IDs + "channels": [], + // a list of category IDs + "categories": [], + }, + "denied": { + // a list of user IDs + "users": [], + // a list of role IDs + "roles": [], + // a list of channel IDs + "channels": [], + // a list of category IDs. + "categories": [], + }, + // if you also want to restrict permissions per submodule, it can be done as below + "submodulePermissions": { + // each key in this object is the name of a submodule, so for a command like `/factoid all`, + // you could restrict it with a key named `"all"`. + "": { + // the same structure can be nested + "requiredPerms": [], + // you can also nest submodulePermissions to restrict a command 3 layers deep, like "/factoid all html" + "submodulePermissions": { + "": {} + } + } + } + } + ``` + +## Administration \ No newline at end of file diff --git a/secrets.default.jsonc b/secrets.default.jsonc new file mode 100644 index 0000000..58a2dfa --- /dev/null +++ b/secrets.default.jsonc @@ -0,0 +1,22 @@ +// The contents of this file should *never* be shared. +// It contains authentication data +{ + // The token used to login to discord + "discordAuthToken": "MTA5MjEzMTE0OTQzNTkwODI2OQ.G1VRKF.HEOswNI2olKBrlY_znhk3DuDhy2NyqorpFhbac", + // database credentials + "mongodb": { + // If using Atlas, set the protocol to mongodb+srv://, and include a `/` at the end of your database URL + // Otherwise set it to mongodb:// + "protocol": "mongodb://", + // the IP address or URL of your mongodb instance + "address": "mongodb.example.com", + // authentication credentials + "username": "YOUR_MONGO_USERNAME", + "password": "YOUR_MONGO_PASSWORD" + }, + // these credentials are shared between the google and youtube modules. + "google": { + "apiKey": "YOUR_GOOGLE_API_KEY_HERE", + "cseId": "YOUR_GOOGLE_CSE_ID_HERE" + } +} \ No newline at end of file diff --git a/src/core/config.ts b/src/core/config.ts index d1aec37..8cf5817 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -11,6 +11,7 @@ import {EventCategory, logEvent} from './logger.js'; * Path of the config on the filesystem relative to where node is being run from */ const CONFIG_LOCATION = './config.jsonc'; +const SECRETS_LOCATION = './secrets.jsonc'; /** * This is an object mirror of `config.jsonc`. You can load the config from the filesystem with `readConfigFromFileSystem()`. @@ -29,6 +30,8 @@ export const botConfig: any = { botConfig, parseJSONC(readFileSync(CONFIG_LOCATION, 'utf-8')) ); + // read `secrets.jsonc` into the config, under `.secrets` + botConfig.secrets = parseJSONC(readFileSync(SECRETS_LOCATION, 'utf-8')); } catch (err) { throw new Error( 'Unable to locate or process config.jsonc, are you sure it exists?' diff --git a/src/core/main.ts b/src/core/main.ts index 3e3e3f1..d7da4ef 100644 --- a/src/core/main.ts +++ b/src/core/main.ts @@ -106,7 +106,7 @@ client.on(Events.InteractionCreate, async interaction => { }); // Login to discord -client.login(botConfig.authToken); +client.login(botConfig.secrets.discordAuthToken); /** * This function imports the default export from the file specified, and pushes each module to diff --git a/src/core/mongo.ts b/src/core/mongo.ts index 1461acf..62e226a 100644 --- a/src/core/mongo.ts +++ b/src/core/mongo.ts @@ -11,7 +11,7 @@ import {Dependency} from './modules.js'; * @type Db */ export const mongo = new Dependency('MongoDB', async () => { - const mongoConfig = botConfig.mongodb; + const mongoConfig = botConfig.secrets.mongodb; // https://www.mongodb.com/docs/manual/reference/connection-string/ const connectionString = `${mongoConfig.protocol}${mongoConfig.username}:${mongoConfig.password}` + diff --git a/src/core/slash_commands.ts b/src/core/slash_commands.ts index dbceeea..0f4340b 100644 --- a/src/core/slash_commands.ts +++ b/src/core/slash_commands.ts @@ -292,7 +292,7 @@ export async function registerSlashCommandSet( const guild = client.guilds.cache.first()!; // ship the provided list off to discord to discord // https://discordjs.guide/creating-your-bot/command-deployment.html#guild-commands - const rest = new REST().setToken(botConfig.authToken); + const rest = new REST().setToken(botConfig.secrets.discordAuthToken); /** list of slash commands, converted to json, to be sent off to discord */ const commands: RESTPostAPIChatInputApplicationCommandsJSONBody[] = []; for (const command of commandSet) { @@ -301,7 +301,8 @@ export async function registerSlashCommandSet( // send everything to discord // The put method is used to fully refresh all commands in the guild with the current set await rest.put( - Routes.applicationGuildCommands(botConfig.applicationId, guild.id), + // non-null assertion: this code should only be run when the bot is logged in + Routes.applicationGuildCommands(client.application!.id, guild.id), { body: commands, } diff --git a/src/modules/channel_logging/channel_logging.ts b/src/modules/channel_logging/channel_logging.ts index 5e5fd14..aa0e9fc 100644 --- a/src/modules/channel_logging/channel_logging.ts +++ b/src/modules/channel_logging/channel_logging.ts @@ -49,7 +49,7 @@ channelLogging.onInitialize(async () => { const worker = new Worker(workerPath, { workerData: { config: channelLogging.config, - authToken: util.botConfig.authToken, + authToken: util.botConfig.secrets.discordAuthToken, }, }); worker.on('message', (message: WorkerMessage) => { diff --git a/src/modules/google.ts b/src/modules/google.ts index cfc8d8d..9b203c3 100644 --- a/src/modules/google.ts +++ b/src/modules/google.ts @@ -39,12 +39,12 @@ googleModule.registerSubModule( ], async (args, interaction) => { // Key checks - const API_KEY: string | undefined = googleModule.config.ApiKey; - const CSE_ID: string | undefined = googleModule.config.CseId; + const API_KEY: string | undefined = util.botConfig.secrets.google.apiKey; + const CSE_ID: string | undefined = util.botConfig.secrets.google.cseId; if ( - [undefined, ''].includes(API_KEY) || - [undefined, ''].includes(CSE_ID) + [undefined, '', 'YOUR_GOOGLE_API_KEY_HERE'].includes(API_KEY) || + [undefined, '', 'YOUR_GOOGLE_CSE_ID_HERE'].includes(CSE_ID) ) { util.logEvent( util.EventCategory.Warning,