diff --git a/.vscode/settings.json b/.vscode/settings.json index e701d78..a3b540f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,5 +11,9 @@ "spellright.documentTypes": [], "deno.suggest.imports.hosts": { "https://deno.land": true - } + }, + "cSpell.words": [ + "sproc", + "USTZ" + ] } \ No newline at end of file diff --git a/PRIVACY.md b/PRIVACY.md new file mode 100644 index 0000000..6f05a88 --- /dev/null +++ b/PRIVACY.md @@ -0,0 +1,41 @@ +# Group Up's Privacy Policy +## Information relating to Discord Interactions +### Public Bot Information +Publicly available versions of `Group Up#1305` (Discord ID: `847256159123013722`) (herein referred to as _The Bot_ or _Bot_) do not automatically track or collect user information via Discord. + +Upon inviting _The Bot_ to a user's guild, _The Bot_ sends the guild name, Discord Guild ID, and current count of guild members to Burn_E99#1062 (herein referred to as _The Developer_) via a private Discord Guild. The guild name, Discord Guild ID, and current count of guild members are only used to roughly gage how popular _The Bot_ is and to determine if _The Bot_'s hosting solution needs to be improved. These pieces of information will never be sold or shared with anyone. + +_The Bot_ reads every message that it is allowed to, meaning if _The Bot_ is allowed to see a channel in a guild, it reads every new message sent in said channel. This is for the automated cleanup of designated Event Channels. + +_The Bot_ does not read any user messages sent in the past, but does read its own past messages. This is to limit the amount of data that _The Bot_ needs to store outside of Discord, giving users better control of their own data. + +* Messages that do not begin with _The Bot_'s command prefix are not saved or stored anywhere. Messages that do not begin with _The Bot_'s command prefix that are sent outside of a designated Event Channel are ignored and not processed. +* Slash Commands sent to _The Bot_ do not automatically log user data, and most commands to not log any data. The commands that log data are the report command (in Discord, this command is known as `/report [message]`), the Event Channel setup command (known as `/setup`), and the Create New Event command (known as `/create-event` or the `Create New Event` button). + * The report command only stores the text placed within the message that is directly after the command (herein referred to as _The Report Text_). This command is entirely optional, meaning users never need to run this command under normal usage of _The Bot_. This command is only intended to be used to report roll commands that did not output what was expected. This command will accept any value for _The Report Text_, thus it is up to the user to remove any sensitive information before sending the command. _The Report Text_ is stored in a private Discord Guild in a channel that only _The Developer_ can see. _The Report Text_ is solely used to improve _The Bot_, either by providing a feature suggestions or alerting _The Developer_ to bugs that need patched. + * The Event Channel setup command only stores Discord IDs. The setup command will always store the Discord Channel ID and Discord Guild ID from where it was run. + * If the Event Channel setup command was run with the `with-manager-role` option, the submitted Discord Role ID and Discord Channel ID for the desired Manager Role and Log Channel will also be stored. + * The Create New Event command stores the following data for every event that is created: + * The Discord Message ID, Discord Channel ID, and Discord Guild ID of the event that the user created. These IDs are stored so that _The Bot_ can locate the event for future use, including but not limited to: editing or deleting the event, and notifying the members of the event. + * The Discord User ID of the creator. This ID is stored so that _The Bot_ knows who created the event to control who can edit or delete the event. This ID is also used when _The Bot_ fails to find an event so that the event creator is aware that _The Bot_ was unable to notify the event members. + * The full Date and Time of the event. The Date and Time of the event are stored so that _The Bot_ can send notifications to members of events when the event starts. + * If a Custom Activity is created during the Create New Event command, the following additional data is stored: + * The Activity Title, Subtitle, and Max Members entered by the event creator. These are stored so that _The Developer_ can determine if a new activity preset is necessary to improve user experience while using _The Bot_. + * The Discord Guild ID that the Custom Activity was created in. This ID is stored so that _The Bot_ can delete the Custom Activity from its database if _The Bot_ is removed from the Guild. + +All commands contribute to a global counter to track the number of times a command is used. These counters do not keep track of where commands were run, only counting the number of times the command has been called. These counters have no way of being tracked back to the individual commands run by the users. + +If the Discord interaction is not explicitly mentioned above, it does not collect any data at all. + +### Private Bot Information +Privately hosted versions of Group Up (in other words, bots running Group Up's source code, but not running under the publicly available _Bot_, `Group Up#1305`) (herein referred to as _Rehosts_ or _Rehost_) may exist. _The Developer_ is not responsible for _Rehosts_, thus _Rehosts_ of _The Bot_ are not recommended to be used. + +All policies described in **Public Bot Information** apply to _Rehosts_. + +Due to the nature of open source code, _Rehosts_ may not use the same codebase that is available in this repository. _The Developer_ does not moderate what other developers do to this codebase. This means that if you are not using the publicly available _Bot_ and instead using a _Rehost_, this _Rehost_ could collect any information it desires. + +# Deleting Your Data +## Event Deletion +If you wish to remove all data that _The Bot_ has on your Guild, simply remove _The Bot_ from your Guild. Upon removal, _The Bot_ deletes all data on Event Channel, all data on Events created in the Guild, and all Custom Activities created in the Guild. + +## User Data Deletion +If you would like to ensure that all of your submitted reports are removed from _The Bot_'s private development server, please contact _The Developer_ via Discord (by sending a direct message to `Burn_E99#1062`) or via email () with a message along the lines of `"Please remove all of my submitted reports from your development server."`. Submitted reports are deleted from the server as they are processed, which happens roughly once a week, but this can be accelerated if requested. diff --git a/README.md b/README.md index 14d6175..410a629 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,59 @@ -# Group Up - An Event Scheduling Discord Bot | V0.5.8 - 2022/12/09 +# Group Up - An Event Scheduling Discord Bot | V1.0.0 - 2022/05/03 [![SonarCloud](https://sonarcloud.io/images/project_badges/sonarcloud-orange.svg)](https://sonarcloud.io/summary/new_code?id=GroupUp) [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=GroupUp&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=GroupUp) [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=GroupUp&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=GroupUp) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=GroupUp&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=GroupUp) [![Bugs](https://sonarcloud.io/api/project_badges/measure?project=GroupUp&metric=bugs)](https://sonarcloud.io/summary/new_code?id=GroupUp) [![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=GroupUp&metric=duplicated_lines_density)](https://sonarcloud.io/summary/new_code?id=GroupUp) [![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=GroupUp&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=GroupUp) -Group Up is a Discord bot built for scheduling events in your Guild. +Group Up is a Discord bot built for scheduling events in your Guild. The bot utilizes user-friendly Buttons, Slash Commands, and Forms to create events. Currently, Group Up has built in support for Destiny 2 and Among Us, but any event can be created for any game using the Create Custom Activity button. -# Invite Link -https://discord.com/api/oauth2/authorize?client_id=847256159123013722&permissions=92160&scope=bot +This bot was originally designed to replace the less reliable event scheduling provided by the Destiny 2 bot, Charlemagne, utilizing new Discord features many months before Charlemagne did. + +## Using Group Up +I am hosting this bot for public use and you may find its invite link below. If you would like to host this bot yourself, details of how to do so are found at the end of this README, but I do not recommend this unless you are experienced with running Discord bots. + +After inviting the bot, if you want to create a dedicated event channel, simply run `/setup` in the desired channel and follow the on-screen prompts. If you don't want a dedicated channel, just run `/create-event` anywhere. + +Note: The `MANAGE_GUILD`, `MANAGE_CHANNELS`, and `MANAGE_ROLES` permissions are only necessary for the `/setup` command. Once you create all of the event channel that you need, you may remove these permissions from the bot without causing any issues. + +[Bot Invite Link](https://discord.com/api/oauth2/authorize?client_id=847256159123013722&permissions=268527664&scope=bot%20applications.commands) + +[Support Server Invite Link](https://discord.gg/peHASXMZYv) + +--- + +## Available Commands +* `/help` + * Provides a message to help users get Group Up set up in their guild. +* `/info` + * Outputs some information and links relating to the bot including the Privacy Policy and Terms of Service. +* `/report [issue, feature request]` + * People aren't perfect, but this bot is trying to be. + * If you encounter a command that errors out or returns something unexpected, please use this command to alert the developers of the problem. + * Additionally, if you have a feature request, this is one of the ways to request one +* `/create-event` + * Starts the event creation process. +* `/setup [options]` **ONLY Available to Guild Members with the `ADMINISTRATOR` permission** + * Designates the current channel as a Event Channel. After the command successfully runs, Group Up will be in control of the channel for running events. +* `/delete-lfg-channel` **ONLY Available to Guild Members with the `ADMINISTRATOR` permission** + * Removes the Event Channel designation from the current channel +* `/event [options]` **ONLY Available to Guild Members with a Group Up Manager role in a managed Event Channel** + * Allows Group Up Managers to Join/Leave/Alternate members to events + +## Problems? Feature requests? +If you run into any errors or problems with the bot, or think you have a good idea to add to the bot, please submit a new GitHub issue detailing it. If you don't have a GitHub account, a report command (detailed above) is provided for use in Discord. + +--- + +## Self Hosting Group Up +Group Up is built on [Deno](https://deno.land/) `v1.33.1` using [Discordeno](https://discordeno.mod.land/) `v17.0.1`. If you choose to run this yourself, you will need to rename `config.example.ts` to `config.ts` and edit some values. You will need to create a new [Discord Application](https://discord.com/developers/applications) and copy the newly generated token into the `"token"` field. If you want to utilize some of the bots dev features, you will need to fill in the keys `"logChannel"` and `"reportChannel"` with text channel IDs and `"devServer"` with a guild ID. + +You will also need to install and setup a MySQL database with a user for the bot to use to add/modify the database. This user must have the `"DB Manager"` admin rights and `"REFERENCES"` Global Privileges. Once the DB is installed and a user is setup, run the provided `db\initialize.ts` to create the schema and tables. After this, run `db\populateDefaults.ts` to insert some needed values into the tables. + +Once everything is set up, starting the bot can simply be done with `deno run --allow-write=./logs --allow-net .\mod.ts`. + +--- + +## Privacy Policy and Terms of Service +Group Up has a Privacy Policy and Terms of Service to detail expectations of what user data is stored and how users should use Group Up. The following Privacy Policy and Terms of Service only apply to the officially hosted version of Group Up (`Group Up#1305`, Discord ID: `847256159123013722`). + +Privacy Policy TL;DR: Group Up stores data relating to events, event channels, and text from the `/report` command. For more detailed information, please check out the full [PRIVACY POLICY](https://github.com/Burn-E99/TheArtificer/blob/master/PRIVACY.md). + +Terms of Service TL;DR: Don't abuse or attempt to hack/damage Group Up. If you do, you may be banned from use. For more detailed information, please check out the full [TERMS OF SERVICE](https://github.com/Burn-E99/TheArtificer/blob/master/TERMS.md). diff --git a/TERMS.md b/TERMS.md new file mode 100644 index 0000000..75ae135 --- /dev/null +++ b/TERMS.md @@ -0,0 +1,14 @@ +# Group Up's Terms of Service/User Agreement +By using Group Up, you agree to the following terms. Breaking these terms may result in your account being banned from using Group Up (Discord Bot). + +1. **User Conduct**. You agree to obey all applicable laws in using the Service, and agree that you are responsible for the content and/or communications you send to or initiate via Group Up. You agree that you are responsible for everything that you transmit to or in relation to Group Up and you specifically agree (in relation to Group Up) not to participate in any form of activity which is unlawful, harassing, libellous, defamatory, abusive, threatening, harmful, vulgar, obscene, profane, sexually-oriented, racially-offensive or otherwise includes objectionable material; + + * not to collect personal data about other Users (for any purpose); + * not to register more than one account for yourself or anyone else; + * not to use Group Up to engage in any commercial activities not approved in writing by the Developer, Ean Milligan; + * not to impose an unreasonable or disproportionately large load on our infrastructure; and + * not to attempt to gain unauthorized access to Group Up's computer systems or engage in any activity that disrupts, diminishes the quality of, interferes with the performance of, or impairs the functionality of Group Up. + +2. **Hacking**. You agree and undertake not to attempt to damage, deny service to, hack, crack, or otherwise interfere (collectively, "Interfere") with Group Up in any manner. If you in any way Interfere with these, you agree to pay all damages we incur as a result. We reserve the right to deny any or all access or service to any User for any reason, at any time, at our sole discretion. You agree that we may block your access, and at our sole discretion to disallow your continued use of Group Up. We reserve the right to take any action we may deem appropriate in our sole discretion with respect to violations or enforcement of the terms of this Agreement, and we expressly reserve all rights and remedies available to us at law or in equity. +3. **Termination of this Agreement**. Group Up may at any time terminate this legal Agreement, in our sole discretion without prior notice to you, if we believe that you may have breached (or acted in a manner indicating that you do not intend to or are unable to comply with) any term herein, or if we are legally required to do so by law, or if continuation is likely to be no longer commercially viable. +4. **Contacting Us**. If you have any questions, please contact us via email at . diff --git a/config.example.ts b/config.example.ts index 9252ed0..cfec9c7 100644 --- a/config.example.ts +++ b/config.example.ts @@ -1,21 +1,27 @@ export const config = { 'name': 'Group Up', // Name of the bot - 'version': '0.5.9', // Version of the bot + 'version': '1.0.0', // Version of the bot 'token': 'the_bot_token', // Discord API Token for this bot - 'localtoken': 'local_testing_token', // Discord API Token for a secondary OPTIONAL testing bot, THIS MUST BE DIFFERENT FROM "token" - 'prefix': 'gu!', // Prefix for all commands + 'localToken': 'local_testing_token', // Discord API Token for a secondary OPTIONAL testing bot, THIS MUST BE DIFFERENT FROM "token" + 'prefix': '/', // Prefix for all commands 'db': { // Settings for the MySQL database, this is required for use with the API, if you do not want to set this up, you will need to rip all code relating to the DB out of the bot 'host': '', // IP address for the db, usually localhost 'localhost': '', // IP address for a secondary OPTIONAL local testing DB, usually also is localhost, but depends on your dev environment 'port': 3306, // Port for the db - 'username': '', // Username for the account that will access your DB, this account will need "DB Manager" admin rights and "REFERENCES" Global Privalages + 'username': '', // Username for the account that will access your DB, this account will need "DB Manager" admin rights and "REFERENCES" Global Privileges 'password': '', // Password for the account, user account may need to be authenticated with the "Standard" Authentication Type if this does not work out of the box 'name': '', // Name of the database Schema to use for the bot }, - 'logChannel': 'the_log_channel', // Discord channel ID where the bot should put startup messages and other error messages needed - 'reportChannel': 'the_report_channel', // Discord channel ID where reports will be sent when using the built-in report command - 'devServer': 'the_dev_server', // Discord guild ID where testing of indev features/commands will be handled, used in conjuction with the DEVMODE bool in mod.ts - 'owner': 'the_bot_owner', // Discord user ID of the bot admin + 'link': { // Links to various sites + 'sourceCode': 'https://github.com/Burn-E99/GroupUp', // Link to the repository + 'supportServer': '', // Invite link to the Discord support server + 'addToCalendar': '', // Link to where the icsGenerator is hosted + 'creatorIcon': '', // Link to where the GroupUpSinglePerson.png (or similar image) is hosted + }, + 'logChannel': 0n, // Discord channel ID where the bot should put startup messages and other error messages needed + 'reportChannel': 0n, // Discord channel ID where reports will be sent when using the built-in report command + 'devServer': 0n, // Discord guild ID where testing of indev features/commands will be handled, used in conjunction with the DEVMODE bool in mod.ts + 'owner': 0n, // Discord user ID of the bot admin 'botLists': [ // Array of objects containing all bot lists that stats should be posted to { // Bot List object, duplicate for each bot list 'name': 'Bot List Name', // Name of bot list, not used diff --git a/db/initialize.ts b/db/initialize.ts index d850fd8..44add0a 100644 --- a/db/initialize.ts +++ b/db/initialize.ts @@ -1,21 +1,8 @@ -// This file will create all tables for the artificer schema +// This file will create all tables for the groupup schema // DATA WILL BE LOST IF DB ALREADY EXISTS, RUN AT OWN RISK -import { - // MySQL deps - Client, -} from '../deps.ts'; - -import { LOCALMODE } from '../flags.ts'; import config from '../config.ts'; - -// Log into the MySQL DB -const dbClient = await new Client().connect({ - hostname: LOCALMODE ? config.db.localhost : config.db.host, - port: config.db.port, - username: config.db.username, - password: config.db.password, -}); +import { dbClient } from '../src/db.ts'; console.log('Attempting to create DB'); await dbClient.execute(`CREATE SCHEMA IF NOT EXISTS ${config.db.name};`); @@ -23,11 +10,12 @@ await dbClient.execute(`USE ${config.db.name}`); console.log('DB created'); console.log('Attempt to drop all tables'); +await dbClient.execute(`DROP VIEW IF EXISTS db_size;`); await dbClient.execute(`DROP PROCEDURE IF EXISTS INC_CNT;`); await dbClient.execute(`DROP TABLE IF EXISTS command_cnt;`); -await dbClient.execute(`DROP TABLE IF EXISTS guild_prefix;`); -await dbClient.execute(`DROP TABLE IF EXISTS guild_mod_role;`); -await dbClient.execute(`DROP TABLE IF EXISTS guild_clean_channel;`); +await dbClient.execute(`DROP TABLE IF EXISTS guild_settings;`); +await dbClient.execute(`DROP TABLE IF EXISTS active_events;`); +await dbClient.execute(`DROP TABLE IF EXISTS custom_activities;`); console.log('Tables dropped'); console.log('Attempting to create table command_cnt'); @@ -47,44 +35,80 @@ await dbClient.execute(` IN cmd CHAR(20) ) BEGIN - declare oldcnt bigint unsigned; - set oldcnt = (SELECT count FROM command_cnt WHERE command = cmd); - UPDATE command_cnt SET count = oldcnt + 1 WHERE command = cmd; + declare oldCnt bigint unsigned; + set oldCnt = (SELECT count FROM command_cnt WHERE command = cmd); + UPDATE command_cnt SET count = oldCnt + 1 WHERE command = cmd; END `); console.log('Stored Procedure created'); -console.log('Attempting to create table guild_prefix'); +console.log('Attempting to create table guild_settings'); await dbClient.execute(` - CREATE TABLE guild_prefix ( + CREATE TABLE guild_settings ( guildId bigint unsigned NOT NULL, - prefix char(10) NOT NULL, - PRIMARY KEY (guildid), - UNIQUE KEY guild_prefix_guildid_UNIQUE (guildid) + lfgChannelId bigint unsigned NOT NULL, + managerRoleId bigint unsigned NOT NULL, + logChannelId bigint unsigned NOT NULL, + PRIMARY KEY (guildId, lfgChannelId) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; `); console.log('Table created'); -console.log('Attempting to create table guild_mod_role'); +console.log('Attempting to create table active_events'); await dbClient.execute(` - CREATE TABLE guild_mod_role ( - guildId bigint unsigned NOT NULL, - roleId bigint unsigned NOT NULL, - PRIMARY KEY (guildid), - UNIQUE KEY guild_mod_role_guildid_UNIQUE (guildid) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -`); -console.log('Table created'); - -console.log('Attempting to create table guild_clean_channel'); -await dbClient.execute(` - CREATE TABLE guild_clean_channel ( - guildId bigint unsigned NOT NULL, + CREATE TABLE active_events ( + messageId bigint unsigned NOT NULL, channelId bigint unsigned NOT NULL, - PRIMARY KEY (guildid, channelId) + guildId bigint unsigned NOT NULL, + ownerId bigint unsigned NOT NULL, + eventTime datetime NOT NULL, + notifiedFlag tinyint(1) NOT NULL DEFAULT 0, + lockedFlag tinyint(1) NOT NULL DEFAULT 0, + PRIMARY KEY (messageId, channelId) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; `); console.log('Table created'); +/** + * notifiedFlag + * 0 = Not notified + * 1 = Notified Successfully + * -1 = Failed to notify + * lockedFlag + * 0 = Not locked + * 1 = Locked Successfully + * -1 = Failed to lock + * + * If both are -1, the event failed to delete + */ + +console.log('Attempting to create table custom_activities'); +await dbClient.execute(` + CREATE TABLE custom_activities ( + id int unsigned NOT NULL AUTO_INCREMENT, + guildId bigint unsigned NOT NULL, + activityTitle char(35) NOT NULL, + activitySubtitle char(50) NOT NULL, + maxMembers tinyint NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY custom_activities_id_UNIQUE (id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +`); +console.log('Table created'); + +// Database sizes view +console.log('Attempting to create view db_size'); +await dbClient.execute(` + CREATE VIEW db_size AS + SELECT + table_name AS "table", + ROUND(((data_length + index_length) / 1024 / 1024), 3) AS "size", + table_rows AS "rows" + FROM information_schema.TABLES + WHERE + table_schema = "${config.db.name}" + AND table_name <> "db_size"; +`); +console.log('View Created'); await dbClient.close(); console.log('Done!'); diff --git a/db/populateDefaults.ts b/db/populateDefaults.ts index 4eb9fae..102570e 100644 --- a/db/populateDefaults.ts +++ b/db/populateDefaults.ts @@ -1,26 +1,44 @@ // This file will populate the tables with default values +import { dbClient } from '../src/db.ts'; -import { - // MySQL deps - Client, -} from '../deps.ts'; - -import { LOCALMODE } from '../flags.ts'; -import config from '../config.ts'; - -// Log into the MySQL DB -const dbClient = await new Client().connect({ - hostname: LOCALMODE ? config.db.localhost : config.db.host, - port: config.db.port, - db: config.db.name, - username: config.db.username, - password: config.db.password, -}); - -console.log('Attempting to insert default commands into command_cnt'); -const commands = ['ping', 'help', 'info', 'version', 'report', 'privacy', 'lfg', 'prefix']; -for (const command of commands) { - await dbClient.execute('INSERT INTO command_cnt(command) values(?)', [command]).catch((e) => { +console.log('Attempting to insert default actions into command_cnt'); +const actions = [ + 'msg-mention', + 'cmd-audit', + 'cmd-delete', + 'cmd-help', + 'cmd-info', + 'cmd-report', + 'cmd-setup', + 'cmd-gameSel', + 'cmd-join', + 'cmd-leave', + 'cmd-alternate', + 'btn-gameSel', + 'btn-customAct', + 'btn-createEvt', + 'btn-createWLEvt', + 'btn-joinEvent', + 'btn-joinWLEvent', + 'btn-leaveEvent', + 'btn-leaveEventViaDM', + 'btn-altEvent', + 'btn-joinReqApprove', + 'btn-joinReqDeny', + 'btn-joinReqAlt', + 'btn-delEvent', + 'btn-confirmDelEvent', + 'btn-editEvent', + 'btn-eeChangeAct', + 'btn-eeCustomAct', + 'btn-eeChangeTime', + 'btn-eeChangeDesc', + 'btn-eeMakePublic', + 'btn-eeMakeWL', + 'lfg-filled', +]; +for (const action of actions) { + await dbClient.execute('INSERT INTO command_cnt(command) values(?)', [action]).catch((e) => { console.log(`Failed to insert into database`, e); }); } diff --git a/deno.json b/deno.json index fda4cb6..ce9e5f1 100644 --- a/deno.json +++ b/deno.json @@ -1,31 +1,47 @@ { "compilerOptions": { "allowJs": true, - "lib": ["deno.window"], + "lib": [ + "deno.window" + ], "strict": true }, "lint": { "files": { - "include": ["src/", "db/", "mod.ts", "deps.ts", "config.ts", "config.example.ts"], + "include": [ + "src/", + "db/", + "mod.ts", + "deps.ts", + "config.ts", + "config.example.ts" + ], "exclude": [] }, "rules": { - "tags": ["recommended"], - "include": ["ban-untagged-todo"], + "tags": [ + "recommended" + ], + "include": [ + "ban-untagged-todo" + ], "exclude": [] } }, "fmt": { - "files": { - "include": ["src/", "db/", "mod.ts", "deps.ts", "config.ts", "config.example.ts"], - "exclude": [] - }, - "options": { - "useTabs": true, - "lineWidth": 200, - "indentWidth": 2, - "singleQuote": true, - "proseWrap": "preserve" - } + "include": [ + "src/", + "db/", + "mod.ts", + "deps.ts", + "config.ts", + "config.example.ts" + ], + "exclude": [], + "useTabs": true, + "lineWidth": 200, + "indentWidth": 2, + "singleQuote": true, + "proseWrap": "preserve" } } \ No newline at end of file diff --git a/deps.ts b/deps.ts index 8d0130d..3364d84 100644 --- a/deps.ts +++ b/deps.ts @@ -1,40 +1,49 @@ -// All external dependancies are to be loaded here to make updating dependancy versions much easier -export { - botId, - cache, - cacheHandlers, - deleteMessage, - DiscordActivityTypes, - DiscordButtonStyles, - DiscordInteractionResponseTypes, - DiscordInteractionTypes, - editBotNickname, - editBotStatus, - getGuild, - getMessage, - getUser, - hasGuildPermissions, - Intents, - sendDirectMessage, - sendInteractionResponse, - sendMessage, - startBot, - structures, -} from 'https://deno.land/x/discordeno@12.0.1/mod.ts'; +// All external dependencies are to be loaded here to make updating dependency versions much easier +import { getBotIdFromToken } from 'https://deno.land/x/discordeno@17.0.1/mod.ts'; +import config from './config.ts'; +import { LOCALMODE } from './flags.ts'; +export const botId = getBotIdFromToken(LOCALMODE ? config.localToken : config.token); +export { enableCachePlugin, enableCacheSweepers } from 'https://deno.land/x/discordeno@17.0.1/plugins/cache/mod.ts'; +export type { BotWithCache } from 'https://deno.land/x/discordeno@17.0.1/plugins/cache/mod.ts'; + +export { + ActivityTypes, + ApplicationCommandFlags, + ApplicationCommandOptionTypes, + ApplicationCommandTypes, + BitwisePermissionFlags, + ButtonStyles, + ChannelTypes, + createBot, + getBotIdFromToken, + Intents, + InteractionResponseTypes, + MessageComponentTypes, + OverwriteTypes, + startBot, + TextStyles, +} from 'https://deno.land/x/discordeno@17.0.1/mod.ts'; export type { ActionRow, + ApplicationCommand, + ApplicationCommandOption, + Bot, ButtonComponent, - ButtonData, + CreateApplicationCommand, CreateMessage, - DebugArg, - DiscordenoGuild, - DiscordenoMember, - DiscordenoMessage, + DiscordEmbedField, Embed, - EmbedField, + EventHandlers, + Guild, Interaction, -} from 'https://deno.land/x/discordeno@12.0.1/mod.ts'; + InteractionResponse, + MakeRequired, + Message, + PermissionStrings, + SelectMenuComponent, + SelectOption, +} from 'https://deno.land/x/discordeno@17.0.1/mod.ts'; export { Client } from 'https://deno.land/x/mysql@v2.11.0/mod.ts'; diff --git a/flags.ts b/flags.ts index 939f73a..450b49a 100644 --- a/flags.ts +++ b/flags.ts @@ -2,5 +2,5 @@ export const DEVMODE = false; // DEBUG is used to toggle the cmdPrompt export const DEBUG = false; -// LOCALMODE is used to run a differnt bot token for local testing -export const LOCALMODE = false; +// LOCALMODE is used to run a different bot token for local testing +export const LOCALMODE = true; diff --git a/groupup.rc b/groupup.rc new file mode 100644 index 0000000..405645b --- /dev/null +++ b/groupup.rc @@ -0,0 +1,20 @@ +#!/bin/sh + +# PROVIDE: groupup + +. /etc/rc.subr + +name="groupup" +rcvar="groupup_enable" +pidfile="/var/dbots/GroupUp/groupup.pid" + +groupup_root="/var/dbots/GroupUp" +groupup_write="./logs/" +groupup_log="/var/log/groupup.log" + +groupup_chdir="${groupup_root}" +command="/usr/sbin/daemon" +command_args="-f -R 5 -P ${pidfile} -o ${groupup_log} /usr/local/bin/deno run --allow-write=${groupup_write} --allow-net ${groupup_root}/mod.ts" + +load_rc_config groupup +run_rc_command "$1" diff --git a/mod.ts b/mod.ts index b138bef..6aca384 100644 --- a/mod.ts +++ b/mod.ts @@ -1,1275 +1,22 @@ -import { - ActionRow, - botId, - ButtonComponent, - ButtonData, - cache, - cacheHandlers, - // MySQL Driver deps - Client, - DebugArg, - deleteMessage, - DiscordActivityTypes, - DiscordButtonStyles, - DiscordenoGuild, - DiscordenoMessage, - DiscordInteractionResponseTypes, - DiscordInteractionTypes, - editBotNickname, - editBotStatus, - Embed, - getGuild, - getMessage, - getUser, - hasGuildPermissions, - initLog, - Intents, - log, - // Log4Deno deps - LT, - sendDirectMessage, - sendInteractionResponse, - sendMessage, - // Discordeno deps - startBot, -} from './deps.ts'; - -import { ActiveLFG, BuildingLFG, GuildCleanChannels, GuildModRoles, GuildPrefixes } from './src/mod.d.ts'; -import intervals from './src/intervals.ts'; -import { LFGActivities } from './src/games.ts'; -import { JoinLeaveType } from './src/lfgHandlers.d.ts'; -import { handleLFGStep, handleMemberJoin, handleMemberLeave, urlToIds } from './src/lfgHandlers.ts'; -import { constantCmds, editBtns, lfgStepQuestions } from './src/constantCmds.ts'; -import { jsonParseBig, jsonStringifyBig } from './src/utils.ts'; - -import { DEBUG, LOCALMODE } from './flags.ts'; import config from './config.ts'; - -// Initialize DB client -const dbClient = await new Client().connect({ - hostname: LOCALMODE ? config.db.localhost : config.db.host, - port: config.db.port, - db: config.db.name, - username: config.db.username, - password: config.db.password, -}); +import { DEBUG, LOCALMODE } from './flags.ts'; +import { createBot, enableCachePlugin, enableCacheSweepers, initLog, Intents, startBot } from './deps.ts'; +import { events } from './src/events.ts'; +import { createSlashCommands } from './src/commands/_index.ts'; // Initialize logging client with folder to use for logs, needs --allow-write set on Deno startup initLog('logs', DEBUG); -log(LT.INFO, `${config.name} Starting up . . .`); -// Handle idling out the active builders -const activeBuilders: Array = []; -setInterval(() => { - intervals.buildingTimeout(activeBuilders); -}, 1000); +// Set up the Discord Bot +const bot = enableCachePlugin(createBot({ + token: LOCALMODE ? config.localToken : config.token, + intents: Intents.MessageContent | Intents.GuildMessages | Intents.DirectMessages | Intents.Guilds | Intents.GuildMessageReactions, + events, +})); +enableCacheSweepers(bot); -const activeLFGPosts: Array = jsonParseBig(localStorage.getItem('activeLFGPosts') || '[]'); -log(LT.INFO, `Loaded ${activeLFGPosts.length} activeLFGPosts`); -setInterval(() => { - intervals.lfgNotifier(activeLFGPosts); -}, 60000); +// Start the bot +await startBot(bot); -const guildPrefixes: Map = new Map(); -const getGuildPrefixes = await dbClient.query('SELECT * FROM guild_prefix'); -getGuildPrefixes.forEach((g: GuildPrefixes) => { - guildPrefixes.set(g.guildId, g.prefix); -}); - -const guildModRoles: Map = new Map(); -const getGuildModRoles = await dbClient.query('SELECT * FROM guild_mod_role'); -getGuildModRoles.forEach((g: GuildModRoles) => { - guildModRoles.set(g.guildId, g.roleId); -}); - -const cleanChannels: Map> = new Map(); -const getCleanChannels = await dbClient.query('SELECT * FROM guild_clean_channel'); -getCleanChannels.forEach((g: GuildCleanChannels) => { - const tempArr = cleanChannels.get(g.guildId) || []; - tempArr.push(g.channelId); - cleanChannels.set(g.guildId, tempArr); -}); - -const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; - -// Start up the Discord Bot -startBot({ - token: LOCALMODE ? config.localtoken : config.token, - intents: [Intents.GuildMessages, Intents.DirectMessages, Intents.Guilds], - eventHandlers: { - ready: () => { - log(LT.INFO, `${config.name} Logged in!`); - editBotStatus({ - activities: [{ - name: 'Booting up . . .', - type: DiscordActivityTypes.Game, - createdAt: new Date().getTime(), - }], - status: 'online', - }); - - // Interval to rotate the status text every 30 seconds to show off more commands - setInterval(async () => { - log(LT.LOG, 'Changing bot status'); - try { - const cachedCount = await cacheHandlers.size('guilds'); - // Wrapped in try-catch due to hard crash possible - editBotStatus({ - activities: [{ - name: intervals.getRandomStatus(cachedCount), - type: DiscordActivityTypes.Game, - createdAt: new Date().getTime(), - }], - status: 'online', - }); - } catch (e) { - log(LT.ERROR, `Failed to update status: ${jsonStringifyBig(e)}`); - } - }, 30000); - - // Interval to update bot list stats every 24 hours - LOCALMODE ? log(LT.INFO, 'updateListStatistics not running') : setInterval(() => { - log(LT.LOG, 'Updating all bot lists statistics'); - intervals.updateListStatistics(botId, cache.guilds.size); - }, 86400000); - - // setTimeout added to make sure the startup message does not error out - setTimeout(() => { - LOCALMODE && editBotNickname(config.devServer, `LOCAL - ${config.name}`); - LOCALMODE ? log(LT.INFO, 'updateListStatistics not running') : intervals.updateListStatistics(botId, cache.guilds.size); - editBotStatus({ - activities: [{ - name: 'Booting Complete', - type: DiscordActivityTypes.Game, - createdAt: new Date().getTime(), - }], - status: 'online', - }); - sendMessage(config.logChannel, `${config.name} has started, running version ${config.version}.`).catch((e) => { - log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(e)}`); - }); - }, 1000); - }, - guildCreate: (guild: DiscordenoGuild) => { - log(LT.LOG, `Handling joining guild ${jsonStringifyBig(guild)}`); - sendMessage(config.logChannel, `New guild joined: ${guild.name} (id: ${guild.id}). This guild has ${guild.memberCount} members!`).catch((e) => { - log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(e)}`); - }); - }, - guildDelete: async (guild: DiscordenoGuild) => { - log(LT.LOG, `Handling leaving guild ${jsonStringifyBig(guild)}`); - sendMessage(config.logChannel, `I have been removed from: ${guild.name} (id: ${guild.id}).`).catch((e) => { - log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(e)}`); - }); - - try { - await dbClient.execute('DELETE FROM guild_prefix WHERE guildId = ?', [guild.id]); - await dbClient.execute('DELETE FROM guild_mod_role WHERE guildId = ?', [guild.id]); - await dbClient.execute('DELETE FROM guild_clean_channel WHERE guildId = ?', [guild.id]); - } catch (e) { - log(LT.WARN, `Failed to remove guild from DB: ${jsonStringifyBig(e)}`); - } - }, - debug: (dmsg: string | DebugArg, data?: string) => log(LT.LOG, `Debug Message | ${jsonStringifyBig(dmsg)} | ${jsonStringifyBig(data)}`, false), - messageCreate: async (message: DiscordenoMessage) => { - // Ignore all other bots - if (message.isBot) return; - - const prefix = guildPrefixes.get(message.guildId) || config.prefix; - - // Handle messages not starting with the prefix - if (message.content.indexOf(prefix) !== 0) { - // Mentions - if (message.mentionedUserIds[0] === botId && (message.content.trim().startsWith(`<@${botId}>`) || message.content.trim().startsWith(`<@!${botId}>`))) { - // Light telemetry to see how many times a command is being run - await dbClient.execute(`CALL INC_CNT("prefix");`).catch((e) => { - log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${jsonStringifyBig(e)}`); - }); - - if (message.content.trim() === `<@${botId}>` || message.content.trim() === `<@!${botId}>`) { - message.send({ - embeds: [{ - title: `Hello ${message.member?.username}, and thanks for using Group Up!`, - fields: [ - { - name: `My prefix in this guild is: \`${prefix}\``, - value: 'Mention me with a new prefix to change it.', - }, - ], - }], - }).catch((e) => { - log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); - }); - } else if (await hasGuildPermissions(message.guildId, message.authorId, ['ADMINISTRATOR'])) { - const newPrefix = message.content.replace(`<@!${botId}>`, '').replace(`<@${botId}>`, '').trim(); - - if (newPrefix.length <= 10) { - let success = true; - if (guildPrefixes.has(message.guildId)) { - // Execute the DB update - await dbClient.execute('UPDATE guild_prefix SET prefix = ? WHERE guildId = ?', [newPrefix, message.guildId]).catch((e) => { - log(LT.ERROR, `Failed to insert into database: ${jsonStringifyBig(e)}`); - success = false; - }); - } else { - // Execute the DB insertion - await dbClient.execute('INSERT INTO guild_prefix(guildId,prefix) values(?,?)', [message.guildId, newPrefix]).catch((e) => { - log(LT.ERROR, `Failed to insert into database: ${jsonStringifyBig(e)}`); - success = false; - }); - } - - if (success) { - guildPrefixes.set(message.guildId, newPrefix); - message.send({ - embeds: [{ - fields: [ - { - name: `My prefix in this guild is now: \`${newPrefix}\``, - value: 'Mention me with a new prefix to change it.', - }, - ], - }], - }).catch((e) => { - log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); - }); - } else { - message.send({ - embeds: [{ - fields: [ - { - name: 'Something went wrong!', - value: `My prefix is still \`${prefix}\`. Please try again, and if the problem persists, please report this to the developers using \`${prefix}report\`.`, - }, - ], - }], - }).catch((e) => { - log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); - }); - } - } else { - message.send({ - embeds: [{ - fields: [ - { - name: 'Prefix too long, please set a prefix less than 10 characters long.', - value: 'Mention me with a new prefix to change it.', - }, - ], - }], - }).catch((e) => { - log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); - }); - } - } - return; - } - - // Other - const activeIdx = activeBuilders.findIndex((x) => (message.channelId === x.channelId && message.authorId === x.userId)); - if (activeIdx > -1) { - activeBuilders[activeIdx].lastTouch = new Date(); - activeBuilders[activeIdx] = await handleLFGStep(activeBuilders[activeIdx], message.content); - - if (activeBuilders[activeIdx].step === 'done') { - if (message.member) { - const memberJoined = handleMemberJoin(activeBuilders[activeIdx].lfgMsg.embeds[0].fields || [], message.member, false); - - const newTimestamp = new Date(parseInt(memberJoined.embed[1].value.split('#')[1])); - const newLfgUid = ALPHABET[Math.floor(Math.random() * 26)] + ALPHABET[Math.floor(Math.random() * 26)]; - - const tempMembers = memberJoined.embed[4].name.split(':')[1].split('/'); - const currentMembers = parseInt(tempMembers[0]); - const maxMembers = parseInt(tempMembers[1]); - - if (activeBuilders[activeIdx].editing) { - if (currentMembers > maxMembers) { - const currentPeople = memberJoined.embed[4].value.split('\n'); - const newAlts = currentPeople.splice(maxMembers); - memberJoined.embed[4].value = currentPeople.join('\n') || 'None'; - memberJoined.embed[5].value = `${newAlts.join('\n')}\n${memberJoined.embed[5].value === 'None' ? '' : memberJoined.embed[5].value}`; - memberJoined.embed[4].name = `Members Joined: ${maxMembers}/${maxMembers}`; - } - } - - await activeBuilders[activeIdx].lfgMsg.edit({ - content: '', - embeds: [{ - fields: memberJoined.embed, - footer: { - text: `Created by: ${message.member.username} | ${newLfgUid}`, - }, - timestamp: newTimestamp.toISOString(), - }], - components: [ - { - type: 1, - components: [ - { - type: 2, - label: 'Join', - customId: 'active@join_group', - style: DiscordButtonStyles.Success, - }, - { - type: 2, - label: 'Leave', - customId: 'active@leave_group', - style: DiscordButtonStyles.Danger, - }, - { - type: 2, - label: 'Join as Alternate', - customId: 'active@alternate_group', - style: DiscordButtonStyles.Primary, - }, - ], - }, - ], - }).catch((e) => { - log(LT.WARN, `Failed to edit message | ${jsonStringifyBig(e)}`); - }); - - if (activeBuilders[activeIdx]) { - const activeLFGIdx = activeLFGPosts.findIndex( - (lfg) => (lfg.channelId === activeBuilders[activeIdx].channelId && lfg.messageId === activeBuilders[activeIdx].lfgMsg.id && lfg.ownerId === activeBuilders[activeIdx].userId), - ); - if (activeLFGIdx >= 0) { - activeLFGPosts[activeLFGIdx].lfgUid = newLfgUid; - activeLFGPosts[activeLFGIdx].lfgTime = newTimestamp.getTime(); - activeLFGPosts[activeLFGIdx].notified = false; - activeLFGPosts[activeLFGIdx].locked = false; - } else { - activeLFGPosts.push({ - messageId: activeBuilders[activeIdx].lfgMsg.id, - channelId: activeBuilders[activeIdx].lfgMsg.channelId, - ownerId: message.authorId, - lfgUid: newLfgUid, - lfgTime: newTimestamp.getTime(), - notified: false, - locked: false, - }); - } - localStorage.setItem('activeLFGPosts', jsonStringifyBig(activeLFGPosts)); - } - } - - await activeBuilders[activeIdx].questionMsg.delete().catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - activeBuilders.splice(activeIdx, 1); - } - await message.delete().catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - return; - } - - // Should this get cleaned up? - const enabledCleanChannels = cleanChannels.get(message.guildId); - if (enabledCleanChannels && enabledCleanChannels.length && enabledCleanChannels.indexOf(message.channelId) > -1) { - message.delete('Cleaning Channel').catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - return; - } - return; - } else { - // User is sending a command, make sure its a lfg command if its being sent in a clean channel - const enabledCleanChannels = cleanChannels.get(message.guildId); - if (enabledCleanChannels && enabledCleanChannels.length && enabledCleanChannels.indexOf(message.channelId) > -1 && message.content.indexOf(`${prefix}lfg`) !== 0) { - message.delete('Cleaning Channel').catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - return; - } - } - - log(LT.LOG, `Handling message ${jsonStringifyBig(message)}`); - - // Split into standard command + args format - const args = message.content.slice(prefix.length).trim().split(/[ \n]+/g); - const command = args.shift()?.toLowerCase(); - - // All commands below here - - // ping - // Its a ping test, what else do you want. - if (command === 'ping') { - // Light telemetry to see how many times a command is being run - dbClient.execute(`CALL INC_CNT("ping");`).catch((e) => { - log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${jsonStringifyBig(e)}`); - }); - - // Calculates ping between sending a message and editing it, giving a nice round-trip latency. - try { - const m = await message.send({ - embeds: [{ - title: 'Ping?', - }], - }); - m.edit({ - embeds: [{ - title: `Pong! Latency is ${m.timestamp - message.timestamp}ms.`, - }], - }); - } catch (e) { - log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); - } - } // lfg - // Handles all LFG commands, creating, editing, deleting - else if (command === 'lfg') { - // Light telemetry to see how many times a command is being run - dbClient.execute(`CALL INC_CNT("lfg");`).catch((e) => { - log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${jsonStringifyBig(e)}`); - }); - - const subcmd = (args[0] || 'help').toLowerCase(); - const lfgUid = (args[1] || '').toUpperCase(); - - // Learn how the LFG command works - if (subcmd === 'help' || subcmd === 'h' || subcmd === '?') { - message.send(constantCmds.lfgHelp).catch((e) => { - log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); - }); - } // Create a new LFG - else if (subcmd === 'create' || subcmd === 'c') { - try { - const lfgMsg = await message.send(`Creating new LFG post for <@${message.authorId}>. Please reply with the requested information and watch as your LFG post gets created!`); - - const gameButtons: Array = Object.keys(LFGActivities).map((game) => { - return { - type: 2, - label: game, - customId: `building@set_game#${game}`, - style: DiscordButtonStyles.Primary, - }; - }); - - const buttonComps: Array = []; - - const temp: Array = []; - - gameButtons.forEach((btn, idx) => { - if (!temp[Math.floor(idx / 5)]) { - temp[Math.floor(idx / 5)] = [btn]; - } else { - temp[Math.floor(idx / 5)].push(btn); - } - }); - - temp.forEach((btns) => { - if (btns.length && btns.length <= 5) { - buttonComps.push({ - type: 1, - components: btns, - }); - } - }); - - const question = await message.send({ - content: lfgStepQuestions.set_game, - components: buttonComps, - }); - - activeBuilders.push({ - userId: message.authorId, - channelId: message.channelId, - step: 'set_game', - lfgMsg: lfgMsg, - questionMsg: question, - lastTouch: new Date(), - maxIdle: 60, - editing: false, - }); - - message.delete().catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - } catch (e) { - log(LT.WARN, `LFG failed at step | create | ${jsonStringifyBig(e)}`); - } - } // Delete an existing LFG - else if (subcmd === 'delete' || subcmd === 'd') { - try { - // User provided a Uid, use it - if (lfgUid) { - const matches = activeLFGPosts.filter((lfg) => (message.authorId === lfg.ownerId && message.channelId === lfg.channelId && lfgUid === lfg.lfgUid)); - - // Found one, delete - if (matches.length) { - await deleteMessage(matches[0].channelId, matches[0].messageId, 'User requested LFG to be deleted.').catch((e) => { - log(LT.WARN, `Failed to find message to delete | ${jsonStringifyBig(e)}`); - }); - const lfgIdx = activeLFGPosts.findIndex((lfg) => (message.authorId === lfg.ownerId && message.channelId === lfg.channelId && lfgUid === lfg.lfgUid)); - - activeLFGPosts.splice(lfgIdx, 1); - - localStorage.setItem('activeLFGPosts', jsonStringifyBig(activeLFGPosts)); - - const m = await message.send(constantCmds.lfgDelete3); - - m.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - message.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - } // Did not find one - else { - const m = await message.send(constantCmds.lfgDelete1); - - m.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - message.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - } - } // User did not provide a Uid, find it automatically - else { - const matches = activeLFGPosts.filter((lfg) => (message.authorId === lfg.ownerId && message.channelId === lfg.channelId)); - - // Found one, delete - if (matches.length === 1) { - await deleteMessage(matches[0].channelId, matches[0].messageId, 'User requested LFG to be deleted.').catch((e) => { - log(LT.WARN, `Failed to find message to delete | ${jsonStringifyBig(e)}`); - }); - const lfgIdx = activeLFGPosts.findIndex((lfg) => (message.authorId === lfg.ownerId && message.channelId === lfg.channelId)); - - activeLFGPosts.splice(lfgIdx, 1); - - localStorage.setItem('activeLFGPosts', jsonStringifyBig(activeLFGPosts)); - - const m = await message.send(constantCmds.lfgDelete3); - - m.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - message.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - } // Found multiple, notify user - else if (matches.length) { - const deleteMsg = constantCmds.lfgDelete2; - const deepCloningFailedSoThisIsTheSolution = constantCmds.lfgDelete2.embeds[0].fields[0].value; - matches.forEach((mt) => { - deleteMsg.embeds[0].fields[0].value += `[${mt.lfgUid}](https://discord.com/channels/${message.guildId}/${mt.channelId}/${mt.messageId})\n`; - }); - - deleteMsg.embeds[0].fields[0].value += '\nThis message will self descruct in 30 seconds.'; - - const m = await message.send(deleteMsg); - constantCmds.lfgDelete2.embeds[0].fields[0].value = deepCloningFailedSoThisIsTheSolution; - - m.delete('Channel Cleanup', 30000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - message.delete('Channel Cleanup', 30000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - } // Found none, notify user you cannot delete other's lfgs - else { - const m = await message.send(constantCmds.lfgDelete1); - - m.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - message.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - } - } - } catch (e) { - log(LT.WARN, `LFG failed at step | delete | ${jsonStringifyBig(e)}`); - } - } // Edit an existing LFG - else if (subcmd === 'edit' || subcmd === 'e') { - try { - // User provided a Uid, use it - if (lfgUid) { - const matches = activeLFGPosts.filter((lfg) => (message.authorId === lfg.ownerId && message.channelId === lfg.channelId && lfgUid === lfg.lfgUid)); - - // Found one, edit - if (matches.length) { - const lfgMessage = await (await getMessage(matches[0].channelId, matches[0].messageId)).edit({ - content: `Editing new LFG post for <@${matches[0].ownerId}>. Please reply with the requested information and watch as your LFG post gets edited!`, - }); - const question = await message.send({ - content: 'Please select an item to edit from the buttons below:', - components: [{ - type: 1, - components: editBtns, - }], - }); - - activeBuilders.push({ - userId: matches[0].ownerId, - channelId: matches[0].channelId, - step: 'edit_btn', - lfgMsg: lfgMessage, - questionMsg: question, - lastTouch: new Date(), - maxIdle: 60, - editing: true, - }); - - message.delete().catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - } // Did not find one - else { - const m = await message.send(constantCmds.lfgEdit1); - - m.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - message.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - } - } // User did not provide a Uid, find it automatically - else { - const matches = activeLFGPosts.filter((lfg) => (message.authorId === lfg.ownerId && message.channelId === lfg.channelId)); - - // Found one, edit - if (matches.length === 1) { - const lfgMessage = await (await getMessage(matches[0].channelId, matches[0].messageId)).edit({ - content: `Editing new LFG post for <@${matches[0].ownerId}>. Please reply with the requested information and watch as your LFG post gets edited!`, - }); - const question = await message.send({ - content: 'Please select an item to edit from the buttons below:', - components: [{ - type: 1, - components: editBtns, - }], - }); - - activeBuilders.push({ - userId: matches[0].ownerId, - channelId: matches[0].channelId, - step: 'edit_btn', - lfgMsg: lfgMessage, - questionMsg: question, - lastTouch: new Date(), - maxIdle: 60, - editing: true, - }); - - message.delete().catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - } // Found multiple, notify user - else if (matches.length) { - const deleteMsg = constantCmds.lfgEdit2; - const deepCloningFailedSoThisIsTheSolution = constantCmds.lfgEdit2.embeds[0].fields[0].value; - matches.forEach((mt) => { - deleteMsg.embeds[0].fields[0].value += `[${mt.lfgUid}](https://discord.com/channels/${message.guildId}/${mt.channelId}/${mt.messageId})\n`; - }); - - deleteMsg.embeds[0].fields[0].value += '\nThis message will self descruct in 30 seconds.'; - - const m = await message.send(deleteMsg); - constantCmds.lfgEdit2.embeds[0].fields[0].value = deepCloningFailedSoThisIsTheSolution; - - m.delete('Channel Cleanup', 30000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - message.delete('Channel Cleanup', 30000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - } // Found none, notify user you cannot edit other's lfgs - else { - const m = await message.send(constantCmds.lfgEdit1); - - m.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - message.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - } - } - } catch (e) { - log(LT.WARN, `LFG failed at step | edit | ${jsonStringifyBig(e)}`); - } - } // Join a LFG on behalf of a user - // gu!lfg join [url] [join/leave/alternate] [member?] - else if (subcmd === 'join' || subcmd === 'leave' || subcmd === 'alternate') { - try { - const action = subcmd; - const lfgIds = urlToIds(args[1] || ''); - const memberStr = args[2] || `<@${message.authorId}>`; - const member = await message.guild?.members.get(BigInt(memberStr.substr(3, memberStr.length - 4))); - - const modRole = guildModRoles.get(message.guildId) || 0n; - - // Join yourself (or others if you are a guild mod) to an LFG - if (lfgIds.guildId === message.guildId && member && (member.id === message.authorId || message.guildMember?.roles.includes(modRole))) { - const lfgMessage = await getMessage(lfgIds.channelId, lfgIds.messageId); - - const embeds = lfgMessage.embeds[0].fields || []; - let results: JoinLeaveType = { - embed: [], - success: false, - full: true, - justFilled: false, - }; - let actionResp: string; - switch (action) { - case 'join': - results = handleMemberJoin(embeds, member, false); - actionResp = 'joined'; - break; - case 'leave': - results = handleMemberLeave(embeds, member); - actionResp = 'left'; - break; - case 'alternate': - results = handleMemberJoin(embeds, member, true); - actionResp = 'joined as alternate'; - break; - } - - let resp: string; - if (results.success && lfgMessage.components) { - const buttonRow: ActionRow = lfgMessage.components[0] as ActionRow; - - await lfgMessage.edit({ - embeds: [{ - fields: results.embed, - footer: lfgMessage.embeds[0].footer, - timestamp: lfgMessage.embeds[0].timestamp, - }], - components: [buttonRow], - }); - - if (results.justFilled) { - const thisLFGPost = activeLFGPosts.filter((lfg) => (lfgMessage.id === lfg.messageId && lfgMessage.channelId === lfg.channelId))[0]; - const thisLFG = (await getMessage(thisLFGPost.channelId, thisLFGPost.messageId)).embeds[0].fields || []; - sendDirectMessage(thisLFGPost.ownerId, { - embeds: [{ - title: `Hello ${(await getUser(thisLFGPost.ownerId)).username}! Your event in ${lfgMessage.guild?.name || (await getGuild(message.guildId, { counts: false, addToCache: false })).name - } has filled up!`, - fields: [ - thisLFG[0], - { - name: 'Your members are:', - value: thisLFG[4].value, - }, - ], - }], - }); - } - - resp = `Successfully ${actionResp} LFG.`; - } else { - resp = `Failed to ${action} LFG.`; - } - - const m = await message.send({ - embeds: [{ - title: resp, - }], - }); - - m.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - message.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - } - } catch (e) { - log(LT.WARN, `Member Join/Leave/Alt command failed: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); - - const m = await message.send({ - embeds: [{ - title: 'Failed to find LFG.', - }], - }); - - m.delete('Channel Cleanup').catch((e) => { - log(LT.WARN, `Failed to clean up joiner | joining on behalf | ${jsonStringifyBig(e)}`); - }); - message.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to clean up joiner | joining on behalf | ${jsonStringifyBig(e)}`); - }); - } - } // Sets the mod role - else if (subcmd === 'set_mod_role' && (await hasGuildPermissions(message.guildId, message.authorId, ['ADMINISTRATOR']))) { - const mentionedRole = args[1] || ''; - const roleId = BigInt(mentionedRole.substr(3, mentionedRole.length - 4)); - if (message.guild?.roles.has(roleId)) { - let success = true; - if (guildModRoles.has(message.guildId)) { - // Execute the DB update - await dbClient.execute('UPDATE guild_mod_role SET roleId = ? WHERE guildId = ?', [roleId, message.guildId]).catch((e) => { - log(LT.ERROR, `Failed to insert into database: ${jsonStringifyBig(e)}`); - success = false; - }); - } else { - // Execute the DB insertion - await dbClient.execute('INSERT INTO guild_mod_role(guildId,roleId) values(?,?)', [message.guildId, roleId]).catch((e) => { - log(LT.ERROR, `Failed to insert into database: ${jsonStringifyBig(e)}`); - success = false; - }); - } - - if (success) { - guildModRoles.set(message.guildId, roleId); - message.send({ - embeds: [{ - fields: [ - { - name: 'LFG Mod Role set successfully', - value: `LFG Mod Role set to ${args[1]}.`, - }, - ], - }], - }).catch((e) => { - log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); - }); - } else { - message.send({ - embeds: [{ - fields: [ - { - name: 'Something went wrong!', - value: 'LFG Mod Role has been left unchanged.', - }, - ], - }], - }).catch((e) => { - log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); - }); - } - } else { - if (guildModRoles.has(message.guildId)) { - message.send({ - embeds: [{ - fields: [ - { - name: 'LFG Mod Role is currently set to:', - value: `<@&${guildModRoles.get(message.guildId)}>`, - }, - ], - }], - }).catch((e) => { - log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); - }); - } else { - message.send({ - embeds: [{ - fields: [ - { - name: 'There is no LFG Mod Role set for this guild.', - value: `To set one, run this command again with the role mentioned.\n\nExample: \`${prefix}lfg set_mod_role @newModRole\``, - }, - ], - }], - }).catch((e) => { - log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); - }); - } - } - } // Sets the channel cleaning up for LFG channels to keep LFG events visible and prevent conversations - else if (subcmd === 'set_clean_channel' && (await hasGuildPermissions(message.guildId, message.authorId, ['ADMINISTRATOR']))) { - const cleanSetting = (args[1] || 'list').toLowerCase(); - let success = true; - if (cleanSetting === 'on') { - // Execute the DB insertion - await dbClient.execute('INSERT INTO guild_clean_channel(guildId,channelId) values(?,?)', [message.guildId, message.channelId]).catch((e) => { - log(LT.ERROR, `Failed to insert into database: ${jsonStringifyBig(e)}`); - success = false; - }); - - if (success) { - const tempArr = cleanChannels.get(message.guildId) || []; - tempArr.push(message.channelId); - cleanChannels.set(message.guildId, tempArr); - - const m = await message.send({ - embeds: [{ - fields: [ - { - name: 'Channel Cleaning turned ON.', - value: 'This message will self destruct in 5 seconds.', - }, - ], - }], - }).catch((e) => { - log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); - }); - - m && m.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to clean up | set_clean_channel | ${jsonStringifyBig(e)}`); - }); - message.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to clean up | set_clean_channel | ${jsonStringifyBig(e)}`); - }); - } else { - message.send({ - embeds: [{ - fields: [ - { - name: 'Something went wrong!', - value: 'Channel Clean status left off.', - }, - ], - }], - }).catch((e) => { - log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); - }); - } - } else if (cleanSetting === 'off') { - // turns clean off for channel - // Execute the DB insertion - await dbClient.execute('DELETE FROM guild_clean_channel WHERE guildId = ? AND channelId = ?', [message.guildId, message.channelId]).catch((e) => { - log(LT.ERROR, `Failed to delete from database: ${jsonStringifyBig(e)}`); - success = false; - }); - - if (success) { - let tempArr = cleanChannels.get(message.guildId) || []; - tempArr = tempArr.filter((channelId) => channelId !== message.channelId); - cleanChannels.set(message.guildId, tempArr); - - const m = await message.send({ - embeds: [{ - fields: [ - { - name: 'Channel Cleaning turned OFF.', - value: 'This message will self destruct in 5 seconds.', - }, - ], - }], - }).catch((e) => { - log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); - }); - - m && m.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to clean up | set_clean_channel | ${jsonStringifyBig(e)}`); - }); - message.delete('Channel Cleanup', 5000).catch((e) => { - log(LT.WARN, `Failed to clean up | set_clean_channel | ${jsonStringifyBig(e)}`); - }); - } else { - message.send({ - embeds: [{ - fields: [ - { - name: 'Something went wrong!', - value: 'Channel Clean status left on.', - }, - ], - }], - }).catch((e) => { - log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); - }); - } - } else if (cleanSetting === 'list') { - // send list of channels with clean on - let cleanChannelStr = ''; - - for (const channelId of cleanChannels.get(message.guildId) || []) { - cleanChannelStr += `<#${channelId}>\n`; - } - cleanChannelStr = cleanChannelStr.substr(0, cleanChannelStr.length - 1); - - const tmpEmbed: Embed = {}; - - if (cleanChannelStr) { - tmpEmbed.fields = [ - { - name: 'Clean Channels enabled for this guild:', - value: cleanChannelStr, - }, - ]; - } else { - tmpEmbed.title = 'No Clean Channels are enabled for this guild.'; - } - - await message.send({ - embeds: [tmpEmbed], - }).catch((e) => { - log(LT.WARN, `Failed to send message | ${jsonStringifyBig(e)}`); - }); - } - } - } // report or r (command that failed) - // Manually report something that screwed up - else if (command === 'report' || command === 'r') { - // Light telemetry to see how many times a command is being run - dbClient.execute(`CALL INC_CNT("report");`).catch((e) => { - log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${jsonStringifyBig(e)}`); - }); - - sendMessage(config.reportChannel, 'USER REPORT:\n' + args.join(' ')).catch((e) => { - log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); - }); - message.send(constantCmds.report).catch((e) => { - log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); - }); - } // version or v - // Returns version of the bot - else if (command === 'version' || command === 'v') { - // Light telemetry to see how many times a command is being run - dbClient.execute(`CALL INC_CNT("version");`).catch((e) => { - log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${jsonStringifyBig(e)}`); - }); - - message.send(constantCmds.version).catch((e) => { - log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); - }); - } // info or i - // Info command, prints short desc on bot and some links - else if (command === 'info' || command === 'i') { - // Light telemetry to see how many times a command is being run - dbClient.execute(`CALL INC_CNT("info");`).catch((e) => { - log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${jsonStringifyBig(e)}`); - }); - - message.send(constantCmds.info).catch((e) => { - log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); - }); - } // help or h or ? - // Help command, prints available commands - else if (command === 'help' || command === 'h' || command === '?') { - // Light telemetry to see how many times a command is being run - dbClient.execute(`CALL INC_CNT("help");`).catch((e) => { - log(LT.ERROR, `Failed to call stored procedure INC_CNT: ${jsonStringifyBig(e)}`); - }); - - message.send(constantCmds.help).catch((e) => { - log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); - }); - } // announce - // Sends important announcement about rewrite of bot to all server owners - else if (command === 'announce' && message.authorId === config.owner) { - message.send('Sending Announcement to all Server Owners:'); - - const owners: Array = []; - cache.guilds.forEach(x => { - if (!owners.includes(x.ownerId)) { - owners.push(x.ownerId); - } - }); - message.send(`Sending DM to following user Ids: ${owners.join(', ')}`); - for (const ownerId of owners) { - if (args[0] === 'all') { - console.log(`Message sent to ${ownerId}`); - sendDirectMessage(ownerId, constantCmds.announcement).then(() => sendDirectMessage(ownerId, constantCmds.announcementPart2).catch((e) => { - log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); - })).catch((e) => { - log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); - }); - - } else if (args[0] === 'dry') { - console.log(`Fake Message sent to ${ownerId}`); - } - } - message.send(constantCmds.announcement).then(() => message.send(constantCmds.announcementPart2).catch((e) => { - log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); - })).catch((e) => { - log(LT.ERROR, `Failed to send message: ${jsonStringifyBig(message)} | ${jsonStringifyBig(e)}`); - }); - - } - }, - interactionCreate: async (interact, member) => { - try { - if (interact.type === DiscordInteractionTypes.MessageComponent) { - if (interact.message && interact.data && (interact.data as ButtonData).customId && member) { - log(LT.INFO, `Handling Button ${(interact.data as ButtonData).customId}`); - log(LT.LOG, `Button Data | ${jsonStringifyBig(interact)}`); - - sendInteractionResponse(BigInt(interact.id), interact.token, { - type: DiscordInteractionResponseTypes.DeferredUpdateMessage, - }); - - const [handler, stepInfo] = (interact.data as ButtonData).customId.split('@'); - const [action, value] = stepInfo.split('#'); - switch (handler) { - case 'building': { - await activeBuilders.some(async (x, i) => { - if (x.channelId === BigInt(interact.channelId || '0') && member && x.userId === BigInt(member.id)) { - x.lastTouch = new Date(); - x = await handleLFGStep(x, value); - - if (x.step === 'done' && x.lfgMsg.components) { - const currentLFG = (x.lfgMsg.embeds[0].fields || []); - const newTimestamp = new Date(parseInt(currentLFG[1].value.split('#')[1])); - const newLfgUid = ALPHABET[Math.floor(Math.random() * 26)] + ALPHABET[Math.floor(Math.random() * 26)]; - - const tempMembers = currentLFG[4].name.split(':')[1].split('/'); - const currentMembers = parseInt(tempMembers[0]); - const maxMembers = parseInt(tempMembers[1]); - - const buttonRow: ActionRow = x.lfgMsg.components[0] as ActionRow; - - if (currentMembers > maxMembers) { - const currentPeople = currentLFG[4].value.split('\n'); - const newAlts = currentPeople.splice(maxMembers - 1); - currentLFG[4].value = currentPeople.join('\n'); - currentLFG[5].value = `${newAlts.join('\n')}\n${currentLFG[5].value}`; - currentLFG[4].name = `Members Joined: ${maxMembers}/${maxMembers}`; - } - - await x.lfgMsg.edit({ - content: '', - embeds: [{ - fields: currentLFG, - footer: { - text: `Created by: ${member.username} | ${newLfgUid}`, - }, - timestamp: newTimestamp.toISOString(), - }], - components: [buttonRow], - }); - - const activeIdx = activeLFGPosts.findIndex((lfg) => (lfg.channelId === x.channelId && lfg.messageId === x.lfgMsg.id && lfg.ownerId === x.userId)); - activeLFGPosts[activeIdx].lfgTime = newTimestamp.getTime(); - activeLFGPosts[activeIdx].lfgUid = newLfgUid; - localStorage.setItem('activeLFGPosts', jsonStringifyBig(activeLFGPosts)); - - await activeBuilders[i].questionMsg.delete().catch((e) => { - log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`); - }); - activeBuilders.splice(i, 1); - } else { - activeBuilders[i] = x; - } - - return true; - } - }); - break; - } - case 'active': { - const message = await getMessage(BigInt(interact.channelId || '0'), BigInt(interact.message.id)); - - const embeds = message.embeds[0].fields || []; - let results: JoinLeaveType = { - embed: [], - success: false, - full: true, - justFilled: false, - }; - switch (action) { - case 'join_group': - results = handleMemberJoin(embeds, member, false); - break; - case 'leave_group': - results = handleMemberLeave(embeds, member); - break; - case 'alternate_group': - results = handleMemberJoin(embeds, member, true); - break; - } - - if (results.success && message.components) { - await message.edit({ - embeds: [{ - fields: results.embed, - footer: message.embeds[0].footer, - timestamp: message.embeds[0].timestamp, - }], - }); - - if (results.justFilled) { - const thisLFGPost = activeLFGPosts.filter((lfg) => (message.id === lfg.messageId && message.channelId === lfg.channelId))[0]; - const thisLFG = (await getMessage(thisLFGPost.channelId, thisLFGPost.messageId)).embeds[0].fields || []; - sendDirectMessage(thisLFGPost.ownerId, { - embeds: [{ - title: `Hello ${(await getUser(thisLFGPost.ownerId)).username}! Your event in ${message.guild?.name || (await getGuild(message.guildId, { counts: false, addToCache: false })).name - } has filled up!`, - fields: [ - thisLFG[0], - { - name: 'Your members are:', - value: thisLFG[4].value, - }, - ], - }], - }); - } - } - - break; - } - case 'editing': { - await activeBuilders.some(async (x, i) => { - if (x.editing && x.channelId === BigInt(interact.channelId || '0') && member && x.userId === BigInt(member.id)) { - x.step = action; - x.lastTouch = new Date(); - let nextQuestion = ''; - const nextComponents: Array = []; - switch (action) { - case 'set_game': { - nextQuestion = lfgStepQuestions.set_game; - - const gameButtons: Array = Object.keys(LFGActivities).map((game) => { - return { - type: 2, - label: game, - customId: `building@set_game#${game}`, - style: DiscordButtonStyles.Primary, - }; - }); - - const temp: Array = []; - - gameButtons.forEach((btn, idx) => { - if (!temp[Math.floor(idx / 5)]) { - temp[Math.floor(idx / 5)] = [btn]; - } else { - temp[Math.floor(idx / 5)].push(btn); - } - }); - - temp.forEach((btns) => { - if (btns.length && btns.length <= 5) { - nextComponents.push({ - type: 1, - components: btns, - }); - } - }); - break; - } - case 'set_time': { - nextQuestion = 'Please enter the time of the activity:'; - break; - } - case 'set_desc': { - nextQuestion = 'Please enter a description for the activity. Enter `none` to skip:'; - break; - } - default: - break; - } - - x.questionMsg = await x.questionMsg.edit({ - content: nextQuestion, - components: nextComponents, - }); - - activeBuilders[i] = x; - - return true; - } - }); - break; - } - default: - break; - } - } - } - } catch (e) { - log(LT.ERROR, `Interaction failed: ${jsonStringifyBig(interact)} | ${jsonStringifyBig(member)} | ${jsonStringifyBig(e)}`); - } - }, - }, -}); +// Announce the slash commands so users can use them +await createSlashCommands(bot); diff --git a/src/botListPoster.ts b/src/botListPoster.ts new file mode 100644 index 0000000..61e0b82 --- /dev/null +++ b/src/botListPoster.ts @@ -0,0 +1,26 @@ +import { botId, log, LT } from '../deps.ts'; +import config from '../config.ts'; + +// updateListStatistics(bot ID, current guild count) returns nothing, posts to botlists +// Sends the current server count to all bot list sites we are listed on +export const updateBotListStatistics = (serverCount: number): void => { + config.botLists.forEach(async (botList) => { + try { + log(LT.LOG, `Updating statistics for ${JSON.stringify(botList)}`); + if (botList.enabled) { + const tempHeaders = new Headers(); + tempHeaders.append(botList.headers[0].header, botList.headers[0].value); + tempHeaders.append('Content-Type', 'application/json'); + // ?{} is a template used in config, just need to replace it with the real value + const response = await fetch(botList.apiUrl.replace('?{bot_id}', botId.toString()), { + 'method': 'POST', + 'headers': tempHeaders, + 'body': JSON.stringify(botList.body).replace('"?{server_count}"', serverCount.toString()), // ?{server_count} needs the "" removed from around it aswell to make sure its sent as a number + }); + log(LT.INFO, `Posted server count to ${botList.name}. Results: ${JSON.stringify(response)}`); + } + } catch (err) { + log(LT.ERROR, `Failed to update statistics for ${botList.name} | Error: ${err.name} - ${err.message}`) + } + }); +}; diff --git a/src/buttons/_index.ts b/src/buttons/_index.ts new file mode 100644 index 0000000..39bea92 --- /dev/null +++ b/src/buttons/_index.ts @@ -0,0 +1,48 @@ +import { Button } from '../types/commandTypes.ts'; +import { gameSelectionButton } from './event-creation/step1-gameSelection.ts'; +import { createCustomEventButton } from './event-creation/step1a-openCustomModal.ts'; +import { verifyCustomEventButton } from './event-creation/step1b-verifyCustomActivity.ts'; +import { finalizeEventButton } from './event-creation/step2-finalize.ts'; +import { createEventButton } from './event-creation/step3-createEvent.ts'; +import { joinEventButton } from './live-event/joinEvent.ts'; +import { leaveEventButton } from './live-event/leaveEvent.ts'; +import { alternateEventButton } from './live-event/alternateEvent.ts'; +import { joinRequestButton } from './live-event/joinRequest.ts'; +import { alternateRequestButton } from './live-event/alternateRequest.ts'; +import { deleteEventButton } from './live-event/deleteEvent.ts'; +import { deleteConfirmedButton } from './live-event/deleteConfirmed.ts'; +import { editEventButton } from './live-event/editEvent.ts'; +import { editDescriptionButton } from './live-event/editDescription.ts'; +import { editDateTimeButton } from './live-event/editDateTime.ts'; +import { applyDescriptionButton } from './live-event/applyDescription.ts'; +import { applyDateTimeButton } from './live-event/applyDateTime.ts'; +import { updateEventButton } from './live-event/updateEvent.ts'; +import { toggleWLStatusButton } from './live-event/toggleWLStatus.ts'; +import { editActivityButton } from './live-event/editActivity.ts'; +import { editActivityCustomButton } from './live-event/editActivity-custom.ts'; +import { leaveEventViaDMButton } from './live-event/leaveViaDM.ts'; + +export const buttons: Array