Merge branch 'rewrite'
This commit is contained in:
commit
5660e08574
|
@ -11,5 +11,9 @@
|
|||
"spellright.documentTypes": [],
|
||||
"deno.suggest.imports.hosts": {
|
||||
"https://deno.land": true
|
||||
}
|
||||
},
|
||||
"cSpell.words": [
|
||||
"sproc",
|
||||
"USTZ"
|
||||
]
|
||||
}
|
|
@ -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 (<ean@milligan.dev>) 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.
|
59
README.md
59
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).
|
||||
|
|
|
@ -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 <ean@milligan.dev>.
|
|
@ -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
|
||||
|
|
106
db/initialize.ts
106
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!');
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
36
deno.json
36
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": {
|
||||
"include": [
|
||||
"src/",
|
||||
"db/",
|
||||
"mod.ts",
|
||||
"deps.ts",
|
||||
"config.ts",
|
||||
"config.example.ts"
|
||||
],
|
||||
"exclude": [],
|
||||
"useTabs": true,
|
||||
"lineWidth": 200,
|
||||
"indentWidth": 2,
|
||||
"singleQuote": true,
|
||||
"proseWrap": "preserve"
|
||||
}
|
||||
}
|
||||
}
|
69
deps.ts
69
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';
|
||||
|
||||
|
|
4
flags.ts
4
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;
|
||||
|
|
|
@ -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"
|
|
@ -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}`)
|
||||
}
|
||||
});
|
||||
};
|
|
@ -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<Button> = [
|
||||
gameSelectionButton,
|
||||
createCustomEventButton,
|
||||
verifyCustomEventButton,
|
||||
finalizeEventButton,
|
||||
createEventButton,
|
||||
joinEventButton,
|
||||
leaveEventButton,
|
||||
alternateEventButton,
|
||||
joinRequestButton,
|
||||
alternateRequestButton,
|
||||
deleteEventButton,
|
||||
deleteConfirmedButton,
|
||||
editEventButton,
|
||||
editDescriptionButton,
|
||||
editDateTimeButton,
|
||||
applyDescriptionButton,
|
||||
applyDateTimeButton,
|
||||
updateEventButton,
|
||||
toggleWLStatusButton,
|
||||
editActivityButton,
|
||||
editActivityCustomButton,
|
||||
leaveEventViaDMButton,
|
||||
];
|
|
@ -0,0 +1,211 @@
|
|||
// Activity should either have maxMembers or options specified, NOT both
|
||||
export type Activity = {
|
||||
name: string;
|
||||
maxMembers?: number;
|
||||
options?: Array<Activity>;
|
||||
};
|
||||
|
||||
// Max depth is limited to 4, 5th component row must be reserved for the custom button
|
||||
export const Activities: Array<Activity> = [
|
||||
{
|
||||
name: 'Destiny 2',
|
||||
options: [
|
||||
{
|
||||
name: 'Raids',
|
||||
options: [
|
||||
{
|
||||
name: 'Root of Nightmares',
|
||||
maxMembers: 6,
|
||||
},
|
||||
{
|
||||
name: 'King\'s Fall',
|
||||
maxMembers: 6,
|
||||
},
|
||||
{
|
||||
name: 'Vow of the Disciple',
|
||||
maxMembers: 6,
|
||||
},
|
||||
{
|
||||
name: 'Vault of Glass',
|
||||
maxMembers: 6,
|
||||
},
|
||||
{
|
||||
name: 'Deep Stone Crypt',
|
||||
maxMembers: 6,
|
||||
},
|
||||
{
|
||||
name: 'Garden of Salvation',
|
||||
maxMembers: 6,
|
||||
},
|
||||
{
|
||||
name: 'Last Wish',
|
||||
maxMembers: 6,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Dungeons',
|
||||
options: [
|
||||
{
|
||||
name: 'Spire of the Watcher',
|
||||
maxMembers: 3,
|
||||
},
|
||||
{
|
||||
name: 'Duality',
|
||||
maxMembers: 3,
|
||||
},
|
||||
{
|
||||
name: 'Grasp of Avarice',
|
||||
maxMembers: 3,
|
||||
},
|
||||
{
|
||||
name: 'Prophecy',
|
||||
maxMembers: 3,
|
||||
},
|
||||
{
|
||||
name: 'Pit of Heresy',
|
||||
maxMembers: 3,
|
||||
},
|
||||
{
|
||||
name: 'Shattered Throne',
|
||||
maxMembers: 3,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Crucible',
|
||||
options: [
|
||||
{
|
||||
name: 'Crucible Labs',
|
||||
maxMembers: 6,
|
||||
},
|
||||
{
|
||||
name: 'Competitive',
|
||||
maxMembers: 3,
|
||||
},
|
||||
{
|
||||
name: 'Clash',
|
||||
maxMembers: 6,
|
||||
},
|
||||
{
|
||||
name: 'Weekly Mode',
|
||||
maxMembers: 3,
|
||||
},
|
||||
{
|
||||
name: 'Iron Banner',
|
||||
maxMembers: 6,
|
||||
},
|
||||
{
|
||||
name: 'Trials of Osiris',
|
||||
maxMembers: 3,
|
||||
},
|
||||
{
|
||||
name: 'Private Match',
|
||||
maxMembers: 12,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Gambit',
|
||||
options: [
|
||||
{
|
||||
name: 'Classic',
|
||||
maxMembers: 4,
|
||||
},
|
||||
{
|
||||
name: 'Private Match',
|
||||
maxMembers: 8,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Vanguard',
|
||||
options: [
|
||||
{
|
||||
name: 'Vanguard Ops',
|
||||
maxMembers: 3,
|
||||
},
|
||||
{
|
||||
name: 'Nightfall',
|
||||
maxMembers: 3,
|
||||
},
|
||||
{
|
||||
name: 'Grandmaster Nightfall',
|
||||
maxMembers: 3,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Exotic Missions',
|
||||
options: [
|
||||
{
|
||||
name: '//node.ovrd.AVALON//',
|
||||
maxMembers: 3,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Miscellaneous',
|
||||
options: [
|
||||
{
|
||||
name: 'Defiant Battlegrounds',
|
||||
maxMembers: 3,
|
||||
},
|
||||
{
|
||||
name: 'Terminal Overload',
|
||||
maxMembers: 12,
|
||||
},
|
||||
{
|
||||
name: 'Vex Incursion Zone',
|
||||
maxMembers: 12,
|
||||
},
|
||||
{
|
||||
name: 'Partition: Ordnance',
|
||||
maxMembers: 3,
|
||||
},
|
||||
{
|
||||
name: 'Lightfall Campaign Mission',
|
||||
maxMembers: 3,
|
||||
},
|
||||
{
|
||||
name: 'Weekly Witch Queen Campaign Mission',
|
||||
maxMembers: 3,
|
||||
},
|
||||
{
|
||||
name: 'Wellspring',
|
||||
maxMembers: 6,
|
||||
},
|
||||
{
|
||||
name: 'Dares of Eternity',
|
||||
maxMembers: 6,
|
||||
},
|
||||
{
|
||||
name: 'Wrathborn Hunt',
|
||||
maxMembers: 3,
|
||||
},
|
||||
{
|
||||
name: 'Empire Hunt',
|
||||
maxMembers: 3,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Among Us',
|
||||
options: [
|
||||
{
|
||||
name: 'Vanilla',
|
||||
maxMembers: 15,
|
||||
},
|
||||
{
|
||||
name: 'Hide n Seek',
|
||||
maxMembers: 15,
|
||||
},
|
||||
{
|
||||
name: 'Modded',
|
||||
maxMembers: 15,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
|
@ -0,0 +1,176 @@
|
|||
const monthsLong: Array<string> = ['JANUARY', 'FEBRUARY', 'MARCH', 'APRIL', 'MAY', 'JUNE', 'JULY', 'AUGUST', 'SEPTEMBER', 'OCTOBER', 'NOVEMBER', 'DECEMBER'];
|
||||
export const monthsShort: Array<string> = monthsLong.map((month) => month.slice(0, 3));
|
||||
const tzMap: Map<string, string> = new Map([
|
||||
['CDT', '-05:00'],
|
||||
['CST', '-06:00'],
|
||||
['PST', '-08:00'],
|
||||
['IST', '+05:30'],
|
||||
['GMT', '+00:00'],
|
||||
['EAT', '+03:00'],
|
||||
['CET', '+01:00'],
|
||||
['WAT', '+01:00'],
|
||||
['CAT', '+02:00'],
|
||||
['EET', '+02:00'],
|
||||
['CEST', '+02:00'],
|
||||
['SAST', '+02:00'],
|
||||
['HST', '-10:00'],
|
||||
['HDT', '-09:00'],
|
||||
['AKST', '-09:00'],
|
||||
['AKDT', '-08:00'],
|
||||
['AST', '-04:00'],
|
||||
['EST', '-05:00'],
|
||||
['MST', '-07:00'],
|
||||
['MDT', '-06:00'],
|
||||
['EDT', '-04:00'],
|
||||
['PDT', '-07:00'],
|
||||
['ADT', '-03:00'],
|
||||
['NST', '-03:30'],
|
||||
['NDT', '-02:30'],
|
||||
['AEST', '+10:00'],
|
||||
['AEDT', '+11:00'],
|
||||
['NZST', '+12:00'],
|
||||
['NZDT', '+13:00'],
|
||||
['EEST', '+03:00'],
|
||||
['HKT', '+08:00'],
|
||||
['WIB', '+07:00'],
|
||||
['WIT', '+09:00'],
|
||||
['IDT', '+03:00'],
|
||||
['PKT', '+05:00'],
|
||||
['WITA', '+08:00'],
|
||||
['KST', '+09:00'],
|
||||
['JST', '+09:00'],
|
||||
['WET', '+00:00'],
|
||||
['WEST', '+01:00'],
|
||||
['ACST', '+09:30'],
|
||||
['ACDT', '+10:30'],
|
||||
['AWST', '+08:00'],
|
||||
['UTC', '+00:00'],
|
||||
['BST', '+01:00'],
|
||||
['MSK', '+03:00'],
|
||||
['MET', '+01:00'],
|
||||
['MEST', '+02:00'],
|
||||
['CHST', '+10:00'],
|
||||
['SST', '-11:00'],
|
||||
]);
|
||||
const shorthandUSTZ: Array<string> = ['ET', 'CT', 'MT', 'PT'];
|
||||
|
||||
// Takes user input Time and makes it actually usable
|
||||
const parseEventTime = (preParsedEventTime: string): [string, string, string] => {
|
||||
let parsedEventTimePeriod = '';
|
||||
// Get AM or PM out of the rawTime
|
||||
if (preParsedEventTime.endsWith('AM') || preParsedEventTime.endsWith('PM')) {
|
||||
parsedEventTimePeriod = preParsedEventTime.slice(-2);
|
||||
preParsedEventTime = preParsedEventTime.slice(0, -2).trim();
|
||||
}
|
||||
let parsedEventTimeHours: string;
|
||||
let parsedEventTimeMinutes: string;
|
||||
// Get Hours and Minutes out of rawTime
|
||||
if (preParsedEventTime.length > 2) {
|
||||
parsedEventTimeMinutes = preParsedEventTime.slice(-2);
|
||||
parsedEventTimeHours = preParsedEventTime.slice(0, -2).trim();
|
||||
} else {
|
||||
parsedEventTimeHours = preParsedEventTime.trim();
|
||||
parsedEventTimeMinutes = '00';
|
||||
}
|
||||
// Determine if we need to remove the time period
|
||||
if (parseInt(parsedEventTimeHours) > 12) {
|
||||
parsedEventTimePeriod = '';
|
||||
}
|
||||
|
||||
if (!parsedEventTimePeriod && parsedEventTimeHours.length < 2) {
|
||||
parsedEventTimeHours = `0${parsedEventTimeHours}`;
|
||||
}
|
||||
|
||||
return [parsedEventTimeHours, parsedEventTimeMinutes, parsedEventTimePeriod];
|
||||
};
|
||||
|
||||
// Check if DST is currently active
|
||||
export const isDSTActive = (): boolean => {
|
||||
const today = new Date();
|
||||
const jan = new Date(today.getFullYear(), 0, 1);
|
||||
const jul = new Date(today.getFullYear(), 6, 1);
|
||||
|
||||
return today.getTimezoneOffset() < Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset());
|
||||
};
|
||||
|
||||
// Takes user input Time Zone and makes it actually usable
|
||||
const parseEventTimeZone = (preParsedEventTimeZone: string): [string, string] => {
|
||||
if (shorthandUSTZ.includes(preParsedEventTimeZone)) {
|
||||
// Handle shorthand US timezones, adding S for standard time and D for Daylight Savings
|
||||
if (isDSTActive()) {
|
||||
preParsedEventTimeZone = `${preParsedEventTimeZone.slice(0, 1)}DT`;
|
||||
} else {
|
||||
preParsedEventTimeZone = `${preParsedEventTimeZone.slice(0, 1)}ST`;
|
||||
}
|
||||
}
|
||||
if (tzMap.has(preParsedEventTimeZone)) {
|
||||
// TZ is proper abbreviation, use our map to convert
|
||||
return [`UTC${tzMap.get(preParsedEventTimeZone)}`, preParsedEventTimeZone];
|
||||
} else {
|
||||
// Determine if user put in UTC4, which needs to be UTC+4
|
||||
let addPlusSign = false;
|
||||
if (!preParsedEventTimeZone.includes('+') && !preParsedEventTimeZone.includes('-')) {
|
||||
addPlusSign = true;
|
||||
}
|
||||
// Determine if we need to prepend UTC/GMT, handle adding the + into the string
|
||||
if (!preParsedEventTimeZone.startsWith('UTC') && preParsedEventTimeZone.startsWith('GMT')) {
|
||||
preParsedEventTimeZone = `UTC${addPlusSign && '+'}${preParsedEventTimeZone}`;
|
||||
} else if (addPlusSign) {
|
||||
preParsedEventTimeZone = `${preParsedEventTimeZone.slice(0, 3)}+${preParsedEventTimeZone.slice(3)}`;
|
||||
}
|
||||
return [preParsedEventTimeZone, preParsedEventTimeZone];
|
||||
}
|
||||
};
|
||||
|
||||
// Takes user input Date and makes it actually usable
|
||||
const parseEventDate = (preParsedEventDate: string): [string, string, string] => {
|
||||
const today = new Date();
|
||||
let [parsedEventMonth, parsedEventDay, parsedEventYear] = preParsedEventDate.split(/[\s,\\/-]+/g);
|
||||
|
||||
if (isNaN(parseInt(parsedEventDay))) {
|
||||
// User only provided one word, we're assuming it was TOMORROW, and all others will be treated as today
|
||||
if (parsedEventMonth.includes('TOMORROW')) {
|
||||
const tomorrow = new Date(today);
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
|
||||
parsedEventYear = tomorrow.getFullYear().toString();
|
||||
parsedEventMonth = monthsLong[tomorrow.getMonth()];
|
||||
parsedEventDay = tomorrow.getDate().toString();
|
||||
} else {
|
||||
parsedEventYear = today.getFullYear().toString();
|
||||
parsedEventMonth = monthsLong[today.getMonth()];
|
||||
parsedEventDay = today.getDate().toString();
|
||||
}
|
||||
} else {
|
||||
// Month and Day exist, so determine year and parse month/day
|
||||
parsedEventYear = (isNaN(parseInt(parsedEventYear)) ? today.getFullYear() : parseInt(parsedEventYear)).toString();
|
||||
parsedEventDay = parseInt(parsedEventDay).toString();
|
||||
if (!monthsLong.includes(parsedEventMonth) && !monthsShort.includes(parsedEventMonth)) {
|
||||
parsedEventMonth = monthsShort[parseInt(parsedEventMonth) - 1];
|
||||
}
|
||||
}
|
||||
|
||||
return [parsedEventYear, parsedEventMonth, parsedEventDay];
|
||||
};
|
||||
|
||||
// Take full raw Date/Time input and convert it to a proper Date
|
||||
export const getDateFromRawInput = (rawEventTime: string, rawEventTimeZone: string, rawEventDate: string): [Date, string, boolean, boolean] => {
|
||||
// Verify/Set Time
|
||||
const [parsedEventTimeHours, parsedEventTimeMinutes, parsedEventTimePeriod] = parseEventTime(rawEventTime.replaceAll(':', '').toUpperCase());
|
||||
|
||||
// Verify/Set Time Zone
|
||||
const [parsedEventTimeZone, userInputTimeZone] = parseEventTimeZone(rawEventTimeZone.replaceAll(' ', '').trim().toUpperCase());
|
||||
|
||||
// Verify/Set Date
|
||||
const [parsedEventYear, parsedEventMonth, parsedEventDay] = parseEventDate(rawEventDate.trim().toUpperCase());
|
||||
|
||||
const parsedDateTime = new Date(`${parsedEventMonth} ${parsedEventDay}, ${parsedEventYear} ${parsedEventTimeHours}:${parsedEventTimeMinutes} ${parsedEventTimePeriod} ${parsedEventTimeZone}`);
|
||||
return [
|
||||
parsedDateTime,
|
||||
`${parsedEventTimeHours}${parsedEventTimePeriod ? ':' : ''}${parsedEventTimeMinutes} ${parsedEventTimePeriod} ${userInputTimeZone} ${parsedEventMonth.slice(0, 1)}${
|
||||
parsedEventMonth.slice(1, 3).toLowerCase()
|
||||
} ${parsedEventDay}, ${parsedEventYear}`,
|
||||
parsedDateTime.getTime() > new Date().getTime(),
|
||||
!isNaN(parsedDateTime.getTime()),
|
||||
];
|
||||
};
|
|
@ -0,0 +1,150 @@
|
|||
import { ActionRow, ApplicationCommandFlags, ApplicationCommandTypes, Bot, ButtonStyles, Interaction, InteractionResponseTypes, MessageComponentTypes, SelectMenuComponent } from '../../../deps.ts';
|
||||
import { infoColor1, somethingWentWrong } from '../../commandUtils.ts';
|
||||
import { CommandDetails } from '../../types/commandTypes.ts';
|
||||
import { Activities } from './activities.ts';
|
||||
import { generateActionRow, getNestedActivity, invalidDateTimeStr } from './utils.ts';
|
||||
import { dateTimeFields, descriptionTextField, fillerChar, idSeparator, LfgEmbedIndexes, lfgStartTimeName, pathIdxEnder, pathIdxSeparator } from '../eventUtils.ts';
|
||||
import { addTokenToMap, deleteTokenEarly, generateMapId, selfDestructMessage, tokenMap } from '../tokenCleanup.ts';
|
||||
import utils from '../../utils.ts';
|
||||
import { customId as createCustomActivityBtnId } from './step1a-openCustomModal.ts';
|
||||
import { customId as finalizeEventBtnId } from './step2-finalize.ts';
|
||||
import { monthsShort } from './dateTimeUtils.ts';
|
||||
import { dbClient, queries } from '../../db.ts';
|
||||
import { createEventSlashName } from '../../commands/slashCommandNames.ts';
|
||||
|
||||
export const customId = 'gameSel';
|
||||
const details: CommandDetails = {
|
||||
name: createEventSlashName,
|
||||
description: 'Creates a new event in this channel.',
|
||||
type: ApplicationCommandTypes.ChatInput,
|
||||
};
|
||||
|
||||
const generateCustomEventRow = (title: string, subtitle: string): ActionRow => ({
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
components: [{
|
||||
type: MessageComponentTypes.Button,
|
||||
style: ButtonStyles.Primary,
|
||||
label: 'Create Custom Event',
|
||||
customId: `${createCustomActivityBtnId}${idSeparator}${title}${pathIdxSeparator}${subtitle}${pathIdxSeparator}`,
|
||||
}],
|
||||
});
|
||||
|
||||
const execute = async (bot: Bot, interaction: Interaction) => {
|
||||
if (interaction.data && (interaction.data.name === createEventSlashName || interaction.data.customId) && interaction.member && interaction.guildId && interaction.channelId) {
|
||||
// Light Telemetry
|
||||
if (interaction.data.name === createEventSlashName) {
|
||||
dbClient.execute(queries.callIncCnt('cmd-gameSel')).catch((e) => utils.commonLoggers.dbError('step1-gameSelection.ts@cmd', 'call sproc INC_CNT on', e));
|
||||
}
|
||||
if (interaction.data.customId === customId) {
|
||||
dbClient.execute(queries.callIncCnt('btn-gameSel')).catch((e) => utils.commonLoggers.dbError('step1-gameSelection.ts@btn', 'call sproc INC_CNT on', e));
|
||||
}
|
||||
|
||||
// Check if we are done
|
||||
const customIdIdxPath = (interaction.data.customId || '').substring((interaction.data.customId || '').indexOf(idSeparator) + 1) || '';
|
||||
const valuesIdxPath = interaction.data?.values?.[0] || '';
|
||||
const strippedIdxPath = interaction.data.customId?.includes(idSeparator) ? customIdIdxPath : valuesIdxPath;
|
||||
const finalizedIdxPath = strippedIdxPath.substring(0, strippedIdxPath.lastIndexOf(pathIdxEnder));
|
||||
if ((interaction.data.customId?.includes(idSeparator) && interaction.data.customId.endsWith(pathIdxEnder)) || interaction.data?.values?.[0].endsWith(pathIdxEnder)) {
|
||||
let prefillTime = '';
|
||||
let prefillTimeZone = '';
|
||||
let prefillDate = '';
|
||||
let prefillDescription = '';
|
||||
if (interaction.message && interaction.message.embeds[0].fields && interaction.message.embeds[0].fields[LfgEmbedIndexes.StartTime].name === lfgStartTimeName) {
|
||||
if (interaction.message.embeds[0].fields[LfgEmbedIndexes.StartTime].value !== invalidDateTimeStr) {
|
||||
let rawEventDateTime = interaction.message.embeds[0].fields[LfgEmbedIndexes.StartTime].value.split('\n')[0].split(' ');
|
||||
const monthIdx = rawEventDateTime.findIndex((item) => monthsShort.includes(item.toUpperCase()));
|
||||
prefillTime = rawEventDateTime.slice(0, monthIdx - 1).join(' ').trim();
|
||||
prefillTimeZone = (rawEventDateTime[monthIdx - 1] || '').trim();
|
||||
prefillDate = rawEventDateTime.slice(monthIdx).join(' ').trim();
|
||||
}
|
||||
prefillDescription = interaction.message.embeds[0].fields[LfgEmbedIndexes.Description].value.trim();
|
||||
}
|
||||
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.Modal,
|
||||
data: {
|
||||
title: 'Enter Event Details',
|
||||
customId: `${finalizeEventBtnId}${idSeparator}${finalizedIdxPath}`,
|
||||
components: [...dateTimeFields(prefillTime, prefillTimeZone, prefillDate), descriptionTextField(prefillDescription)],
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('step1-gameSelection.ts:modal', interaction, e));
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse indexPath from the select value
|
||||
const rawIdxPath: Array<string> = interaction.data.values ? interaction.data.values[0].split(pathIdxSeparator) : [''];
|
||||
const idxPath: Array<number> = rawIdxPath.map((rawIdx) => rawIdx ? parseInt(rawIdx) : -1);
|
||||
const selectMenus: Array<ActionRow> = [];
|
||||
// Use fillerChar to create unique customIds for dropdowns
|
||||
// We also leverage this to determine if its the first time the user has entered gameSel
|
||||
let selectMenuCustomId = `${customId}${fillerChar}`;
|
||||
let currentBaseValue = '';
|
||||
|
||||
for (let i = 0; i < idxPath.length; i++) {
|
||||
const idx = idxPath[i];
|
||||
const idxPathCopy = [...idxPath].slice(0, i);
|
||||
selectMenus.push(generateActionRow(currentBaseValue, getNestedActivity(idxPathCopy, Activities), selectMenuCustomId, idx));
|
||||
|
||||
selectMenuCustomId = `${selectMenuCustomId}${fillerChar}`;
|
||||
currentBaseValue = `${currentBaseValue}${idx}${pathIdxSeparator}`;
|
||||
}
|
||||
|
||||
// Prefill the custom event modal
|
||||
const prefillArray: Array<string> = [];
|
||||
selectMenus.forEach((menu) => {
|
||||
try {
|
||||
const menuOption = (menu.components[0] as SelectMenuComponent).options.find((option) => option.default);
|
||||
if (menuOption) {
|
||||
prefillArray.push(menuOption.label);
|
||||
}
|
||||
} catch (_e) {
|
||||
// do nothing, don't care
|
||||
}
|
||||
});
|
||||
selectMenus.push(generateCustomEventRow(prefillArray.length ? prefillArray[0] : '', prefillArray.length > 1 ? prefillArray[prefillArray.length - 1] : ''));
|
||||
|
||||
if (interaction.data.customId && interaction.data.customId.includes(fillerChar)) {
|
||||
// Let discord know we didn't ignore the user
|
||||
await bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.DeferredUpdateMessage,
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('step1-gameSelection.ts:ping', interaction, e));
|
||||
|
||||
// Update the original game selector
|
||||
await bot.helpers.editOriginalInteractionResponse(tokenMap.get(generateMapId(interaction.guildId, interaction.channelId, interaction.member.id))?.token || '', {
|
||||
components: selectMenus,
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('step1-gameSelection.ts:edit', interaction, e));
|
||||
} else {
|
||||
// Delete old token entry if it exists
|
||||
await deleteTokenEarly(bot, interaction, interaction.guildId, interaction.channelId, interaction.member.id);
|
||||
|
||||
// Store token for later use
|
||||
addTokenToMap(bot, interaction, interaction.guildId, interaction.channelId, interaction.member.id);
|
||||
|
||||
// Send initial interaction
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
embeds: [{
|
||||
title: 'Please select a Game and Activity, or create a Custom Event.',
|
||||
description: selfDestructMessage(new Date().getTime()),
|
||||
color: infoColor1,
|
||||
}],
|
||||
flags: ApplicationCommandFlags.Ephemeral,
|
||||
components: selectMenus,
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('step1-gameSelection.ts:init', interaction, e));
|
||||
}
|
||||
} else {
|
||||
somethingWentWrong(bot, interaction, 'missingCoreValuesOnGameSel');
|
||||
}
|
||||
};
|
||||
|
||||
export const gameSelectionCommand = {
|
||||
details,
|
||||
execute,
|
||||
};
|
||||
|
||||
export const gameSelectionButton = {
|
||||
customId,
|
||||
execute,
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
import { Bot, Interaction, InteractionResponseTypes } from '../../../deps.ts';
|
||||
import { generateCustomActivityFields, idSeparator, pathIdxSeparator } from '../eventUtils.ts';
|
||||
import { customId as verifyCustomActivityId } from './step1b-verifyCustomActivity.ts';
|
||||
import utils from '../../utils.ts';
|
||||
import { dbClient, queries } from '../../db.ts';
|
||||
|
||||
export const customId = 'customAct';
|
||||
|
||||
const execute = async (bot: Bot, interaction: Interaction) => {
|
||||
if (interaction.data?.customId && interaction.member && interaction.guildId && interaction.channelId) {
|
||||
// Light Telemetry
|
||||
dbClient.execute(queries.callIncCnt('btn-customAct')).catch((e) => utils.commonLoggers.dbError('step1a-openCustomModal.ts', 'call sproc INC_CNT on', e));
|
||||
|
||||
const [actTitle, actSubtitle, activityMaxPlayers] = (interaction.data.customId.split(idSeparator)[1] || '').split(pathIdxSeparator);
|
||||
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.Modal,
|
||||
data: {
|
||||
title: 'Create Custom Activity',
|
||||
customId: verifyCustomActivityId,
|
||||
components: generateCustomActivityFields(actTitle, actSubtitle, activityMaxPlayers),
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('step1a-openCustomModal.ts:modal', interaction, e));
|
||||
}
|
||||
};
|
||||
|
||||
export const createCustomEventButton = {
|
||||
customId,
|
||||
execute,
|
||||
};
|
|
@ -0,0 +1,95 @@
|
|||
import config from '../../../config.ts';
|
||||
import { ApplicationCommandFlags, Bot, ButtonStyles, Interaction, InteractionResponseTypes, MessageComponentTypes } from '../../../deps.ts';
|
||||
import { failColor, infoColor1, safelyDismissMsg, somethingWentWrong } from '../../commandUtils.ts';
|
||||
import { activityMaxPlayersId, activitySubtitleId, activityTitleId, idSeparator, pathIdxEnder, pathIdxSeparator } from '../eventUtils.ts';
|
||||
import { addTokenToMap, deleteTokenEarly, selfDestructMessage } from '../tokenCleanup.ts';
|
||||
import { customId as gameSelectionId } from './step1-gameSelection.ts';
|
||||
import { customId as openCustomModalId } from './step1a-openCustomModal.ts';
|
||||
import utils from '../../utils.ts';
|
||||
|
||||
export const customId = 'verifyCustomActivity';
|
||||
|
||||
const execute = async (bot: Bot, interaction: Interaction) => {
|
||||
if (interaction.data?.components?.length && interaction.guildId && interaction.channelId && interaction.member) {
|
||||
await deleteTokenEarly(bot, interaction, interaction.guildId, interaction.channelId, interaction.member.id);
|
||||
// Parse out our data
|
||||
const tempDataMap: Map<string, string> = new Map();
|
||||
for (const row of interaction.data.components) {
|
||||
if (row.components?.[0]) {
|
||||
const textField = row.components[0];
|
||||
tempDataMap.set(textField.customId || 'missingCustomId', textField.value || 'missingValue');
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any pipe characters to avoid issues down the process
|
||||
const activityTitle = (tempDataMap.get(activityTitleId) || '').replace(/\|/g, '');
|
||||
const activitySubtitle = (tempDataMap.get(activitySubtitleId) || '').replace(/\|/g, '');
|
||||
const activityMaxPlayers = parseInt(tempDataMap.get(activityMaxPlayersId) || '0');
|
||||
if (isNaN(activityMaxPlayers) || activityMaxPlayers < 1 || activityMaxPlayers > 99) {
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: ApplicationCommandFlags.Ephemeral,
|
||||
embeds: [{
|
||||
color: failColor,
|
||||
title: 'Invalid Max Member count!',
|
||||
description: `${config.name} parsed the max members as \`${
|
||||
isNaN(activityMaxPlayers) ? 'Not a Number' : activityMaxPlayers
|
||||
}\`, which is outside of the allowed range. Please recreate this activity, but make sure the maximum player count is between 1 and 99.\n\n${safelyDismissMsg}`,
|
||||
}],
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('step1b-verifyCustomActivity.ts:invalidPlayer', interaction, e));
|
||||
return;
|
||||
}
|
||||
if (!activityMaxPlayers || !activitySubtitle || !activityTitle) {
|
||||
// Verify fields exist
|
||||
somethingWentWrong(bot, interaction, `missingFieldFromCustomActivity@${activityTitle}|${activitySubtitle}|${activityMaxPlayers}$`);
|
||||
return;
|
||||
}
|
||||
|
||||
addTokenToMap(bot, interaction, interaction.guildId, interaction.channelId, interaction.member.id);
|
||||
const idxPath = `${idSeparator}${activityTitle}${pathIdxSeparator}${activitySubtitle}${pathIdxSeparator}${activityMaxPlayers}`;
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: ApplicationCommandFlags.Ephemeral,
|
||||
embeds: [{
|
||||
color: infoColor1,
|
||||
title: 'Please verify the following Custom Event details:',
|
||||
description: `Please note, pipe characters (\`|\`) are not allowed and will be automatically removed.\n\n${selfDestructMessage(new Date().getTime())}`,
|
||||
fields: [{
|
||||
name: 'Activity Title:',
|
||||
value: activityTitle,
|
||||
}, {
|
||||
name: 'Activity Subtitle:',
|
||||
value: activitySubtitle,
|
||||
}, {
|
||||
name: 'Maximum Players:',
|
||||
value: `${activityMaxPlayers}`,
|
||||
}],
|
||||
}],
|
||||
components: [{
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
components: [{
|
||||
type: MessageComponentTypes.Button,
|
||||
style: ButtonStyles.Success,
|
||||
label: 'Yup, looks great!',
|
||||
customId: `${gameSelectionId}${idxPath}${pathIdxEnder}`,
|
||||
}, {
|
||||
type: MessageComponentTypes.Button,
|
||||
style: ButtonStyles.Danger,
|
||||
label: 'Nope, let me change something.',
|
||||
customId: `${openCustomModalId}${idxPath}`,
|
||||
}],
|
||||
}],
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('step1b-verifyCustomActivity.ts:message', interaction, e));
|
||||
} else {
|
||||
somethingWentWrong(bot, interaction, 'noDataFromCustomActivityModal');
|
||||
}
|
||||
};
|
||||
|
||||
export const verifyCustomEventButton = {
|
||||
customId,
|
||||
execute,
|
||||
};
|
|
@ -0,0 +1,99 @@
|
|||
import { Bot, Interaction } from '../../../deps.ts';
|
||||
import { somethingWentWrong } from '../../commandUtils.ts';
|
||||
import { createLFGPost, getFinalActivity } from './utils.ts';
|
||||
import { eventDateId, eventDescriptionId, eventTimeId, eventTimeZoneId, idSeparator, noDescProvided, pathIdxSeparator } from '../eventUtils.ts';
|
||||
import { addTokenToMap, deleteTokenEarly } from '../tokenCleanup.ts';
|
||||
import { Activities, Activity } from './activities.ts';
|
||||
import { getDateFromRawInput } from './dateTimeUtils.ts';
|
||||
import utils from '../../utils.ts';
|
||||
import { dbClient, queries } from '../../db.ts';
|
||||
|
||||
export const customId = 'finalize';
|
||||
|
||||
const execute = async (bot: Bot, interaction: Interaction) => {
|
||||
if (interaction.data?.components?.length && interaction.guildId && interaction.channelId && interaction.member && interaction.member.user) {
|
||||
// User selected activity and has filled out fields, delete the selectMenus
|
||||
await deleteTokenEarly(bot, interaction, interaction.guildId, interaction.channelId, interaction.member.id);
|
||||
|
||||
const tempDataMap: Map<string, string> = new Map();
|
||||
for (const row of interaction.data.components) {
|
||||
if (row.components?.[0]) {
|
||||
const textField = row.components[0];
|
||||
tempDataMap.set(textField.customId || 'missingCustomId', textField.value || '');
|
||||
}
|
||||
}
|
||||
|
||||
const customIdIdxPath = (interaction.data.customId || '').substring((interaction.data.customId || '').indexOf(idSeparator) + 1) || '';
|
||||
const rawIdxPath: Array<string> = customIdIdxPath.split(pathIdxSeparator);
|
||||
const idxPath: Array<number> = rawIdxPath.map((rawIdx) => rawIdx ? parseInt(rawIdx) : -1);
|
||||
let category: string;
|
||||
let activity: Activity;
|
||||
let customAct = false;
|
||||
if (idxPath.some((idx) => isNaN(idx) || idx < 0)) {
|
||||
customAct = true;
|
||||
// Handle custom activity
|
||||
category = rawIdxPath[0];
|
||||
activity = {
|
||||
name: rawIdxPath[1],
|
||||
maxMembers: parseInt(rawIdxPath[2]) || NaN,
|
||||
};
|
||||
} else {
|
||||
// Handle preset activity
|
||||
category = Activities[idxPath[0]].name;
|
||||
activity = getFinalActivity(idxPath, Activities);
|
||||
}
|
||||
|
||||
if (!category || !activity.name || !activity.maxMembers || isNaN(activity.maxMembers)) {
|
||||
// Error out if our activity or category is missing
|
||||
somethingWentWrong(bot, interaction, `missingActivityFromFinalize@${category}_${activity.name}_${activity.maxMembers}`);
|
||||
}
|
||||
|
||||
// Log custom event to see if we should add it as a preset
|
||||
if (customAct) {
|
||||
dbClient.execute(queries.insertCustomActivity, [interaction.guildId, category, activity.name, activity.maxMembers]).catch((e) => utils.commonLoggers.dbError('step2-finalize.ts@custom', 'insert into', e));
|
||||
}
|
||||
|
||||
const rawEventTime = tempDataMap.get(eventTimeId) || '';
|
||||
const rawEventTimeZone = tempDataMap.get(eventTimeZoneId) || '';
|
||||
const rawEventDate = tempDataMap.get(eventDateId) || '';
|
||||
const eventDescription = tempDataMap.get(eventDescriptionId) || noDescProvided;
|
||||
if (!rawEventTime || !rawEventTimeZone || !rawEventDate) {
|
||||
// Error out if user somehow failed to provide one of the fields (eventDescription is allowed to be null/empty)
|
||||
somethingWentWrong(bot, interaction, `missingFieldFromEventDescription@${rawEventTime}_${rawEventTimeZone}_${rawEventDate}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get Date Object from user input
|
||||
const [eventDateTime, eventDateTimeStr, eventInFuture, dateTimeValid] = getDateFromRawInput(rawEventTime, rawEventTimeZone, rawEventDate);
|
||||
|
||||
addTokenToMap(bot, interaction, interaction.guildId, interaction.channelId, interaction.member.id);
|
||||
bot.helpers.sendInteractionResponse(
|
||||
interaction.id,
|
||||
interaction.token,
|
||||
createLFGPost(
|
||||
category,
|
||||
activity,
|
||||
eventDateTime,
|
||||
eventDateTimeStr,
|
||||
eventDescription,
|
||||
interaction.member.id,
|
||||
interaction.member.user.username,
|
||||
[{
|
||||
id: interaction.member.id,
|
||||
name: interaction.member.user.username,
|
||||
}],
|
||||
[],
|
||||
customIdIdxPath,
|
||||
eventInFuture,
|
||||
dateTimeValid,
|
||||
),
|
||||
).catch((e: Error) => utils.commonLoggers.interactionSendError('step2-finalize.ts', interaction, e));
|
||||
} else {
|
||||
somethingWentWrong(bot, interaction, 'noDataFromEventDescriptionModal');
|
||||
}
|
||||
};
|
||||
|
||||
export const finalizeEventButton = {
|
||||
customId,
|
||||
execute,
|
||||
};
|
|
@ -0,0 +1,84 @@
|
|||
import { ApplicationCommandFlags, Bot, Interaction, InteractionResponseTypes, MessageComponentTypes } from '../../../deps.ts';
|
||||
import { generateLFGButtons } from './utils.ts';
|
||||
import { idSeparator, LfgEmbedIndexes } from '../eventUtils.ts';
|
||||
import { deleteTokenEarly } from '../tokenCleanup.ts';
|
||||
import { dmTestMessage, safelyDismissMsg, sendDirectMessage, somethingWentWrong, warnColor } from '../../commandUtils.ts';
|
||||
import { dbClient, queries } from '../../db.ts';
|
||||
import utils from '../../utils.ts';
|
||||
|
||||
export const customId = 'createEvent';
|
||||
|
||||
const execute = async (bot: Bot, interaction: Interaction) => {
|
||||
if (
|
||||
interaction.data?.customId && interaction.member && interaction.guildId && interaction.channelId && interaction.message && interaction.message.embeds[0] && interaction.message.embeds[0].fields
|
||||
) {
|
||||
// Light Telemetry
|
||||
dbClient.execute(queries.callIncCnt(interaction.data.customId.includes(idSeparator) ? 'btn-createWLEvt' : 'btn-createEvt')).catch((e) =>
|
||||
utils.commonLoggers.dbError('step3-createEvent.ts', 'call sproc INC_CNT on', e)
|
||||
);
|
||||
|
||||
deleteTokenEarly(bot, interaction, interaction.guildId, interaction.channelId, interaction.member.id);
|
||||
|
||||
// Get OwnerId and EventTime from embed for DB
|
||||
const ownerId: bigint = BigInt(interaction.message.embeds[0].footer?.iconUrl?.split('#')[1] || '0');
|
||||
const eventTime: Date = new Date(parseInt(interaction.message.embeds[0].fields[LfgEmbedIndexes.ICSLink].value.split('?t=')[1].split('&n=')[0] || '0'));
|
||||
|
||||
// Check if we need to ensure DMs are open
|
||||
if (interaction.data.customId.includes(idSeparator)) {
|
||||
const dmSuccess = Boolean(await sendDirectMessage(bot, interaction.member.id, dmTestMessage).catch((e: Error) => utils.commonLoggers.messageSendError('toggleWLStatus.ts', 'send DM fail', e)));
|
||||
if (!dmSuccess) {
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: ApplicationCommandFlags.Ephemeral,
|
||||
embeds: [{
|
||||
color: warnColor,
|
||||
title: 'Event not created.',
|
||||
description: `In order to create a whitelisted event, your DMs must be open to receive Join Requests. Please open your DMs and try again.\n\n${safelyDismissMsg}`,
|
||||
}],
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('toggleWLStatus.ts@dmFail', interaction, e));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Send Event Message
|
||||
const eventMessage = await bot.helpers.sendMessage(interaction.channelId, {
|
||||
embeds: [interaction.message.embeds[0]],
|
||||
components: [{
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
components: generateLFGButtons(interaction.data.customId.includes(idSeparator)),
|
||||
}],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('step3-createEvent.ts', 'createEvent', e));
|
||||
if (!eventMessage) {
|
||||
somethingWentWrong(bot, interaction, 'creatingEventSendMessageFinalizeEventStep');
|
||||
return;
|
||||
}
|
||||
|
||||
// Store in DB
|
||||
let dbErrorOut = false;
|
||||
await dbClient.execute(queries.insertEvent, [eventMessage.id, eventMessage.channelId, interaction.guildId, ownerId, eventTime]).catch((e) => {
|
||||
utils.commonLoggers.dbError('step3-createEvent.ts', 'INSERT event to DB', e);
|
||||
dbErrorOut = true;
|
||||
});
|
||||
if (dbErrorOut) {
|
||||
bot.helpers.deleteMessage(eventMessage.channelId, eventMessage.id, 'Failed to log event to DB').catch((e: Error) =>
|
||||
utils.commonLoggers.messageDeleteError('step3-createEvent.ts', 'deleteEventFailedDB', e)
|
||||
);
|
||||
somethingWentWrong(bot, interaction, 'creatingEventDBStoreFinalizeEventStep');
|
||||
return;
|
||||
}
|
||||
|
||||
// Let discord know we didn't ignore the user
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.DeferredUpdateMessage,
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('step3-createEvent.ts', interaction, e));
|
||||
} else {
|
||||
somethingWentWrong(bot, interaction, 'noDataFromFinalizeEventStep');
|
||||
}
|
||||
};
|
||||
|
||||
export const createEventButton = {
|
||||
customId,
|
||||
execute,
|
||||
};
|
|
@ -0,0 +1,183 @@
|
|||
import { ActionRow, ApplicationCommandFlags, ButtonComponent, ButtonStyles, InteractionResponse, InteractionResponseTypes, MessageComponentTypes, SelectOption } from '../../../deps.ts';
|
||||
import config from '../../../config.ts';
|
||||
import { Activity } from './activities.ts';
|
||||
import {
|
||||
alternateEventBtnStr,
|
||||
generateAlternateList,
|
||||
generateMemberList,
|
||||
generateMemberTitle,
|
||||
idSeparator,
|
||||
joinEventBtnStr,
|
||||
leaveEventBtnStr,
|
||||
lfgStartTimeName,
|
||||
pathIdxEnder,
|
||||
pathIdxSeparator,
|
||||
requestToJoinEventBtnStr,
|
||||
} from '../eventUtils.ts';
|
||||
import { selfDestructMessage } from '../tokenCleanup.ts';
|
||||
import { successColor, warnColor } from '../../commandUtils.ts';
|
||||
import { LFGMember } from '../../types/commandTypes.ts';
|
||||
import { customId as gameSelCustomId } from './step1-gameSelection.ts';
|
||||
import { customId as createEventCustomId } from './step3-createEvent.ts';
|
||||
import { customId as joinEventCustomId } from '../live-event/joinEvent.ts';
|
||||
import { customId as leaveEventCustomId } from '../live-event/leaveEvent.ts';
|
||||
import { customId as alternateEventCustomId } from '../live-event/alternateEvent.ts';
|
||||
import { customId as deleteEventCustomId } from '../live-event/deleteEvent.ts';
|
||||
import { customId as editEventCustomId } from '../live-event/editEvent.ts';
|
||||
|
||||
export const getNestedActivity = (idxPath: Array<number>, activities: Array<Activity>): Array<Activity> => {
|
||||
const nextIdx = idxPath[0];
|
||||
if (idxPath.length && activities[nextIdx] && activities[nextIdx].options) {
|
||||
idxPath.shift();
|
||||
return getNestedActivity(idxPath, activities[nextIdx].options || []);
|
||||
} else {
|
||||
return activities;
|
||||
}
|
||||
};
|
||||
|
||||
export const getFinalActivity = (idxPath: Array<number>, activities: Array<Activity>): Activity => getNestedActivity(idxPath, activities)[idxPath[idxPath.length - 1]];
|
||||
|
||||
const getSelectOptions = (baseValue: string, activities: Array<Activity>, defaultIdx?: number): Array<SelectOption> =>
|
||||
activities.map((act, idx) => ({
|
||||
label: act.name,
|
||||
value: `${baseValue}${idx}${act.maxMembers ? pathIdxEnder : pathIdxSeparator}`,
|
||||
default: idx === defaultIdx,
|
||||
}));
|
||||
|
||||
export const generateActionRow = (baseValue: string, activities: Array<Activity>, customId: string, defaultIdx?: number): ActionRow => ({
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
components: [{
|
||||
type: MessageComponentTypes.SelectMenu,
|
||||
customId,
|
||||
options: getSelectOptions(baseValue, activities, defaultIdx),
|
||||
}],
|
||||
});
|
||||
|
||||
const createEventBtnName = 'Create Event';
|
||||
const createWhitelistedBtnName = 'Create Whitelisted Event';
|
||||
const editEventDetailsBtnName = 'Edit Event Details';
|
||||
export const invalidDateTimeStr = '`Invalid Date/Time`';
|
||||
const finalizeButtons = (idxPath: string, eventInFuture: boolean): [ButtonComponent, ButtonComponent, ButtonComponent] | [ButtonComponent] => {
|
||||
const editButton: ButtonComponent = {
|
||||
type: MessageComponentTypes.Button,
|
||||
label: editEventDetailsBtnName,
|
||||
style: ButtonStyles.Secondary,
|
||||
customId: `${gameSelCustomId}${idSeparator}${idxPath}${pathIdxEnder}`,
|
||||
};
|
||||
if (eventInFuture) {
|
||||
return [{
|
||||
type: MessageComponentTypes.Button,
|
||||
label: createEventBtnName,
|
||||
style: ButtonStyles.Success,
|
||||
customId: createEventCustomId,
|
||||
}, {
|
||||
type: MessageComponentTypes.Button,
|
||||
label: createWhitelistedBtnName,
|
||||
style: ButtonStyles.Primary,
|
||||
customId: `${createEventCustomId}${idSeparator}`,
|
||||
}, editButton];
|
||||
} else {
|
||||
return [editButton];
|
||||
}
|
||||
};
|
||||
|
||||
export const generateLFGButtons = (whitelist: boolean): [ButtonComponent, ButtonComponent, ButtonComponent, ButtonComponent, ButtonComponent] => [{
|
||||
type: MessageComponentTypes.Button,
|
||||
label: whitelist ? requestToJoinEventBtnStr : joinEventBtnStr,
|
||||
style: ButtonStyles.Success,
|
||||
customId: `${joinEventCustomId}${whitelist ? idSeparator : ''}`,
|
||||
}, {
|
||||
type: MessageComponentTypes.Button,
|
||||
label: leaveEventBtnStr,
|
||||
style: ButtonStyles.Danger,
|
||||
customId: leaveEventCustomId,
|
||||
}, {
|
||||
type: MessageComponentTypes.Button,
|
||||
label: alternateEventBtnStr,
|
||||
style: ButtonStyles.Primary,
|
||||
customId: alternateEventCustomId,
|
||||
}, {
|
||||
type: MessageComponentTypes.Button,
|
||||
label: '',
|
||||
style: ButtonStyles.Secondary,
|
||||
customId: editEventCustomId,
|
||||
emoji: {
|
||||
name: '✏️',
|
||||
},
|
||||
}, {
|
||||
type: MessageComponentTypes.Button,
|
||||
label: '',
|
||||
style: ButtonStyles.Secondary,
|
||||
customId: deleteEventCustomId,
|
||||
emoji: {
|
||||
name: '🗑️',
|
||||
},
|
||||
}];
|
||||
|
||||
export const generateTimeFieldStr = (eventDateTimeStr: string, eventDateTime: Date) => `${eventDateTimeStr}\n<t:${Math.floor(eventDateTime.getTime() / 1000)}:R>`;
|
||||
|
||||
export const createLFGPost = (
|
||||
category: string,
|
||||
activity: Activity,
|
||||
eventDateTime: Date,
|
||||
eventDateTimeStr: string,
|
||||
eventDescription: string,
|
||||
authorId: bigint,
|
||||
author: string,
|
||||
memberList: Array<LFGMember>,
|
||||
alternateList: Array<LFGMember>,
|
||||
idxPath: string,
|
||||
eventInFuture: boolean,
|
||||
dateTimeValid: boolean,
|
||||
): InteractionResponse => {
|
||||
const icsDetails = `${category}: ${activity.name}`;
|
||||
return {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: ApplicationCommandFlags.Ephemeral,
|
||||
content: eventInFuture
|
||||
? `Please verify the information below, then click on the \`${createEventBtnName}\` or \`${createWhitelistedBtnName}\` button, or change the event \`Date/Time\` or \`Description\` with the \`${editEventDetailsBtnName}\` button below. \n\n${
|
||||
selfDestructMessage(new Date().getTime())
|
||||
}`
|
||||
: `You cannot create an event ${dateTimeValid ? 'in the past' : 'with an invalid date/time'}. Please change the event's \`Date/Time\` to be ${
|
||||
dateTimeValid ? 'in the future' : 'valid'
|
||||
} with the \`${editEventDetailsBtnName}\` button below.`,
|
||||
embeds: [{
|
||||
color: eventInFuture ? successColor : warnColor,
|
||||
fields: [{
|
||||
name: `${category}:`,
|
||||
value: activity.name,
|
||||
inline: true,
|
||||
}, {
|
||||
name: lfgStartTimeName,
|
||||
value: dateTimeValid ? generateTimeFieldStr(eventDateTimeStr, eventDateTime) : invalidDateTimeStr,
|
||||
inline: true,
|
||||
}, {
|
||||
name: 'Add to Calendar:',
|
||||
value: `[Download ICS File](${config.links.addToCalendar}?t=${eventDateTime.getTime()}&n=${icsDetails.replaceAll(' ', '+')})`,
|
||||
inline: true,
|
||||
}, {
|
||||
name: 'Description:',
|
||||
value: eventDescription,
|
||||
}, {
|
||||
name: generateMemberTitle(memberList, activity.maxMembers || 0),
|
||||
value: generateMemberList(memberList),
|
||||
inline: true,
|
||||
}, {
|
||||
name: 'Alternates:',
|
||||
value: generateAlternateList(alternateList),
|
||||
inline: true,
|
||||
}],
|
||||
footer: {
|
||||
text: `Created by: ${author}`,
|
||||
iconUrl: `${config.links.creatorIcon}#${authorId}`,
|
||||
},
|
||||
timestamp: eventDateTime.getTime(),
|
||||
}],
|
||||
components: [{
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
components: finalizeButtons(idxPath, eventInFuture),
|
||||
}],
|
||||
},
|
||||
};
|
||||
};
|
|
@ -0,0 +1,134 @@
|
|||
import { ActionRow, MessageComponentTypes, TextStyles } from '../../deps.ts';
|
||||
import { LFGMember } from '../types/commandTypes.ts';
|
||||
import { isDSTActive } from './event-creation/dateTimeUtils.ts';
|
||||
|
||||
// Index enum to standardize access to the field
|
||||
export enum LfgEmbedIndexes {
|
||||
Activity,
|
||||
StartTime,
|
||||
ICSLink,
|
||||
Description,
|
||||
JoinedMembers,
|
||||
AlternateMembers,
|
||||
}
|
||||
|
||||
// Common strings
|
||||
export const idSeparator = '@';
|
||||
export const pathIdxSeparator = '|';
|
||||
export const pathIdxEnder = '&';
|
||||
export const fillerChar = '$';
|
||||
export const lfgStartTimeName = 'Start Time:';
|
||||
export const noMembersStr = 'None';
|
||||
export const joinEventBtnStr = 'Join';
|
||||
export const requestToJoinEventBtnStr = 'Request to Join';
|
||||
export const leaveEventBtnStr = 'Leave';
|
||||
export const alternateEventBtnStr = 'Join as Alternate';
|
||||
export const noDescProvided = 'No description provided.';
|
||||
|
||||
// Member List generators
|
||||
export const generateMemberTitle = (memberList: Array<LFGMember>, maxMembers: number): string => `Members Joined: ${memberList.length}/${maxMembers}`;
|
||||
export const generateMemberList = (memberList: Array<LFGMember>): string => memberList.length ? memberList.map((member) => `${member.name} - <@${member.id}>`).join('\n') : noMembersStr;
|
||||
export const generateAlternateList = (alternateList: Array<LFGMember>): string =>
|
||||
alternateList.length ? alternateList.map((member) => `${member.name} - <@${member.id}>${member.joined ? ' *' : ''}`).join('\n') : noMembersStr;
|
||||
|
||||
// Fields for event creation and editing modals
|
||||
export const eventTimeId = 'eventTime';
|
||||
export const eventTimeZoneId = 'eventTimeZone';
|
||||
export const eventDateId = 'eventDate';
|
||||
export const eventDescriptionId = 'eventDescription';
|
||||
|
||||
export const descriptionTextField = (prefillDescription = ''): ActionRow => ({
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
components: [{
|
||||
type: MessageComponentTypes.InputText,
|
||||
customId: eventDescriptionId,
|
||||
label: 'Description:',
|
||||
placeholder: 'Briefly describe the event',
|
||||
style: TextStyles.Paragraph,
|
||||
required: false,
|
||||
minLength: 0,
|
||||
maxLength: 1000,
|
||||
value: prefillDescription || undefined,
|
||||
}],
|
||||
});
|
||||
|
||||
// DST notice to try to get people to use the right TZ
|
||||
const dstNotice = isDSTActive() ? '(Note: DST is in effect in NA)' : '';
|
||||
export const dateTimeFields = (prefillTime = '', prefillTimeZone = '', prefillDate = ''): ActionRow[] => [{
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
components: [{
|
||||
type: MessageComponentTypes.InputText,
|
||||
customId: eventTimeId,
|
||||
label: 'Start Time:',
|
||||
placeholder: 'Enter the start time as "HH:MM AM/PM"',
|
||||
style: TextStyles.Short,
|
||||
minLength: 1,
|
||||
maxLength: 8,
|
||||
value: prefillTime || undefined,
|
||||
}],
|
||||
}, {
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
components: [{
|
||||
type: MessageComponentTypes.InputText,
|
||||
customId: eventTimeZoneId,
|
||||
label: `Time Zone: ${dstNotice}`,
|
||||
placeholder: 'Enter your time zone abbreviation (UTC±## also works)',
|
||||
style: TextStyles.Short,
|
||||
minLength: 2,
|
||||
maxLength: 8,
|
||||
value: prefillTimeZone || undefined,
|
||||
}],
|
||||
}, {
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
components: [{
|
||||
type: MessageComponentTypes.InputText,
|
||||
customId: eventDateId,
|
||||
label: 'Start Date:',
|
||||
placeholder: 'Enter date as "MONTH/DAY/YEAR" or "Month Day, Year"',
|
||||
style: TextStyles.Short,
|
||||
minLength: 1,
|
||||
maxLength: 20,
|
||||
value: prefillDate || undefined,
|
||||
}],
|
||||
}];
|
||||
|
||||
export const activityTitleId = 'activityTitle';
|
||||
export const activitySubtitleId = 'activitySubtitle';
|
||||
export const activityMaxPlayersId = 'activityMaxPlayers';
|
||||
export const generateCustomActivityFields = (actTitle = '', actSubtitle = '', activityMaxPlayers = ''): ActionRow[] => [{
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
components: [{
|
||||
type: MessageComponentTypes.InputText,
|
||||
customId: activityTitleId,
|
||||
label: 'Activity Title:',
|
||||
placeholder: 'The name of the game or event.',
|
||||
style: TextStyles.Short,
|
||||
minLength: 1,
|
||||
maxLength: 35,
|
||||
value: actTitle || undefined,
|
||||
}],
|
||||
}, {
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
components: [{
|
||||
type: MessageComponentTypes.InputText,
|
||||
customId: activitySubtitleId,
|
||||
label: 'Activity Subtitle:',
|
||||
placeholder: 'The specific activity within the game or event.',
|
||||
style: TextStyles.Short,
|
||||
minLength: 1,
|
||||
maxLength: 50,
|
||||
value: actSubtitle || undefined,
|
||||
}],
|
||||
}, {
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
components: [{
|
||||
type: MessageComponentTypes.InputText,
|
||||
customId: activityMaxPlayersId,
|
||||
label: 'Maximum Players:',
|
||||
placeholder: 'Please enter a number between 1 and 99.',
|
||||
style: TextStyles.Short,
|
||||
minLength: 1,
|
||||
maxLength: 2,
|
||||
value: activityMaxPlayers || undefined,
|
||||
}],
|
||||
}];
|
|
@ -0,0 +1,27 @@
|
|||
import { Bot, Interaction } from '../../../deps.ts';
|
||||
import { dbClient, queries } from '../../db.ts';
|
||||
import { somethingWentWrong } from '../../commandUtils.ts';
|
||||
import utils from '../../utils.ts';
|
||||
import { alternateMemberToEvent } from './utils.ts';
|
||||
|
||||
export const customId = 'alternateEvent';
|
||||
|
||||
const execute = async (bot: Bot, interaction: Interaction) => {
|
||||
if (interaction.data?.customId && interaction.member && interaction.member.user && interaction.channelId && interaction.message && interaction.message.embeds[0]) {
|
||||
// Light Telemetry
|
||||
dbClient.execute(queries.callIncCnt('btn-altEvent')).catch((e) => utils.commonLoggers.dbError('alternateEvent.ts', 'call sproc INC_CNT on', e));
|
||||
|
||||
// Alternate user to event
|
||||
alternateMemberToEvent(bot, interaction, interaction.message.embeds[0], interaction.message.id, interaction.channelId, {
|
||||
id: interaction.member.id,
|
||||
name: interaction.member.user.username,
|
||||
});
|
||||
} else {
|
||||
somethingWentWrong(bot, interaction, 'noDataFromAlternateEventButton');
|
||||
}
|
||||
};
|
||||
|
||||
export const alternateEventButton = {
|
||||
customId,
|
||||
execute,
|
||||
};
|
|
@ -0,0 +1,44 @@
|
|||
import { Bot, Interaction } from '../../../deps.ts';
|
||||
import { dbClient, queries } from '../../db.ts';
|
||||
import { somethingWentWrong } from '../../commandUtils.ts';
|
||||
import utils from '../../utils.ts';
|
||||
import { alternateMemberToEvent } from './utils.ts';
|
||||
|
||||
export const customId = 'alternateRequest';
|
||||
|
||||
const execute = async (bot: Bot, interaction: Interaction) => {
|
||||
if (interaction.data?.customId && interaction.user && interaction.message && interaction.message.embeds[0] && interaction.message.embeds[0].description) {
|
||||
// Light Telemetry
|
||||
dbClient.execute(queries.callIncCnt('btn-joinReqAlt')).catch((e) => utils.commonLoggers.dbError('alternateRequest.ts', 'call sproc INC_CNT on', e));
|
||||
|
||||
// Get details from message
|
||||
const eventIds = utils.messageUrlToIds(interaction.message.embeds[0].description.split(')')[0] || '');
|
||||
const eventMessage = await bot.helpers.getMessage(eventIds.channelId, eventIds.messageId).catch((e: Error) => utils.commonLoggers.messageGetError('alternateRequest.ts', 'get eventMessage', e));
|
||||
|
||||
// Try to alternate the member to the event
|
||||
if (eventMessage) {
|
||||
alternateMemberToEvent(
|
||||
bot,
|
||||
interaction,
|
||||
eventMessage.embeds[0],
|
||||
eventIds.messageId,
|
||||
eventIds.channelId,
|
||||
{
|
||||
id: interaction.user.id,
|
||||
name: interaction.user.username,
|
||||
},
|
||||
false,
|
||||
true,
|
||||
);
|
||||
} else {
|
||||
somethingWentWrong(bot, interaction, 'eventMissingFromAlternateRequestButton');
|
||||
}
|
||||
} else {
|
||||
somethingWentWrong(bot, interaction, 'noDataFromAlternateRequestButton');
|
||||
}
|
||||
};
|
||||
|
||||
export const alternateRequestButton = {
|
||||
customId,
|
||||
execute,
|
||||
};
|
|
@ -0,0 +1,90 @@
|
|||
import { ApplicationCommandFlags, Bot, Interaction, InteractionResponseTypes } from '../../../deps.ts';
|
||||
import { somethingWentWrong, warnColor } from '../../commandUtils.ts';
|
||||
import { eventDateId, eventTimeId, eventTimeZoneId, idSeparator, LfgEmbedIndexes, pathIdxEnder, pathIdxSeparator } from '../eventUtils.ts';
|
||||
import { addTokenToMap, deleteTokenEarly } from '../tokenCleanup.ts';
|
||||
import utils from '../../utils.ts';
|
||||
import { applyEditButtons, applyEditMessage } from './utils.ts';
|
||||
import { getDateFromRawInput } from '../event-creation/dateTimeUtils.ts';
|
||||
import { generateTimeFieldStr } from '../event-creation/utils.ts';
|
||||
|
||||
export const customId = 'applyDateTime';
|
||||
|
||||
const execute = async (bot: Bot, interaction: Interaction) => {
|
||||
if (interaction.data?.customId && interaction.data?.components?.length && interaction.member && interaction.channelId && interaction.guildId) {
|
||||
await deleteTokenEarly(bot, interaction, interaction.guildId, interaction.channelId, interaction.member.id);
|
||||
const [evtChannelId, evtMessageId] = (interaction.data.customId.replaceAll(pathIdxEnder, '').split(idSeparator)[1] || '').split(pathIdxSeparator).map((id) => BigInt(id || '0'));
|
||||
const eventMessage = await bot.helpers.getMessage(evtChannelId, evtMessageId).catch((e: Error) => utils.commonLoggers.messageGetError('applyDateTime.ts', 'get eventMessage', e));
|
||||
|
||||
const tempDataMap: Map<string, string> = new Map();
|
||||
for (const row of interaction.data.components) {
|
||||
if (row.components?.[0]) {
|
||||
const textField = row.components[0];
|
||||
tempDataMap.set(textField.customId || 'missingCustomId', textField.value || '');
|
||||
}
|
||||
}
|
||||
const newTime = tempDataMap.get(eventTimeId);
|
||||
const newTimeZone = tempDataMap.get(eventTimeZoneId);
|
||||
const newDate = tempDataMap.get(eventDateId);
|
||||
|
||||
if (!newTime || !newTimeZone || !newDate) {
|
||||
// Error out if user somehow failed to provide one of the fields (eventDescription is allowed to be null/empty)
|
||||
somethingWentWrong(bot, interaction, `missingFieldFromEventDescription@${newTime}_${newTimeZone}_${newDate}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get Date Object from user input
|
||||
const [eventDateTime, eventDateTimeStr, eventInFuture, dateTimeValid] = getDateFromRawInput(newTime, newTimeZone, newDate);
|
||||
if (!eventInFuture || !dateTimeValid) {
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: ApplicationCommandFlags.Ephemeral,
|
||||
embeds: [{
|
||||
color: warnColor,
|
||||
title: dateTimeValid ? 'You cannot create an event in the past.' : 'Could not parse date/time.',
|
||||
description: `Please dismiss this message and try again with a ${dateTimeValid ? 'date in the future' : 'valid date/time'}.`,
|
||||
fields: dateTimeValid
|
||||
? [{
|
||||
name: 'Date/Time Entered:',
|
||||
value: generateTimeFieldStr(eventDateTimeStr, eventDateTime),
|
||||
}]
|
||||
: undefined,
|
||||
}],
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('applyDateTime.ts', interaction, e));
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventMessage && eventMessage.embeds[0].fields) {
|
||||
// eventMessage.embeds[0].fields[LfgEmbedIndexes.Description].value = newDescription || noDescProvided;
|
||||
eventMessage.embeds[0].fields[LfgEmbedIndexes.StartTime].value = generateTimeFieldStr(eventDateTimeStr, eventDateTime);
|
||||
const tIdx = eventMessage.embeds[0].fields[LfgEmbedIndexes.ICSLink].value.indexOf('?t=') + 3;
|
||||
const nIdx = eventMessage.embeds[0].fields[LfgEmbedIndexes.ICSLink].value.indexOf('&n=');
|
||||
eventMessage.embeds[0].fields[LfgEmbedIndexes.ICSLink].value = `${eventMessage.embeds[0].fields[LfgEmbedIndexes.ICSLink].value.slice(0, tIdx)}${eventDateTime.getTime()}${
|
||||
eventMessage.embeds[0].fields[LfgEmbedIndexes.ICSLink].value.slice(nIdx)
|
||||
}`;
|
||||
eventMessage.embeds[0].timestamp = eventDateTime.getTime();
|
||||
|
||||
// Send edit confirmation
|
||||
addTokenToMap(bot, interaction, interaction.guildId, interaction.channelId, interaction.member.id);
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: ApplicationCommandFlags.Ephemeral,
|
||||
content: applyEditMessage(new Date().getTime()),
|
||||
embeds: [eventMessage.embeds[0]],
|
||||
components: applyEditButtons(interaction.data.customId.split(idSeparator)[1] || ''),
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('applyDateTime.ts', interaction, e));
|
||||
} else {
|
||||
somethingWentWrong(bot, interaction, 'failedToGetEventMsgInApplyDateTime');
|
||||
}
|
||||
} else {
|
||||
somethingWentWrong(bot, interaction, 'noDataFromApplyDateTime');
|
||||
}
|
||||
};
|
||||
|
||||
export const applyDateTimeButton = {
|
||||
customId,
|
||||
execute,
|
||||
};
|
|
@ -0,0 +1,50 @@
|
|||
import { ApplicationCommandFlags, Bot, Interaction, InteractionResponseTypes } from '../../../deps.ts';
|
||||
import { somethingWentWrong } from '../../commandUtils.ts';
|
||||
import { eventDescriptionId, idSeparator, LfgEmbedIndexes, noDescProvided, pathIdxEnder, pathIdxSeparator } from '../eventUtils.ts';
|
||||
import { addTokenToMap, deleteTokenEarly } from '../tokenCleanup.ts';
|
||||
import utils from '../../utils.ts';
|
||||
import { applyEditButtons, applyEditMessage } from './utils.ts';
|
||||
|
||||
export const customId = 'applyDescription';
|
||||
|
||||
const execute = async (bot: Bot, interaction: Interaction) => {
|
||||
if (interaction.data?.customId && interaction.data?.components?.length && interaction.member && interaction.channelId && interaction.guildId) {
|
||||
await deleteTokenEarly(bot, interaction, interaction.guildId, interaction.channelId, interaction.member.id);
|
||||
const [evtChannelId, evtMessageId] = (interaction.data.customId.replaceAll(pathIdxEnder, '').split(idSeparator)[1] || '').split(pathIdxSeparator).map((id) => BigInt(id || '0'));
|
||||
const eventMessage = await bot.helpers.getMessage(evtChannelId, evtMessageId).catch((e: Error) => utils.commonLoggers.messageGetError('applyDescription.ts', 'get eventMessage', e));
|
||||
|
||||
const tempDataMap: Map<string, string> = new Map();
|
||||
for (const row of interaction.data.components) {
|
||||
if (row.components?.[0]) {
|
||||
const textField = row.components[0];
|
||||
tempDataMap.set(textField.customId || 'missingCustomId', textField.value || '');
|
||||
}
|
||||
}
|
||||
const newDescription = tempDataMap.get(eventDescriptionId);
|
||||
|
||||
if (eventMessage && eventMessage.embeds[0].fields) {
|
||||
eventMessage.embeds[0].fields[LfgEmbedIndexes.Description].value = newDescription || noDescProvided;
|
||||
|
||||
// Send edit confirmation
|
||||
addTokenToMap(bot, interaction, interaction.guildId, interaction.channelId, interaction.member.id);
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: ApplicationCommandFlags.Ephemeral,
|
||||
content: applyEditMessage(new Date().getTime()),
|
||||
embeds: [eventMessage.embeds[0]],
|
||||
components: applyEditButtons(interaction.data.customId.split(idSeparator)[1] || ''),
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('applyDescription.ts', interaction, e));
|
||||
} else {
|
||||
somethingWentWrong(bot, interaction, 'failedToGetEventMsgInApplyDescription');
|
||||
}
|
||||
} else {
|
||||
somethingWentWrong(bot, interaction, 'noDataFromApplyDescription');
|
||||
}
|
||||
};
|
||||
|
||||
export const applyDescriptionButton = {
|
||||
customId,
|
||||
execute,
|
||||
};
|
|
@ -0,0 +1,124 @@
|
|||
import { ApplicationCommandFlags, Bot, Interaction, InteractionResponseTypes } from '../../../deps.ts';
|
||||
import { dbClient, generateGuildSettingKey, lfgChannelSettings, queries } from '../../db.ts';
|
||||
import { failColor, infoColor1, infoColor2, safelyDismissMsg, sendDirectMessage, somethingWentWrong, successColor } from '../../commandUtils.ts';
|
||||
import { generateMemberList, idSeparator, pathIdxEnder, pathIdxSeparator } from '../eventUtils.ts';
|
||||
import utils from '../../utils.ts';
|
||||
import config from '../../../config.ts';
|
||||
import { getGuildName } from './utils.ts';
|
||||
|
||||
export const customId = 'deleteConfirmed';
|
||||
export const confirmedCustomId = 'confirmedCustomId';
|
||||
export const confirmStr = 'yes';
|
||||
|
||||
const execute = async (bot: Bot, interaction: Interaction) => {
|
||||
if (interaction.data?.customId && interaction.data?.components?.length && interaction.channelId && interaction.guildId && interaction.member && interaction.member.user) {
|
||||
// Light Telemetry
|
||||
dbClient.execute(queries.callIncCnt('btn-confirmDelEvent')).catch((e) => utils.commonLoggers.dbError('deleteConfirmed.ts@incCnt', 'call sproc INC_CNT on', e));
|
||||
|
||||
// Parse out our data
|
||||
const tempDataMap: Map<string, string> = new Map();
|
||||
for (const row of interaction.data.components) {
|
||||
if (row.components?.[0]) {
|
||||
const textField = row.components[0];
|
||||
tempDataMap.set(textField.customId || 'missingCustomId', textField.value || 'missingValue');
|
||||
}
|
||||
}
|
||||
const actionByManager = interaction.data.customId.endsWith(pathIdxEnder);
|
||||
const [evtChannelId, evtMessageId] = (interaction.data.customId.replaceAll(pathIdxEnder, '').split(idSeparator)[1] || '').split(pathIdxSeparator).map((id) => BigInt(id || '0'));
|
||||
const lfgChannelSetting = lfgChannelSettings.get(generateGuildSettingKey(interaction.guildId, interaction.channelId)) || {
|
||||
managed: false,
|
||||
managerRoleId: 0n,
|
||||
logChannelId: 0n,
|
||||
};
|
||||
|
||||
if (tempDataMap.get(confirmedCustomId)?.toLowerCase() === confirmStr) {
|
||||
const guildName = await getGuildName(bot, interaction.guildId);
|
||||
const eventMessage = await bot.helpers.getMessage(evtChannelId, evtMessageId).catch((e: Error) => utils.commonLoggers.messageGetError('deleteConfirmed.ts', 'get eventMessage', e));
|
||||
const userId = interaction.member.id;
|
||||
const userName = interaction.member.user.username;
|
||||
// Delete event
|
||||
bot.helpers.deleteMessage(evtChannelId, evtMessageId, 'User deleted event').then(() => {
|
||||
dbClient.execute(queries.deleteEvent, [evtChannelId, evtMessageId]).catch((e) => utils.commonLoggers.dbError('deleteConfirmed.ts@deleteEvent', 'delete event from', e));
|
||||
|
||||
// Acknowledge user so discord doesn't get annoyed
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: ApplicationCommandFlags.Ephemeral,
|
||||
embeds: [{
|
||||
color: successColor,
|
||||
title: 'Event successfully deleted.',
|
||||
description: safelyDismissMsg,
|
||||
}],
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('deleteConfirmed.ts', interaction, e));
|
||||
|
||||
if (actionByManager) {
|
||||
const ownerId = BigInt(eventMessage?.embeds[0].footer?.iconUrl?.split('#')[1] || '0');
|
||||
const eventEmbed = eventMessage?.embeds[0] || { title: 'Event not found', color: failColor };
|
||||
bot.helpers.sendMessage(lfgChannelSetting.logChannelId, {
|
||||
embeds: [{
|
||||
color: infoColor2,
|
||||
title: `Event deleted by a ${config.name} Manager`,
|
||||
description: `The following event was deleted by ${userName} - <@${userId}>.`,
|
||||
timestamp: new Date().getTime(),
|
||||
}, eventEmbed],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('deleteConfirmed.ts', 'send log message', e));
|
||||
|
||||
sendDirectMessage(bot, ownerId, {
|
||||
embeds: [{
|
||||
color: infoColor2,
|
||||
title: `Notice: A ${config.name} Manager has deleted one of your events in ${guildName}`,
|
||||
description: 'The deleted event is listed below.',
|
||||
fields: [
|
||||
{
|
||||
name: `${config.name} Manager:`,
|
||||
value: generateMemberList([{
|
||||
id: userId,
|
||||
name: userName,
|
||||
}]),
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Are you unhappy with this action?',
|
||||
value: `Please reach out to the ${config.name} Manager that performed this action, or the moderators/administrators of ${guildName}.`,
|
||||
},
|
||||
],
|
||||
}, eventEmbed],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('deleteConfirmed.ts', 'send DM fail', e));
|
||||
}
|
||||
}).catch((e) => {
|
||||
utils.commonLoggers.messageDeleteError('deleteConfirmed.ts', 'deleteEventFailedDB', e);
|
||||
somethingWentWrong(bot, interaction, 'deleteEventMessageInDeleteConfirmedButton');
|
||||
});
|
||||
} else {
|
||||
// User either did not type yes confirm field was missing, lets see which it was
|
||||
if (tempDataMap.get(confirmedCustomId)) {
|
||||
// User did not type yes.
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: ApplicationCommandFlags.Ephemeral,
|
||||
embeds: [{
|
||||
color: infoColor1,
|
||||
title: 'Event not deleted.',
|
||||
description: `If you are trying to delete the event, please make sure you type \`${confirmStr}\` into the field provided.
|
||||
|
||||
${safelyDismissMsg}`,
|
||||
}],
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('deleteConfirmed.ts', interaction, e));
|
||||
} else {
|
||||
// Field was missing
|
||||
somethingWentWrong(bot, interaction, 'noIdsFromDeleteConfirmedButton');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
somethingWentWrong(bot, interaction, 'noDataFromDeleteConfirmedButton');
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteConfirmedButton = {
|
||||
customId,
|
||||
execute,
|
||||
};
|
|
@ -0,0 +1,57 @@
|
|||
import { Bot, Interaction, InteractionResponseTypes, MessageComponentTypes, TextStyles } from '../../../deps.ts';
|
||||
import { dbClient, generateGuildSettingKey, lfgChannelSettings, queries } from '../../db.ts';
|
||||
import { somethingWentWrong, stopThat } from '../../commandUtils.ts';
|
||||
import { idSeparator, pathIdxEnder, pathIdxSeparator } from '../eventUtils.ts';
|
||||
import { confirmedCustomId, confirmStr, customId as deleteConfirmedCustomId } from './deleteConfirmed.ts';
|
||||
import utils from '../../utils.ts';
|
||||
|
||||
export const customId = 'deleteEvent';
|
||||
|
||||
const execute = async (bot: Bot, interaction: Interaction) => {
|
||||
if (interaction.data?.customId && interaction.member && interaction.member.user && interaction.channelId && interaction.guildId && interaction.message && interaction.message.embeds[0]) {
|
||||
// Light Telemetry
|
||||
dbClient.execute(queries.callIncCnt('btn-delEvent')).catch((e) => utils.commonLoggers.dbError('deleteEvent.ts', 'call sproc INC_CNT on', e));
|
||||
|
||||
const ownerId = BigInt(interaction.message.embeds[0].footer?.iconUrl?.split('#')[1] || '0');
|
||||
const lfgChannelSetting = lfgChannelSettings.get(generateGuildSettingKey(interaction.guildId, interaction.channelId)) || {
|
||||
managed: false,
|
||||
managerRoleId: 0n,
|
||||
logChannelId: 0n,
|
||||
};
|
||||
|
||||
// Make sure this is being done by the owner or a Group Up Manager
|
||||
if (interaction.member.user.id === ownerId || (lfgChannelSetting.managed && interaction.member.roles.includes(lfgChannelSetting.managerRoleId))) {
|
||||
const actionByManager = interaction.member.user.id !== ownerId;
|
||||
// Open Delete Confirmation
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.Modal,
|
||||
data: {
|
||||
title: 'Are you sure you want to delete this event?',
|
||||
customId: `${deleteConfirmedCustomId}${idSeparator}${interaction.channelId}${pathIdxSeparator}${interaction.message.id}${actionByManager ? pathIdxEnder : ''}`,
|
||||
components: [{
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
components: [{
|
||||
type: MessageComponentTypes.InputText,
|
||||
customId: confirmedCustomId,
|
||||
label: `To confirm, type '${confirmStr}' in the field below:`,
|
||||
placeholder: 'To cancel, just click cancel on this modal.',
|
||||
style: TextStyles.Short,
|
||||
minLength: 3,
|
||||
maxLength: 3,
|
||||
}],
|
||||
}],
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('step1a-openCustomModal.ts:modal', interaction, e));
|
||||
} else {
|
||||
// Not owner or manager, tell user they can't
|
||||
stopThat(bot, interaction, 'delete');
|
||||
}
|
||||
} else {
|
||||
somethingWentWrong(bot, interaction, 'noDataFromDeleteEventButton');
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteEventButton = {
|
||||
customId,
|
||||
execute,
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
import { Bot, Interaction, InteractionResponseTypes } from '../../../deps.ts';
|
||||
import { generateCustomActivityFields, idSeparator } from '../eventUtils.ts';
|
||||
import { customId as editActivityCustomId } from './editActivity.ts';
|
||||
import utils from '../../utils.ts';
|
||||
import { dbClient, queries } from '../../db.ts';
|
||||
|
||||
export const customId = 'editActivityCustom';
|
||||
|
||||
const execute = async (bot: Bot, interaction: Interaction) => {
|
||||
if (interaction.data?.customId && interaction.member && interaction.guildId && interaction.channelId) {
|
||||
// Light Telemetry
|
||||
dbClient.execute(queries.callIncCnt('btn-eeCustomAct')).catch((e) => utils.commonLoggers.dbError('step1a-openCustomModal.ts', 'call sproc INC_CNT on', e));
|
||||
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.Modal,
|
||||
data: {
|
||||
title: 'Create Custom Activity',
|
||||
customId: `${editActivityCustomId}${idSeparator}${interaction.data.customId.split(idSeparator)[1] || ''}`,
|
||||
components: generateCustomActivityFields(),
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('editActivity-custom.ts', interaction, e));
|
||||
}
|
||||
};
|
||||
|
||||
export const editActivityCustomButton = {
|
||||
customId,
|
||||
execute,
|
||||
};
|
|
@ -0,0 +1,224 @@
|
|||
import { ActionRow, ApplicationCommandFlags, Bot, ButtonStyles, Interaction, InteractionResponseTypes, MessageComponentTypes } from '../../../deps.ts';
|
||||
import { failColor, infoColor1, safelyDismissMsg, somethingWentWrong } from '../../commandUtils.ts';
|
||||
import { Activities, Activity } from '../event-creation/activities.ts';
|
||||
import { generateActionRow, getFinalActivity, getNestedActivity } from '../event-creation/utils.ts';
|
||||
import {
|
||||
activityMaxPlayersId,
|
||||
activitySubtitleId,
|
||||
activityTitleId,
|
||||
fillerChar,
|
||||
generateAlternateList,
|
||||
generateMemberList,
|
||||
generateMemberTitle,
|
||||
idSeparator,
|
||||
LfgEmbedIndexes,
|
||||
pathIdxEnder,
|
||||
pathIdxSeparator,
|
||||
} from '../eventUtils.ts';
|
||||
import { addTokenToMap, deleteTokenEarly, generateMapId, selfDestructMessage, tokenMap } from '../tokenCleanup.ts';
|
||||
import utils from '../../utils.ts';
|
||||
import config from '../../../config.ts';
|
||||
import { dbClient, queries } from '../../db.ts';
|
||||
import { customId as editActivityCustomCustomId } from './editActivity-custom.ts';
|
||||
import { applyEditButtons, applyEditMessage, getEventMemberCount, getLfgMembers } from './utils.ts';
|
||||
import { LFGMember } from '../../types/commandTypes.ts';
|
||||
|
||||
export const customId = 'editActivity';
|
||||
|
||||
const makeCustomEventRow = (customIdIdxPath: string): ActionRow => ({
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
components: [{
|
||||
type: MessageComponentTypes.Button,
|
||||
style: ButtonStyles.Primary,
|
||||
label: 'Create Custom Event',
|
||||
customId: `${editActivityCustomCustomId}${idSeparator}${customIdIdxPath}`,
|
||||
}],
|
||||
});
|
||||
|
||||
const execute = async (bot: Bot, interaction: Interaction) => {
|
||||
if (interaction.data?.customId && interaction.member && interaction.guildId && interaction.channelId) {
|
||||
const [evtChannelId, evtMessageId] = (interaction.data.customId.replaceAll(pathIdxEnder, '').replaceAll(fillerChar, '').split(idSeparator)[1] || '').split(pathIdxSeparator).map((id) =>
|
||||
BigInt(id || '0')
|
||||
);
|
||||
|
||||
// Check if we are done
|
||||
const valuesIdxPath = interaction.data?.values?.[0] || '';
|
||||
const finalizedIdxPath = valuesIdxPath.substring(0, valuesIdxPath.lastIndexOf(pathIdxEnder));
|
||||
if (interaction.data?.values?.[0].endsWith(pathIdxEnder) || (interaction.data?.components && interaction.data.components.length > 0)) {
|
||||
// User selected activity, give them the confirmation message and delete the selectMenus
|
||||
await deleteTokenEarly(bot, interaction, interaction.guildId, interaction.channelId, interaction.member.id);
|
||||
|
||||
// Fill in the activity details
|
||||
let selectedCategory = '';
|
||||
let selectedActivity: Activity = {
|
||||
name: '',
|
||||
maxMembers: 0,
|
||||
};
|
||||
if (interaction.data.components) {
|
||||
// Parse out our data
|
||||
const tempDataMap: Map<string, string> = new Map();
|
||||
for (const row of interaction.data.components) {
|
||||
if (row.components?.[0]) {
|
||||
const textField = row.components[0];
|
||||
tempDataMap.set(textField.customId || 'missingCustomId', textField.value || '');
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any pipe characters to avoid issues down the process
|
||||
const activityTitle = (tempDataMap.get(activityTitleId) || '').replace(/\|/g, '');
|
||||
const activitySubtitle = (tempDataMap.get(activitySubtitleId) || '').replace(/\|/g, '');
|
||||
const activityMaxPlayers = parseInt(tempDataMap.get(activityMaxPlayersId) || '0');
|
||||
if (isNaN(activityMaxPlayers) || activityMaxPlayers < 1 || activityMaxPlayers > 99) {
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: ApplicationCommandFlags.Ephemeral,
|
||||
embeds: [{
|
||||
color: failColor,
|
||||
title: 'Invalid Max Member count!',
|
||||
description: `${config.name} parsed the max members as \`${
|
||||
isNaN(activityMaxPlayers) ? 'Not a Number' : activityMaxPlayers
|
||||
}\`, which is outside of the allowed range. Please re-edit this activity, but make sure the maximum player count is between 1 and 99.\n\n${safelyDismissMsg}`,
|
||||
}],
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('editActivity.ts@invalidPlayer', interaction, e));
|
||||
return;
|
||||
}
|
||||
if (!activityMaxPlayers || !activitySubtitle || !activityTitle) {
|
||||
// Verify fields exist
|
||||
somethingWentWrong(bot, interaction, `missingFieldFromEditCustomActivity@${activityTitle}|${activitySubtitle}|${activityMaxPlayers}$`);
|
||||
return;
|
||||
}
|
||||
selectedCategory = activityTitle;
|
||||
selectedActivity.name = activitySubtitle;
|
||||
selectedActivity.maxMembers = activityMaxPlayers;
|
||||
|
||||
// Log custom event to see if we should add it as a preset
|
||||
dbClient.execute(queries.insertCustomActivity, [interaction.guildId, selectedCategory, selectedActivity.name, selectedActivity.maxMembers]).catch((e) =>
|
||||
utils.commonLoggers.dbError('editActivity.ts@custom', 'insert into', e)
|
||||
);
|
||||
} else {
|
||||
const rawIdxPath: Array<string> = finalizedIdxPath.split(pathIdxSeparator);
|
||||
const idxPath: Array<number> = rawIdxPath.map((rawIdx) => rawIdx ? parseInt(rawIdx) : -1);
|
||||
selectedCategory = Activities[idxPath[0]].name;
|
||||
selectedActivity = getFinalActivity(idxPath, Activities);
|
||||
}
|
||||
|
||||
if (
|
||||
!selectedActivity.maxMembers || !selectedCategory || !selectedActivity.name || (isNaN(selectedActivity.maxMembers) || (selectedActivity.maxMembers < 1 || selectedActivity.maxMembers > 99))
|
||||
) {
|
||||
// Verify fields exist
|
||||
somethingWentWrong(bot, interaction, `parseFailInEditCustomActivity@${selectedCategory}|${selectedActivity.name}|${selectedActivity.maxMembers || 'undefined'}$`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get event to apply edit
|
||||
const eventMessage = await bot.helpers.getMessage(evtChannelId, evtMessageId).catch((e: Error) => utils.commonLoggers.messageGetError('editActivity.ts', 'get eventMessage', e));
|
||||
if (eventMessage && eventMessage.embeds[0].fields) {
|
||||
await deleteTokenEarly(bot, interaction, interaction.guildId, interaction.channelId, interaction.member.id);
|
||||
// Update member lists
|
||||
const [currentMemberCount, _oldMaxMemberCount] = getEventMemberCount(eventMessage.embeds[0].fields[LfgEmbedIndexes.JoinedMembers].name);
|
||||
const currentMembers = getLfgMembers(eventMessage.embeds[0].fields[LfgEmbedIndexes.JoinedMembers].value);
|
||||
const currentAlternates = getLfgMembers(eventMessage.embeds[0].fields[LfgEmbedIndexes.AlternateMembers].value);
|
||||
if (currentMemberCount > selectedActivity.maxMembers) {
|
||||
const membersToAlternate = currentMembers.splice(selectedActivity.maxMembers).map((member): LFGMember => ({
|
||||
id: member.id,
|
||||
name: member.name,
|
||||
joined: true,
|
||||
}));
|
||||
currentAlternates.unshift(...membersToAlternate);
|
||||
}
|
||||
|
||||
// Apply edits
|
||||
eventMessage.embeds[0].fields[LfgEmbedIndexes.Activity].name = `${selectedCategory}:`;
|
||||
eventMessage.embeds[0].fields[LfgEmbedIndexes.Activity].value = selectedActivity.name;
|
||||
eventMessage.embeds[0].fields[LfgEmbedIndexes.JoinedMembers].name = generateMemberTitle(currentMembers, selectedActivity.maxMembers);
|
||||
eventMessage.embeds[0].fields[LfgEmbedIndexes.JoinedMembers].value = generateMemberList(currentMembers);
|
||||
eventMessage.embeds[0].fields[LfgEmbedIndexes.AlternateMembers].value = generateAlternateList(currentAlternates);
|
||||
|
||||
// Send edit confirmation
|
||||
addTokenToMap(bot, interaction, interaction.guildId, interaction.channelId, interaction.member.id);
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: ApplicationCommandFlags.Ephemeral,
|
||||
content: applyEditMessage(new Date().getTime()),
|
||||
embeds: [eventMessage.embeds[0]],
|
||||
components: applyEditButtons(interaction.data.customId.replaceAll(fillerChar, '').split(idSeparator)[1] || ''),
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('editActivity.ts', interaction, e));
|
||||
} else {
|
||||
somethingWentWrong(bot, interaction, 'failedToGetEventMsgInEditActivity');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse indexPath from the select value
|
||||
const rawIdxPath: Array<string> = interaction.data.values ? interaction.data.values[0].split(pathIdxSeparator) : [''];
|
||||
const idxPath: Array<number> = rawIdxPath.map((rawIdx) => rawIdx ? parseInt(rawIdx) : -1);
|
||||
const selectMenus: Array<ActionRow> = [];
|
||||
// Use fillerChar to create unique customIds for dropdowns
|
||||
// We also leverage this to determine if its the first time the user has entered gameSel
|
||||
let selectMenuCustomId = `${interaction.data.customId.replaceAll(fillerChar, '')}${fillerChar}`;
|
||||
let currentBaseValue = '';
|
||||
|
||||
for (let i = 0; i < idxPath.length; i++) {
|
||||
const idx = idxPath[i];
|
||||
const idxPathCopy = [...idxPath].slice(0, i);
|
||||
selectMenus.push(generateActionRow(currentBaseValue, getNestedActivity(idxPathCopy, Activities), selectMenuCustomId, idx));
|
||||
|
||||
selectMenuCustomId = `${selectMenuCustomId}${fillerChar}`;
|
||||
currentBaseValue = `${currentBaseValue}${idx}${pathIdxSeparator}`;
|
||||
}
|
||||
|
||||
selectMenus.push(makeCustomEventRow(interaction.data.customId.replaceAll(fillerChar, '').split(idSeparator)[1] || ''));
|
||||
|
||||
if (interaction.data.customId && interaction.data.customId.includes(fillerChar)) {
|
||||
// Let discord know we didn't ignore the user
|
||||
await bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.DeferredUpdateMessage,
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('editActivity.ts@ping', interaction, e));
|
||||
|
||||
// Update the original game selector
|
||||
await bot.helpers.editOriginalInteractionResponse(tokenMap.get(generateMapId(interaction.guildId, interaction.channelId, interaction.member.id))?.token || '', {
|
||||
components: selectMenus,
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('editActivity.ts@edit', interaction, e));
|
||||
} else {
|
||||
// Light Telemetry (placed here so it only gets called on first run)
|
||||
dbClient.execute(queries.callIncCnt('btn-eeChangeAct')).catch((e) => utils.commonLoggers.dbError('editActivity.ts', 'call sproc INC_CNT on', e));
|
||||
|
||||
// Delete old token entry if it exists
|
||||
await deleteTokenEarly(bot, interaction, interaction.guildId, interaction.channelId, interaction.member.id);
|
||||
|
||||
// Store token for later use
|
||||
addTokenToMap(bot, interaction, interaction.guildId, interaction.channelId, interaction.member.id);
|
||||
|
||||
// Send initial interaction
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
embeds: [{
|
||||
title: 'Please select a Game and Activity, or create a Custom Event.',
|
||||
description: `Changing activity for [this event](${
|
||||
utils.idsToMessageUrl({
|
||||
guildId: interaction.guildId,
|
||||
channelId: evtChannelId,
|
||||
messageId: evtMessageId,
|
||||
})
|
||||
}).\n\n${selfDestructMessage(new Date().getTime())}`,
|
||||
color: infoColor1,
|
||||
}],
|
||||
flags: ApplicationCommandFlags.Ephemeral,
|
||||
components: selectMenus,
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('editActivity.ts@init', interaction, e));
|
||||
}
|
||||
} else {
|
||||
somethingWentWrong(bot, interaction, 'missingCoreValuesOnEditActivity');
|
||||
}
|
||||
};
|
||||
|
||||
export const editActivityButton = {
|
||||
customId,
|
||||
execute,
|
||||
};
|
|
@ -0,0 +1,41 @@
|
|||
import { Bot, Interaction, InteractionResponseTypes } from '../../../deps.ts';
|
||||
import { dbClient, queries } from '../../db.ts';
|
||||
import { somethingWentWrong } from '../../commandUtils.ts';
|
||||
import { dateTimeFields, idSeparator, LfgEmbedIndexes, pathIdxEnder, pathIdxSeparator } from '../eventUtils.ts';
|
||||
import { monthsShort } from '../event-creation/dateTimeUtils.ts';
|
||||
import utils from '../../utils.ts';
|
||||
import { customId as applyDateTimeCustomId } from './applyDateTime.ts';
|
||||
|
||||
export const customId = 'editDateTime';
|
||||
|
||||
const execute = async (bot: Bot, interaction: Interaction) => {
|
||||
if (interaction.data?.customId && interaction.member && interaction.channelId && interaction.guildId) {
|
||||
// Light Telemetry
|
||||
dbClient.execute(queries.callIncCnt('btn-eeChangeTime')).catch((e) => utils.commonLoggers.dbError('editDateTime.ts', 'call sproc INC_CNT on', e));
|
||||
|
||||
const [evtChannelId, evtMessageId] = (interaction.data.customId.replaceAll(pathIdxEnder, '').split(idSeparator)[1] || '').split(pathIdxSeparator).map((id) => BigInt(id || '0'));
|
||||
const eventMessage = await bot.helpers.getMessage(evtChannelId, evtMessageId).catch((e: Error) => utils.commonLoggers.messageGetError('editDateTime.ts', 'get eventMessage', e));
|
||||
let rawEventDateTime = eventMessage?.embeds[0].fields ? eventMessage.embeds[0].fields[LfgEmbedIndexes.StartTime].value.trim().split('\n')[0].split(' ') : [];
|
||||
const monthIdx = rawEventDateTime.findIndex((item) => monthsShort.includes(item.toUpperCase()));
|
||||
const prefillTime = rawEventDateTime.slice(0, monthIdx - 1).join(' ').trim();
|
||||
const prefillTimeZone = rawEventDateTime[monthIdx - 1].trim();
|
||||
const prefillDate = rawEventDateTime.slice(monthIdx).join(' ').trim();
|
||||
|
||||
// Open Edit Date/Time Modal
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.Modal,
|
||||
data: {
|
||||
title: 'Edit Event Date/Time',
|
||||
customId: `${applyDateTimeCustomId}${idSeparator}${interaction.data.customId.split(idSeparator)[1] || ''}`,
|
||||
components: dateTimeFields(prefillTime, prefillTimeZone, prefillDate),
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('editDateTime.ts', interaction, e));
|
||||
} else {
|
||||
somethingWentWrong(bot, interaction, 'noDataFromEditDateTimeButton');
|
||||
}
|
||||
};
|
||||
|
||||
export const editDateTimeButton = {
|
||||
customId,
|
||||
execute,
|
||||
};
|
|
@ -0,0 +1,36 @@
|
|||
import { Bot, Interaction, InteractionResponseTypes } from '../../../deps.ts';
|
||||
import { dbClient, queries } from '../../db.ts';
|
||||
import { somethingWentWrong } from '../../commandUtils.ts';
|
||||
import { descriptionTextField, idSeparator, LfgEmbedIndexes, pathIdxEnder, pathIdxSeparator } from '../eventUtils.ts';
|
||||
import utils from '../../utils.ts';
|
||||
import { customId as applyDescriptionCustomId } from './applyDescription.ts';
|
||||
|
||||
export const customId = 'editDescription';
|
||||
|
||||
const execute = async (bot: Bot, interaction: Interaction) => {
|
||||
if (interaction.data?.customId && interaction.member && interaction.channelId && interaction.guildId) {
|
||||
// Light Telemetry
|
||||
dbClient.execute(queries.callIncCnt('btn-eeChangeDesc')).catch((e) => utils.commonLoggers.dbError('editDescription.ts', 'call sproc INC_CNT on', e));
|
||||
|
||||
const [evtChannelId, evtMessageId] = (interaction.data.customId.replaceAll(pathIdxEnder, '').split(idSeparator)[1] || '').split(pathIdxSeparator).map((id) => BigInt(id || '0'));
|
||||
const eventMessage = await bot.helpers.getMessage(evtChannelId, evtMessageId).catch((e: Error) => utils.commonLoggers.messageGetError('editDescription.ts', 'get eventMessage', e));
|
||||
const prefillDescription = eventMessage?.embeds[0].fields ? eventMessage.embeds[0].fields[LfgEmbedIndexes.Description].value.trim() : '';
|
||||
|
||||
// Open Edit Description Modal
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.Modal,
|
||||
data: {
|
||||
title: 'Edit Event Description',
|
||||
customId: `${applyDescriptionCustomId}${idSeparator}${interaction.data.customId.split(idSeparator)[1] || ''}`,
|
||||
components: [descriptionTextField(prefillDescription)],
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('editDescription.ts', interaction, e));
|
||||
} else {
|
||||
somethingWentWrong(bot, interaction, 'noDataFromEditDescriptionButton');
|
||||
}
|
||||
};
|
||||
|
||||
export const editDescriptionButton = {
|
||||
customId,
|
||||
execute,
|
||||
};
|
|
@ -0,0 +1,113 @@
|
|||
import { ActionRow, ApplicationCommandFlags, Bot, ButtonStyles, Interaction, InteractionResponseTypes, MessageComponentTypes } from '../../../deps.ts';
|
||||
import { dbClient, generateGuildSettingKey, lfgChannelSettings, queries } from '../../db.ts';
|
||||
import { infoColor1, somethingWentWrong, stopThat } from '../../commandUtils.ts';
|
||||
import { idSeparator, pathIdxEnder, pathIdxSeparator } from '../eventUtils.ts';
|
||||
import { addTokenToMap, selfDestructMessage } from '../tokenCleanup.ts';
|
||||
import utils from '../../utils.ts';
|
||||
import { customId as editActivityCustomId } from './editActivity.ts';
|
||||
import { customId as editDescriptionCustomId } from './editDescription.ts';
|
||||
import { customId as editDateTimeCustomId } from './editDateTime.ts';
|
||||
import { customId as toggleWLStatusCustomId } from './toggleWLStatus.ts';
|
||||
|
||||
export const customId = 'editEvent';
|
||||
|
||||
const execute = async (bot: Bot, interaction: Interaction) => {
|
||||
if (
|
||||
interaction.data?.customId && interaction.member && interaction.channelId && interaction.guildId && interaction.message && interaction.message.components &&
|
||||
interaction.message.components[0].components
|
||||
) {
|
||||
// Light Telemetry
|
||||
dbClient.execute(queries.callIncCnt('btn-editEvent')).catch((e) => utils.commonLoggers.dbError('editEvent.ts', 'call sproc INC_CNT on', e));
|
||||
|
||||
const ownerId = BigInt(interaction.message.embeds[0].footer?.iconUrl?.split('#')[1] || '0');
|
||||
const lfgChannelSetting = lfgChannelSettings.get(generateGuildSettingKey(interaction.guildId, interaction.channelId)) || {
|
||||
managed: false,
|
||||
managerRoleId: 0n,
|
||||
logChannelId: 0n,
|
||||
};
|
||||
|
||||
// Make sure this is being done by the owner or a Group Up Manager
|
||||
if (interaction.member.id === ownerId || (lfgChannelSetting.managed && interaction.member.roles.includes(lfgChannelSetting.managerRoleId))) {
|
||||
const actionByManager = interaction.member.id !== ownerId;
|
||||
const baseEditIdxPath = `${idSeparator}${interaction.channelId}${pathIdxSeparator}${interaction.message.id}`;
|
||||
const editIdxPath = `${baseEditIdxPath}${actionByManager ? pathIdxEnder : ''}`;
|
||||
const whitelistedEvent = interaction.message.components[0].components.some((component) => component.customId?.includes(idSeparator));
|
||||
|
||||
// Store token for later use
|
||||
addTokenToMap(bot, interaction, interaction.guildId, interaction.channelId, interaction.member.id);
|
||||
|
||||
const editButtons: ActionRow = {
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
components: [{
|
||||
type: MessageComponentTypes.Button,
|
||||
label: 'Change Activity',
|
||||
style: ButtonStyles.Primary,
|
||||
customId: `${editActivityCustomId}${editIdxPath}`,
|
||||
}, {
|
||||
type: MessageComponentTypes.Button,
|
||||
label: 'Change Date/Time',
|
||||
style: ButtonStyles.Primary,
|
||||
customId: `${editDateTimeCustomId}${editIdxPath}`,
|
||||
}, {
|
||||
type: MessageComponentTypes.Button,
|
||||
label: 'Edit Description',
|
||||
style: ButtonStyles.Primary,
|
||||
customId: `${editDescriptionCustomId}${editIdxPath}`,
|
||||
}],
|
||||
};
|
||||
if (!actionByManager) {
|
||||
editButtons.components.push(
|
||||
whitelistedEvent
|
||||
? {
|
||||
type: MessageComponentTypes.Button,
|
||||
label: 'Make Event Public',
|
||||
style: ButtonStyles.Primary,
|
||||
customId: `${toggleWLStatusCustomId}${baseEditIdxPath}`,
|
||||
}
|
||||
: {
|
||||
type: MessageComponentTypes.Button,
|
||||
label: 'Make Event Whitelisted',
|
||||
style: ButtonStyles.Primary,
|
||||
customId: `${toggleWLStatusCustomId}${baseEditIdxPath}${pathIdxEnder}`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Open Edit Options
|
||||
bot.helpers.sendInteractionResponse(
|
||||
interaction.id,
|
||||
interaction.token,
|
||||
{
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: ApplicationCommandFlags.Ephemeral,
|
||||
embeds: [{
|
||||
color: infoColor1,
|
||||
title: 'Edit Menu',
|
||||
description: `Now editing [this event](${
|
||||
utils.idsToMessageUrl({
|
||||
guildId: interaction.guildId,
|
||||
channelId: interaction.channelId,
|
||||
messageId: interaction.message.id,
|
||||
})
|
||||
}). Please select an option below.
|
||||
|
||||
${selfDestructMessage(new Date().getTime())}`,
|
||||
}],
|
||||
components: [editButtons],
|
||||
},
|
||||
},
|
||||
).catch((e: Error) => utils.commonLoggers.interactionSendError('editEvent.ts', interaction, e));
|
||||
} else {
|
||||
// Not owner or manager, tell user they can't
|
||||
stopThat(bot, interaction, 'edit');
|
||||
}
|
||||
} else {
|
||||
somethingWentWrong(bot, interaction, 'noDataFromEditEventButton');
|
||||
}
|
||||
};
|
||||
|
||||
export const editEventButton = {
|
||||
customId,
|
||||
execute,
|
||||
};
|
|
@ -0,0 +1,128 @@
|
|||
import { ApplicationCommandFlags, Bot, Interaction, InteractionResponseTypes } from '../../../deps.ts';
|
||||
import { dbClient, generateGuildSettingKey, lfgChannelSettings, queries } from '../../db.ts';
|
||||
import { infoColor1, safelyDismissMsg, sendDirectMessage, somethingWentWrong, successColor, warnColor } from '../../commandUtils.ts';
|
||||
import { generateMemberList, idSeparator, LfgEmbedIndexes } from '../eventUtils.ts';
|
||||
import utils from '../../utils.ts';
|
||||
import config from '../../../config.ts';
|
||||
import { generateMapId, getGuildName, getLfgMembers, joinMemberToEvent, joinRequestMap, joinRequestResponseButtons, JoinRequestStatus } from './utils.ts';
|
||||
|
||||
export const customId = 'joinEvent';
|
||||
|
||||
const execute = async (bot: Bot, interaction: Interaction) => {
|
||||
if (
|
||||
interaction.data?.customId && interaction.member && interaction.member.user && interaction.channelId && interaction.guildId && interaction.message && interaction.message.embeds[0] &&
|
||||
interaction.message.embeds[0].fields
|
||||
) {
|
||||
// Light Telemetry
|
||||
dbClient.execute(queries.callIncCnt(interaction.data.customId.includes(idSeparator) ? 'btn-joinWLEvent' : 'btn-joinEvent')).catch((e) =>
|
||||
utils.commonLoggers.dbError('joinEvent.ts', 'call sproc INC_CNT on', e)
|
||||
);
|
||||
const ownerId = BigInt(interaction.message.embeds[0].footer?.iconUrl?.split('#')[1] || '0');
|
||||
const memberId = interaction.member.id;
|
||||
|
||||
// Check if event is whitelisted
|
||||
if (interaction.data.customId.includes(idSeparator) && memberId !== ownerId) {
|
||||
// Initialize WL vars
|
||||
const joinRequestKey = generateMapId(interaction.message.id, interaction.channelId, memberId);
|
||||
const messageUrl = utils.idsToMessageUrl({
|
||||
guildId: interaction.guildId,
|
||||
channelId: interaction.channelId,
|
||||
messageId: interaction.message.id,
|
||||
});
|
||||
const lfgChannelSetting = lfgChannelSettings.get(generateGuildSettingKey(interaction.guildId, interaction.channelId)) || { managed: false };
|
||||
const urgentManagerStr = lfgChannelSetting.managed ? ` a ${config.name} Manager (members with the <@&${lfgChannelSetting.managerRoleId}> role in this guild) or ` : ' ';
|
||||
const eventMembers = getLfgMembers(interaction.message.embeds[0].fields[LfgEmbedIndexes.JoinedMembers].value);
|
||||
|
||||
if (eventMembers.find((lfgMember) => lfgMember.id === memberId)) {
|
||||
// User is already joined to event, block request
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: ApplicationCommandFlags.Ephemeral,
|
||||
embeds: [{
|
||||
color: warnColor,
|
||||
title: 'Notice: Request Blocked',
|
||||
description: `To reduce spam, ${config.name} has blocked this request to join as you have already joined this event.
|
||||
|
||||
${safelyDismissMsg}`,
|
||||
}],
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('joinEvent.ts@userAlreadyJoined', interaction, e));
|
||||
} else if (joinRequestMap.has(joinRequestKey)) {
|
||||
// User has already sent request, block new one
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: ApplicationCommandFlags.Ephemeral,
|
||||
embeds: [{
|
||||
color: warnColor,
|
||||
title: 'Notice: Request Blocked',
|
||||
description: `To reduce spam, ${config.name} has blocked this request to join as you have recently sent a request for this event.
|
||||
|
||||
If this request is urgent, please speak with${urgentManagerStr}the owner of [this event](${messageUrl}), <@${ownerId}>, to resolve the issue.
|
||||
|
||||
The status of your recent Join Request for [this event](${messageUrl}) is: \`${joinRequestMap.get(joinRequestKey)?.status || 'Failed to retrieve status'}\`
|
||||
|
||||
${safelyDismissMsg}`,
|
||||
}],
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('joinEvent.ts@requestBlocked', interaction, e));
|
||||
} else {
|
||||
const guildName = await getGuildName(bot, interaction.guildId);
|
||||
// User is not joined and this is first request, send the Join Request
|
||||
sendDirectMessage(bot, ownerId, {
|
||||
embeds: [{
|
||||
color: infoColor1,
|
||||
title: 'New Join Request!',
|
||||
description: `A member has requested to join [your event](${messageUrl}) in \`${guildName}\`. Please use the buttons below this message to Approve or Deny the request.`,
|
||||
fields: [{
|
||||
name: 'Member Details:',
|
||||
value: generateMemberList([{
|
||||
id: memberId,
|
||||
name: interaction.member.user.username,
|
||||
}]),
|
||||
}],
|
||||
}],
|
||||
components: joinRequestResponseButtons(false),
|
||||
}).then(() => {
|
||||
// Alert requester that join request has been sent
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: ApplicationCommandFlags.Ephemeral,
|
||||
embeds: [{
|
||||
color: successColor,
|
||||
title: 'Notice: Request Received',
|
||||
description: `The owner of [this event](${messageUrl}), <@${ownerId}>, has been notified of your request. You will receive a Direct Message when <@${ownerId}> responds to the request.
|
||||
|
||||
${safelyDismissMsg}`,
|
||||
}],
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('joinEvent.ts@requestReceived', interaction, e));
|
||||
|
||||
// Track the request to prevent spam
|
||||
joinRequestMap.set(joinRequestKey, {
|
||||
status: JoinRequestStatus.Pending,
|
||||
timestamp: new Date().getTime(),
|
||||
});
|
||||
}).catch((e: Error) => {
|
||||
somethingWentWrong(bot, interaction, 'failedToDMOwnerInRequestToJoinEventButton');
|
||||
utils.commonLoggers.messageSendError('joinEvent.ts@dmOwner', 'failed to DM owner for join request', e);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Join user to event
|
||||
joinMemberToEvent(bot, interaction, interaction.message.embeds[0], interaction.message.id, interaction.channelId, {
|
||||
id: memberId,
|
||||
name: interaction.member.user.username,
|
||||
}, interaction.guildId);
|
||||
}
|
||||
} else {
|
||||
somethingWentWrong(bot, interaction, 'noDataFromJoinEventButton');
|
||||
}
|
||||
};
|
||||
|
||||
export const joinEventButton = {
|
||||
customId,
|
||||
execute,
|
||||
};
|
|
@ -0,0 +1,88 @@
|
|||
import { Bot, ButtonStyles, Interaction, InteractionResponseTypes, MessageComponentTypes } from '../../../deps.ts';
|
||||
import { sendDirectMessage, somethingWentWrong, successColor, warnColor } from '../../commandUtils.ts';
|
||||
import { generateMapId, getLfgMembers, joinMemberToEvent, joinRequestMap, joinRequestResponseButtons, JoinRequestStatus } from './utils.ts';
|
||||
import { alternateEventBtnStr, idSeparator } from '../eventUtils.ts';
|
||||
import { dbClient, queries } from '../../db.ts';
|
||||
import { customId as alternateRequestCustomId } from './alternateRequest.ts';
|
||||
import utils from '../../utils.ts';
|
||||
|
||||
export const customId = 'joinRequest';
|
||||
export const approveStr = 'approved';
|
||||
export const denyStr = 'denied';
|
||||
|
||||
const execute = async (bot: Bot, interaction: Interaction) => {
|
||||
if (
|
||||
interaction.data?.customId && interaction.user && interaction.channelId && interaction.message && interaction.message.embeds[0] && interaction.message.embeds[0].fields &&
|
||||
interaction.message.embeds[0].description
|
||||
) {
|
||||
const memberRequesting = getLfgMembers(interaction.message.embeds[0].fields[0].value || '')[0];
|
||||
const approved = interaction.data.customId.includes(approveStr);
|
||||
const responseStr = interaction.data.customId.split(idSeparator)[1] || '';
|
||||
const capResponseStr = utils.capitalizeFirstChar(responseStr);
|
||||
const eventIds = utils.messageUrlToIds(interaction.message.embeds[0].description.split(')')[0] || '');
|
||||
const eventUrl = utils.idsToMessageUrl(eventIds);
|
||||
const joinRequestMapId = generateMapId(eventIds.messageId, eventIds.channelId, memberRequesting.id);
|
||||
|
||||
// Light Telemetry
|
||||
dbClient.execute(queries.callIncCnt(approved ? 'btn-joinReqApprove' : 'btn-joinReqDeny')).catch((e) => utils.commonLoggers.dbError('joinRequest.ts', 'call sproc INC_CNT on', e));
|
||||
|
||||
if (approved) {
|
||||
// If member was approved, get the event and add them to it
|
||||
const eventMessage = await bot.helpers.getMessage(eventIds.channelId, eventIds.messageId).catch((e: Error) => utils.commonLoggers.messageGetError('joinRequest.ts', 'get eventMessage', e));
|
||||
if (eventMessage) {
|
||||
joinMemberToEvent(bot, interaction, eventMessage.embeds[0], eventIds.messageId, eventIds.channelId, memberRequesting, eventIds.guildId);
|
||||
} else {
|
||||
somethingWentWrong(bot, interaction, 'eventMissingFromJoinRequestButton');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// If denied, send deferredUpdate so discord doesn't think we ignored the user (approved is handled in joinMemberToEvent)
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.DeferredUpdateMessage,
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('joinRequest.ts', interaction, e));
|
||||
}
|
||||
|
||||
// Update the JoinRequestMap
|
||||
joinRequestMap.set(joinRequestMapId, {
|
||||
status: approved ? JoinRequestStatus.Approved : JoinRequestStatus.Denied,
|
||||
timestamp: new Date().getTime(),
|
||||
});
|
||||
|
||||
// Send DM to the requesting member to let them know of the result
|
||||
sendDirectMessage(bot, memberRequesting.id, {
|
||||
embeds: [{
|
||||
color: approved ? successColor : warnColor,
|
||||
title: `Notice: Join Request ${capResponseStr}`,
|
||||
description: `The owner of [this event](${eventUrl}), <@${interaction.user.id}>, has ${responseStr} your join request.${
|
||||
approved ? '' : ' If you would like to join the event as an alternate, please click on the button below.'
|
||||
}`,
|
||||
}],
|
||||
components: approved ? undefined : [{
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
components: [{
|
||||
type: MessageComponentTypes.Button,
|
||||
label: alternateEventBtnStr,
|
||||
style: ButtonStyles.Primary,
|
||||
customId: alternateRequestCustomId,
|
||||
}],
|
||||
}],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('joinRequest.ts', 'send DM fail', e));
|
||||
|
||||
// Update request DM to indicate if it was approved or denied and disable buttons
|
||||
interaction.message.embeds[0].fields.push({
|
||||
name: 'Your response:',
|
||||
value: capResponseStr,
|
||||
});
|
||||
bot.helpers.editMessage(interaction.channelId, interaction.message.id, {
|
||||
embeds: [interaction.message.embeds[0]],
|
||||
components: joinRequestResponseButtons(true),
|
||||
}).catch((e: Error) => utils.commonLoggers.messageEditError('joinRequest.ts', 'event edit fail', e));
|
||||
} else {
|
||||
somethingWentWrong(bot, interaction, 'noDataFromJoinRequestButton');
|
||||
}
|
||||
};
|
||||
|
||||
export const joinRequestButton = {
|
||||
customId,
|
||||
execute,
|
||||
};
|
|
@ -0,0 +1,24 @@
|
|||
import { Bot, Interaction } from '../../../deps.ts';
|
||||
import { dbClient, queries } from '../../db.ts';
|
||||
import { somethingWentWrong } from '../../commandUtils.ts';
|
||||
import utils from '../../utils.ts';
|
||||
import { removeMemberFromEvent } from './utils.ts';
|
||||
|
||||
export const customId = 'leaveEvent';
|
||||
|
||||
const execute = async (bot: Bot, interaction: Interaction) => {
|
||||
if (interaction.data?.customId && interaction.member && interaction.channelId && interaction.guildId && interaction.message && interaction.message.embeds[0]) {
|
||||
// Light Telemetry
|
||||
dbClient.execute(queries.callIncCnt('btn-leaveEvent')).catch((e) => utils.commonLoggers.dbError('leaveEvent.ts', 'call sproc INC_CNT on', e));
|
||||
|
||||
// Remove user from event
|
||||
removeMemberFromEvent(bot, interaction, interaction.message.embeds[0], interaction.message.id, interaction.channelId, interaction.member.id, interaction.guildId);
|
||||
} else {
|
||||
somethingWentWrong(bot, interaction, 'noDataFromLeaveEventButton');
|
||||
}
|
||||
};
|
||||
|
||||
export const leaveEventButton = {
|
||||
customId,
|
||||
execute,
|
||||
};
|
|
@ -0,0 +1,32 @@
|
|||
import { Bot, Interaction } from '../../../deps.ts';
|
||||
import { dbClient, queries } from '../../db.ts';
|
||||
import { somethingWentWrong } from '../../commandUtils.ts';
|
||||
import utils from '../../utils.ts';
|
||||
import { removeMemberFromEvent } from './utils.ts';
|
||||
import { idSeparator, pathIdxEnder, pathIdxSeparator } from '../eventUtils.ts';
|
||||
|
||||
export const customId = 'leaveEventViaDM';
|
||||
|
||||
const execute = async (bot: Bot, interaction: Interaction) => {
|
||||
if (interaction.data?.customId) {
|
||||
// Light Telemetry
|
||||
dbClient.execute(queries.callIncCnt('btn-leaveEventViaDM')).catch((e) => utils.commonLoggers.dbError('leaveEventViaDM.ts', 'call sproc INC_CNT on', e));
|
||||
|
||||
const [evtGuildId, evtChannelId, evtMessageId] = (interaction.data.customId.replaceAll(pathIdxEnder, '').split(idSeparator)[1] || '').split(pathIdxSeparator).map((id) => BigInt(id || '0'));
|
||||
const eventMessage = await bot.helpers.getMessage(evtChannelId, evtMessageId).catch((e: Error) => utils.commonLoggers.messageGetError('editActivity.ts', 'get eventMessage', e));
|
||||
|
||||
if (eventMessage && eventMessage.embeds[0]) {
|
||||
// Remove user from event
|
||||
removeMemberFromEvent(bot, interaction, eventMessage.embeds[0], evtMessageId, evtChannelId, interaction.user.id, evtGuildId, true);
|
||||
} else {
|
||||
somethingWentWrong(bot, interaction, 'getEventFailInLeaveEventViaDMButton');
|
||||
}
|
||||
} else {
|
||||
somethingWentWrong(bot, interaction, 'noDataFromLeaveEventViaDMButton');
|
||||
}
|
||||
};
|
||||
|
||||
export const leaveEventViaDMButton = {
|
||||
customId,
|
||||
execute,
|
||||
};
|
|
@ -0,0 +1,66 @@
|
|||
import { ApplicationCommandFlags, Bot, Interaction, InteractionResponseTypes, MessageComponentTypes } from '../../../deps.ts';
|
||||
import { dmTestMessage, safelyDismissMsg, sendDirectMessage, somethingWentWrong, successColor, warnColor } from '../../commandUtils.ts';
|
||||
import { idSeparator, pathIdxEnder, pathIdxSeparator } from '../eventUtils.ts';
|
||||
import { deleteTokenEarly } from '../tokenCleanup.ts';
|
||||
import utils from '../../utils.ts';
|
||||
import { dbClient, queries } from '../../db.ts';
|
||||
import { generateLFGButtons } from '../event-creation/utils.ts';
|
||||
|
||||
export const customId = 'toggleWLStatus';
|
||||
|
||||
const execute = async (bot: Bot, interaction: Interaction) => {
|
||||
if (interaction.data?.customId && interaction.member && interaction.channelId && interaction.guildId) {
|
||||
deleteTokenEarly(bot, interaction, interaction.guildId, interaction.channelId, interaction.member.id);
|
||||
const makeWhitelisted = interaction.data.customId.endsWith(pathIdxEnder);
|
||||
dbClient.execute(queries.callIncCnt(makeWhitelisted ? 'btn-eeMakeWL' : 'btn-eeMakePublic')).catch((e) => utils.commonLoggers.dbError('toggleWLStatus.ts', 'call sproc INC_CNT on', e));
|
||||
const [evtChannelId, evtMessageId] = (interaction.data.customId.replaceAll(pathIdxEnder, '').split(idSeparator)[1] || '').split(pathIdxSeparator).map((id) => BigInt(id || '0'));
|
||||
|
||||
// Check if we need to ensure DMs are open
|
||||
if (makeWhitelisted) {
|
||||
const dmSuccess = Boolean(await sendDirectMessage(bot, interaction.member.id, dmTestMessage).catch((e: Error) => utils.commonLoggers.messageSendError('toggleWLStatus.ts', 'send DM fail', e)));
|
||||
if (!dmSuccess) {
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: ApplicationCommandFlags.Ephemeral,
|
||||
embeds: [{
|
||||
color: warnColor,
|
||||
title: 'Event not modified.',
|
||||
description: `In order to turn the whitelist on, your DMs must be open to receive Join Requests. Please open your DMs and try again.\n\n${safelyDismissMsg}`,
|
||||
}],
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('toggleWLStatus.ts@dmFail', interaction, e));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bot.helpers.editMessage(evtChannelId, evtMessageId, {
|
||||
components: [{
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
components: generateLFGButtons(makeWhitelisted),
|
||||
}],
|
||||
}).then(() =>
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: ApplicationCommandFlags.Ephemeral,
|
||||
embeds: [{
|
||||
color: successColor,
|
||||
title: 'Update successfully applied.',
|
||||
description: `This event is now ${makeWhitelisted ? 'whitelisted, meaning you will have to approve join requests from all future members' : 'public'}.\n\n${safelyDismissMsg}`,
|
||||
}],
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('toggleWLStatus.ts@dmSuccess', interaction, e))
|
||||
).catch((e) => {
|
||||
utils.commonLoggers.messageEditError('toggleWLStatus.ts', 'toggleWLStatusFailed', e);
|
||||
somethingWentWrong(bot, interaction, 'editFailedInToggleWLStatusButton');
|
||||
});
|
||||
} else {
|
||||
somethingWentWrong(bot, interaction, 'noDataFromToggleWLStatusButton');
|
||||
}
|
||||
};
|
||||
|
||||
export const toggleWLStatusButton = {
|
||||
customId,
|
||||
execute,
|
||||
};
|
|
@ -0,0 +1,114 @@
|
|||
import { ApplicationCommandFlags, Bot, Interaction, InteractionResponseTypes } from '../../../deps.ts';
|
||||
import { failColor, infoColor1, infoColor2, safelyDismissMsg, sendDirectMessage, somethingWentWrong, successColor } from '../../commandUtils.ts';
|
||||
import { generateMemberList, idSeparator, LfgEmbedIndexes, pathIdxEnder, pathIdxSeparator } from '../eventUtils.ts';
|
||||
import { deleteTokenEarly } from '../tokenCleanup.ts';
|
||||
import utils from '../../utils.ts';
|
||||
import config from '../../../config.ts';
|
||||
import { dbClient, generateGuildSettingKey, lfgChannelSettings, queries } from '../../db.ts';
|
||||
import { getGuildName } from './utils.ts';
|
||||
|
||||
export const customId = 'updateEvent';
|
||||
|
||||
const execute = async (bot: Bot, interaction: Interaction) => {
|
||||
if (interaction.data?.customId && interaction.member?.user && interaction.channelId && interaction.guildId && interaction.message?.embeds[0].fields) {
|
||||
deleteTokenEarly(bot, interaction, interaction.guildId, interaction.channelId, interaction.member.id);
|
||||
|
||||
const lfgChannelSetting = lfgChannelSettings.get(generateGuildSettingKey(interaction.guildId, interaction.channelId)) || {
|
||||
managed: false,
|
||||
managerRoleId: 0n,
|
||||
logChannelId: 0n,
|
||||
};
|
||||
const actionByManager = interaction.data.customId.endsWith(pathIdxEnder);
|
||||
const [evtChannelId, evtMessageId] = (interaction.data.customId.replaceAll(pathIdxEnder, '').split(idSeparator)[1] || '').split(pathIdxSeparator).map((id) => BigInt(id || '0'));
|
||||
const eventTime: Date = new Date(parseInt(interaction.message.embeds[0].fields[LfgEmbedIndexes.ICSLink].value.split('?t=')[1].split('&n=')[0] || '0'));
|
||||
const guildName = await getGuildName(bot, interaction.guildId);
|
||||
const oldEventEmbed = (await bot.helpers.getMessage(evtChannelId, evtMessageId).catch((e: Error) => utils.commonLoggers.messageGetError('updateEvent.ts', 'get eventMessage', e)))?.embeds[0];
|
||||
const newEventEmbed = interaction.message.embeds[0];
|
||||
const userId = interaction.member.id;
|
||||
const userName = interaction.member.user.username;
|
||||
const eventLink = utils.idsToMessageUrl({
|
||||
guildId: interaction.guildId,
|
||||
channelId: evtChannelId,
|
||||
messageId: evtMessageId,
|
||||
});
|
||||
|
||||
bot.helpers.editMessage(evtChannelId, evtMessageId, { embeds: [interaction.message.embeds[0]] }).then(() => {
|
||||
dbClient.execute(queries.updateEventTime, [eventTime, evtChannelId, evtMessageId]).then(() => {
|
||||
// Acknowledge user so discord doesn't get annoyed
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: ApplicationCommandFlags.Ephemeral,
|
||||
embeds: [{
|
||||
color: successColor,
|
||||
title: 'Update successfully applied.',
|
||||
description: safelyDismissMsg,
|
||||
}],
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('updateEvent.ts', interaction, e));
|
||||
|
||||
if (actionByManager) {
|
||||
const ownerId = BigInt(oldEventEmbed?.footer?.iconUrl?.split('#')[1] || '0');
|
||||
const missingOldEmbed = { title: 'Failed to get old event', color: failColor };
|
||||
if (oldEventEmbed) {
|
||||
oldEventEmbed.color = infoColor1;
|
||||
}
|
||||
bot.helpers.sendMessage(lfgChannelSetting.logChannelId, {
|
||||
embeds: [
|
||||
{
|
||||
color: infoColor2,
|
||||
title: `Event edited by a ${config.name} Manager`,
|
||||
description: `[This event](${eventLink}) was edited by ${userName} - <@${userId}>. The old event is listed first and marked with a blue bar.`,
|
||||
timestamp: new Date().getTime(),
|
||||
},
|
||||
oldEventEmbed || missingOldEmbed,
|
||||
newEventEmbed,
|
||||
],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('updateEvent.ts', 'send log message', e));
|
||||
|
||||
sendDirectMessage(bot, ownerId, {
|
||||
embeds: [
|
||||
{
|
||||
color: infoColor2,
|
||||
title: `Notice: A ${config.name} Manager has edited one of your events in ${guildName}`,
|
||||
description: `[This event](${eventLink}) was edited. The old event is listed first and marked with a blue bar.`,
|
||||
fields: [
|
||||
{
|
||||
name: `${config.name} Manager:`,
|
||||
value: generateMemberList([{
|
||||
id: userId,
|
||||
name: userName,
|
||||
}]),
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Are you unhappy with this action?',
|
||||
value: `Please reach out to the ${config.name} Manager that performed this action, or the moderators/administrators of ${guildName}.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
oldEventEmbed || missingOldEmbed,
|
||||
newEventEmbed,
|
||||
],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('managerJLA.ts', 'send DM fail', e));
|
||||
}
|
||||
}).catch((e) => {
|
||||
utils.commonLoggers.dbError('updateEvent.ts', 'update event in', e);
|
||||
if (oldEventEmbed) {
|
||||
bot.helpers.editMessage(evtChannelId, evtMessageId, { embeds: [oldEventEmbed] }).catch((e) => utils.commonLoggers.messageEditError('updateEvent.ts', 'resetEventFailed', e));
|
||||
}
|
||||
somethingWentWrong(bot, interaction, 'updateDBInUpdateEventButton');
|
||||
});
|
||||
}).catch((e) => {
|
||||
utils.commonLoggers.messageEditError('updateEvent.ts', 'updateEventFailed', e);
|
||||
somethingWentWrong(bot, interaction, 'updateEventMessageInUpdateEventButton');
|
||||
});
|
||||
} else {
|
||||
somethingWentWrong(bot, interaction, 'noDataFromUpdateEvent');
|
||||
}
|
||||
};
|
||||
|
||||
export const updateEventButton = {
|
||||
customId,
|
||||
execute,
|
||||
};
|
|
@ -0,0 +1,365 @@
|
|||
import { ActionRow, ApplicationCommandFlags, Bot, ButtonStyles, Embed, Interaction, InteractionResponseTypes, MessageComponentTypes } from '../../../deps.ts';
|
||||
import { LFGMember, UrlIds } from '../../types/commandTypes.ts';
|
||||
import { infoColor1, safelyDismissMsg, sendDirectMessage, somethingWentWrong, successColor } from '../../commandUtils.ts';
|
||||
import { generateAlternateList, generateMemberList, generateMemberTitle, idSeparator, leaveEventBtnStr, LfgEmbedIndexes, noMembersStr, pathIdxSeparator } from '../eventUtils.ts';
|
||||
import { selfDestructMessage } from '../tokenCleanup.ts';
|
||||
import { approveStr, customId as joinRequestCustomId, denyStr } from './joinRequest.ts';
|
||||
import { customId as updateEventCustomId } from './updateEvent.ts';
|
||||
import { customId as leaveViaDMCustomId } from './leaveViaDM.ts';
|
||||
import { dbClient, queries } from '../../db.ts';
|
||||
import utils from '../../utils.ts';
|
||||
|
||||
// Join status map to prevent spamming the system
|
||||
export enum JoinRequestStatus {
|
||||
Pending = 'Pending',
|
||||
Approved = 'Approved',
|
||||
Denied = 'Denied',
|
||||
}
|
||||
export const generateMapId = (messageId: bigint, channelId: bigint, userId: bigint) => `${messageId}-${channelId}-${userId}`;
|
||||
export const joinRequestMap: Map<string, {
|
||||
status: JoinRequestStatus;
|
||||
timestamp: number;
|
||||
}> = new Map();
|
||||
|
||||
// Join request map cleaner
|
||||
const oneHour = 1000 * 60 * 60;
|
||||
const oneDay = oneHour * 24;
|
||||
const oneWeek = oneDay * 7;
|
||||
setInterval(() => {
|
||||
const now = new Date().getTime();
|
||||
joinRequestMap.forEach((joinRequest, key) => {
|
||||
switch (joinRequest.status) {
|
||||
case JoinRequestStatus.Approved:
|
||||
// Delete Approved when over 1 hour old
|
||||
if (joinRequest.timestamp > now - oneHour) {
|
||||
joinRequestMap.delete(key);
|
||||
}
|
||||
break;
|
||||
case JoinRequestStatus.Pending:
|
||||
// Delete Pending when over 1 day old
|
||||
if (joinRequest.timestamp > now - oneDay) {
|
||||
joinRequestMap.delete(key);
|
||||
}
|
||||
break;
|
||||
case JoinRequestStatus.Denied:
|
||||
// Delete Rejected when over 1 week old
|
||||
if (joinRequest.timestamp > now - oneWeek) {
|
||||
joinRequestMap.delete(key);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
// Run cleaner every hour
|
||||
}, oneHour);
|
||||
|
||||
// Get Member Counts from the title
|
||||
export const getEventMemberCount = (rawMemberTitle: string): [number, number] => {
|
||||
const [rawCurrentCount, rawMaxCount] = rawMemberTitle.split('/');
|
||||
const currentMemberCount = parseInt(rawCurrentCount.split(':')[1] || '0');
|
||||
const maxMemberCount = parseInt(rawMaxCount || '0');
|
||||
return [currentMemberCount, maxMemberCount];
|
||||
};
|
||||
|
||||
// Get LFGMember objects from string list
|
||||
export const getLfgMembers = (rawMemberList: string): Array<LFGMember> =>
|
||||
rawMemberList.trim() === noMembersStr ? [] : rawMemberList.split('\n').map((rawMember) => {
|
||||
const [memberName, memberMention] = rawMember.split('-');
|
||||
const lfgMember: LFGMember = {
|
||||
id: BigInt(memberMention.split('<@')[1].split('>')[0].trim() || '0'),
|
||||
name: memberName.trim(),
|
||||
joined: rawMember.endsWith('*'),
|
||||
};
|
||||
return lfgMember;
|
||||
});
|
||||
|
||||
// Remove LFGMember from array filter
|
||||
const removeLfgMember = (memberList: Array<LFGMember>, memberId: bigint): Array<LFGMember> => memberList.filter((member) => member.id !== memberId);
|
||||
|
||||
// Handles updating the fields and editing the event
|
||||
const editEvent = async (
|
||||
bot: Bot,
|
||||
interaction: Interaction,
|
||||
evtMessageEmbed: Embed,
|
||||
evtMessageId: bigint,
|
||||
evtChannelId: bigint,
|
||||
memberList: Array<LFGMember>,
|
||||
maxMemberCount: number,
|
||||
alternateList: Array<LFGMember>,
|
||||
loudAcknowledge: boolean,
|
||||
) => {
|
||||
if (evtMessageEmbed.fields) {
|
||||
// Update the fields
|
||||
evtMessageEmbed.fields[LfgEmbedIndexes.JoinedMembers].name = generateMemberTitle(memberList, maxMemberCount);
|
||||
evtMessageEmbed.fields[LfgEmbedIndexes.JoinedMembers].value = generateMemberList(memberList);
|
||||
evtMessageEmbed.fields[LfgEmbedIndexes.AlternateMembers].value = generateAlternateList(alternateList);
|
||||
|
||||
// Edit the event
|
||||
await bot.helpers.editMessage(evtChannelId, evtMessageId, {
|
||||
embeds: [evtMessageEmbed],
|
||||
}).then(() => {
|
||||
// Let discord know we didn't ignore the user
|
||||
if (loudAcknowledge) {
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: ApplicationCommandFlags.Ephemeral,
|
||||
embeds: [{
|
||||
color: successColor,
|
||||
title: 'Event Updated',
|
||||
description: `The action requested was completed successfully.
|
||||
|
||||
${safelyDismissMsg}`,
|
||||
}],
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('utils.ts', interaction, e));
|
||||
} else {
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.DeferredUpdateMessage,
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('utils.ts', interaction, e));
|
||||
}
|
||||
}).catch((e: Error) => {
|
||||
// Edit failed, try to notify user
|
||||
utils.commonLoggers.messageEditError('utils.ts', 'event edit fail', e);
|
||||
somethingWentWrong(bot, interaction, 'editFailedInUpdateEvent');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Generic no response response
|
||||
const noEdit = async (bot: Bot, interaction: Interaction, loudAcknowledge: boolean) => {
|
||||
if (loudAcknowledge) {
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: ApplicationCommandFlags.Ephemeral,
|
||||
embeds: [{
|
||||
color: infoColor1,
|
||||
title: 'No Changes Made',
|
||||
description: `The action requested was not performed as it was not necessary.
|
||||
|
||||
${safelyDismissMsg}`,
|
||||
}],
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('utils.ts', interaction, e));
|
||||
} else {
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.DeferredUpdateMessage,
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('utils.ts', interaction, e));
|
||||
}
|
||||
};
|
||||
|
||||
// Get Guild Name
|
||||
export const getGuildName = async (bot: Bot, guildId: bigint): Promise<string> =>
|
||||
(await bot.helpers.getGuild(guildId).catch((e: Error) => utils.commonLoggers.messageGetError('utils.ts', 'get guild', e)) || { name: 'failed to get guild name' }).name;
|
||||
|
||||
// Remove member from the event
|
||||
export const removeMemberFromEvent = async (
|
||||
bot: Bot,
|
||||
interaction: Interaction,
|
||||
evtMessageEmbed: Embed,
|
||||
evtMessageId: bigint,
|
||||
evtChannelId: bigint,
|
||||
userId: bigint,
|
||||
evtGuildId: bigint,
|
||||
loudAcknowledge = false,
|
||||
): Promise<boolean> => {
|
||||
if (evtMessageEmbed.fields) {
|
||||
// Get old counts
|
||||
const [oldMemberCount, maxMemberCount] = getEventMemberCount(evtMessageEmbed.fields[LfgEmbedIndexes.JoinedMembers].name);
|
||||
// Remove user from event
|
||||
const oldMemberList = getLfgMembers(evtMessageEmbed.fields[LfgEmbedIndexes.JoinedMembers].value);
|
||||
const memberList = removeLfgMember(oldMemberList, userId);
|
||||
const oldAlternateList = getLfgMembers(evtMessageEmbed.fields[LfgEmbedIndexes.AlternateMembers].value);
|
||||
let alternateList = removeLfgMember(oldAlternateList, userId);
|
||||
|
||||
// Check if user actually left event
|
||||
if (oldMemberList.length !== memberList.length || oldAlternateList.length !== alternateList.length) {
|
||||
// Check if we need to auto-promote a member
|
||||
const memberToPromote = alternateList.find((member) => member.joined);
|
||||
if (oldMemberCount !== memberList.length && oldMemberCount === maxMemberCount && memberToPromote) {
|
||||
// Promote member
|
||||
alternateList = removeLfgMember(alternateList, memberToPromote.id);
|
||||
memberList.push(memberToPromote);
|
||||
|
||||
const urlIds: UrlIds = {
|
||||
guildId: evtGuildId,
|
||||
channelId: evtChannelId,
|
||||
messageId: evtMessageId,
|
||||
};
|
||||
|
||||
// Notify member of promotion
|
||||
await sendDirectMessage(bot, memberToPromote.id, {
|
||||
embeds: [{
|
||||
color: successColor,
|
||||
title: 'Good news, you\'ve been promoted!',
|
||||
description: `A member left [the full event](${utils.idsToMessageUrl(urlIds)}) in \`${await getGuildName(
|
||||
bot,
|
||||
evtGuildId,
|
||||
)}\` you tried to join, leaving space for me to promote you from the alternate list to the joined list.\n\nPlease verify the event details below. If you are no longer available for this event, please click on the '${leaveEventBtnStr}' button below`,
|
||||
fields: [
|
||||
evtMessageEmbed.fields[LfgEmbedIndexes.Activity],
|
||||
evtMessageEmbed.fields[LfgEmbedIndexes.StartTime],
|
||||
evtMessageEmbed.fields[LfgEmbedIndexes.ICSLink],
|
||||
],
|
||||
}],
|
||||
components: [{
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
components: [{
|
||||
type: MessageComponentTypes.Button,
|
||||
label: leaveEventBtnStr,
|
||||
style: ButtonStyles.Danger,
|
||||
customId: `${leaveViaDMCustomId}${idSeparator}${evtGuildId}${pathIdxSeparator}${evtChannelId}${pathIdxSeparator}${evtMessageId}`,
|
||||
}],
|
||||
}],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('utils.ts', 'user promotion dm', e));
|
||||
}
|
||||
|
||||
// Update the event
|
||||
await editEvent(bot, interaction, evtMessageEmbed, evtMessageId, evtChannelId, memberList, maxMemberCount, alternateList, loudAcknowledge);
|
||||
return true;
|
||||
} else {
|
||||
// Send noEdit response because user did not actually leave
|
||||
await noEdit(bot, interaction, loudAcknowledge);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
await somethingWentWrong(bot, interaction, 'noFieldsInRemoveMember');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Alternate member to the event
|
||||
export const alternateMemberToEvent = async (
|
||||
bot: Bot,
|
||||
interaction: Interaction,
|
||||
evtMessageEmbed: Embed,
|
||||
evtMessageId: bigint,
|
||||
evtChannelId: bigint,
|
||||
member: LFGMember,
|
||||
userJoinOnFull = false,
|
||||
loudAcknowledge = false,
|
||||
): Promise<boolean> => {
|
||||
if (evtMessageEmbed.fields) {
|
||||
member.joined = userJoinOnFull;
|
||||
// Get current alternates
|
||||
let alternateList = getLfgMembers(evtMessageEmbed.fields[LfgEmbedIndexes.AlternateMembers].value);
|
||||
|
||||
// Verify user is not already on the alternate list
|
||||
if (!alternateList.find((alternateMember) => alternateMember.id === member.id && alternateMember.joined === member.joined)) {
|
||||
// Add user to event, remove first to update joined status if necessary
|
||||
alternateList = removeLfgMember(alternateList, member.id);
|
||||
alternateList.push(member);
|
||||
|
||||
// Get member count and remove user from joined list (if they are there)
|
||||
const [_oldMemberCount, maxMemberCount] = getEventMemberCount(evtMessageEmbed.fields[LfgEmbedIndexes.JoinedMembers].name);
|
||||
const memberList = removeLfgMember(getLfgMembers(evtMessageEmbed.fields[LfgEmbedIndexes.JoinedMembers].value), member.id);
|
||||
|
||||
// Update the event
|
||||
evtMessageEmbed.fields[LfgEmbedIndexes.AlternateMembers].value = generateAlternateList(alternateList);
|
||||
await editEvent(bot, interaction, evtMessageEmbed, evtMessageId, evtChannelId, memberList, maxMemberCount, alternateList, loudAcknowledge);
|
||||
return true;
|
||||
} else {
|
||||
// Send noEdit response because user was already an alternate and joined status did not change
|
||||
await noEdit(bot, interaction, loudAcknowledge);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// No fields, can't alternate
|
||||
await somethingWentWrong(bot, interaction, 'noFieldsInAlternateMember');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Join member to the event
|
||||
export const joinMemberToEvent = async (
|
||||
bot: Bot,
|
||||
interaction: Interaction,
|
||||
evtMessageEmbed: Embed,
|
||||
evtMessageId: bigint,
|
||||
evtChannelId: bigint,
|
||||
member: LFGMember,
|
||||
evtGuildId: bigint,
|
||||
loudAcknowledge = false,
|
||||
): Promise<boolean> => {
|
||||
if (evtMessageEmbed.fields) {
|
||||
// Get current member list and count
|
||||
const [oldMemberCount, maxMemberCount] = getEventMemberCount(evtMessageEmbed.fields[LfgEmbedIndexes.JoinedMembers].name);
|
||||
const memberList = getLfgMembers(evtMessageEmbed.fields[LfgEmbedIndexes.JoinedMembers].value);
|
||||
// Verify user is not already on the joined list
|
||||
if (memberList.find((joinedMember) => joinedMember.id === member.id)) {
|
||||
// Send noEdit response because user was already joined
|
||||
await noEdit(bot, interaction, loudAcknowledge);
|
||||
return false;
|
||||
} else if (oldMemberCount === maxMemberCount) {
|
||||
// Event full, add member to alternate list
|
||||
return await alternateMemberToEvent(bot, interaction, evtMessageEmbed, evtMessageId, evtChannelId, member, true, loudAcknowledge);
|
||||
} else {
|
||||
// Join member to event
|
||||
memberList.push(member);
|
||||
|
||||
// Remove user from alternate list (if they are there)
|
||||
const alternateList = removeLfgMember(getLfgMembers(evtMessageEmbed.fields[LfgEmbedIndexes.AlternateMembers].value), member.id);
|
||||
|
||||
// Update the event
|
||||
await editEvent(bot, interaction, evtMessageEmbed, evtMessageId, evtChannelId, memberList, maxMemberCount, alternateList, loudAcknowledge);
|
||||
|
||||
// Check if we need to notify the owner that their event has filled
|
||||
if (memberList.length === maxMemberCount) {
|
||||
dbClient.execute(queries.callIncCnt('lfg-filled')).catch((e) => utils.commonLoggers.dbError('utils.ts@lfg-filled', 'call sproc INC_CNT on', e));
|
||||
const urlIds: UrlIds = {
|
||||
guildId: evtGuildId,
|
||||
channelId: evtChannelId,
|
||||
messageId: evtMessageId,
|
||||
};
|
||||
const guildName = await getGuildName(bot, evtGuildId);
|
||||
// Notify member of promotion
|
||||
await sendDirectMessage(bot, BigInt(evtMessageEmbed.footer?.iconUrl?.split('#')[1] || '0'), {
|
||||
embeds: [{
|
||||
color: successColor,
|
||||
title: `Good news, your event in ${guildName} has filled!`,
|
||||
description: `[Click here](${utils.idsToMessageUrl(urlIds)}) to view the event in ${guildName}.`,
|
||||
fields: evtMessageEmbed.fields,
|
||||
}],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('utils.ts', 'event filled dm', e));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// No fields, can't join
|
||||
await somethingWentWrong(bot, interaction, 'noFieldsInJoinMember');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Join Request Approve/Deny Buttons
|
||||
export const joinRequestResponseButtons = (disabled: boolean): ActionRow[] => [{
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
components: [{
|
||||
type: MessageComponentTypes.Button,
|
||||
label: 'Approve Request',
|
||||
style: ButtonStyles.Success,
|
||||
customId: `${joinRequestCustomId}${idSeparator}${approveStr}`,
|
||||
disabled,
|
||||
}, {
|
||||
type: MessageComponentTypes.Button,
|
||||
label: 'Deny Request',
|
||||
style: ButtonStyles.Danger,
|
||||
customId: `${joinRequestCustomId}${idSeparator}${denyStr}`,
|
||||
disabled,
|
||||
}],
|
||||
}];
|
||||
|
||||
export const applyEditButtonName = 'Apply Edit';
|
||||
export const applyEditMessage = (currentTime: number) =>
|
||||
`Please verify the updated event below, then click on the \`${applyEditButtonName}\` button. If this does not look right, please dismiss this message and start over.\n\n${
|
||||
selfDestructMessage(currentTime)
|
||||
}`;
|
||||
export const applyEditButtons = (idxPath: string): ActionRow[] => [{
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
components: [{
|
||||
type: MessageComponentTypes.Button,
|
||||
label: applyEditButtonName,
|
||||
style: ButtonStyles.Success,
|
||||
customId: `${updateEventCustomId}${idSeparator}${idxPath}`,
|
||||
}],
|
||||
}];
|
|
@ -0,0 +1,40 @@
|
|||
import { Bot, Interaction } from '../../deps.ts';
|
||||
import utils from '../utils.ts';
|
||||
|
||||
// Discord Interaction Tokens last 15 minutes, we will self kill after 14.5 minutes
|
||||
const tokenTimeoutS = (15 * 60) - 30;
|
||||
const tokenTimeoutMS = tokenTimeoutS * 1000;
|
||||
export const selfDestructMessage = (currentTime: number) =>
|
||||
`**Please note:** This message will self destruct <t:${Math.floor((currentTime + tokenTimeoutMS) / 1000)}:R> due to limits imposed by the Discord API.`;
|
||||
|
||||
export const tokenMap: Map<string, {
|
||||
token: string;
|
||||
timeoutId: number;
|
||||
}> = new Map();
|
||||
|
||||
export const generateMapId = (guildId: bigint, channelId: bigint, userId: bigint) => `${guildId}-${channelId}-${userId}`;
|
||||
|
||||
export const addTokenToMap = (bot: Bot, interaction: Interaction, guildId: bigint, channelId: bigint, userId: bigint) =>
|
||||
tokenMap.set(generateMapId(guildId, channelId, userId), {
|
||||
token: interaction.token,
|
||||
timeoutId: setTimeout(
|
||||
(guildId, channelId, userId) => {
|
||||
deleteTokenEarly(bot, interaction, guildId, channelId, userId);
|
||||
},
|
||||
tokenTimeoutMS,
|
||||
guildId,
|
||||
channelId,
|
||||
userId,
|
||||
),
|
||||
});
|
||||
|
||||
export const deleteTokenEarly = async (bot: Bot, interaction: Interaction, guildId: bigint, channelId: bigint, userId: bigint) => {
|
||||
const tokenMapEntry = tokenMap.get(generateMapId(guildId, channelId, userId));
|
||||
if (tokenMapEntry && tokenMapEntry.token) {
|
||||
await bot.helpers.deleteOriginalInteractionResponse(tokenMap.get(generateMapId(guildId, channelId, userId))?.token || '').catch((e: Error) =>
|
||||
utils.commonLoggers.interactionSendError('tokenCleanup.ts:deleteTokenEarly', interaction, e)
|
||||
);
|
||||
clearTimeout(tokenMapEntry.timeoutId);
|
||||
tokenMap.delete(generateMapId(guildId, channelId, userId));
|
||||
}
|
||||
};
|
|
@ -0,0 +1,100 @@
|
|||
import { ApplicationCommandFlags, Bot, CreateMessage, Embed, Interaction, InteractionResponseTypes } from '../deps.ts';
|
||||
import config from '../config.ts';
|
||||
import { generateGuildSettingKey, lfgChannelSettings } from './db.ts';
|
||||
import utils from './utils.ts';
|
||||
import { helpSlashName, infoSlashName, reportSlashName } from './commands/slashCommandNames.ts';
|
||||
|
||||
export const failColor = 0xe71212;
|
||||
export const warnColor = 0xe38f28;
|
||||
export const successColor = 0x0f8108;
|
||||
export const infoColor1 = 0x313bf9;
|
||||
export const infoColor2 = 0x6805e9;
|
||||
|
||||
export const safelyDismissMsg = 'You may safely dismiss this message.';
|
||||
|
||||
export const getRandomStatus = (guildCount: number): string => {
|
||||
const statuses = [
|
||||
`Running V${config.version}`,
|
||||
`${config.prefix}${infoSlashName} to learn more`,
|
||||
`${config.prefix}${helpSlashName} to learn more`,
|
||||
`Running LFGs in ${guildCount} servers`,
|
||||
];
|
||||
return statuses[Math.floor(Math.random() * statuses.length)];
|
||||
};
|
||||
|
||||
export const isLFGChannel = (guildId: bigint, channelId: bigint) => {
|
||||
return (lfgChannelSettings.has(generateGuildSettingKey(guildId, channelId)) || channelId === 0n || guildId === 0n) ? ApplicationCommandFlags.Ephemeral : undefined;
|
||||
};
|
||||
|
||||
// Tell user to try again or report issue
|
||||
export const somethingWentWrong = async (bot: Bot, interaction: Interaction, errorCode: string) =>
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: ApplicationCommandFlags.Ephemeral,
|
||||
embeds: [{
|
||||
color: failColor,
|
||||
title: 'Something went wrong...',
|
||||
description: `You should not be able to get here. Please try again and if the issue continues, \`/${reportSlashName}\` this issue to the developers with the error code below.`,
|
||||
fields: [{
|
||||
name: 'Error Code:',
|
||||
value: `\`${errorCode}\``,
|
||||
}],
|
||||
}],
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('commandUtils.ts@somethingWentWrong', interaction, e));
|
||||
|
||||
// Smack the user for trying to modify an event that isn't theirs
|
||||
export const stopThat = async (bot: Bot, interaction: Interaction, stopWhat: string) =>
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: ApplicationCommandFlags.Ephemeral,
|
||||
embeds: [{
|
||||
color: warnColor,
|
||||
title: 'Hey! Stop that!',
|
||||
description: `You are neither the owner of this event nor a ${config.name} manager in this guild, meaning you are not allowed to ${stopWhat} this event.
|
||||
|
||||
${safelyDismissMsg}`,
|
||||
}],
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('commandUtils.ts@stopThat', interaction, e));
|
||||
|
||||
// Send DM to User
|
||||
export const sendDirectMessage = async (bot: Bot, userId: bigint, message: CreateMessage) => {
|
||||
const userDmChannel = await bot.helpers.getDmChannel(userId).catch((e: Error) => utils.commonLoggers.messageGetError('commandUtils.ts', 'get userDmChannel', e));
|
||||
// Actually send the DM
|
||||
return bot.helpers.sendMessage(userDmChannel?.id || 0n, message);
|
||||
};
|
||||
|
||||
// Info Embed Object (for info command and @mention command)
|
||||
export const infoEmbed: Embed = {
|
||||
color: infoColor2,
|
||||
title: `${config.name}, the LFG bot`,
|
||||
description: `${config.name} is developed by Ean AKA Burn_E99.
|
||||
Want to check out my source code? Check it out [here](${config.links.sourceCode}).
|
||||
Need help with this bot? Join my support server [here](${config.links.supportServer}).
|
||||
|
||||
Ran into a bug? Report it to my developers using \`/${reportSlashName} [issue description]\`.`,
|
||||
fields: [{
|
||||
name: 'Privacy Policy and Terms of Service:',
|
||||
value: `**${config.name} does not automatically track or collect user information via Discord.**
|
||||
|
||||
${config.name} stores data relating to events, event channels, and text from the \`/${reportSlashName}\` command.
|
||||
|
||||
For more details, please check out the Privacy Policy on my GitHub [here](${config.links.sourceCode}/blob/master/PRIVACY.md)
|
||||
|
||||
Terms of Service can also be found on my GitHub [here](${config.links.sourceCode}/blob/master/TERMS.md).`,
|
||||
}],
|
||||
footer: {
|
||||
text: `Current Version: ${config.version}`,
|
||||
},
|
||||
};
|
||||
|
||||
export const dmTestMessage: CreateMessage = {
|
||||
embeds: [{
|
||||
color: infoColor2,
|
||||
title: 'Heyo! Just making sure I can reach you.',
|
||||
description: 'This message is just to make sure I can DM you any Join Requests to your event. If you are reading this message, it means you have everything set up correctly.',
|
||||
}],
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
import { Bot, CreateApplicationCommand, log, LT, MakeRequired } from '../../deps.ts';
|
||||
import { Command } from '../types/commandTypes.ts';
|
||||
import utils from '../utils.ts';
|
||||
|
||||
import audit from './audit.ts';
|
||||
import info from './info.ts';
|
||||
import help from './help.ts';
|
||||
import report from './report.ts';
|
||||
import setup from './setup.ts';
|
||||
import deleteCmd from './delete.ts';
|
||||
import managerJLA from './managerJLA.ts';
|
||||
import { gameSelectionCommand } from '../buttons/event-creation/step1-gameSelection.ts';
|
||||
|
||||
export const commands: Array<Command> = [deleteCmd, info, report, setup, gameSelectionCommand, managerJLA, help, audit];
|
||||
|
||||
export const createSlashCommands = async (bot: Bot) => {
|
||||
const globalCommands: MakeRequired<CreateApplicationCommand, 'name'>[] = [];
|
||||
for (const command of commands) {
|
||||
globalCommands.push({
|
||||
name: command.details.name,
|
||||
description: command.details.description,
|
||||
type: command.details.type,
|
||||
options: command.details.options ? command.details.options : undefined,
|
||||
dmPermission: command.details.dmPermission ? command.details.dmPermission : false,
|
||||
defaultMemberPermissions: command.details.defaultMemberPermissions ? command.details.defaultMemberPermissions : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
await bot.helpers.upsertGlobalApplicationCommands(globalCommands).catch((errMsg) => log(LT.ERROR, `Failed to upsert application commands | ${utils.jsonStringifyBig(errMsg)}`));
|
||||
};
|
|
@ -0,0 +1,88 @@
|
|||
import config from '../../config.ts';
|
||||
import { ApplicationCommandTypes, ApplicationCommandOptionTypes, Bot, Interaction, InteractionResponseTypes, DiscordEmbedField } from '../../deps.ts';
|
||||
import { infoColor2, infoEmbed, isLFGChannel, somethingWentWrong } from '../commandUtils.ts';
|
||||
import { dbClient, queries } from '../db.ts';
|
||||
import { CommandDetails } from '../types/commandTypes.ts';
|
||||
import utils from '../utils.ts';
|
||||
import { auditSlashName } from './slashCommandNames.ts';
|
||||
|
||||
const auditDbName = 'database';
|
||||
const auditCustomActivities = 'custom-activities';
|
||||
const auditGuildName = 'guilds';
|
||||
|
||||
const details: CommandDetails = {
|
||||
name: auditSlashName,
|
||||
description: `Developer Command for checking in on ${config.name}'s health.`,
|
||||
type: ApplicationCommandTypes.ChatInput,
|
||||
defaultMemberPermissions: ['ADMINISTRATOR'],
|
||||
options: [
|
||||
{
|
||||
name: auditDbName,
|
||||
type: ApplicationCommandOptionTypes.SubCommand,
|
||||
description: `Developer Command: Checks ${config.name}'s DB size.`,
|
||||
},
|
||||
{
|
||||
name: auditCustomActivities,
|
||||
type: ApplicationCommandOptionTypes.SubCommand,
|
||||
description: 'Developer Command: Checks for duplicate custom activities.',
|
||||
},
|
||||
{
|
||||
name: auditGuildName,
|
||||
type: ApplicationCommandOptionTypes.SubCommand,
|
||||
description: `Developer Command: Checks in on channel and member counts of guilds that ${config.name} is in.`,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const execute = async (bot: Bot, interaction: Interaction) => {
|
||||
if (interaction.member && interaction.guildId && interaction.data?.options?.[0].options) {
|
||||
dbClient.execute(queries.callIncCnt('cmd-audit')).catch((e) => utils.commonLoggers.dbError('audit.ts@inc', 'call sproc INC_CNT on', e));
|
||||
const auditName = interaction.data.options[0].name;
|
||||
switch (auditName) {
|
||||
case auditDbName:
|
||||
// Get DB statistics
|
||||
const auditQuery = await dbClient.query(`SELECT * FROM db_size;`).catch((e) => utils.commonLoggers.dbError('audit.ts@dbSize', 'query', e));
|
||||
|
||||
// Turn all tables into embed fields, currently only properly will handle 25 tables, but we'll fix that when group up gets 26 tables
|
||||
const embedFields: Array<DiscordEmbedField> = [];
|
||||
auditQuery.forEach((row: any) => {
|
||||
embedFields.push({
|
||||
name: `${row.table}`,
|
||||
value: `**Size:** ${row.size} MB
|
||||
**Rows:** ${row.rows}`,
|
||||
inline: true,
|
||||
});
|
||||
});
|
||||
bot.helpers.sendInteractionResponse(
|
||||
interaction.id,
|
||||
interaction.token,
|
||||
{
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: isLFGChannel(interaction.guildId || 0n, interaction.channelId || 0n),
|
||||
embeds: [{
|
||||
color: infoColor2,
|
||||
title: 'Database Audit',
|
||||
description: 'Lists all tables with their current size and row count.',
|
||||
timestamp: new Date().getTime(),
|
||||
fields: embedFields.slice(0, 25),
|
||||
}],
|
||||
},
|
||||
},
|
||||
).catch((e: Error) => utils.commonLoggers.interactionSendError('audit.ts@dbSize', interaction, e));
|
||||
break;
|
||||
case auditCustomActivities:
|
||||
case auditGuildName:
|
||||
default:
|
||||
somethingWentWrong(bot, interaction, `auditNameNotHandled@${auditName}`);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
somethingWentWrong(bot, interaction, 'auditMissingData')
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
details,
|
||||
execute,
|
||||
};
|
|
@ -0,0 +1,70 @@
|
|||
import config from '../../config.ts';
|
||||
import { ApplicationCommandFlags, ApplicationCommandTypes, Bot, Interaction, InteractionResponseTypes } from '../../deps.ts';
|
||||
import { failColor, safelyDismissMsg, somethingWentWrong, successColor } from '../commandUtils.ts';
|
||||
import { dbClient, generateGuildSettingKey, lfgChannelSettings, queries } from '../db.ts';
|
||||
import { CommandDetails } from '../types/commandTypes.ts';
|
||||
import utils from '../utils.ts';
|
||||
import { deleteSlashName, setupSlashName } from './slashCommandNames.ts';
|
||||
|
||||
const details: CommandDetails = {
|
||||
name: deleteSlashName,
|
||||
description: `Removes all settings from ${config.name} related to this LFG channel. Events will not be deleted.`,
|
||||
type: ApplicationCommandTypes.ChatInput,
|
||||
defaultMemberPermissions: ['ADMINISTRATOR'],
|
||||
};
|
||||
|
||||
const execute = async (bot: Bot, interaction: Interaction) => {
|
||||
dbClient.execute(queries.callIncCnt('cmd-delete')).catch((e) => utils.commonLoggers.dbError('delete.ts', 'call sproc INC_CNT on', e));
|
||||
|
||||
if (interaction.guildId && interaction.channelId) {
|
||||
const lfgChannelSettingKey = generateGuildSettingKey(interaction.guildId, interaction.channelId);
|
||||
if (!lfgChannelSettings.has(lfgChannelSettingKey)) {
|
||||
// Cannot delete a lfg channel that has not been set up
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: ApplicationCommandFlags.Ephemeral,
|
||||
embeds: [{
|
||||
color: failColor,
|
||||
title: 'Unable to delete LFG channel.',
|
||||
description:
|
||||
`This channel is already is not an LFG channel. If you need to setup the channel, please run \`/${setupSlashName}\` in this channel.\n\nThis will not harm any active events in this channel and simply resets the settings for this channel.`,
|
||||
}],
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('delete.ts', interaction, e));
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove it from the DB
|
||||
let dbError = false;
|
||||
await dbClient.execute('DELETE FROM guild_settings WHERE guildId = ? AND lfgChannelId = ?', [interaction.guildId, interaction.channelId]).catch((e) => {
|
||||
utils.commonLoggers.dbError('delete.ts', 'delete guild/lfgChannel', e);
|
||||
dbError = true;
|
||||
});
|
||||
if (dbError) {
|
||||
somethingWentWrong(bot, interaction, 'deleteDBDeleteFail');
|
||||
return;
|
||||
}
|
||||
lfgChannelSettings.delete(lfgChannelSettingKey);
|
||||
|
||||
// Complete the interaction
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: ApplicationCommandFlags.Ephemeral,
|
||||
embeds: [{
|
||||
color: successColor,
|
||||
title: 'LFG Channel settings removed!',
|
||||
description: `${config.name} has finished removing the settings for this channel. ${safelyDismissMsg}`,
|
||||
}],
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('delete.ts', interaction, e));
|
||||
} else {
|
||||
somethingWentWrong(bot, interaction, 'deleteMissingGuildIdChannelId');
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
details,
|
||||
execute,
|
||||
};
|
|
@ -0,0 +1,50 @@
|
|||
import config from '../../config.ts';
|
||||
import { ApplicationCommandTypes, Bot, Interaction, InteractionResponseTypes } from '../../deps.ts';
|
||||
import { infoColor1, isLFGChannel } from '../commandUtils.ts';
|
||||
import { dbClient, queries } from '../db.ts';
|
||||
import { CommandDetails } from '../types/commandTypes.ts';
|
||||
import utils from '../utils.ts';
|
||||
import { createEventSlashName, helpSlashName, setupSlashName } from './slashCommandNames.ts';
|
||||
|
||||
const details: CommandDetails = {
|
||||
name: helpSlashName,
|
||||
description: `How to set up and use ${config.name} in your guild.`,
|
||||
type: ApplicationCommandTypes.ChatInput,
|
||||
};
|
||||
|
||||
const execute = (bot: Bot, interaction: Interaction) => {
|
||||
dbClient.execute(queries.callIncCnt('cmd-help')).catch((e) => utils.commonLoggers.dbError('help.ts', 'call sproc INC_CNT on', e));
|
||||
bot.helpers.sendInteractionResponse(
|
||||
interaction.id,
|
||||
interaction.token,
|
||||
{
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: isLFGChannel(interaction.guildId || 0n, interaction.channelId || 0n),
|
||||
embeds: [{
|
||||
color: infoColor1,
|
||||
title: `Getting Started with ${config.name}:`,
|
||||
description: `Thanks for inviting ${config.name}, the event scheduling bot. There are two ways you can use the bot:`,
|
||||
fields: [{
|
||||
name: 'Dedicated Event/LFG Channel:',
|
||||
value:
|
||||
`To create a dedicated event/LFG channel, simply have the guild owner or member with the \`ADMINISTRATOR\` permission run the \`/${setupSlashName}\` in the desired channel. This command will walk you through everything necessary to set up the channel.`,
|
||||
inline: true,
|
||||
}, {
|
||||
name: 'Chat channel with events mixed into:',
|
||||
value: `To create events in any chat channel ${config.name} can see, simply run the \`/${createEventSlashName}\` command.`,
|
||||
inline: true,
|
||||
}, {
|
||||
name: 'Need help or have questions?',
|
||||
value: `Just join the official support server by [clicking here](${config.links.supportServer}) and ask away!`,
|
||||
}],
|
||||
}],
|
||||
},
|
||||
},
|
||||
).catch((e: Error) => utils.commonLoggers.interactionSendError('help.ts', interaction, e));
|
||||
};
|
||||
|
||||
export default {
|
||||
details,
|
||||
execute,
|
||||
};
|
|
@ -0,0 +1,33 @@
|
|||
import config from '../../config.ts';
|
||||
import { ApplicationCommandTypes, Bot, Interaction, InteractionResponseTypes } from '../../deps.ts';
|
||||
import { infoEmbed, isLFGChannel } from '../commandUtils.ts';
|
||||
import { dbClient, queries } from '../db.ts';
|
||||
import { CommandDetails } from '../types/commandTypes.ts';
|
||||
import utils from '../utils.ts';
|
||||
import { infoSlashName } from './slashCommandNames.ts';
|
||||
|
||||
const details: CommandDetails = {
|
||||
name: infoSlashName,
|
||||
description: `Information about ${config.name}, its Terms of Service, its Privacy Policy, and its developer.`,
|
||||
type: ApplicationCommandTypes.ChatInput,
|
||||
};
|
||||
|
||||
const execute = (bot: Bot, interaction: Interaction) => {
|
||||
dbClient.execute(queries.callIncCnt('cmd-info')).catch((e) => utils.commonLoggers.dbError('info.ts', 'call sproc INC_CNT on', e));
|
||||
bot.helpers.sendInteractionResponse(
|
||||
interaction.id,
|
||||
interaction.token,
|
||||
{
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: isLFGChannel(interaction.guildId || 0n, interaction.channelId || 0n),
|
||||
embeds: [infoEmbed],
|
||||
},
|
||||
},
|
||||
).catch((e: Error) => utils.commonLoggers.interactionSendError('info.ts', interaction, e));
|
||||
};
|
||||
|
||||
export default {
|
||||
details,
|
||||
execute,
|
||||
};
|
|
@ -0,0 +1,183 @@
|
|||
import { ApplicationCommandFlags, ApplicationCommandOptionTypes, ApplicationCommandTypes, Bot, Interaction, InteractionResponseTypes } from '../../deps.ts';
|
||||
import { alternateMemberToEvent, getGuildName, joinMemberToEvent, removeMemberFromEvent } from '../buttons/live-event/utils.ts';
|
||||
import { generateMemberList } from '../buttons/eventUtils.ts';
|
||||
import { dbClient, generateGuildSettingKey, lfgChannelSettings, queries } from '../db.ts';
|
||||
import { infoColor2, safelyDismissMsg, sendDirectMessage, somethingWentWrong, stopThat, warnColor } from '../commandUtils.ts';
|
||||
import { CommandDetails, LFGMember } from '../types/commandTypes.ts';
|
||||
import config from '../../config.ts';
|
||||
import utils from '../utils.ts';
|
||||
import { managerJLASlashName } from './slashCommandNames.ts';
|
||||
|
||||
export const joinName = 'join';
|
||||
export const leaveName = 'leave';
|
||||
export const alternateName = 'alternate';
|
||||
export const eventLinkName = 'event-link';
|
||||
export const userName = 'user';
|
||||
|
||||
// Create command with three nearly identical subcommands
|
||||
const generateOptions = (commandName: string) => ({
|
||||
name: commandName,
|
||||
description: `${config.name} Manager Command: ${utils.capitalizeFirstChar(commandName)}s a user to an event in this channel.`,
|
||||
type: ApplicationCommandOptionTypes.SubCommand,
|
||||
options: [
|
||||
{
|
||||
name: eventLinkName,
|
||||
type: ApplicationCommandOptionTypes.String,
|
||||
description: 'Please copy the message link for the desired event.',
|
||||
required: true,
|
||||
minLength: 31,
|
||||
maxLength: 100,
|
||||
},
|
||||
{
|
||||
name: userName,
|
||||
type: ApplicationCommandOptionTypes.User,
|
||||
description: `The user you wish to ${commandName}.`,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
const details: CommandDetails = {
|
||||
name: managerJLASlashName,
|
||||
description: `${config.name} Manager Command`,
|
||||
type: ApplicationCommandTypes.ChatInput,
|
||||
options: [generateOptions(joinName), generateOptions(leaveName), generateOptions(alternateName)],
|
||||
};
|
||||
|
||||
const execute = async (bot: Bot, interaction: Interaction) => {
|
||||
if (interaction.data?.options?.[0].options && interaction.channelId && interaction.guildId && interaction.member && interaction.member.user) {
|
||||
// Get action and log to db
|
||||
const actionName = interaction.data.options[0].name;
|
||||
dbClient.execute(queries.callIncCnt(`cmd-${actionName}`)).catch((e) => utils.commonLoggers.dbError('managerJLA.ts', 'call sproc INC_CNT on', e));
|
||||
const lfgChannelSetting = lfgChannelSettings.get(generateGuildSettingKey(interaction.guildId, interaction.channelId)) || {
|
||||
managed: false,
|
||||
managerRoleId: 0n,
|
||||
logChannelId: 0n,
|
||||
};
|
||||
|
||||
// Check if guild is managed and if user is a manager
|
||||
if (lfgChannelSetting.managed && interaction.member.roles.includes(lfgChannelSetting.managerRoleId)) {
|
||||
// User is a manager, parse out our data
|
||||
const tempDataMap: Map<string, string> = new Map();
|
||||
for (const option of interaction.data.options[0].options) {
|
||||
tempDataMap.set(option.name || 'missingCustomId', option.value as string || '');
|
||||
}
|
||||
const eventLink = tempDataMap.get(eventLinkName) || '';
|
||||
const userToAdd = BigInt(tempDataMap.get(userName) || '0');
|
||||
const eventIds = utils.messageUrlToIds(eventLink);
|
||||
|
||||
// Verify fields exist
|
||||
if (!eventLink || !userToAdd || !eventIds.guildId || !eventIds.channelId || !eventIds.messageId) {
|
||||
somethingWentWrong(bot, interaction, 'missingLinkOrUserInManagerJLA');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get event from link
|
||||
const eventMessage = await bot.helpers.getMessage(eventIds.channelId, eventIds.messageId).catch((e: Error) => utils.commonLoggers.messageGetError('managerJLA.ts', 'get eventMessage', e));
|
||||
|
||||
// Prevent managers from adding people to locked events
|
||||
if (eventMessage && !eventMessage.components?.length) {
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: ApplicationCommandFlags.Ephemeral,
|
||||
embeds: [{
|
||||
color: warnColor,
|
||||
title: 'Hey! Stop that!',
|
||||
description: `You are not allowed to ${actionName} users to an event that has already started.
|
||||
|
||||
${safelyDismissMsg}`,
|
||||
}],
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('commandUtils.ts@stopThat', interaction, e));
|
||||
return;
|
||||
}
|
||||
|
||||
const userDetails = await bot.helpers.getUser(userToAdd).catch((e: Error) => utils.commonLoggers.messageGetError('managerJLA.ts', 'get userDetails', e));
|
||||
if (eventMessage && userDetails) {
|
||||
// Perform the action
|
||||
const userInfo: LFGMember = {
|
||||
id: userToAdd,
|
||||
name: userDetails.username,
|
||||
};
|
||||
let changeMade = false;
|
||||
switch (actionName) {
|
||||
case joinName:
|
||||
changeMade = await joinMemberToEvent(bot, interaction, eventMessage.embeds[0], eventIds.messageId, eventIds.channelId, userInfo, eventIds.guildId, true);
|
||||
break;
|
||||
case leaveName:
|
||||
changeMade = await removeMemberFromEvent(bot, interaction, eventMessage.embeds[0], eventIds.messageId, eventIds.channelId, userToAdd, eventIds.guildId, true);
|
||||
break;
|
||||
case alternateName:
|
||||
changeMade = await alternateMemberToEvent(bot, interaction, eventMessage.embeds[0], eventIds.messageId, eventIds.channelId, userInfo, false, true);
|
||||
break;
|
||||
default:
|
||||
somethingWentWrong(bot, interaction, 'actionNameWrongManagerJLA');
|
||||
break;
|
||||
}
|
||||
|
||||
if (changeMade) {
|
||||
// userToAdd was had JLA done to them, DM them with details\
|
||||
const guildName = await getGuildName(bot, interaction.guildId);
|
||||
const commonFields = [{
|
||||
name: 'Event Link:',
|
||||
value: `[Click Here](${eventLink}) to view the event.`,
|
||||
inline: true,
|
||||
}, {
|
||||
name: 'Action Performed:',
|
||||
value: utils.capitalizeFirstChar(actionName),
|
||||
inline: true,
|
||||
}];
|
||||
sendDirectMessage(bot, userToAdd, {
|
||||
embeds: [{
|
||||
color: infoColor2,
|
||||
title: `Notice: A ${config.name} Manager has performed an action for you in ${guildName}`,
|
||||
fields: [
|
||||
{
|
||||
name: `${config.name} Manager:`,
|
||||
value: generateMemberList([{
|
||||
id: interaction.member.id,
|
||||
name: interaction.member.user.username,
|
||||
}]),
|
||||
inline: true,
|
||||
},
|
||||
...commonFields,
|
||||
{
|
||||
name: 'Are you unhappy with this action?',
|
||||
value: `Please reach out to the ${config.name} Manager that performed this action, or the moderators/administrators of ${guildName}.`,
|
||||
},
|
||||
],
|
||||
}],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('managerJLA.ts', 'send DM fail', e));
|
||||
|
||||
// Log this action
|
||||
bot.helpers.sendMessage(lfgChannelSetting.logChannelId, {
|
||||
embeds: [{
|
||||
color: infoColor2,
|
||||
title: `A ${config.name} Manager has performed an action on behalf of a user.`,
|
||||
description: `The following user had an action by ${interaction.member.user.username} - <@${interaction.member.id}>.`,
|
||||
fields: [...commonFields, {
|
||||
name: 'User:',
|
||||
value: generateMemberList([userInfo]),
|
||||
inline: true,
|
||||
}],
|
||||
timestamp: new Date().getTime(),
|
||||
}],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('deleteConfirmed.ts', 'send log message', e));
|
||||
}
|
||||
} else {
|
||||
somethingWentWrong(bot, interaction, 'eventOrUserMissingFromManagerJLA');
|
||||
}
|
||||
} else {
|
||||
// User not a manager
|
||||
stopThat(bot, interaction, `${actionName} users to`);
|
||||
}
|
||||
} else {
|
||||
// All data missing
|
||||
somethingWentWrong(bot, interaction, 'missingDataInManagerJLA');
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
details,
|
||||
execute,
|
||||
};
|
|
@ -0,0 +1,58 @@
|
|||
import config from '../../config.ts';
|
||||
import { ApplicationCommandOptionTypes, ApplicationCommandTypes, Bot, Interaction, InteractionResponseTypes } from '../../deps.ts';
|
||||
import { infoColor2, isLFGChannel, somethingWentWrong, successColor } from '../commandUtils.ts';
|
||||
import { dbClient, queries } from '../db.ts';
|
||||
import { CommandDetails } from '../types/commandTypes.ts';
|
||||
import utils from '../utils.ts';
|
||||
import { reportSlashName } from './slashCommandNames.ts';
|
||||
|
||||
const details: CommandDetails = {
|
||||
name: reportSlashName,
|
||||
description: `Report an issue with ${config.name} to its developer.`,
|
||||
type: ApplicationCommandTypes.ChatInput,
|
||||
options: [
|
||||
{
|
||||
name: 'issue',
|
||||
type: ApplicationCommandOptionTypes.String,
|
||||
description: 'Please describe the issue you were having.',
|
||||
required: true,
|
||||
minLength: 1,
|
||||
maxLength: 2000,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const execute = (bot: Bot, interaction: Interaction) => {
|
||||
dbClient.execute(queries.callIncCnt('cmd-report')).catch((e) => utils.commonLoggers.dbError('report.ts', 'call sproc INC_CNT on', e));
|
||||
if (interaction.data?.options?.[0].value) {
|
||||
bot.helpers.sendMessage(config.reportChannel, {
|
||||
embeds: [{
|
||||
color: infoColor2,
|
||||
title: 'USER REPORT:',
|
||||
description: interaction.data.options[0].value as string,
|
||||
}],
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('report.ts:28', interaction, e));
|
||||
bot.helpers.sendInteractionResponse(
|
||||
interaction.id,
|
||||
interaction.token,
|
||||
{
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: isLFGChannel(interaction.guildId || 0n, interaction.channelId || 0n),
|
||||
embeds: [{
|
||||
color: successColor,
|
||||
title: 'Failed command has been reported to my developer.',
|
||||
description: `For more in depth support, and information about planned maintenance, please join the support server [here](${config.links.supportServer}).`,
|
||||
}],
|
||||
},
|
||||
},
|
||||
).catch((e: Error) => utils.commonLoggers.interactionSendError('report.ts:44', interaction, e));
|
||||
} else {
|
||||
somethingWentWrong(bot, interaction, 'reportMissingAllOptions');
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
details,
|
||||
execute,
|
||||
};
|
|
@ -0,0 +1,401 @@
|
|||
import config from '../../config.ts';
|
||||
import {
|
||||
ApplicationCommandFlags,
|
||||
ApplicationCommandOptionTypes,
|
||||
ApplicationCommandTypes,
|
||||
Bot,
|
||||
botId,
|
||||
ButtonStyles,
|
||||
ChannelTypes,
|
||||
DiscordEmbedField,
|
||||
Interaction,
|
||||
InteractionResponseTypes,
|
||||
MessageComponentTypes,
|
||||
OverwriteTypes,
|
||||
} from '../../deps.ts';
|
||||
import { failColor, infoColor2, safelyDismissMsg, somethingWentWrong, successColor } from '../commandUtils.ts';
|
||||
import { dbClient, generateGuildSettingKey, lfgChannelSettings, queries } from '../db.ts';
|
||||
import { CommandDetails } from '../types/commandTypes.ts';
|
||||
import utils from '../utils.ts';
|
||||
import { customId as gameSelId } from '../buttons/event-creation/step1-gameSelection.ts';
|
||||
import { alternateEventBtnStr, joinEventBtnStr, leaveEventBtnStr, LfgEmbedIndexes, requestToJoinEventBtnStr } from '../buttons/eventUtils.ts';
|
||||
import { alternateName, eventLinkName, joinName, leaveName, userName } from './managerJLA.ts';
|
||||
import { createEventSlashName, deleteSlashName, managerJLASlashName, reportSlashName, setupSlashName } from './slashCommandNames.ts';
|
||||
import { generateLFGButtons, generateTimeFieldStr } from '../buttons/event-creation/utils.ts';
|
||||
import { getLfgMembers } from '../buttons/live-event/utils.ts';
|
||||
|
||||
const withoutMgrRole = 'without-manager-role';
|
||||
const withMgrRole = 'with-manager-role';
|
||||
const managerRoleStr = 'manager-role';
|
||||
const logChannelStr = 'log-channel';
|
||||
|
||||
const details: CommandDetails = {
|
||||
name: setupSlashName,
|
||||
description: `Configures this channel to be a dedicated event channel to be managed by ${config.name}.`,
|
||||
type: ApplicationCommandTypes.ChatInput,
|
||||
defaultMemberPermissions: ['ADMINISTRATOR'],
|
||||
options: [
|
||||
{
|
||||
name: withoutMgrRole,
|
||||
type: ApplicationCommandOptionTypes.SubCommand,
|
||||
description: `This will configure ${config.name} without a manager role.`,
|
||||
},
|
||||
{
|
||||
name: withMgrRole,
|
||||
type: ApplicationCommandOptionTypes.SubCommand,
|
||||
description: `This will configure ${config.name} with a manager role.`,
|
||||
options: [
|
||||
{
|
||||
name: managerRoleStr,
|
||||
type: ApplicationCommandOptionTypes.Role,
|
||||
description: 'This role will be allowed to manage all events in this guild.',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: logChannelStr,
|
||||
type: ApplicationCommandOptionTypes.Channel,
|
||||
description: `This channel is where ${config.name} will send Audit Messages whenever a manager updates an event.`,
|
||||
required: true,
|
||||
channelTypes: [ChannelTypes.GuildText],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const execute = async (bot: Bot, interaction: Interaction) => {
|
||||
dbClient.execute(queries.callIncCnt('cmd-setup')).catch((e) => utils.commonLoggers.dbError('setup.ts', 'call sproc INC_CNT on', e));
|
||||
|
||||
const setupOpts = interaction.data?.options?.[0];
|
||||
|
||||
if (setupOpts?.name && interaction.channelId && interaction.guildId) {
|
||||
const lfgChannelSettingKey = generateGuildSettingKey(interaction.guildId, interaction.channelId);
|
||||
if (lfgChannelSettings.has(lfgChannelSettingKey)) {
|
||||
// Cannot setup a lfg channel that is already set up
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: ApplicationCommandFlags.Ephemeral,
|
||||
embeds: [{
|
||||
color: failColor,
|
||||
title: 'Unable to setup LFG channel.',
|
||||
description:
|
||||
`This channel is already set as an LFG channel. If you need to edit the channel, please run \`/${deleteSlashName}\` in this channel and then run \`/${setupSlashName}\` again.\n\nThis will not harm any active events in this channel and simply resets the settings for this channel.`,
|
||||
}],
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('setup.ts', interaction, e));
|
||||
return;
|
||||
}
|
||||
|
||||
const messages = await bot.helpers.getMessages(interaction.channelId, { limit: 100 });
|
||||
if (messages.size < 100) {
|
||||
let logChannelId = 0n;
|
||||
let managerRoleId = 0n;
|
||||
let logChannelErrorOut = false;
|
||||
let mgrRoleErrorOut = false;
|
||||
const introFields: Array<DiscordEmbedField> = [{
|
||||
name: 'Joining Events:',
|
||||
value:
|
||||
`To join an event, simply click on the \`${joinEventBtnStr}\` or \`${requestToJoinEventBtnStr}\` button. If you try to join a full event, you will be placed in the Alternates column with an \`*\` next to your name. Members with an \`*\` next to their name will automatically get promoted to the Joined list if someone leaves the event.`,
|
||||
}, {
|
||||
name: 'Leaving Events:',
|
||||
value: `To leave an event, simply click on the \`${leaveEventBtnStr}\` button.`,
|
||||
inline: true,
|
||||
}, {
|
||||
name: 'Joining Events as an Alternate:',
|
||||
value: `To join as a backup or indicate you might be available, simply click on the \`${alternateEventBtnStr}\` button.`,
|
||||
inline: true,
|
||||
}, {
|
||||
name: 'Editing/Deleting your event:',
|
||||
value: 'To edit or delete your event, simply click on the ✏️ or 🗑️ buttons respectively.',
|
||||
}];
|
||||
const permissionFields: Array<DiscordEmbedField> = [
|
||||
{
|
||||
name: `Please make sure ${config.name} has the following permissions:`,
|
||||
value: '`MANAGE_GUILD`\n`MANAGE_CHANNELS`\n`MANAGE_ROLES`\n`MANAGE_MESSAGES`\n\nThe only permission that is required after setup completes is `MANAGE_MESSAGES`.',
|
||||
},
|
||||
];
|
||||
if (setupOpts.name === withMgrRole) {
|
||||
if (setupOpts.options?.length) {
|
||||
setupOpts.options.forEach((opt) => {
|
||||
if (opt.name === managerRoleStr) {
|
||||
managerRoleId = BigInt(opt.value as string || '0');
|
||||
} else if (opt.name === logChannelStr) {
|
||||
logChannelId = BigInt(opt.value as string || '0');
|
||||
}
|
||||
});
|
||||
|
||||
if (logChannelId === 0n || managerRoleId === 0n) {
|
||||
// One or both Ids did not get parsed
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: ApplicationCommandFlags.Ephemeral,
|
||||
embeds: [{
|
||||
color: failColor,
|
||||
title: 'Unable to setup log channel or manager role.',
|
||||
description:
|
||||
`${config.name} attempted to set the log channel or manager role, but one or both were undefined. Please try again and if the issue continues, \`/${reportSlashName}\` this issue to the developers with the error code below.`,
|
||||
fields: [{
|
||||
name: 'Error Code:',
|
||||
value: `setupLog${logChannelId}Mgr${managerRoleId}`,
|
||||
}],
|
||||
}],
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('setup.ts', interaction, e));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Discord broke?
|
||||
somethingWentWrong(bot, interaction, 'setupMissingRoleMgrOptions');
|
||||
return;
|
||||
}
|
||||
introFields.push({
|
||||
name: `${config.name} Manager Details:`,
|
||||
value: `${config.name} Managers with the <@&${managerRoleId}> role may edit or delete events in this guild, along with using the following commands to update the activity members:
|
||||
|
||||
\`/${managerJLASlashName} [${joinName} | ${leaveName} | ${alternateName}] [${eventLinkName}] [${userName}]\`
|
||||
|
||||
The Discord Slash Command system will ensure you provide all the required details.`,
|
||||
});
|
||||
|
||||
// Set permissions for self, skip if we already failed to set roles
|
||||
!logChannelErrorOut && await bot.helpers.editChannelPermissionOverrides(logChannelId, {
|
||||
id: botId,
|
||||
type: OverwriteTypes.Member,
|
||||
allow: ['SEND_MESSAGES', 'VIEW_CHANNEL', 'EMBED_LINKS'],
|
||||
}).catch((e: Error) => {
|
||||
utils.commonLoggers.channelUpdateError('setup.ts', 'self-allow', e);
|
||||
mgrRoleErrorOut = true;
|
||||
});
|
||||
|
||||
// Test sending a message to the logChannel
|
||||
!logChannelErrorOut && await bot.helpers.sendMessage(logChannelId, {
|
||||
embeds: [{
|
||||
title: `This is the channel ${config.name} will be logging events to.`,
|
||||
description: `${config.name} will only send messages here as frequently as your event managers update events.`,
|
||||
color: infoColor2,
|
||||
}],
|
||||
}).catch((e: Error) => {
|
||||
utils.commonLoggers.messageSendError('setup.ts', 'log-test', e);
|
||||
logChannelErrorOut = true;
|
||||
});
|
||||
if (logChannelErrorOut) {
|
||||
// Cannot send message into log channel, error out
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: ApplicationCommandFlags.Ephemeral,
|
||||
embeds: [{
|
||||
color: failColor,
|
||||
title: 'Unable to setup log channel.',
|
||||
description: `${config.name} attempted to send a message to the specified log channel.`,
|
||||
fields: [
|
||||
{
|
||||
name: `Please allow ${config.name} to send messages in the requested channel.`,
|
||||
value: `<#${logChannelId}>`,
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('setup.ts', interaction, e));
|
||||
return;
|
||||
}
|
||||
|
||||
// Set permissions for managerId
|
||||
await bot.helpers.editChannelPermissionOverrides(interaction.channelId, {
|
||||
id: managerRoleId,
|
||||
type: OverwriteTypes.Role,
|
||||
allow: ['SEND_MESSAGES'],
|
||||
}).catch((e: Error) => {
|
||||
utils.commonLoggers.channelUpdateError('setup.ts', 'manager-allow', e);
|
||||
mgrRoleErrorOut = true;
|
||||
});
|
||||
}
|
||||
|
||||
// Set permissions for everyone, skip if we already failed to set roles
|
||||
!mgrRoleErrorOut && await bot.helpers.editChannelPermissionOverrides(interaction.channelId, {
|
||||
id: interaction.guildId,
|
||||
type: OverwriteTypes.Role,
|
||||
deny: ['SEND_MESSAGES'],
|
||||
}).catch((e: Error) => {
|
||||
utils.commonLoggers.channelUpdateError('setup.ts', 'everyone-deny', e);
|
||||
mgrRoleErrorOut = true;
|
||||
});
|
||||
|
||||
// Set permissions for self, skip if we already failed to set roles
|
||||
!mgrRoleErrorOut && await bot.helpers.editChannelPermissionOverrides(interaction.channelId, {
|
||||
id: botId,
|
||||
type: OverwriteTypes.Member,
|
||||
allow: ['SEND_MESSAGES', 'VIEW_CHANNEL', 'EMBED_LINKS'],
|
||||
}).catch((e: Error) => {
|
||||
utils.commonLoggers.channelUpdateError('setup.ts', 'self-allow', e);
|
||||
mgrRoleErrorOut = true;
|
||||
});
|
||||
|
||||
if (mgrRoleErrorOut) {
|
||||
// Cannot update role overrides on channel, error out
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: ApplicationCommandFlags.Ephemeral,
|
||||
embeds: [{
|
||||
color: failColor,
|
||||
title: 'Unable to set lfg channel permissions.',
|
||||
description: `${config.name} attempted to update the permissions for the current channel, but could not.`,
|
||||
fields: permissionFields,
|
||||
}],
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('setup.ts', interaction, e));
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete all messages that are not LFG posts
|
||||
const msgsToDel: Array<bigint> = [];
|
||||
const oldLfgMsgs: Array<bigint> = [];
|
||||
messages.forEach((msg) => {
|
||||
if (msg.authorId === botId && msg.embeds.length && msg.embeds[0].footer && msg.embeds[0].footer.text.includes('Created by:')) {
|
||||
oldLfgMsgs.push(msg.id);
|
||||
} else {
|
||||
msgsToDel.push(msg.id);
|
||||
}
|
||||
});
|
||||
if (msgsToDel.length) {
|
||||
for (const msgToDel of msgsToDel) {
|
||||
await bot.helpers.deleteMessage(interaction.channelId, msgToDel, 'Initial LFG Channel Cleanup').catch((e: Error) =>
|
||||
utils.commonLoggers.messageDeleteError('setup.ts', 'bulk-msg-cleanup', e)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Retrofit all old LFG posts that we found
|
||||
oldLfgMsgs.forEach((oldEventId) => {
|
||||
const oldEvent = messages.get(oldEventId);
|
||||
if (oldEvent && oldEvent.embeds[0].fields && oldEvent.embeds[0].footer) {
|
||||
const eventMembers = [...getLfgMembers(oldEvent.embeds[0].fields[LfgEmbedIndexes.JoinedMembers].value), ...getLfgMembers(oldEvent.embeds[0].fields[LfgEmbedIndexes.AlternateMembers].value)];
|
||||
const eventDateTime = new Date(parseInt((oldEvent.embeds[0].fields[LfgEmbedIndexes.StartTime].value.split('tz#')[1] || ' ').slice(0, -1)));
|
||||
if (!isNaN(eventDateTime.getTime())) {
|
||||
const eventDateTimeStr = (oldEvent.embeds[0].fields[LfgEmbedIndexes.StartTime].value.split('](')[0] || ' ').slice(1);
|
||||
oldEvent.embeds[0].fields[LfgEmbedIndexes.StartTime].value = generateTimeFieldStr(eventDateTimeStr, eventDateTime);
|
||||
oldEvent.embeds[0].footer.text = oldEvent.embeds[0].footer.text.split(' | ')[0];
|
||||
const ownerName = oldEvent.embeds[0].footer.text.split(': ')[1];
|
||||
const ownerId = eventMembers.find((member) => ownerName === member.name)?.id || 0n;
|
||||
oldEvent.embeds[0].footer.iconUrl = `${config.links.creatorIcon}#${ownerId}`;
|
||||
bot.helpers.editMessage(oldEvent.channelId, oldEvent.id, {
|
||||
content: '',
|
||||
embeds: [oldEvent.embeds[0]],
|
||||
components: [{
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
components: generateLFGButtons(false),
|
||||
}],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageEditError('setup.ts', 'retrofit event', e));
|
||||
dbClient.execute(queries.insertEvent, [oldEvent.id, oldEvent.channelId, interaction.guildId, ownerId, eventDateTime]).catch((e) =>
|
||||
utils.commonLoggers.dbError('setup.ts@retrofit', 'INSERT event to DB', e)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Store the ids to the db
|
||||
let dbErrorOut = false;
|
||||
await dbClient.execute('INSERT INTO guild_settings(guildId,lfgChannelId,managerRoleId,logChannelId) values(?,?,?,?)', [interaction.guildId, interaction.channelId, managerRoleId, logChannelId])
|
||||
.catch((e) => {
|
||||
utils.commonLoggers.dbError('setup.ts', 'insert into guild_settings', e);
|
||||
dbErrorOut = true;
|
||||
});
|
||||
if (dbErrorOut) {
|
||||
// DB died?
|
||||
somethingWentWrong(bot, interaction, 'setupDBInsertFailed');
|
||||
return;
|
||||
}
|
||||
// Store the ids to the active map
|
||||
lfgChannelSettings.set(lfgChannelSettingKey, {
|
||||
managed: setupOpts.name === withMgrRole,
|
||||
managerRoleId,
|
||||
logChannelId,
|
||||
});
|
||||
|
||||
// Send the initial introduction message
|
||||
const createNewEventBtn = 'Create New Event';
|
||||
const introMsg = await bot.helpers.sendMessage(interaction.channelId, {
|
||||
content: `Welcome to <#${interaction.channelId}>, managed by <@${botId}>!`,
|
||||
embeds: [{
|
||||
title: `To get started, click on the '${createNewEventBtn}' button below!`,
|
||||
color: infoColor2,
|
||||
fields: introFields,
|
||||
}],
|
||||
components: [{
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
components: [{
|
||||
type: MessageComponentTypes.Button,
|
||||
label: createNewEventBtn,
|
||||
customId: gameSelId,
|
||||
style: ButtonStyles.Success,
|
||||
}],
|
||||
}],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('setup.ts', 'init-msg', e));
|
||||
|
||||
if (introMsg) {
|
||||
bot.helpers.pinMessage(interaction.channelId, introMsg.id).catch((e: Error) => utils.commonLoggers.messageSendError('setup.ts', 'pin-init-msg', e));
|
||||
// Complete the interaction
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: ApplicationCommandFlags.Ephemeral,
|
||||
embeds: [{
|
||||
color: successColor,
|
||||
title: 'LFG Channel setup complete!',
|
||||
description: `${config.name} has finished setting up this channel. ${safelyDismissMsg}`,
|
||||
}],
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('setup.ts', interaction, e));
|
||||
} else {
|
||||
// Could not send initial message
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: ApplicationCommandFlags.Ephemeral,
|
||||
embeds: [{
|
||||
color: failColor,
|
||||
title: 'Failed to send the initial message!',
|
||||
fields: permissionFields,
|
||||
}],
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('setup.ts', interaction, e));
|
||||
}
|
||||
} else {
|
||||
// Too many messages to delete, give up
|
||||
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
flags: ApplicationCommandFlags.Ephemeral,
|
||||
embeds: [{
|
||||
color: failColor,
|
||||
title: 'Unable to setup LFG channel.',
|
||||
description: `${config.name} attempted to clean this channel, but encountered too many messages (100 or more). There are two ways to move forward:`,
|
||||
fields: [
|
||||
{
|
||||
name: 'Is this channel a dedicated LFG Channel?',
|
||||
value: 'You either need to manually clean this channel or create a brand new channel for events.',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Is this a chat channel that you want events mixed into?',
|
||||
value: `You do not need to run the \`/${setupSlashName}\` command, and instead should use the \`/${createEventSlashName}\` command.`,
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.interactionSendError('setup.ts', interaction, e));
|
||||
}
|
||||
} else {
|
||||
// Discord fucked up?
|
||||
somethingWentWrong(bot, interaction, 'setupMissingAllOptions');
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
details,
|
||||
execute,
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
export const auditSlashName = 'zzz-audit';
|
||||
export const createEventSlashName = 'create-event';
|
||||
export const deleteSlashName = 'delete-lfg-channel';
|
||||
export const managerJLASlashName = 'event';
|
||||
export const helpSlashName = 'help';
|
||||
export const infoSlashName = 'info';
|
||||
export const reportSlashName = 'report';
|
||||
export const setupSlashName = 'setup';
|
|
@ -1,252 +0,0 @@
|
|||
import { ActionRow, DiscordButtonStyles } from '../deps.ts';
|
||||
|
||||
import config from '../config.ts';
|
||||
|
||||
export const constantCmds = {
|
||||
help: {
|
||||
embeds: [{
|
||||
title: `${config.name} Help`,
|
||||
fields: [
|
||||
{
|
||||
name: 'All commands must have the bot\'s prefix before them.',
|
||||
value: `Default is \`${config.prefix}\`, send <@847256159123013722> to change it.`,
|
||||
},
|
||||
{
|
||||
name: 'LFG Commands',
|
||||
value: `
|
||||
\`lfg help\` - More detailed help for the LFG commands
|
||||
\`lfg create\` - Create a new LFG post
|
||||
\`lfg edit\` - Edit an existing LFG post
|
||||
\`lfg delete\` - Delete an existing LFG post
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'Utility Commands',
|
||||
value: `
|
||||
\`info\` - Information about the bot
|
||||
\`ping\` - Pings the bot to check its connection
|
||||
\`report [TEXT]\` - Report an issue to the developer
|
||||
\`version\` - Prints the bot's current version
|
||||
`,
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
lfgHelp: {
|
||||
embeds: [{
|
||||
title: `${config.name} LFG Help`,
|
||||
fields: [
|
||||
{
|
||||
name: 'All commands must have the bot\'s prefix before them.',
|
||||
value: `Default is \`${config.prefix}\`, send <@847256159123013722> to change it.`,
|
||||
},
|
||||
{
|
||||
name: 'lfg create',
|
||||
value: `
|
||||
\`lfg create\`, alternatively \`lfg c\`, will walk you through creating a new LFG post. Simply follow the prompts and the bot will walk you through building a new LFG.
|
||||
|
||||
Make sure you run this command in the channel you wish the LFG post to be created in.
|
||||
`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'lfg edit',
|
||||
value: `
|
||||
\`lfg edit [id?]\`, alternatively \`lfg e [id?]\`, will walk you through editing an existing LFG. Like \`lfg create\`, the bot will walk you through editing it.
|
||||
|
||||
Simply run \`lfg edit\` in the channel where the LFG post lives.
|
||||
If you only have one LFG in this channel, the editing process will begin.
|
||||
If you have more than one LFG in this channel, the bot will ask you to specify the LFG post using a two character id.
|
||||
`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'lfg delete',
|
||||
value: `
|
||||
\`lfg delete [id?]\`, alternatively \`lfg d [id?]\`, will delete an existing LFG. You only can delete LFG posts that you own.
|
||||
|
||||
Simply run \`lfg delete\` in the channel where the LFG post lives.
|
||||
If you only have one LFG in this channel, the LFG will be deleted.
|
||||
If you have more than one LFG in this channel, the bot will ask you to specify the LFG post using a two character id.
|
||||
`,
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
info: {
|
||||
embeds: [{
|
||||
fields: [
|
||||
{
|
||||
name: 'Group Up, the LFG bot',
|
||||
value: `Group Up is developed by Ean AKA Burn_E99.
|
||||
Want to check out my source code? Check it out [here](https://github.com/Burn-E99/GroupUp).
|
||||
Need help with this bot? Join my support server [here](https://discord.gg/peHASXMZYv).`,
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
version: {
|
||||
embeds: [{
|
||||
title: `My current version is ${config.version}`,
|
||||
}],
|
||||
},
|
||||
report: {
|
||||
embeds: [{
|
||||
fields: [
|
||||
{
|
||||
name: 'Failed command has been reported to my developer.',
|
||||
value: 'For more in depth support, and information about planned maintenance, please join the support server [here](https://discord.gg/peHASXMZYv).',
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
lfgDelete1: {
|
||||
embeds: [{
|
||||
fields: [
|
||||
{
|
||||
name: 'Could not find any LFGs to delete.',
|
||||
value: 'Make sure you are the owner of the LFG and are running this command in the same channel as the LFG',
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
lfgDelete2: {
|
||||
embeds: [{
|
||||
fields: [
|
||||
{
|
||||
name: `Multiple LFGs found, please run this command again with the two character ID of the LFG you wish to delete.\n\nExample: \`${config.prefix}lfg delete XX\``,
|
||||
value: 'Click on the two character IDs below to view the LFG:\n',
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
lfgDelete3: {
|
||||
embeds: [{
|
||||
title: 'LFG deleted.',
|
||||
}],
|
||||
},
|
||||
lfgEdit1: {
|
||||
embeds: [{
|
||||
fields: [
|
||||
{
|
||||
name: 'Could not find any LFGs to edit.',
|
||||
value: 'Make sure you are the owner of the LFG and are running this command in the same channel as the LFG',
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
lfgEdit2: {
|
||||
embeds: [{
|
||||
fields: [
|
||||
{
|
||||
name: `Multiple LFGs found, please run this command again with the two character ID of the LFG you wish to edit.\n\nExample: \`${config.prefix}lfg edit XX\``,
|
||||
value: 'Click on the two character IDs below to view the LFG:\n',
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
announcement: {
|
||||
content: `Hi! You're receiving this message as you have me in one of your servers. This is a very important announcement regarding how you and your community uses this bot in your guild. Please read the following details carefully.
|
||||
|
||||
This announcement feature is reserved for important breaking changes only. Group Up will be reaching version 1.0 with this major update, and will not have any more event breaking changes for a significant amount of time.`,
|
||||
embeds: [{
|
||||
color: 0x313bf9,
|
||||
title: 'Version 1.0.0 is coming!',
|
||||
description: `Group Up is coming up on a major milestone, giving your community an even better and more user-friendly experience.
|
||||
|
||||
**NOTICE:** All Guild settings will be deleted when V1.0.0 is released.
|
||||
**NOTICE:** All Guilds are forced to use the \`/\` command prefix via the Discord Slash Command system.`,
|
||||
fields: [
|
||||
{
|
||||
name: 'When is this update coming out?',
|
||||
value: 'This update will be released <t:1683136800:R>, on <t:1683136800:F>. Group Up will be brought offline one hour before the update releases (<t:1683133200:R>, on <t:1683133200:F>) to handle a small migration and the deployment of the major update.',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'What is changing?',
|
||||
value: 'Group Up is moving to a fully Button and Slash Command based system. This means less commands to memorize and gives Group Up users more friendly methods to interact with, such as Forms, and Ephemeral messages.',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'What do you need to do?',
|
||||
value: `Once the update is live, there are a couple things you will need to do:
|
||||
|
||||
1. The bot will now require the \`MANAGE_GUILD\`, \`MANAGE_CHANNELS\`, \`MANAGE_ROLES\`, and \`MANAGE_MESSAGES\` permissions. Simply edit the role named \`Group Up\` in your server to add these permissions. The increased permissions are only necessary for the initial setup. After the setup is complete, the bot only requires the \`MANAGE_MESSAGES\` permission.
|
||||
2. Go to or create the channel you want Group Up to use for event scheduling. Group Up will be taking full control of this channel, so please make sure you are okay with that before continuing.
|
||||
3. Once in the desired channel, run \`/setup\` and follow the on-screen prompts. This will discover any pre-existing events, update them to the new design, and reconfigure the channel to work with the new system.`,
|
||||
},
|
||||
{
|
||||
name: 'A Note for Pre-existing Events:',
|
||||
value: 'If there are any pre-existing events, they will end up above the new instruction message Group Up sends. This is expected and unavoidable, but these events should still function fine.',
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
announcementPart2: {
|
||||
embeds: [{
|
||||
color: 0x313bf9,
|
||||
fields: [
|
||||
{
|
||||
name: 'If you used the Group Up Manager system before V1.0.0:',
|
||||
value: `The Group Up Manager System is more powerful now. This system now grants managers the ability to edit and delete events in addition to the original Join/Leave/Alternate commands.
|
||||
|
||||
This system now requires a log channel which allows you to audit what your Group Up Managers are doing. Additionally, the owner of an event that a Group Up Manager modifies will be notified of any changes.
|
||||
|
||||
To enable this system, simply run \`/setup with-manager-role\` and fill in the requested fields.`,
|
||||
},
|
||||
{
|
||||
name: 'What if you don\'t want to update?',
|
||||
value: `As hosting costs money and I am running this bot for free, only the new system (V1.0.0) will be available from the official Group Up Discord bot.
|
||||
|
||||
If you *really* want to keep the old text based system, you may do so by hosting the bot yourself. This is **not recommended and not supported**, but as this project is open source, you may check out [my GitHub](https://github.com/Burn-E99/GroupUp) for details on how to host the last text based version (V0.5.8) privately.`
|
||||
},
|
||||
{
|
||||
name: 'Have more questions?',
|
||||
value: 'I have tried to anticipate every possible question, but if you still have any, please join [my support server](https://discord.gg/peHASXMZYv).',
|
||||
},
|
||||
{
|
||||
name: 'Final Words',
|
||||
value: `As I want to avoid these unexpected DMs in the future, please join [my support server](https://discord.gg/peHASXMZYv). All future announcements will be sent via this server.
|
||||
|
||||
Thank you for using Group Up, and I hope you enjoy this major update.
|
||||
|
||||
~Ean AKA Burn_E99`,
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
export const editBtns: ActionRow['components'] = [
|
||||
{
|
||||
type: 2,
|
||||
label: 'Change Game/Activity',
|
||||
customId: `editing@set_game`,
|
||||
style: DiscordButtonStyles.Primary,
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
label: 'Change Time',
|
||||
customId: `editing@set_time`,
|
||||
style: DiscordButtonStyles.Primary,
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
label: 'Change Description',
|
||||
customId: `editing@set_desc`,
|
||||
style: DiscordButtonStyles.Primary,
|
||||
},
|
||||
];
|
||||
|
||||
export const lfgStepQuestions = {
|
||||
'set_game': 'Please select a game from the list below. If your game is not listed, please type it out:',
|
||||
'set_activity_with_button':
|
||||
'Please select an Activity from the list below. Depending on the game selected, these may be categories you can use to drill down to a specific activity.\n\nIf your activity is not listed, please type it out:',
|
||||
'set_activity_with_text': 'Please type the activity name out:',
|
||||
'set_activity_from_category': 'Please select an Activity from the list below.\n\nIf your activity is not listed, please type it out:',
|
||||
'set_player_cnt': 'Please enter the max number of members for this activity:',
|
||||
'set_time': 'Please enter the time of the activity:\nRecommended format: `h:mm am/pm tz month/day`',
|
||||
'set_desc': 'Please enter a description for the activity. Enter `none` to skip:',
|
||||
'set_done': 'Finalizing, please wait. . .',
|
||||
};
|
|
@ -0,0 +1,34 @@
|
|||
import config from '../config.ts';
|
||||
import { Client } from '../deps.ts';
|
||||
import { LOCALMODE } from '../flags.ts';
|
||||
import { DBGuildSettings, LfgChannelSetting } from './types/commandTypes.ts';
|
||||
|
||||
export 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,
|
||||
});
|
||||
|
||||
export const queries = {
|
||||
callIncCnt: (cmdName: string) => `CALL INC_CNT("${cmdName}");`,
|
||||
selectEvents: (notifiedFlag: number, lockedFlag: number) => `SELECT * FROM active_events WHERE notifiedFlag = ${notifiedFlag} AND lockedFlag = ${lockedFlag} AND eventTime < ?`,
|
||||
selectFailedEvents: 'SELECT * FROM active_events WHERE (notifiedFlag = -1 OR lockedFlag = -1) AND eventTime < ?',
|
||||
insertEvent: 'INSERT INTO active_events(messageId,channelId,guildId,ownerId,eventTime) values(?,?,?,?,?)',
|
||||
updateEventTime: 'UPDATE active_events SET eventTime = ? WHERE channelId = ? AND messageId = ?',
|
||||
updateEventFlags: (notifiedFlag: number, lockedFlag: number) => `UPDATE active_events SET notifiedFlag = ${notifiedFlag}, lockedFlag = ${lockedFlag} WHERE channelId = ? AND messageId = ?`,
|
||||
deleteEvent: 'DELETE FROM active_events WHERE channelId = ? AND messageId = ?',
|
||||
insertCustomActivity: 'INSERT INTO custom_activities(guildId,activityTitle,activitySubtitle,maxMembers) values(?,?,?,?)',
|
||||
};
|
||||
|
||||
export const lfgChannelSettings: Map<string, LfgChannelSetting> = new Map();
|
||||
export const generateGuildSettingKey = (guildId: bigint, channelId: bigint) => `${guildId}-${channelId}`;
|
||||
const getGuildSettings = await dbClient.query('SELECT * FROM guild_settings');
|
||||
getGuildSettings.forEach((g: DBGuildSettings) => {
|
||||
lfgChannelSettings.set(generateGuildSettingKey(g.guildId, g.lfgChannelId), {
|
||||
managed: g.managerRoleId !== 0n && g.logChannelId !== 0n,
|
||||
managerRoleId: g.managerRoleId,
|
||||
logChannelId: g.logChannelId,
|
||||
});
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
import { DEVMODE } from '../flags.ts';
|
||||
import { EventHandlers } from '../deps.ts';
|
||||
import eventHandlers from './events/_index.ts';
|
||||
|
||||
export const events: Partial<EventHandlers> = {};
|
||||
|
||||
events.ready = eventHandlers.ready;
|
||||
events.guildCreate = eventHandlers.guildCreate;
|
||||
events.guildDelete = eventHandlers.guildDelete;
|
||||
events.messageCreate = eventHandlers.messageCreate;
|
||||
events.interactionCreate = eventHandlers.interactionCreate;
|
||||
|
||||
if (DEVMODE) {
|
||||
events.debug = eventHandlers.debug;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { ready } from './ready.ts';
|
||||
import { guildCreate } from './guildCreate.ts';
|
||||
import { guildDelete } from './guildDelete.ts';
|
||||
import { debug } from './debug.ts';
|
||||
import { messageCreate } from './messageCreate.ts';
|
||||
import { interactionCreate } from './interactionCreate.ts';
|
||||
|
||||
export default {
|
||||
ready,
|
||||
guildCreate,
|
||||
guildDelete,
|
||||
debug,
|
||||
messageCreate,
|
||||
interactionCreate,
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
import {
|
||||
// Log4Deno deps
|
||||
log,
|
||||
LT,
|
||||
} from '../../deps.ts';
|
||||
import utils from '../utils.ts';
|
||||
|
||||
export const debug = (dmsg: string) => log(LT.LOG, `Debug Message | ${utils.jsonStringifyBig(dmsg)}`);
|
|
@ -0,0 +1,31 @@
|
|||
import config from '../../config.ts';
|
||||
import { Bot, Guild, log, LT } from '../../deps.ts';
|
||||
import { infoColor1 } from '../commandUtils.ts';
|
||||
import utils from '../utils.ts';
|
||||
|
||||
export const guildCreate = (bot: Bot, guild: Guild) => {
|
||||
log(LT.LOG, `Handling joining guild ${utils.jsonStringifyBig(guild)}`);
|
||||
bot.helpers.sendMessage(config.logChannel, {
|
||||
embeds: [{
|
||||
title: 'Guild Joined!',
|
||||
color: infoColor1,
|
||||
fields: [
|
||||
{
|
||||
name: 'Name:',
|
||||
value: `${guild.name}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Id:',
|
||||
value: `${guild.id}`,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Member Count:',
|
||||
value: `${guild.memberCount}`,
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
}],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('mod.ts:95', 'Join Guild', e));
|
||||
};
|
|
@ -0,0 +1,40 @@
|
|||
import config from '../../config.ts';
|
||||
import { Bot, log, LT } from '../../deps.ts';
|
||||
import { warnColor } from '../commandUtils.ts';
|
||||
import { dbClient, lfgChannelSettings } from '../db.ts';
|
||||
import utils from '../utils.ts';
|
||||
|
||||
export const guildDelete = async (bot: Bot, guildId: bigint) => {
|
||||
log(LT.LOG, `Handling leaving guild ${utils.jsonStringifyBig(guildId)}`);
|
||||
|
||||
// Clean the DB
|
||||
try {
|
||||
await dbClient.execute('DELETE FROM guild_settings WHERE guildId = ?', [guildId]);
|
||||
await dbClient.execute('DELETE FROM active_events WHERE guildId = ?', [guildId]);
|
||||
await dbClient.execute('DELETE FROM custom_activities WHERE guildId = ?', [guildId]);
|
||||
} catch (e) {
|
||||
log(LT.WARN, `Failed to remove guild (${guildId}) from DB: ${utils.jsonStringifyBig(e)}`);
|
||||
}
|
||||
|
||||
// Clean lfgChannelSettings
|
||||
lfgChannelSettings.forEach((_val, key) => {
|
||||
if (key.startsWith(`${guildId}-`)) {
|
||||
lfgChannelSettings.delete(key);
|
||||
}
|
||||
});
|
||||
|
||||
// Send Log Message
|
||||
bot.helpers.sendMessage(config.logChannel, {
|
||||
embeds: [{
|
||||
title: 'Removed from Guild',
|
||||
color: warnColor,
|
||||
fields: [
|
||||
{
|
||||
name: 'Id:',
|
||||
value: `${guildId}`,
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
}],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('guildDelete.ts:28', 'Leave Guild', e));
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
import { Bot, BotWithCache, Interaction, log, LT } from '../../deps.ts';
|
||||
import { buttons } from '../buttons/_index.ts';
|
||||
import { commands } from '../commands/_index.ts';
|
||||
import { fillerChar, idSeparator } from '../buttons/eventUtils.ts';
|
||||
|
||||
const commandNames: Array<string> = commands.map((command) => command.details.name);
|
||||
const buttonNames: Array<string> = buttons.map((button) => button.customId);
|
||||
|
||||
export const interactionCreate = (rawBot: Bot, interaction: Interaction) => {
|
||||
const bot = rawBot as BotWithCache;
|
||||
if (interaction.data && interaction.id) {
|
||||
if (interaction.data.name && commandNames.includes(interaction.data.name)) {
|
||||
const cmdIdx = commandNames.indexOf(interaction.data.name);
|
||||
commands[cmdIdx].execute(bot, interaction);
|
||||
return;
|
||||
}
|
||||
|
||||
const tempCustomId = interaction.data.customId ? interaction.data.customId.replaceAll(fillerChar, '') : '';
|
||||
const customId = tempCustomId.split(idSeparator)[0] || '';
|
||||
if (customId && buttonNames.includes(customId)) {
|
||||
const btnIdx = buttonNames.indexOf(customId);
|
||||
buttons[btnIdx].execute(bot, interaction);
|
||||
return;
|
||||
}
|
||||
|
||||
log(LT.WARN, `UNHANDLED INTERACTION!!! customId: ${interaction.data.customId} name: ${interaction.data.name}`);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,40 @@
|
|||
import config from '../../config.ts';
|
||||
import utils from '../utils.ts';
|
||||
import { Bot, botId, Message } from '../../deps.ts';
|
||||
import { infoEmbed } from '../commandUtils.ts';
|
||||
import { dbClient, generateGuildSettingKey, lfgChannelSettings, queries } from '../db.ts';
|
||||
|
||||
export const messageCreate = async (bot: Bot, message: Message) => {
|
||||
// Ignore self
|
||||
if (botId === message.authorId) return;
|
||||
|
||||
// Delete all messages sent to a LFG Channel
|
||||
if (lfgChannelSettings.has(generateGuildSettingKey(message.guildId || 0n, message.channelId))) {
|
||||
bot.helpers.deleteMessage(message.channelId, message.id, 'Cleaning LFG Channel').catch((e: Error) => utils.commonLoggers.messageDeleteError('messageCreate.ts', 'Clean LFG Channel', e));
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore all messages that are not commands
|
||||
if (message.content.indexOf(config.prefix) !== 0) {
|
||||
// Handle @bot messages
|
||||
if (message.mentionedUserIds[0] === botId && (message.content.trim().startsWith(`<@${botId}>`) || message.content.trim().startsWith(`<@!${botId}>`))) {
|
||||
dbClient.execute(queries.callIncCnt('msg-mention')).catch((e) => utils.commonLoggers.dbError('info.ts', 'call sproc INC_CNT on', e));
|
||||
bot.helpers.sendMessage(message.channelId, {
|
||||
embeds: [infoEmbed],
|
||||
messageReference: {
|
||||
messageId: message.id,
|
||||
channelId: message.channelId,
|
||||
guildId: message.guildId,
|
||||
failIfNotExists: false,
|
||||
},
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('messageCreate.ts', '@mention', e));
|
||||
return;
|
||||
}
|
||||
|
||||
// return as we are done handling this command
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore all other bots
|
||||
if (message.isFromBot) return;
|
||||
};
|
|
@ -0,0 +1,99 @@
|
|||
import { ActivityTypes, Bot, BotWithCache, log, LT } from '../../deps.ts';
|
||||
import config from '../../config.ts';
|
||||
import { LOCALMODE } from '../../flags.ts';
|
||||
import { getRandomStatus, successColor } from '../commandUtils.ts';
|
||||
import { ActiveEvent } from '../types/commandTypes.ts';
|
||||
import utils from '../utils.ts';
|
||||
import { dbClient, queries } from '../db.ts';
|
||||
import { deleteEvent, handleFailures, lockEvent, notifyEventMembers, tenMinutes } from '../notificationSystem.ts';
|
||||
import { updateBotListStatistics } from '../botListPoster.ts';
|
||||
|
||||
// Storing intervalIds in case bot soft reboots to prevent multiple of these intervals from stacking
|
||||
let notificationIntervalId: number;
|
||||
let botStatusIntervalId: number;
|
||||
let botListPosterIntervalId: number;
|
||||
|
||||
export const ready = (rawBot: Bot) => {
|
||||
const bot = rawBot as BotWithCache;
|
||||
log(LT.INFO, `${config.name} Logged in!`);
|
||||
bot.helpers.editBotStatus({
|
||||
activities: [{
|
||||
name: 'Booting up . . .',
|
||||
type: ActivityTypes.Game,
|
||||
createdAt: new Date().getTime(),
|
||||
}],
|
||||
status: 'online',
|
||||
}).catch((e) => log(LT.ERROR, `Failed to update status (booting): ${utils.jsonStringifyBig(e)}`));
|
||||
|
||||
// Interval to rotate the status text every 30 seconds to show off more commands
|
||||
if (botStatusIntervalId) clearInterval(botStatusIntervalId);
|
||||
botStatusIntervalId = setInterval(async () => {
|
||||
log(LT.LOG, 'Changing bot status');
|
||||
bot.helpers.editBotStatus({
|
||||
activities: [{
|
||||
name: getRandomStatus(bot.guilds.size + bot.dispatchedGuildIds.size),
|
||||
type: ActivityTypes.Game,
|
||||
createdAt: new Date().getTime(),
|
||||
}],
|
||||
status: 'online',
|
||||
}).catch((e) => log(LT.ERROR, `Failed to update status (in interval): ${utils.jsonStringifyBig(e)}`));
|
||||
}, 30000);
|
||||
|
||||
// Interval to handle event notifications and cleanup every 30 seconds
|
||||
if (notificationIntervalId) clearInterval(notificationIntervalId);
|
||||
notificationIntervalId = setInterval(async () => {
|
||||
log(LT.LOG, 'Running notification system');
|
||||
const now = new Date().getTime();
|
||||
|
||||
// Get all the events
|
||||
const eventsToNotify = await dbClient.execute(queries.selectEvents(0, 0), [new Date(now + tenMinutes)]).catch((e) =>
|
||||
utils.commonLoggers.dbError('ready.ts@notifyMembers', 'SELECT events from', e)
|
||||
);
|
||||
const eventsToLock = await dbClient.execute(queries.selectEvents(1, 0), [new Date(now)]).catch((e) => utils.commonLoggers.dbError('ready.ts@notifyAlternates', 'SELECT events from', e));
|
||||
const eventsToDelete = await dbClient.execute(queries.selectEvents(1, 1), [new Date(now - tenMinutes)]).catch((e) => utils.commonLoggers.dbError('ready.ts@deleteEvent', 'SELECT events from', e));
|
||||
const eventFailuresToHandle = await dbClient.execute(queries.selectFailedEvents, [new Date(now + tenMinutes)]).catch((e) =>
|
||||
utils.commonLoggers.dbError('ready.ts@handleFailures', 'SELECT events from', e)
|
||||
);
|
||||
|
||||
// Run all the handlers
|
||||
eventsToNotify?.rows?.forEach((event) => notifyEventMembers(bot, event as ActiveEvent));
|
||||
eventsToLock?.rows?.forEach((event) => lockEvent(bot, event as ActiveEvent));
|
||||
eventsToDelete?.rows?.forEach((event) => deleteEvent(bot, event as ActiveEvent));
|
||||
eventFailuresToHandle?.rows?.forEach((event) => handleFailures(bot, event as ActiveEvent));
|
||||
}, 30000);
|
||||
|
||||
// Interval to handle updating botList statistics
|
||||
if (botListPosterIntervalId) clearInterval(botListPosterIntervalId)
|
||||
LOCALMODE ? log(LT.INFO, 'updateListStatistics not running') : botListPosterIntervalId = setInterval(() => {
|
||||
log(LT.LOG, 'Updating all bot lists statistics');
|
||||
updateBotListStatistics(bot.guilds.size + bot.dispatchedGuildIds.size);
|
||||
}, 86400000);
|
||||
|
||||
// setTimeout added to make sure the startup message does not error out
|
||||
setTimeout(() => {
|
||||
LOCALMODE && bot.helpers.editBotMember(config.devServer, { nick: `LOCAL - ${config.name}` });
|
||||
|
||||
bot.helpers.editBotStatus({
|
||||
activities: [{
|
||||
name: 'Booting Complete',
|
||||
type: ActivityTypes.Game,
|
||||
createdAt: new Date().getTime(),
|
||||
}],
|
||||
status: 'online',
|
||||
}).catch((e) => log(LT.ERROR, `Failed to update status (boot complete): ${utils.jsonStringifyBig(e)}`));
|
||||
|
||||
bot.helpers.sendMessage(config.logChannel, {
|
||||
embeds: [{
|
||||
title: `${config.name} is now Online`,
|
||||
color: successColor,
|
||||
fields: [
|
||||
{
|
||||
name: 'Version:',
|
||||
value: `${config.version}`,
|
||||
inline: true,
|
||||
},
|
||||
],
|
||||
}],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('ready.ts@startup', 'Startup', e));
|
||||
}, 1000);
|
||||
};
|
59
src/games.ts
59
src/games.ts
|
@ -1,59 +0,0 @@
|
|||
export const LFGActivities = {
|
||||
'Destiny 2': {
|
||||
'Raids': {
|
||||
'King\'s Fall': 6,
|
||||
'Vow of the Disciple': 6,
|
||||
'Vault of Glass': 6,
|
||||
'Deep Stone Crypt': 6,
|
||||
'Garden of Salvation': 6,
|
||||
'Last Wish': 6,
|
||||
},
|
||||
'Dungeons': {
|
||||
'Spire of the Watcher': 3,
|
||||
'Duality': 3,
|
||||
'Grasp of Avarice': 3,
|
||||
'Prophecy': 3,
|
||||
'Pit of Heresy': 3,
|
||||
'Shattered Throne': 3,
|
||||
},
|
||||
'Crucible': {
|
||||
'Crucible (Control)': 6,
|
||||
'Crucible (Survival)': 3,
|
||||
'Crucible (Elimination)': 3,
|
||||
'Crucible (Private Match)': 12,
|
||||
'Iron Banner': 6,
|
||||
'Trials of Osiris': 3,
|
||||
},
|
||||
'Gambit': {
|
||||
'Gambit (Classic)': 4,
|
||||
'Gambit (Private Match)': 8,
|
||||
},
|
||||
"Exotic Missions": {
|
||||
'Operation: Seraph\'s Shield': 3,
|
||||
// "Presage": 3,
|
||||
// "Harbinger": 3
|
||||
},
|
||||
'Nightfall': 3,
|
||||
'Miscellaneous': {
|
||||
'Heist Battlegrounds': 3,
|
||||
'Ketchrash': 6,
|
||||
'Expedition': 3,
|
||||
'Weekly Witch Queen Campaign Mission': 3,
|
||||
'Wellspring': 6,
|
||||
'Dares of Eternity': 6,
|
||||
// "Astral Alignment": 6,
|
||||
// "Shattered Realm": 3,
|
||||
// "Override": 6,
|
||||
// "Expunge": 3,
|
||||
// "Battlegrounds": 3,
|
||||
'Wrathborn Hunt': 3,
|
||||
'Empire Hunt': 3,
|
||||
'Vanguard Operations': 3,
|
||||
// "Nightmare Hunt": 3
|
||||
},
|
||||
},
|
||||
'Among Us': {
|
||||
'Vanilla': 15,
|
||||
'Modded': 15,
|
||||
},
|
||||
};
|
180
src/intervals.ts
180
src/intervals.ts
|
@ -1,180 +0,0 @@
|
|||
import {
|
||||
// Discordeno deps
|
||||
cache,
|
||||
deleteMessage,
|
||||
getGuild,
|
||||
getMessage,
|
||||
log,
|
||||
// Log4Deno deps
|
||||
LT,
|
||||
sendDirectMessage,
|
||||
sendMessage,
|
||||
} from '../deps.ts';
|
||||
|
||||
import { jsonStringifyBig } from './utils.ts';
|
||||
import { ActiveLFG, BuildingLFG } from './mod.d.ts';
|
||||
|
||||
import config from '../config.ts';
|
||||
|
||||
// getRandomStatus() returns status as string
|
||||
// Gets a new random status for the bot
|
||||
const getRandomStatus = (cachedGuilds: number): string => {
|
||||
let status = '';
|
||||
switch (Math.floor((Math.random() * 5) + 1)) {
|
||||
case 1:
|
||||
status = `${config.prefix}help for commands`;
|
||||
break;
|
||||
case 2:
|
||||
status = `Running V${config.version}`;
|
||||
break;
|
||||
case 3:
|
||||
status = `${config.prefix}info to learn more`;
|
||||
break;
|
||||
case 4:
|
||||
status = 'Mention me to check my prefix!';
|
||||
break;
|
||||
default:
|
||||
status = `Running LFGs in ${cachedGuilds + cache.dispatchedGuildIds.size} servers`;
|
||||
break;
|
||||
}
|
||||
|
||||
return status;
|
||||
};
|
||||
|
||||
// updateListStatistics(bot ID, current guild count) returns nothing
|
||||
// Sends the current server count to all bot list sites we are listed on
|
||||
const updateListStatistics = (botID: BigInt, serverCount: number): void => {
|
||||
config.botLists.forEach(async (e) => {
|
||||
if (e.enabled) {
|
||||
log(LT.LOG, `Updating statistics for ${jsonStringifyBig(e)}`);
|
||||
try {
|
||||
const tempHeaders = new Headers();
|
||||
tempHeaders.append(e.headers[0].header, e.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(e.apiUrl.replace('?{bot_id}', botID.toString()), {
|
||||
'method': 'POST',
|
||||
'headers': tempHeaders,
|
||||
'body': jsonStringifyBig(e.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 ${e.name}. Results: ${jsonStringifyBig(response)}`);
|
||||
} catch (e) {
|
||||
log(LT.WARN, `Failed to post statistics to ${e.name} | ${jsonStringifyBig(e)}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const buildingTimeout = async (activeBuilders: Array<BuildingLFG>): Promise<void> => {
|
||||
const currentTime = new Date().getTime();
|
||||
for (let i = 0; i < activeBuilders.length; i++) {
|
||||
if (activeBuilders[i].lastTouch.getTime() + (activeBuilders[i].maxIdle * 1000) < currentTime) {
|
||||
activeBuilders[i].questionMsg.delete().catch((e) => {
|
||||
log(LT.WARN, `Failed to clean up active builder | edit | ${activeBuilders[i].userId}-${activeBuilders[i].channelId} | ${jsonStringifyBig(e)}`);
|
||||
});
|
||||
if (activeBuilders[i].editing) {
|
||||
activeBuilders[i].lfgMsg.edit({
|
||||
content: '',
|
||||
}).catch((e) => {
|
||||
log(LT.WARN, `Failed to clean up active builder | edit | ${activeBuilders[i].userId}-${activeBuilders[i].channelId} | ${jsonStringifyBig(e)}`);
|
||||
});
|
||||
} else {
|
||||
activeBuilders[i].lfgMsg.delete().catch((e) => {
|
||||
log(LT.WARN, `Failed to clean up active builder | delete | ${activeBuilders[i].userId}-${activeBuilders[i].channelId} | ${jsonStringifyBig(e)}`);
|
||||
});
|
||||
}
|
||||
try {
|
||||
const m = await sendMessage(activeBuilders[i].channelId, `<@${activeBuilders[i].userId}>, your LFG ${activeBuilders[i].editing ? 'editing' : 'creation'} has timed out. Please try again.`);
|
||||
|
||||
m.delete('Channel Cleanup', 30000).catch((e) => {
|
||||
log(LT.WARN, `Failed to delete message | ${jsonStringifyBig(e)}`);
|
||||
});
|
||||
} catch (e) {
|
||||
log(LT.WARN, `Failed to clean up active builder | ${activeBuilders[i].userId}-${activeBuilders[i].channelId} | ${jsonStringifyBig(e)}`);
|
||||
} finally {
|
||||
activeBuilders.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const lfgNotifier = async (activeLFGPosts: Array<ActiveLFG>): Promise<void> => {
|
||||
log(LT.INFO, 'Checking for LFG posts to notify/delete/lock');
|
||||
const tenMin = 10 * 60 * 1000;
|
||||
const now = new Date().getTime();
|
||||
for (let i = 0; i < activeLFGPosts.length; i++) {
|
||||
// Send notifications
|
||||
if (!activeLFGPosts[i].notified && activeLFGPosts[i].lfgTime < (now + tenMin)) {
|
||||
log(LT.INFO, `Notifying LFG ${activeLFGPosts[i].ownerId}-${activeLFGPosts[i].lfgUid}`);
|
||||
try {
|
||||
const message = await getMessage(activeLFGPosts[i].channelId, activeLFGPosts[i].messageId);
|
||||
const lfg = message.embeds[0].fields || [];
|
||||
const lfgActivity = `${lfg[0].name.substr(0, lfg[0].name.length - 1)} - ${lfg[0].value}`;
|
||||
const guildName = message.guild?.name || (await getGuild(message.guildId, { counts: false, addToCache: false })).name;
|
||||
const members = lfg[4].value;
|
||||
let editMsg = '';
|
||||
members.split('\n').forEach(async (m) => {
|
||||
if (m !== 'None') {
|
||||
const [name, tmpId] = m.split(' - <@');
|
||||
const userId = BigInt(tmpId.substr(0, tmpId.length - 1));
|
||||
editMsg += `<@${userId}>, `;
|
||||
await sendDirectMessage(userId, {
|
||||
embeds: [{
|
||||
title: `Hello ${name}! Your event in ${guildName} starts in less than 10 minutes.`,
|
||||
fields: [
|
||||
lfg[0],
|
||||
{
|
||||
name: 'Please start grouping up with the other members of this activity:',
|
||||
value: members,
|
||||
},
|
||||
],
|
||||
}],
|
||||
});
|
||||
}
|
||||
});
|
||||
editMsg += `your ${lfgActivity} starts in less than 10 minutes.`;
|
||||
|
||||
await message.edit({
|
||||
content: editMsg,
|
||||
});
|
||||
|
||||
activeLFGPosts[i].notified = true;
|
||||
} catch (err) {
|
||||
log(LT.WARN, `Failed to find LFG ${activeLFGPosts[i].ownerId}-${activeLFGPosts[i].lfgUid} | ${jsonStringifyBig(err)}`);
|
||||
|
||||
activeLFGPosts.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
} // Lock LFG from editing/Joining/Leaving
|
||||
else if (!activeLFGPosts[i].locked && activeLFGPosts[i].lfgTime < now) {
|
||||
log(LT.INFO, `Locking LFG ${activeLFGPosts[i].ownerId}-${activeLFGPosts[i].lfgUid}`);
|
||||
try {
|
||||
const message = await getMessage(activeLFGPosts[i].channelId, activeLFGPosts[i].messageId);
|
||||
|
||||
await message.edit({
|
||||
components: [],
|
||||
});
|
||||
|
||||
activeLFGPosts[i].locked = true;
|
||||
} catch (err) {
|
||||
log(LT.WARN, `Failed to find LFG ${activeLFGPosts[i].ownerId}-${activeLFGPosts[i].lfgUid} | ${jsonStringifyBig(err)}`);
|
||||
|
||||
activeLFGPosts.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
} // Delete old LFG post
|
||||
else if (activeLFGPosts[i].lfgTime < (now - tenMin)) {
|
||||
log(LT.INFO, `Deleting LFG ${activeLFGPosts[i].ownerId}-${activeLFGPosts[i].lfgUid}`);
|
||||
await deleteMessage(activeLFGPosts[i].channelId, activeLFGPosts[i].messageId, 'LFG post expired').catch((e) => {
|
||||
log(LT.WARN, `Failed to delete LFG ${activeLFGPosts[i].ownerId}-${activeLFGPosts[i].lfgUid} | ${jsonStringifyBig(e)}`);
|
||||
});
|
||||
activeLFGPosts.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
localStorage.setItem('activeLFGPosts', jsonStringifyBig(activeLFGPosts));
|
||||
};
|
||||
|
||||
export default { getRandomStatus, updateListStatistics, buildingTimeout, lfgNotifier };
|
|
@ -1,14 +0,0 @@
|
|||
import { EmbedField } from '../deps.ts';
|
||||
|
||||
export type JoinLeaveType = {
|
||||
embed: EmbedField[];
|
||||
success: boolean;
|
||||
full: boolean;
|
||||
justFilled: boolean;
|
||||
};
|
||||
|
||||
export type UrlIds = {
|
||||
guildId: bigint;
|
||||
channelId: bigint;
|
||||
messageId: bigint;
|
||||
};
|
|
@ -1,496 +0,0 @@
|
|||
import { ActionRow, ButtonComponent, DiscordButtonStyles, DiscordenoMember, EmbedField, log, LT } from '../deps.ts';
|
||||
|
||||
import { JoinLeaveType, UrlIds } from './lfgHandlers.d.ts';
|
||||
import { BuildingLFG } from './mod.d.ts';
|
||||
import { LFGActivities } from './games.ts';
|
||||
import { determineTZ } from './timeUtils.ts';
|
||||
import { lfgStepQuestions } from './constantCmds.ts';
|
||||
import { jsonStringifyBig } from './utils.ts';
|
||||
|
||||
export const handleLFGStep = async (wipLFG: BuildingLFG, input: string): Promise<BuildingLFG> => {
|
||||
const currentLFG = (wipLFG.lfgMsg.embeds[0] || { fields: undefined }).fields || [
|
||||
{
|
||||
name: '. . .',
|
||||
value: '. . .',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Start Time:',
|
||||
value: '. . .',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Add to Calendar:',
|
||||
value: '. . .',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Description:',
|
||||
value: '. . .',
|
||||
inline: false,
|
||||
},
|
||||
{
|
||||
name: `Members Joined: 0/?`,
|
||||
value: 'None',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Alternates:',
|
||||
value: 'None',
|
||||
inline: true,
|
||||
},
|
||||
];
|
||||
|
||||
let nextQuestion = '';
|
||||
const nextComponents: Array<ActionRow> = [];
|
||||
let editFlag = true;
|
||||
|
||||
switch (wipLFG.step) {
|
||||
case 'set_game': {
|
||||
currentLFG[0].name = input.substr(0, 254);
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(LFGActivities, input)) {
|
||||
nextQuestion = lfgStepQuestions.set_activity_with_button;
|
||||
|
||||
let tempObj = {};
|
||||
Object.entries(LFGActivities).some((e) => {
|
||||
if (e[0] === input) {
|
||||
tempObj = e[1];
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
const activityButtons: Array<ButtonComponent> = Object.keys(tempObj).map((activity) => {
|
||||
return {
|
||||
type: 2,
|
||||
label: activity,
|
||||
customId: `building@set_activity#${activity}`,
|
||||
style: DiscordButtonStyles.Primary,
|
||||
};
|
||||
});
|
||||
|
||||
const temp: Array<ActionRow['components']> = [];
|
||||
|
||||
activityButtons.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,
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
nextQuestion = lfgStepQuestions.set_activity_with_text;
|
||||
}
|
||||
|
||||
wipLFG.step = 'set_activity';
|
||||
break;
|
||||
}
|
||||
case 'set_activity': {
|
||||
const game = currentLFG[0].name;
|
||||
|
||||
let tempObj;
|
||||
Object.entries(LFGActivities).some((e) => {
|
||||
if (e[0] === game) {
|
||||
Object.entries(e[1]).some((f) => {
|
||||
if (f[0] === input) {
|
||||
tempObj = f[1];
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
currentLFG[0].name = `${game}:`;
|
||||
currentLFG[0].value = input.substr(0, 1023);
|
||||
|
||||
if (typeof tempObj === 'number') {
|
||||
// Activity
|
||||
currentLFG[4].name = `Members Joined: ${currentLFG[4].value === 'None' ? 0 : currentLFG[4].value.split('\n').length}/${tempObj}`;
|
||||
|
||||
nextQuestion = wipLFG.editing ? lfgStepQuestions.set_done : lfgStepQuestions.set_time;
|
||||
|
||||
wipLFG.step = wipLFG.editing ? 'done' : 'set_time';
|
||||
} else if (!tempObj) {
|
||||
// Custom
|
||||
nextQuestion = lfgStepQuestions.set_player_cnt;
|
||||
|
||||
wipLFG.step = 'set_player_cnt';
|
||||
} else {
|
||||
// Category
|
||||
nextQuestion = lfgStepQuestions.set_activity_from_category;
|
||||
|
||||
currentLFG[0].name = game;
|
||||
|
||||
const activityButtons: Array<ButtonComponent> = Object.keys(tempObj).map((activity) => {
|
||||
return {
|
||||
type: 2,
|
||||
label: activity,
|
||||
customId: `building@set_activity_from_category#${activity}`,
|
||||
style: DiscordButtonStyles.Primary,
|
||||
};
|
||||
});
|
||||
|
||||
const temp: Array<ActionRow['components']> = [];
|
||||
|
||||
activityButtons.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,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
wipLFG.step = 'set_activity_from_category';
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case 'set_activity_from_category': {
|
||||
const game = currentLFG[0].name;
|
||||
const category = currentLFG[0].value;
|
||||
|
||||
let tempObj;
|
||||
Object.entries(LFGActivities).some((e) => {
|
||||
if (e[0] === game) {
|
||||
Object.entries(e[1]).some((f) => {
|
||||
if (f[0] === category) {
|
||||
Object.entries(f[1]).some((g) => {
|
||||
if (g[0] === input) {
|
||||
tempObj = g[1];
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
currentLFG[0].name = `${game}:`;
|
||||
currentLFG[0].value = input.substr(0, 1023);
|
||||
|
||||
if (tempObj) {
|
||||
currentLFG[4].name = `Members Joined: ${currentLFG[4].value === 'None' ? 0 : currentLFG[4].value.split('\n').length}/${tempObj}`;
|
||||
|
||||
nextQuestion = wipLFG.editing ? lfgStepQuestions.set_done : lfgStepQuestions.set_time;
|
||||
|
||||
wipLFG.step = wipLFG.editing ? 'done' : 'set_time';
|
||||
} else {
|
||||
nextQuestion = lfgStepQuestions.set_player_cnt;
|
||||
|
||||
wipLFG.step = 'set_player_cnt';
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'set_player_cnt': {
|
||||
if (parseInt(input)) {
|
||||
currentLFG[4].name = `Members Joined: ${currentLFG[4].value === 'None' ? 0 : currentLFG[4].value.split('\n').length}/${Math.abs(parseInt(input)) || 1}`;
|
||||
|
||||
nextQuestion = wipLFG.editing ? lfgStepQuestions.set_done : lfgStepQuestions.set_time;
|
||||
|
||||
wipLFG.step = wipLFG.editing ? 'done' : 'set_time';
|
||||
} else {
|
||||
editFlag = false;
|
||||
|
||||
nextQuestion = `Input max members "${input}" is invalid, please make sure you are only entering a number.\n\n${lfgStepQuestions.set_player_cnt}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'set_time': {
|
||||
const today = new Date();
|
||||
|
||||
let lfgDate = `${today.getMonth() + 1}/${today.getDate()}`,
|
||||
lfgTime = '',
|
||||
lfgTZ = '',
|
||||
lfgPeriod = '',
|
||||
overrodeTZ = false;
|
||||
|
||||
input.split(' ').forEach((c) => {
|
||||
if (c.includes('/')) {
|
||||
lfgDate = c;
|
||||
} else if (c.toLowerCase() === 'am' || c.toLowerCase() === 'pm') {
|
||||
lfgPeriod = c.toLowerCase();
|
||||
} else if (c.toLowerCase().includes('am') || c.toLowerCase().includes('pm')) {
|
||||
lfgTime = c.substr(0, c.length - 2);
|
||||
lfgPeriod = c.toLowerCase().includes('am') ? 'am' : 'pm';
|
||||
} else if (c.includes(':')) {
|
||||
lfgTime = c;
|
||||
} else if (parseInt(c).toString() === (c.replace(/^0+/, '') || '0')) {
|
||||
if (c.length === 4) {
|
||||
if (parseInt(c) >= 1300) {
|
||||
lfgTime = (parseInt(c) - 1200).toString();
|
||||
lfgPeriod = 'pm';
|
||||
} else if (parseInt(c) >= 1200) {
|
||||
lfgTime = c;
|
||||
lfgPeriod = 'pm';
|
||||
} else {
|
||||
lfgTime = c.startsWith('00') ? `12${c.substr(2)}` : c;
|
||||
lfgPeriod = 'am';
|
||||
}
|
||||
|
||||
const hourLen = lfgTime.length === 4 ? 2 : 1;
|
||||
lfgTime = `${lfgTime.substr(0, hourLen)}:${lfgTime.substr(hourLen)}`;
|
||||
} else {
|
||||
lfgTime = c;
|
||||
}
|
||||
} else if (c.match(/^\d/)) {
|
||||
const tzIdx = c.search(/[a-zA-Z]/);
|
||||
lfgTime = c.substr(0, tzIdx);
|
||||
[lfgTZ, overrodeTZ] = determineTZ(c.substr(tzIdx));
|
||||
} else {
|
||||
[lfgTZ, overrodeTZ] = determineTZ(c);
|
||||
}
|
||||
});
|
||||
|
||||
if (!lfgTZ) {
|
||||
[lfgTZ, overrodeTZ] = determineTZ('ET');
|
||||
}
|
||||
|
||||
if (!lfgTime.includes(':')) {
|
||||
lfgTime += ':00';
|
||||
}
|
||||
|
||||
if (!lfgPeriod) {
|
||||
lfgPeriod = today.getHours() >= 12 ? 'pm' : 'am';
|
||||
}
|
||||
|
||||
lfgPeriod = lfgPeriod.toUpperCase();
|
||||
lfgTZ = lfgTZ.toUpperCase();
|
||||
|
||||
lfgDate = `${lfgDate.split('/')[0]}/${lfgDate.split('/')[1]}/${today.getFullYear()}`;
|
||||
|
||||
log(LT.LOG, `Date Time Debug | ${lfgTime} ${lfgPeriod} ${lfgTZ} ${lfgDate}`);
|
||||
|
||||
const lfgDateTime = new Date(`${lfgTime} ${lfgPeriod} ${lfgTZ} ${lfgDate}`);
|
||||
lfgDate = `${lfgDate.split('/')[0]}/${lfgDate.split('/')[1]}`;
|
||||
const lfgDateStr = `[${lfgTime} ${lfgPeriod} ${lfgTZ} ${lfgDate}](https://groupup.eanm.dev/tz#${lfgDateTime.getTime()})`;
|
||||
|
||||
const icsDetails = `${currentLFG[0].name} ${currentLFG[0].value}`;
|
||||
const icsStr = `[Download ICS File](https://groupup.eanm.dev/ics?t=${lfgDateTime.getTime()}&n=${icsDetails.replaceAll(' ', '+')})`;
|
||||
|
||||
currentLFG[1].name = 'Start Time (Click for Conversion):';
|
||||
currentLFG[1].value = lfgDateStr.substr(0, 1023);
|
||||
currentLFG[2].value = icsStr.substr(0, 1023);
|
||||
|
||||
if (isNaN(lfgDateTime.getTime())) {
|
||||
nextQuestion =
|
||||
`Input time "${input}" (parsed as "${lfgTime} ${lfgPeriod} ${lfgTZ} ${lfgDate}") is invalid, please make sure you have the timezone set correctly.\n\n${lfgStepQuestions.set_time}`;
|
||||
|
||||
editFlag = false;
|
||||
} else if (lfgDateTime.getTime() <= today.getTime()) {
|
||||
nextQuestion =
|
||||
`Input time "${input}" (parsed as "${lfgTime} ${lfgPeriod} ${lfgTZ} ${lfgDate}") is in the past, please make sure you are setting up the event to be in the future.\n\n${lfgStepQuestions.set_time}`;
|
||||
|
||||
editFlag = false;
|
||||
} else {
|
||||
nextQuestion = wipLFG.editing ? lfgStepQuestions.set_done : lfgStepQuestions.set_desc;
|
||||
|
||||
wipLFG.step = wipLFG.editing ? 'done' : 'set_desc';
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'set_desc': {
|
||||
if (input === 'none') {
|
||||
input = currentLFG[0].value;
|
||||
}
|
||||
|
||||
currentLFG[3].value = input.substr(0, 1023);
|
||||
|
||||
nextQuestion = lfgStepQuestions.set_done;
|
||||
|
||||
wipLFG.step = 'done';
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
if (editFlag) {
|
||||
wipLFG.lfgMsg = await wipLFG.lfgMsg.edit({
|
||||
embeds: [{
|
||||
fields: currentLFG,
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
wipLFG.questionMsg = await wipLFG.questionMsg.edit({
|
||||
content: nextQuestion,
|
||||
components: nextComponents,
|
||||
});
|
||||
} catch (e) {
|
||||
log(LT.WARN, `Failed to edit active builder | ${wipLFG.userId}-${wipLFG.channelId} | ${jsonStringifyBig(e)}`);
|
||||
}
|
||||
|
||||
return wipLFG;
|
||||
};
|
||||
|
||||
export const handleMemberJoin = (lfg: EmbedField[], member: DiscordenoMember, alternate: boolean): JoinLeaveType => {
|
||||
let success = false;
|
||||
let justFilled = false;
|
||||
|
||||
const userStr = `${member.username} - <@${member.id}>`;
|
||||
|
||||
const tempMembers = lfg[4].name.split(':')[1].split('/');
|
||||
let currentMembers = parseInt(tempMembers[0]);
|
||||
const maxMembers = parseInt(tempMembers[1]);
|
||||
|
||||
if (alternate && !lfg[5].value.includes(member.id.toString())) {
|
||||
// remove from joined list
|
||||
if (lfg[4].value.includes(member.id.toString())) {
|
||||
const tempArr = lfg[4].value.split('\n');
|
||||
const memberIdx = tempArr.findIndex((m) => m.includes(member.id.toString()));
|
||||
tempArr.splice(memberIdx, 1);
|
||||
lfg[4].value = tempArr.join('\n') || 'None';
|
||||
|
||||
if (currentMembers) {
|
||||
currentMembers--;
|
||||
}
|
||||
lfg[4].name = `Members Joined: ${currentMembers}/${maxMembers}`;
|
||||
}
|
||||
|
||||
if (lfg[5].value === 'None') {
|
||||
lfg[5].value = userStr;
|
||||
} else {
|
||||
lfg[5].value += `\n${userStr}`;
|
||||
}
|
||||
|
||||
success = true;
|
||||
} else if (!alternate && currentMembers < maxMembers && !lfg[4].value.includes(member.id.toString())) {
|
||||
// remove from alternate list
|
||||
if (lfg[5].value.includes(member.id.toString())) {
|
||||
const tempArr = lfg[5].value.split('\n');
|
||||
const memberIdx = tempArr.findIndex((m) => m.includes(member.id.toString()));
|
||||
tempArr.splice(memberIdx, 1);
|
||||
lfg[5].value = tempArr.join('\n') || 'None';
|
||||
}
|
||||
|
||||
if (lfg[4].value === 'None') {
|
||||
lfg[4].value = userStr;
|
||||
} else {
|
||||
lfg[4].value += `\n${userStr}`;
|
||||
}
|
||||
currentMembers++;
|
||||
justFilled = currentMembers === maxMembers;
|
||||
|
||||
lfg[4].name = `Members Joined: ${currentMembers}/${maxMembers}`;
|
||||
|
||||
success = true;
|
||||
} else if (!alternate && currentMembers === maxMembers && !lfg[4].value.includes(member.id.toString())) {
|
||||
// update user in alternate list to include the * to make them autojoin
|
||||
if (lfg[5].value.includes(member.id.toString())) {
|
||||
const tempArr = lfg[5].value.split('\n');
|
||||
const memberIdx = tempArr.findIndex((m) => m.includes(member.id.toString()));
|
||||
tempArr[memberIdx] = `${tempArr[memberIdx]} *`;
|
||||
lfg[5].value = tempArr.join('\n');
|
||||
} else {
|
||||
if (lfg[5].value === 'None') {
|
||||
lfg[5].value = `${userStr} *`;
|
||||
} else {
|
||||
lfg[5].value += `\n${userStr} *`;
|
||||
}
|
||||
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
embed: lfg,
|
||||
success: success,
|
||||
full: currentMembers === maxMembers,
|
||||
justFilled: justFilled,
|
||||
};
|
||||
};
|
||||
|
||||
export const handleMemberLeave = (lfg: EmbedField[], member: DiscordenoMember): JoinLeaveType => {
|
||||
let success = false;
|
||||
|
||||
const memberId = member.id.toString();
|
||||
|
||||
const tempMembers = lfg[4].name.split(':')[1].split('/');
|
||||
let currentMembers = parseInt(tempMembers[0]);
|
||||
const maxMembers = parseInt(tempMembers[1]);
|
||||
|
||||
if (lfg[4].value.includes(memberId)) {
|
||||
const tempArr = lfg[4].value.split('\n');
|
||||
const memberIdx = tempArr.findIndex((m) => m.includes(memberId));
|
||||
tempArr.splice(memberIdx, 1);
|
||||
lfg[4].value = tempArr.join('\n') || 'None';
|
||||
|
||||
if (lfg[5].value.includes('*')) {
|
||||
// find first * user and move them to the joined list
|
||||
const tempArr2 = lfg[5].value.split('\n');
|
||||
const memberToMoveIdx = tempArr2.findIndex((m) => m.includes('*'));
|
||||
let memberToMove = tempArr2[memberToMoveIdx];
|
||||
memberToMove = memberToMove.substr(0, memberToMove.length - 2);
|
||||
tempArr.push(memberToMove);
|
||||
lfg[4].value = tempArr.join('\n') || 'None';
|
||||
|
||||
// Remove them from the alt list
|
||||
tempArr2.splice(memberToMoveIdx, 1);
|
||||
lfg[5].value = tempArr2.join('\n') || 'None';
|
||||
} else {
|
||||
// update count since no users were marked as *
|
||||
if (currentMembers) {
|
||||
currentMembers--;
|
||||
}
|
||||
lfg[4].name = `Members Joined: ${currentMembers}/${maxMembers}`;
|
||||
}
|
||||
|
||||
success = true;
|
||||
}
|
||||
|
||||
if (lfg[5].value.includes(memberId)) {
|
||||
const tempArr = lfg[5].value.split('\n');
|
||||
const memberIdx = tempArr.findIndex((m) => m.includes(memberId));
|
||||
tempArr.splice(memberIdx, 1);
|
||||
lfg[5].value = tempArr.join('\n') || 'None';
|
||||
|
||||
success = true;
|
||||
}
|
||||
|
||||
return {
|
||||
embed: lfg,
|
||||
success: success,
|
||||
full: currentMembers === maxMembers,
|
||||
justFilled: false,
|
||||
};
|
||||
};
|
||||
|
||||
export const urlToIds = (url: string): UrlIds => {
|
||||
const strIds = {
|
||||
guildId: '',
|
||||
channelId: '',
|
||||
messageId: '',
|
||||
};
|
||||
|
||||
url = url.toLowerCase();
|
||||
|
||||
[strIds.guildId, strIds.channelId, strIds.messageId] = url.substr(url.indexOf('channels') + 9).split('/');
|
||||
|
||||
return {
|
||||
guildId: BigInt(strIds.guildId),
|
||||
channelId: BigInt(strIds.channelId),
|
||||
messageId: BigInt(strIds.messageId),
|
||||
};
|
||||
};
|
|
@ -1,37 +0,0 @@
|
|||
import { DiscordenoMessage } from '../deps.ts';
|
||||
|
||||
export type BuildingLFG = {
|
||||
userId: bigint;
|
||||
channelId: bigint;
|
||||
step: string;
|
||||
lfgMsg: DiscordenoMessage;
|
||||
questionMsg: DiscordenoMessage;
|
||||
lastTouch: Date;
|
||||
maxIdle: number;
|
||||
editing: boolean;
|
||||
};
|
||||
|
||||
export type ActiveLFG = {
|
||||
messageId: bigint;
|
||||
channelId: bigint;
|
||||
ownerId: bigint;
|
||||
lfgUid: string;
|
||||
lfgTime: number;
|
||||
notified: boolean;
|
||||
locked: boolean;
|
||||
};
|
||||
|
||||
export type GuildPrefixes = {
|
||||
guildId: bigint;
|
||||
prefix: string;
|
||||
};
|
||||
|
||||
export type GuildModRoles = {
|
||||
guildId: bigint;
|
||||
roleId: bigint;
|
||||
};
|
||||
|
||||
export type GuildCleanChannels = {
|
||||
guildId: bigint;
|
||||
channelId: bigint;
|
||||
};
|
|
@ -0,0 +1,237 @@
|
|||
import config from '../config.ts';
|
||||
import { Bot } from '../deps.ts';
|
||||
import { LfgEmbedIndexes } from './buttons/eventUtils.ts';
|
||||
import { getEventMemberCount, getGuildName, getLfgMembers } from './buttons/live-event/utils.ts';
|
||||
import { failColor, infoColor1, sendDirectMessage, warnColor } from './commandUtils.ts';
|
||||
import { reportSlashName } from './commands/slashCommandNames.ts';
|
||||
import { dbClient, queries } from './db.ts';
|
||||
import { ActiveEvent } from './types/commandTypes.ts';
|
||||
import utils from './utils.ts';
|
||||
|
||||
const notifyStepName = 'notify';
|
||||
const lockStepName = 'lock';
|
||||
const deleteStepName = 'delete';
|
||||
|
||||
export const tenMinutes = 10 * 60 * 1000;
|
||||
|
||||
// Join strings with english in mind
|
||||
const joinWithAnd = (words: string[]) => {
|
||||
if (words.length === 0) {
|
||||
return '';
|
||||
} else if (words.length === 1) {
|
||||
return words[0];
|
||||
} else if (words.length === 2) {
|
||||
return words.join(' and ');
|
||||
} else {
|
||||
return words.slice(0, -1).join(', ') + ', and ' + words.slice(-1);
|
||||
}
|
||||
};
|
||||
|
||||
// Log the failure in a loud sense
|
||||
const loudLogFailure = async (bot: Bot, event: ActiveEvent, stepName: string, secondFailure = false) => {
|
||||
const guildName = await getGuildName(bot, event.guildId);
|
||||
const eventUrl = utils.idsToMessageUrl({
|
||||
guildId: event.guildId,
|
||||
channelId: event.channelId,
|
||||
messageId: event.messageId,
|
||||
});
|
||||
|
||||
// Try to DM owner if this is the second time it has failed
|
||||
let dmSuccess = false;
|
||||
if (secondFailure) {
|
||||
const msg = await sendDirectMessage(bot, event.ownerId, {
|
||||
embeds: [{
|
||||
color: failColor,
|
||||
title: `Attention: Failed to ${stepName} one of your events`,
|
||||
description:
|
||||
`${config.name} tried twice to find [this event](${eventUrl}) in ${guildName}, and could not either time. Since ${config.name} has failed twice, ${config.name} has now removed [this event](${eventUrl}) from its list of active events.
|
||||
|
||||
[This event](${eventUrl}) was scheduled to start at <t:${event.eventTime.getTime() / 1000}:F>.
|
||||
|
||||
The message containing this event may have been deleted by a moderator or administrator in ${guildName}. If [the event](${eventUrl}) still exists when you click on the link above, please \`/${reportSlashName}\` this issue to the developers with the full error code below.`,
|
||||
fields: [{
|
||||
name: 'Error Code:',
|
||||
value: `\`loudLog@${event.guildId}|${event.channelId}|${event.messageId}|${event.ownerId}|${event.eventTime.getTime()}|${event.notifiedFlag}|${event.lockedFlag}@\``,
|
||||
}],
|
||||
}],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('notificationSystem.ts@loudLog', 'send DM fail', e));
|
||||
dmSuccess = Boolean(msg);
|
||||
}
|
||||
|
||||
// Log this to bot's log channel
|
||||
bot.helpers.sendMessage(config.logChannel, {
|
||||
content: secondFailure ? `Hey <@${config.owner}>, something may have gone wrong. The owner of this event was ${dmSuccess ? 'SUCCESSFULLY' : 'NOT'} notified.` : undefined,
|
||||
embeds: [{
|
||||
color: secondFailure ? failColor : warnColor,
|
||||
title: `Failed to ${stepName} an event in ${guildName}. This is the ${secondFailure ? 'second' : 'first'} attempt.`,
|
||||
description: `${config.name} failed to ${stepName} [this event](${eventUrl}).\n\nDebug Data:`,
|
||||
fields: [{
|
||||
name: 'Guild ID:',
|
||||
value: `${event.guildId}`,
|
||||
inline: true,
|
||||
}, {
|
||||
name: 'Channel ID:',
|
||||
value: `${event.channelId}`,
|
||||
inline: true,
|
||||
}, {
|
||||
name: 'Message ID:',
|
||||
value: `${event.messageId}`,
|
||||
inline: true,
|
||||
}, {
|
||||
name: 'Owner ID:',
|
||||
value: `${event.ownerId}`,
|
||||
inline: true,
|
||||
}, {
|
||||
name: 'Event Time:',
|
||||
value: `<t:${event.eventTime.getTime() / 1000}:F>`,
|
||||
inline: true,
|
||||
}, {
|
||||
name: 'Notified Flag:',
|
||||
value: `${event.notifiedFlag}`,
|
||||
inline: true,
|
||||
}, {
|
||||
name: 'Locked Flag:',
|
||||
value: `${event.lockedFlag}`,
|
||||
inline: true,
|
||||
}],
|
||||
}],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('notificationSystem.ts@loudLog', 'send log message', e));
|
||||
};
|
||||
|
||||
// Notifies all members of the event and edits the event message
|
||||
export const notifyEventMembers = async (bot: Bot, event: ActiveEvent, secondTry = false): Promise<boolean> => {
|
||||
const eventMessage = await bot.helpers.getMessage(event.channelId, event.messageId).catch((e: Error) => utils.commonLoggers.messageGetError('notificationSystem.ts@notify', 'get event', e));
|
||||
if (eventMessage?.embeds[0].fields) {
|
||||
const activityName = `\`${eventMessage.embeds[0].fields[LfgEmbedIndexes.Activity].name} ${eventMessage.embeds[0].fields[LfgEmbedIndexes.Activity].value}\``;
|
||||
const members = getLfgMembers(eventMessage.embeds[0].fields[LfgEmbedIndexes.JoinedMembers].value);
|
||||
const memberMentionString = joinWithAnd(members.map((member) => `<@${member.id}>`));
|
||||
const guildName = await getGuildName(bot, event.guildId);
|
||||
|
||||
// Edit event in guild
|
||||
await bot.helpers.editMessage(event.channelId, event.messageId, {
|
||||
content: `Attention ${memberMentionString}, your ${activityName} starts in less than 10 minutes.`,
|
||||
}).catch((e: Error) => utils.commonLoggers.messageEditError('notificationSystem.ts@notify', 'event edit fail', e));
|
||||
|
||||
// Send the notifications to the members
|
||||
members.forEach(async (member) => {
|
||||
await sendDirectMessage(bot, member.id, {
|
||||
embeds: [{
|
||||
color: infoColor1,
|
||||
title: `Hello ${member.name}! Your activity in ${guildName} starts in less than 10 minutes.`,
|
||||
description: 'Please start grouping up with the other members of this activity:',
|
||||
}, eventMessage.embeds[0]],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('notificationSystem.ts@notify', 'send DM fail', e));
|
||||
});
|
||||
|
||||
// Update DB to indicate notifications have been sent out
|
||||
dbClient.execute(queries.updateEventFlags(1, 0), [event.channelId, event.messageId]).catch((e) => utils.commonLoggers.dbError('notificationSystem.ts@notifySuccess', 'update event in', e));
|
||||
return true;
|
||||
} else {
|
||||
if (!secondTry) loudLogFailure(bot, event, notifyStepName);
|
||||
// Update DB to indicate notifications have not been sent out
|
||||
dbClient.execute(queries.updateEventFlags(-1, 0), [event.channelId, event.messageId]).catch((e) => utils.commonLoggers.dbError('notificationSystem.ts@notifyFail', 'update event in', e));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Locks the event message and notifies alternates if necessary
|
||||
export const lockEvent = async (bot: Bot, event: ActiveEvent, secondTry = false): Promise<boolean> => {
|
||||
const eventMessage = await bot.helpers.getMessage(event.channelId, event.messageId).catch((e: Error) => utils.commonLoggers.messageGetError('notificationSystem.ts@lock', 'get event', e));
|
||||
if (eventMessage?.embeds[0].fields) {
|
||||
const [currentMemberCount, maxMemberCount] = getEventMemberCount(eventMessage.embeds[0].fields[LfgEmbedIndexes.JoinedMembers].name);
|
||||
const alternates = getLfgMembers(eventMessage.embeds[0].fields[LfgEmbedIndexes.AlternateMembers].value);
|
||||
const memberMentionString = joinWithAnd(alternates.map((member) => `<@${member.id}>`));
|
||||
|
||||
// See if event was filled or not, and if not notify alternates
|
||||
const alternatesNeeded = alternates.length && currentMemberCount < maxMemberCount;
|
||||
if (alternatesNeeded) {
|
||||
const activityName = `\`${eventMessage.embeds[0].fields[LfgEmbedIndexes.Activity].name} ${eventMessage.embeds[0].fields[LfgEmbedIndexes.Activity].value}\``;
|
||||
const guildName = await getGuildName(bot, event.guildId);
|
||||
const peopleShort = maxMemberCount - currentMemberCount;
|
||||
|
||||
// Send the notifications to the members
|
||||
alternates.forEach(async (member) => {
|
||||
await sendDirectMessage(bot, member.id, {
|
||||
embeds: [{
|
||||
color: infoColor1,
|
||||
title: `Hello ${member.name}! An activity in ${guildName} may need your help.`,
|
||||
description: `The ${activityName} in ${guildName} that you marked yourself as an alternate for may be \`${peopleShort}\` ${
|
||||
peopleShort === 1 ? 'person' : 'people'
|
||||
} short. If you are available, please join up with them.`,
|
||||
}, eventMessage.embeds[0]],
|
||||
}).catch((e: Error) => utils.commonLoggers.messageSendError('notificationSystem.ts@lock', 'send DM fail', e));
|
||||
});
|
||||
}
|
||||
|
||||
// Edit event in guild
|
||||
await bot.helpers.editMessage(
|
||||
event.channelId,
|
||||
event.messageId,
|
||||
alternatesNeeded
|
||||
? {
|
||||
content: `${eventMessage.content}\n\nAttention ${memberMentionString}, this activity is \`${maxMemberCount - currentMemberCount}\` people short. Please join up if you are available.`,
|
||||
components: [],
|
||||
}
|
||||
: {
|
||||
components: [],
|
||||
},
|
||||
).catch((e: Error) => utils.commonLoggers.messageEditError('notificationSystem.ts@lock', 'event edit fail', e));
|
||||
|
||||
// Update DB to indicate event has been locked
|
||||
dbClient.execute(queries.updateEventFlags(1, 1), [event.channelId, event.messageId]).catch((e) => utils.commonLoggers.dbError('notificationSystem.ts@lockSuccess', 'update event in', e));
|
||||
return true;
|
||||
} else {
|
||||
if (!secondTry) loudLogFailure(bot, event, lockStepName);
|
||||
// Update DB to indicate event has not been locked
|
||||
dbClient.execute(queries.updateEventFlags(1, -1), [event.channelId, event.messageId]).catch((e) => utils.commonLoggers.dbError('notificationSystem.ts@lockFail', 'update event in', e));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Notifies all members of the event and edits the event message
|
||||
export const deleteEvent = async (bot: Bot, event: ActiveEvent, secondTry = false): Promise<boolean> => {
|
||||
const eventMessage = await bot.helpers.getMessage(event.channelId, event.messageId).catch((e: Error) => utils.commonLoggers.messageGetError('notificationSystem.ts@delete', 'get event', e));
|
||||
if (eventMessage?.embeds[0].fields) {
|
||||
// Delete event in guild
|
||||
await bot.helpers.deleteMessage(event.channelId, event.messageId, 'Cleaning up activity that has started').catch((e: Error) =>
|
||||
utils.commonLoggers.messageDeleteError('notificationSystem.ts@delete', 'event delete fail', e)
|
||||
);
|
||||
|
||||
// Remove event from DB
|
||||
dbClient.execute(queries.deleteEvent, [event.channelId, event.messageId]).catch((e) => utils.commonLoggers.dbError('notificationSystem.ts@deleteSuccess', 'delete event from', e));
|
||||
return true;
|
||||
} else {
|
||||
if (!secondTry) loudLogFailure(bot, event, deleteStepName);
|
||||
// Update DB to indicate delete failed
|
||||
dbClient.execute(queries.updateEventFlags(-1, -1), [event.channelId, event.messageId]).catch((e) => utils.commonLoggers.dbError('notificationSystem.ts@deleteFail', 'update event in', e));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Handles trying again once and cleaning up events that died
|
||||
export const handleFailures = async (bot: Bot, event: ActiveEvent) => {
|
||||
let rerunSuccess: boolean;
|
||||
let stepName: string;
|
||||
// Retry the step that failed
|
||||
if (event.notifiedFlag === -1 && event.lockedFlag === -1) {
|
||||
rerunSuccess = await deleteEvent(bot, event, true);
|
||||
stepName = deleteStepName;
|
||||
} else if (event.lockedFlag === -1) {
|
||||
rerunSuccess = await lockEvent(bot, event, true);
|
||||
stepName = lockStepName;
|
||||
} else if (event.notifiedFlag === -1) {
|
||||
rerunSuccess = await notifyEventMembers(bot, event, true);
|
||||
stepName = notifyStepName;
|
||||
} else {
|
||||
// Should never get here as this func should only be called when event has one of the flags as -1
|
||||
// Set flag to true since it already succeeded?
|
||||
rerunSuccess = true;
|
||||
stepName = '';
|
||||
}
|
||||
|
||||
if (!rerunSuccess) {
|
||||
// Failed at completing a step! Event may have been deleted?
|
||||
loudLogFailure(bot, event, stepName, true);
|
||||
dbClient.execute(queries.deleteEvent, [event.channelId, event.messageId]).catch((e) => utils.commonLoggers.dbError('notificationSystem.ts@handleFailures', 'delete event from', e));
|
||||
}
|
||||
};
|
|
@ -1,22 +0,0 @@
|
|||
export const determineTZ = (tz: string, userOverride = false): [string, boolean] => {
|
||||
tz = tz.toUpperCase();
|
||||
|
||||
let overrode = false;
|
||||
const shortHandUSTZ = (tz === 'ET' || tz === 'CT' || tz === 'MT' || tz === 'PT');
|
||||
const fullUSTZ = (tz.length === 3 && (tz.startsWith('E') || tz.startsWith('C') || tz.startsWith('M') || tz.startsWith('P')) && (tz.endsWith('DT') || tz.endsWith('ST')));
|
||||
|
||||
if (!userOverride && (shortHandUSTZ || fullUSTZ)) {
|
||||
const today = new Date();
|
||||
const jan = new Date(today.getFullYear(), 0, 1);
|
||||
const jul = new Date(today.getFullYear(), 6, 1);
|
||||
if (today.getTimezoneOffset() < Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset())) {
|
||||
if (tz.includes('S')) overrode = true;
|
||||
tz = `${tz.substring(0, 1)}DT`;
|
||||
} else {
|
||||
if (tz.includes('D')) overrode = true;
|
||||
tz = `${tz.substring(0, 1)}ST`;
|
||||
}
|
||||
}
|
||||
|
||||
return [tz, overrode];
|
||||
};
|
|
@ -0,0 +1,55 @@
|
|||
import { ApplicationCommandOption, ApplicationCommandTypes, PermissionStrings } from '../../deps.ts';
|
||||
|
||||
export type CommandDetails = {
|
||||
name: string;
|
||||
description: string;
|
||||
type: ApplicationCommandTypes;
|
||||
options?: ApplicationCommandOption[];
|
||||
dmPermission?: boolean;
|
||||
defaultMemberPermissions?: PermissionStrings[];
|
||||
};
|
||||
|
||||
export type Command = {
|
||||
details: CommandDetails;
|
||||
execute: Function;
|
||||
};
|
||||
|
||||
export type Button = {
|
||||
customId: string;
|
||||
execute: Function;
|
||||
};
|
||||
|
||||
export type LfgChannelSetting = {
|
||||
managed: boolean;
|
||||
managerRoleId: bigint;
|
||||
logChannelId: bigint;
|
||||
};
|
||||
|
||||
export type DBGuildSettings = {
|
||||
guildId: bigint;
|
||||
lfgChannelId: bigint;
|
||||
managerRoleId: bigint;
|
||||
logChannelId: bigint;
|
||||
};
|
||||
|
||||
export type LFGMember = {
|
||||
id: bigint;
|
||||
name: string;
|
||||
joined?: boolean;
|
||||
};
|
||||
|
||||
export type UrlIds = {
|
||||
guildId: bigint;
|
||||
channelId: bigint;
|
||||
messageId: bigint;
|
||||
};
|
||||
|
||||
export type ActiveEvent = {
|
||||
messageId: bigint;
|
||||
channelId: bigint;
|
||||
guildId: bigint;
|
||||
ownerId: bigint;
|
||||
eventTime: Date;
|
||||
notifiedFlag: number;
|
||||
lockedFlag: number;
|
||||
};
|
63
src/utils.ts
63
src/utils.ts
|
@ -1,12 +1,57 @@
|
|||
export const jsonParseBig = (input: string) => {
|
||||
return JSON.parse(input, (_key, value) => {
|
||||
if (typeof value === 'string' && /^\d+n$/.test(value)) {
|
||||
return BigInt(value.substring(0, value.length - 1));
|
||||
}
|
||||
return value;
|
||||
});
|
||||
};
|
||||
import { CreateMessage, Interaction, log, LT, Message } from '../deps.ts';
|
||||
import { UrlIds } from './types/commandTypes.ts';
|
||||
|
||||
export const jsonStringifyBig = (input: any) => {
|
||||
const jsonStringifyBig = (input: any) => {
|
||||
return JSON.stringify(input, (_key, value) => typeof value === 'bigint' ? value.toString() + 'n' : value);
|
||||
};
|
||||
|
||||
// Get/Generate Discord Message URL
|
||||
const idsToMessageUrl = (ids: UrlIds) => `https://discord.com/channels/${ids.guildId}/${ids.channelId}/${ids.messageId}`;
|
||||
const messageUrlToIds = (url: string): UrlIds => {
|
||||
url = url.toLowerCase();
|
||||
const [guildId, channelId, messageId] = (url.split('channels/')[1] || '').split('/');
|
||||
|
||||
return {
|
||||
guildId: BigInt(guildId || '0'),
|
||||
channelId: BigInt(channelId || '0'),
|
||||
messageId: BigInt(messageId || '0'),
|
||||
};
|
||||
};
|
||||
|
||||
const capitalizeFirstChar = (input: string) => `${input.charAt(0).toUpperCase()}${input.slice(1)}`;
|
||||
|
||||
const interactionSendError = (location: string, interaction: Interaction | string, err: Error) =>
|
||||
log(LT.ERROR, `${location} | Failed to respond to interaction: ${jsonStringifyBig(interaction)} | Error: ${err.name} - ${err.message}`);
|
||||
const messageEditError = (location: string, message: Message | string, err: Error) =>
|
||||
log(LT.ERROR, `${location} | Failed to edit message: ${jsonStringifyBig(message)} | Error: ${err.name} - ${err.message}`);
|
||||
const messageGetError = (location: string, message: Message | string, err: Error) =>
|
||||
log(LT.ERROR, `${location} | Failed to get message: ${jsonStringifyBig(message)} | Error: ${err.name} - ${err.message}`);
|
||||
const messageSendError = (location: string, message: Message | CreateMessage | string, err: Error) =>
|
||||
log(LT.ERROR, `${location} | Failed to send message: ${jsonStringifyBig(message)} | Error: ${err.name} - ${err.message}`);
|
||||
const messageDeleteError = (location: string, message: Message | string, err: Error) =>
|
||||
log(LT.ERROR, `${location} | Failed to delete message: ${jsonStringifyBig(message)} | Error: ${err.name} - ${err.message}`);
|
||||
const reactionAddError = (location: string, message: Message | string, err: Error, emoji: string) =>
|
||||
log(LT.ERROR, `${location} | Failed to add emoji (${emoji}) to message: ${jsonStringifyBig(message)} | Error: ${err.name} - ${err.message}`);
|
||||
const reactionDeleteError = (location: string, message: Message | string, err: Error, emoji: string) =>
|
||||
log(LT.ERROR, `${location} | Failed to delete emoji (${emoji}) from message: ${jsonStringifyBig(message)} | Error: ${err.name} - ${err.message}`);
|
||||
const channelUpdateError = (location: string, message: string, err: Error) => log(LT.ERROR, `${location} | Failed to update channel | ${message} | Error: ${err.name} - ${err.message}`);
|
||||
|
||||
const dbError = (location: string, type: string, err: Error) => log(LT.ERROR, `${location} | Failed to ${type} database | Error: ${err.name} - ${err.message}`);
|
||||
|
||||
export default {
|
||||
capitalizeFirstChar,
|
||||
commonLoggers: {
|
||||
channelUpdateError,
|
||||
dbError,
|
||||
interactionSendError,
|
||||
messageGetError,
|
||||
messageEditError,
|
||||
messageSendError,
|
||||
messageDeleteError,
|
||||
reactionAddError,
|
||||
reactionDeleteError,
|
||||
},
|
||||
jsonStringifyBig,
|
||||
messageUrlToIds,
|
||||
idsToMessageUrl,
|
||||
};
|
||||
|
|
|
@ -1 +1 @@
|
|||
deno run --allow-write=./logs --location=https://groupup.local --allow-net .\mod.ts
|
||||
deno run --allow-write=./logs --allow-net .\mod.ts
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
# TZ Data
|
||||
Since the JS/TS Date implementation does not handle shorthand timezones, we must implement our own solution.
|
||||
|
||||
## Data Source
|
||||
https://en.wikipedia.org/wiki/List_of_tz_database_time_zones?useskin=vector
|
||||
|
||||
## How to use this submodule
|
||||
Simply point Excel to the link above and tell it to rip the `List` data table. Then, pull the four columns listed below into a separate table and save as a CSV without headers.
|
||||
|
||||
- UTC offset STD
|
||||
- UTC offset DST
|
||||
- Time Zone abbreviation STD
|
||||
- Time Zone abbreviation DST
|
||||
|
||||
Drop that CSV into this directory under the name `tzTable.csv` and run `deno run --allow-read=./tzTable.csv ./generateUpdatedTZList.ts`
|
||||
|
||||
## Why not have this built directly into Group Up?
|
||||
This is implemented as a submodule so that there is some manual screening required to verify things do not get parsed wrong due to a bad Excel/Wikipedia export. This also may require tweaks if any formats change in Wikipedia.
|
||||
|
||||
## Current quirks
|
||||
- Most, if not all, `-` signs copied out as `?`. Don't ask me how or why, it just happened. The script fixes this.
|
||||
- Due to doubled TZ abbrs, there is an overrides section. This should be cleared out when `tzTable.csv` is updated.
|
|
@ -0,0 +1,56 @@
|
|||
// Get file and inits
|
||||
const csvTZDataU8 = Deno.readFileSync('./tzTable.csv');
|
||||
const csvTZData = new TextDecoder().decode(csvTZDataU8);
|
||||
const csvRows = csvTZData.split('\r\n');
|
||||
const tzMap: Map<string, string> = new Map();
|
||||
|
||||
// Overrides because the world had to be special
|
||||
const tzOverrides: Array<Array<string>> = [
|
||||
['CDT', '-05:00'],
|
||||
['CST', '-06:00'],
|
||||
['PST', '-08:00'],
|
||||
['IST', '+05:30'],
|
||||
];
|
||||
const abbrOverrides: Array<string> = tzOverrides.map(tzSet => tzSet[0]);
|
||||
|
||||
// Prefill the map
|
||||
for (const override of tzOverrides) {
|
||||
tzMap.set(override[0], override[1]);
|
||||
}
|
||||
|
||||
// Attempt to add tz to the map
|
||||
const attemptAdd = (tzAbbr: string, tzOffset: string) => {
|
||||
if (!abbrOverrides.includes(tzAbbr)) {
|
||||
if (tzMap.has(tzAbbr) && tzMap.get(tzAbbr) !== tzOffset) {
|
||||
console.error(`DOUBLED TZ ABBR WITH DIFF OFFSETS: ${tzAbbr} | ${tzOffset} | ${tzMap.get(tzAbbr)}`)
|
||||
} else {
|
||||
if (!tzAbbr.includes('+') && !tzAbbr.includes('-')) {
|
||||
tzMap.set(tzAbbr, tzOffset);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Get each TZ from the csv
|
||||
for (const row of csvRows) {
|
||||
const [rawSTDOffset, rawDSTOffset, rawSTDAbbr, rawDSTAbbr] = row.replaceAll('?', '-').toUpperCase().split(',');
|
||||
const STDOffset = (rawSTDOffset || '');
|
||||
const DSTOffset = (rawDSTOffset || '');
|
||||
const STDAbbr = (rawSTDAbbr || '');
|
||||
const DSTAbbr = (rawDSTAbbr || '');
|
||||
|
||||
attemptAdd(STDAbbr, STDOffset);
|
||||
if (STDAbbr !== DSTAbbr) {
|
||||
attemptAdd(DSTAbbr, DSTOffset);
|
||||
}
|
||||
}
|
||||
|
||||
// Log it out to copy to source
|
||||
const tzIt = tzMap.entries();
|
||||
let tzVal = tzIt.next()
|
||||
while (!tzVal.done) {
|
||||
if (tzVal.value[0]) {
|
||||
console.log(`['${tzVal.value[0]}','${tzVal.value[1]}'],`);
|
||||
}
|
||||
tzVal = tzIt.next();
|
||||
}
|
|
@ -0,0 +1,597 @@
|
|||
+00:00,+00:00,GMT,GMT
|
||||
+00:00,+00:00,GMT,GMT
|
||||
+03:00,+03:00,EAT,EAT
|
||||
+01:00,+01:00,CET,CET
|
||||
+03:00,+03:00,EAT,EAT
|
||||
+03:00,+03:00,EAT,EAT
|
||||
+00:00,+00:00,GMT,GMT
|
||||
+01:00,+01:00,WAT,WAT
|
||||
+00:00,+00:00,GMT,GMT
|
||||
+00:00,+00:00,GMT,GMT
|
||||
+02:00,+02:00,CAT,CAT
|
||||
+01:00,+01:00,WAT,WAT
|
||||
+02:00,+02:00,CAT,CAT
|
||||
+02:00,+02:00,EET,EET
|
||||
+01:00,+00:00,+01,+00
|
||||
+01:00,+02:00,CET,CEST
|
||||
+00:00,+00:00,GMT,GMT
|
||||
+00:00,+00:00,GMT,GMT
|
||||
+03:00,+03:00,EAT,EAT
|
||||
+03:00,+03:00,EAT,EAT
|
||||
+01:00,+01:00,WAT,WAT
|
||||
+01:00,+00:00,+01,+00
|
||||
+00:00,+00:00,GMT,GMT
|
||||
+02:00,+02:00,CAT,CAT
|
||||
+02:00,+02:00,CAT,CAT
|
||||
+02:00,+02:00,SAST,SAST
|
||||
+02:00,+02:00,CAT,CAT
|
||||
+03:00,+03:00,EAT,EAT
|
||||
+02:00,+02:00,CAT,CAT
|
||||
+02:00,+02:00,CAT,CAT
|
||||
+01:00,+01:00,WAT,WAT
|
||||
+01:00,+01:00,WAT,WAT
|
||||
+01:00,+01:00,WAT,WAT
|
||||
+00:00,+00:00,GMT,GMT
|
||||
+01:00,+01:00,WAT,WAT
|
||||
+02:00,+02:00,CAT,CAT
|
||||
+02:00,+02:00,CAT,CAT
|
||||
+01:00,+01:00,WAT,WAT
|
||||
+02:00,+02:00,CAT,CAT
|
||||
+02:00,+02:00,SAST,SAST
|
||||
+02:00,+02:00,SAST,SAST
|
||||
+03:00,+03:00,EAT,EAT
|
||||
+00:00,+00:00,GMT,GMT
|
||||
+03:00,+03:00,EAT,EAT
|
||||
+01:00,+01:00,WAT,WAT
|
||||
+01:00,+01:00,WAT,WAT
|
||||
+00:00,+00:00,GMT,GMT
|
||||
+00:00,+00:00,GMT,GMT
|
||||
+01:00,+01:00,WAT,WAT
|
||||
+00:00,+00:00,GMT,GMT
|
||||
+00:00,+00:00,GMT,GMT
|
||||
+02:00,+02:00,EET,EET
|
||||
+01:00,+01:00,CET,CET
|
||||
+02:00,+02:00,CAT,CAT
|
||||
?10:00,?09:00,HST,HDT
|
||||
?09:00,?08:00,AKST,AKDT
|
||||
?04:00,?04:00,AST,AST
|
||||
?04:00,?04:00,AST,AST
|
||||
?03:00,?03:00,-03,-03
|
||||
?03:00,?03:00,-03,-03
|
||||
?03:00,?03:00,-03,-03
|
||||
?03:00,?03:00,-03,-03
|
||||
?03:00,?03:00,-03,-03
|
||||
?03:00,?03:00,-03,-03
|
||||
?03:00,?03:00,-03,-03
|
||||
?03:00,?03:00,-03,-03
|
||||
?03:00,?03:00,-03,-03
|
||||
?03:00,?03:00,-03,-03
|
||||
?03:00,?03:00,-03,-03
|
||||
?03:00,?03:00,-03,-03
|
||||
?03:00,?03:00,-03,-03
|
||||
?03:00,?03:00,-03,-03
|
||||
?04:00,?04:00,AST,AST
|
||||
?04:00,?03:00,-04,-03
|
||||
?05:00,?05:00,EST,EST
|
||||
?10:00,?09:00,HST,HDT
|
||||
?03:00,?03:00,-03,-03
|
||||
?06:00,?06:00,CST,CST
|
||||
?04:00,?04:00,AST,AST
|
||||
?03:00,?03:00,-03,-03
|
||||
?06:00,?06:00,CST,CST
|
||||
?04:00,?04:00,AST,AST
|
||||
?04:00,?04:00,-04,-04
|
||||
?05:00,?05:00,-05,-05
|
||||
?07:00,?06:00,MST,MDT
|
||||
?03:00,?03:00,-03,-03
|
||||
?07:00,?06:00,MST,MDT
|
||||
?04:00,?04:00,-04,-04
|
||||
?05:00,?05:00,EST,EST
|
||||
?04:00,?04:00,-04,-04
|
||||
?03:00,?03:00,-03,-03
|
||||
?03:00,?03:00,-03,-03
|
||||
?05:00,?05:00,EST,EST
|
||||
?06:00,?05:00,CST,CDT
|
||||
?06:00,?06:00,CST,CST
|
||||
?07:00,?06:00,MST,MDT
|
||||
?05:00,?05:00,EST,EST
|
||||
?03:00,?03:00,-03,-03
|
||||
?06:00,?06:00,CST,CST
|
||||
?07:00,?07:00,MST,MST
|
||||
?04:00,?04:00,-04,-04
|
||||
?04:00,?04:00,AST,AST
|
||||
+00:00,+00:00,GMT,GMT
|
||||
?07:00,?07:00,MST,MST
|
||||
?07:00,?07:00,MST,MST
|
||||
?07:00,?06:00,MST,MDT
|
||||
?05:00,?04:00,EST,EDT
|
||||
?04:00,?04:00,AST,AST
|
||||
?07:00,?06:00,MST,MDT
|
||||
?05:00,?05:00,-05,-05
|
||||
?06:00,?06:00,CST,CST
|
||||
?08:00,?07:00,PST,PDT
|
||||
?07:00,?07:00,MST,MST
|
||||
?05:00,?04:00,EST,EDT
|
||||
?03:00,?03:00,-03,-03
|
||||
?04:00,?03:00,AST,ADT
|
||||
?03:00,?03:00,-03,-03
|
||||
?04:00,?03:00,AST,ADT
|
||||
?05:00,?04:00,EST,EDT
|
||||
?04:00,?04:00,AST,AST
|
||||
?04:00,?04:00,AST,AST
|
||||
?06:00,?06:00,CST,CST
|
||||
?05:00,?05:00,-05,-05
|
||||
?04:00,?04:00,-04,-04
|
||||
?04:00,?03:00,AST,ADT
|
||||
?05:00,?04:00,CST,CDT
|
||||
?07:00,?07:00,MST,MST
|
||||
?05:00,?04:00,EST,EDT
|
||||
?06:00,?05:00,CST,CDT
|
||||
?05:00,?04:00,EST,EDT
|
||||
?05:00,?04:00,EST,EDT
|
||||
?06:00,?05:00,CST,CDT
|
||||
?05:00,?04:00,EST,EDT
|
||||
?05:00,?04:00,EST,EDT
|
||||
?05:00,?04:00,EST,EDT
|
||||
?05:00,?04:00,EST,EDT
|
||||
?07:00,?06:00,MST,MDT
|
||||
?05:00,?04:00,EST,EDT
|
||||
?05:00,?05:00,EST,EST
|
||||
?03:00,?03:00,-03,-03
|
||||
?09:00,?08:00,AKST,AKDT
|
||||
?05:00,?04:00,EST,EDT
|
||||
?05:00,?04:00,EST,EDT
|
||||
?06:00,?05:00,CST,CDT
|
||||
?04:00,?04:00,AST,AST
|
||||
?04:00,?04:00,-04,-04
|
||||
?05:00,?05:00,-05,-05
|
||||
?08:00,?07:00,PST,PDT
|
||||
?05:00,?04:00,EST,EDT
|
||||
?04:00,?04:00,AST,AST
|
||||
?03:00,?03:00,-03,-03
|
||||
?06:00,?06:00,CST,CST
|
||||
?04:00,?04:00,-04,-04
|
||||
?04:00,?04:00,AST,AST
|
||||
?04:00,?04:00,AST,AST
|
||||
?06:00,?05:00,CST,CDT
|
||||
?07:00,?07:00,MST,MST
|
||||
?03:00,?03:00,-03,-03
|
||||
?06:00,?05:00,CST,CDT
|
||||
?06:00,?06:00,CST,CST
|
||||
?09:00,?08:00,AKST,AKDT
|
||||
?06:00,?06:00,CST,CST
|
||||
?03:00,?02:00,-03,-02
|
||||
?04:00,?03:00,AST,ADT
|
||||
?06:00,?06:00,CST,CST
|
||||
?03:00,?03:00,-03,-03
|
||||
?05:00,?04:00,EST,EDT
|
||||
?04:00,?04:00,AST,AST
|
||||
?05:00,?04:00,EST,EDT
|
||||
?05:00,?04:00,EST,EDT
|
||||
?05:00,?04:00,EST,EDT
|
||||
?09:00,?08:00,AKST,AKDT
|
||||
?02:00,?02:00,-02,-02
|
||||
?06:00,?05:00,CST,CDT
|
||||
?06:00,?05:00,CST,CDT
|
||||
?06:00,?05:00,CST,CDT
|
||||
?03:00,?03:00,-03,-03
|
||||
?06:00,?05:00,CST,CDT
|
||||
?05:00,?05:00,EST,EST
|
||||
?05:00,?04:00,EST,EDT
|
||||
?03:00,?03:00,-03,-03
|
||||
?07:00,?07:00,MST,MST
|
||||
?05:00,?04:00,EST,EDT
|
||||
?04:00,?04:00,AST,AST
|
||||
?05:00,?05:00,-05,-05
|
||||
?04:00,?04:00,-04,-04
|
||||
?04:00,?04:00,AST,AST
|
||||
?03:00,?03:00,-03,-03
|
||||
?06:00,?05:00,CST,CDT
|
||||
?06:00,?05:00,CST,CDT
|
||||
?03:00,?03:00,-03,-03
|
||||
?06:00,?06:00,CST,CST
|
||||
?06:00,?05:00,CST,CDT
|
||||
?05:00,?05:00,-05,-05
|
||||
?03:00,?03:00,-03,-03
|
||||
?08:00,?07:00,PST,PDT
|
||||
?03:00,?03:00,-03,-03
|
||||
?04:00,?03:00,-04,-03
|
||||
?04:00,?04:00,AST,AST
|
||||
?03:00,?03:00,-03,-03
|
||||
?01:00,+00:00,-01,+00
|
||||
?07:00,?06:00,MST,MDT
|
||||
?09:00,?08:00,AKST,AKDT
|
||||
?04:00,?04:00,AST,AST
|
||||
?03:30,?02:30,NST,NDT
|
||||
?04:00,?04:00,AST,AST
|
||||
?04:00,?04:00,AST,AST
|
||||
?04:00,?04:00,AST,AST
|
||||
?04:00,?04:00,AST,AST
|
||||
?06:00,?06:00,CST,CST
|
||||
?06:00,?06:00,CST,CST
|
||||
?04:00,?03:00,AST,ADT
|
||||
?05:00,?04:00,EST,EDT
|
||||
?08:00,?07:00,PST,PDT
|
||||
?05:00,?04:00,EST,EDT
|
||||
?04:00,?04:00,AST,AST
|
||||
?08:00,?07:00,PST,PDT
|
||||
?04:00,?04:00,AST,AST
|
||||
?07:00,?07:00,MST,MST
|
||||
?06:00,?05:00,CST,CDT
|
||||
?09:00,?08:00,AKST,AKDT
|
||||
?07:00,?06:00,MST,MDT
|
||||
+11:00,+11:00,+11,+11
|
||||
+07:00,+07:00,+07,+07
|
||||
+10:00,+10:00,+10,+10
|
||||
+10:00,+11:00,AEST,AEDT
|
||||
+05:00,+05:00,+05,+05
|
||||
+12:00,+13:00,NZST,NZDT
|
||||
?03:00,?03:00,-03,-03
|
||||
?03:00,?03:00,-03,-03
|
||||
+12:00,+13:00,NZST,NZDT
|
||||
+03:00,+03:00,+03,+03
|
||||
+00:00,+02:00,+00,+02
|
||||
+06:00,+06:00,+06,+06
|
||||
+01:00,+02:00,CET,CEST
|
||||
+03:00,+03:00,+03,+03
|
||||
+06:00,+06:00,+06,+06
|
||||
+03:00,+03:00,+03,+03
|
||||
+12:00,+12:00,+12,+12
|
||||
+05:00,+05:00,+05,+05
|
||||
+05:00,+05:00,+05,+05
|
||||
+05:00,+05:00,+05,+05
|
||||
+05:00,+05:00,+05,+05
|
||||
+05:00,+05:00,+05,+05
|
||||
+03:00,+03:00,+03,+03
|
||||
+03:00,+03:00,+03,+03
|
||||
+04:00,+04:00,+04,+04
|
||||
+07:00,+07:00,+07,+07
|
||||
+07:00,+07:00,+07,+07
|
||||
+02:00,+03:00,EET,EEST
|
||||
+06:00,+06:00,+06,+06
|
||||
+08:00,+08:00,+08,+08
|
||||
+05:30,+05:30,IST,IST
|
||||
+09:00,+09:00,+09,+09
|
||||
+08:00,+08:00,+08,+08
|
||||
+08:00,+08:00,CST,CST
|
||||
+08:00,+08:00,CST,CST
|
||||
+05:30,+05:30,+0530,+0530
|
||||
+06:00,+06:00,+06,+06
|
||||
+03:00,+03:00,+03,+03
|
||||
+06:00,+06:00,+06,+06
|
||||
+09:00,+09:00,+09,+09
|
||||
+04:00,+04:00,+04,+04
|
||||
+05:00,+05:00,+05,+05
|
||||
+02:00,+03:00,EET,EEST
|
||||
+02:00,+03:00,EET,EEST
|
||||
+08:00,+08:00,CST,CST
|
||||
+02:00,+03:00,EET,EEST
|
||||
+07:00,+07:00,+07,+07
|
||||
+08:00,+08:00,HKT,HKT
|
||||
+07:00,+07:00,+07,+07
|
||||
+08:00,+08:00,+08,+08
|
||||
+03:00,+03:00,+03,+03
|
||||
+07:00,+07:00,WIB,WIB
|
||||
+09:00,+09:00,WIT,WIT
|
||||
+02:00,+03:00,IST,IDT
|
||||
+04:30,+04:30,+0430,+0430
|
||||
+12:00,+12:00,+12,+12
|
||||
+05:00,+05:00,PKT,PKT
|
||||
+06:00,+06:00,+06,+06
|
||||
+05:45,+05:45,+0545,+0545
|
||||
+05:45,+05:45,+0545,+0545
|
||||
+09:00,+09:00,+09,+09
|
||||
+05:30,+05:30,IST,IST
|
||||
+07:00,+07:00,+07,+07
|
||||
+08:00,+08:00,+08,+08
|
||||
+08:00,+08:00,+08,+08
|
||||
+03:00,+03:00,+03,+03
|
||||
+08:00,+08:00,CST,CST
|
||||
+08:00,+08:00,CST,CST
|
||||
+11:00,+11:00,+11,+11
|
||||
+08:00,+08:00,WITA,WITA
|
||||
+08:00,+08:00,PST,PST
|
||||
+04:00,+04:00,+04,+04
|
||||
+02:00,+03:00,EET,EEST
|
||||
+07:00,+07:00,+07,+07
|
||||
+07:00,+07:00,+07,+07
|
||||
+06:00,+06:00,+06,+06
|
||||
+05:00,+05:00,+05,+05
|
||||
+07:00,+07:00,+07,+07
|
||||
+07:00,+07:00,WIB,WIB
|
||||
+09:00,+09:00,KST,KST
|
||||
+03:00,+03:00,+03,+03
|
||||
+06:00,+06:00,+06,+06
|
||||
+05:00,+05:00,+05,+05
|
||||
+06:30,+06:30,+0630,+0630
|
||||
+03:00,+03:00,+03,+03
|
||||
+07:00,+07:00,+07,+07
|
||||
+11:00,+11:00,+11,+11
|
||||
+05:00,+05:00,+05,+05
|
||||
+09:00,+09:00,KST,KST
|
||||
+08:00,+08:00,CST,CST
|
||||
+08:00,+08:00,+08,+08
|
||||
+11:00,+11:00,+11,+11
|
||||
+08:00,+08:00,CST,CST
|
||||
+05:00,+05:00,+05,+05
|
||||
+04:00,+04:00,+04,+04
|
||||
+03:30,+03:30,+0330,+0330
|
||||
+02:00,+03:00,IST,IDT
|
||||
+06:00,+06:00,+06,+06
|
||||
+06:00,+06:00,+06,+06
|
||||
+09:00,+09:00,JST,JST
|
||||
+07:00,+07:00,+07,+07
|
||||
+08:00,+08:00,WITA,WITA
|
||||
+08:00,+08:00,+08,+08
|
||||
+08:00,+08:00,+08,+08
|
||||
+06:00,+06:00,+06,+06
|
||||
+10:00,+10:00,+10,+10
|
||||
+07:00,+07:00,+07,+07
|
||||
+10:00,+10:00,+10,+10
|
||||
+09:00,+09:00,+09,+09
|
||||
+06:30,+06:30,+0630,+0630
|
||||
+05:00,+05:00,+05,+05
|
||||
+04:00,+04:00,+04,+04
|
||||
?01:00,+00:00,-01,+00
|
||||
?04:00,?03:00,AST,ADT
|
||||
+00:00,+01:00,WET,WEST
|
||||
?01:00,?01:00,-01,-01
|
||||
+00:00,+01:00,WET,WEST
|
||||
+00:00,+01:00,WET,WEST
|
||||
+01:00,+02:00,CET,CEST
|
||||
+00:00,+01:00,WET,WEST
|
||||
+00:00,+00:00,GMT,GMT
|
||||
?02:00,?02:00,-02,-02
|
||||
+00:00,+00:00,GMT,GMT
|
||||
?03:00,?03:00,-03,-03
|
||||
+10:00,+11:00,AEST,AEDT
|
||||
+09:30,+10:30,ACST,ACDT
|
||||
+10:00,+10:00,AEST,AEST
|
||||
+09:30,+10:30,ACST,ACDT
|
||||
+10:00,+11:00,AEST,AEDT
|
||||
+10:00,+11:00,AEST,AEDT
|
||||
+09:30,+09:30,ACST,ACST
|
||||
+08:45,+08:45,+0845,+0845
|
||||
+10:00,+11:00,AEST,AEDT
|
||||
+10:30,+11:00,+1030,+11
|
||||
+10:00,+10:00,AEST,AEST
|
||||
+10:30,+11:00,+1030,+11
|
||||
+10:00,+11:00,AEST,AEDT
|
||||
+09:30,+09:30,ACST,ACST
|
||||
+10:00,+11:00,AEST,AEDT
|
||||
+08:00,+08:00,AWST,AWST
|
||||
+10:00,+10:00,AEST,AEST
|
||||
+09:30,+10:30,ACST,ACDT
|
||||
+10:00,+11:00,AEST,AEDT
|
||||
+10:00,+11:00,AEST,AEDT
|
||||
+10:00,+11:00,AEST,AEDT
|
||||
+08:00,+08:00,AWST,AWST
|
||||
+09:30,+10:30,ACST,ACDT
|
||||
?05:00,?05:00,-05,-05
|
||||
?02:00,?02:00,-02,-02
|
||||
?03:00,?03:00,-03,-03
|
||||
?04:00,?04:00,-04,-04
|
||||
?04:00,?03:00,AST,ADT
|
||||
?06:00,?05:00,CST,CDT
|
||||
?05:00,?04:00,EST,EDT
|
||||
?07:00,?06:00,MST,MDT
|
||||
?03:30,?02:30,NST,NDT
|
||||
?08:00,?07:00,PST,PDT
|
||||
?06:00,?06:00,CST,CST
|
||||
?07:00,?07:00,MST,MST
|
||||
+01:00,+02:00,CET,CEST
|
||||
?04:00,?03:00,-04,-03
|
||||
?06:00,?05:00,-06,-05
|
||||
?06:00,?05:00,CST,CDT
|
||||
?05:00,?04:00,CST,CDT
|
||||
+02:00,+03:00,EET,EEST
|
||||
+02:00,+02:00,EET,EET
|
||||
+01:00,+00:00,IST,GMT
|
||||
?05:00,?05:00,EST,EST
|
||||
?05:00,?04:00,EST,EDT
|
||||
+00:00,+00:00,GMT,GMT
|
||||
+00:00,+00:00,GMT,GMT
|
||||
?01:00,?01:00,-01,-01
|
||||
?10:00,?10:00,-10,-10
|
||||
?11:00,?11:00,-11,-11
|
||||
?12:00,?12:00,-12,-12
|
||||
?02:00,?02:00,-02,-02
|
||||
?03:00,?03:00,-03,-03
|
||||
?04:00,?04:00,-04,-04
|
||||
?05:00,?05:00,-05,-05
|
||||
?06:00,?06:00,-06,-06
|
||||
?07:00,?07:00,-07,-07
|
||||
?08:00,?08:00,-08,-08
|
||||
?09:00,?09:00,-09,-09
|
||||
+00:00,+00:00,GMT,GMT
|
||||
+01:00,+01:00,+01,+01
|
||||
+10:00,+10:00,+10,+10
|
||||
+11:00,+11:00,+11,+11
|
||||
+12:00,+12:00,+12,+12
|
||||
+13:00,+13:00,+13,+13
|
||||
+14:00,+14:00,+14,+14
|
||||
+02:00,+02:00,+02,+02
|
||||
+03:00,+03:00,+03,+03
|
||||
+04:00,+04:00,+04,+04
|
||||
+05:00,+05:00,+05,+05
|
||||
+06:00,+06:00,+06,+06
|
||||
+07:00,+07:00,+07,+07
|
||||
+08:00,+08:00,+08,+08
|
||||
+09:00,+09:00,+09,+09
|
||||
+00:00,+00:00,GMT,GMT
|
||||
+00:00,+00:00,GMT,GMT
|
||||
+00:00,+00:00,UTC,UTC
|
||||
+00:00,+00:00,UTC,UTC
|
||||
+00:00,+00:00,UTC,UTC
|
||||
+00:00,+00:00,UTC,UTC
|
||||
+01:00,+02:00,CET,CEST
|
||||
+01:00,+02:00,CET,CEST
|
||||
+04:00,+04:00,+04,+04
|
||||
+02:00,+03:00,EET,EEST
|
||||
+00:00,+01:00,GMT,BST
|
||||
+01:00,+02:00,CET,CEST
|
||||
+01:00,+02:00,CET,CEST
|
||||
+01:00,+02:00,CET,CEST
|
||||
+01:00,+02:00,CET,CEST
|
||||
+02:00,+03:00,EET,EEST
|
||||
+01:00,+02:00,CET,CEST
|
||||
+01:00,+02:00,CET,CEST
|
||||
+02:00,+03:00,EET,EEST
|
||||
+01:00,+02:00,CET,CEST
|
||||
+01:00,+00:00,IST,GMT
|
||||
+01:00,+02:00,CET,CEST
|
||||
+00:00,+01:00,GMT,BST
|
||||
+02:00,+03:00,EET,EEST
|
||||
+00:00,+01:00,GMT,BST
|
||||
+03:00,+03:00,+03,+03
|
||||
+00:00,+01:00,GMT,BST
|
||||
+02:00,+02:00,EET,EET
|
||||
+02:00,+03:00,EET,EEST
|
||||
+03:00,+03:00,+03,+03
|
||||
+02:00,+03:00,EET,EEST
|
||||
+00:00,+01:00,WET,WEST
|
||||
+01:00,+02:00,CET,CEST
|
||||
+00:00,+01:00,GMT,BST
|
||||
+01:00,+02:00,CET,CEST
|
||||
+01:00,+02:00,CET,CEST
|
||||
+01:00,+02:00,CET,CEST
|
||||
+02:00,+03:00,EET,EEST
|
||||
+03:00,+03:00,+03,+03
|
||||
+01:00,+02:00,CET,CEST
|
||||
+03:00,+03:00,MSK,MSK
|
||||
+02:00,+03:00,EET,EEST
|
||||
+01:00,+02:00,CET,CEST
|
||||
+01:00,+02:00,CET,CEST
|
||||
+01:00,+02:00,CET,CEST
|
||||
+01:00,+02:00,CET,CEST
|
||||
+02:00,+03:00,EET,EEST
|
||||
+01:00,+02:00,CET,CEST
|
||||
+04:00,+04:00,+04,+04
|
||||
+01:00,+02:00,CET,CEST
|
||||
+01:00,+02:00,CET,CEST
|
||||
+04:00,+04:00,+04,+04
|
||||
+03:00,+03:00,MSK,MSK
|
||||
+01:00,+02:00,CET,CEST
|
||||
+02:00,+03:00,EET,EEST
|
||||
+01:00,+02:00,CET,CEST
|
||||
+02:00,+03:00,EET,EEST
|
||||
+01:00,+02:00,CET,CEST
|
||||
+02:00,+03:00,EET,EEST
|
||||
+04:00,+04:00,+04,+04
|
||||
+02:00,+03:00,EET,EEST
|
||||
+01:00,+02:00,CET,CEST
|
||||
+01:00,+02:00,CET,CEST
|
||||
+01:00,+02:00,CET,CEST
|
||||
+02:00,+03:00,EET,EEST
|
||||
+03:00,+03:00,+03,+03
|
||||
+01:00,+02:00,CET,CEST
|
||||
+01:00,+02:00,CET,CEST
|
||||
+02:00,+03:00,EET,EEST
|
||||
+01:00,+02:00,CET,CEST
|
||||
+00:00,+00:00,-00,-00
|
||||
+00:00,+01:00,GMT,BST
|
||||
+00:00,+01:00,GMT,BST
|
||||
+00:00,+00:00,GMT,GMT
|
||||
+00:00,+00:00,GMT,GMT
|
||||
+00:00,+00:00,GMT,GMT
|
||||
+00:00,+00:00,GMT,GMT
|
||||
+00:00,+00:00,GMT,GMT
|
||||
+08:00,+08:00,HKT,HKT
|
||||
?10:00,?10:00,HST,HST
|
||||
+00:00,+00:00,GMT,GMT
|
||||
+03:00,+03:00,EAT,EAT
|
||||
+06:00,+06:00,+06,+06
|
||||
+07:00,+07:00,+07,+07
|
||||
+06:30,+06:30,+0630,+0630
|
||||
+03:00,+03:00,EAT,EAT
|
||||
+05:00,+05:00,+05,+05
|
||||
+04:00,+04:00,+04,+04
|
||||
+05:00,+05:00,+05,+05
|
||||
+04:00,+04:00,+04,+04
|
||||
+03:00,+03:00,EAT,EAT
|
||||
+04:00,+04:00,+04,+04
|
||||
+03:30,+03:30,+0330,+0330
|
||||
+02:00,+03:00,IST,IDT
|
||||
?05:00,?05:00,EST,EST
|
||||
+09:00,+09:00,JST,JST
|
||||
+12:00,+12:00,+12,+12
|
||||
+02:00,+02:00,EET,EET
|
||||
+01:00,+02:00,MET,MEST
|
||||
?08:00,?07:00,PST,PDT
|
||||
?07:00,?07:00,MST,MST
|
||||
?06:00,?06:00,CST,CST
|
||||
?07:00,?07:00,MST,MST
|
||||
?07:00,?06:00,MST,MDT
|
||||
?07:00,?06:00,MST,MDT
|
||||
+12:00,+13:00,NZST,NZDT
|
||||
+12:45,+13:45,+1245,+1345
|
||||
+13:00,+13:00,+13,+13
|
||||
+12:00,+13:00,NZST,NZDT
|
||||
+11:00,+11:00,+11,+11
|
||||
+12:45,+13:45,+1245,+1345
|
||||
+10:00,+10:00,+10,+10
|
||||
?06:00,?05:00,-06,-05
|
||||
+11:00,+11:00,+11,+11
|
||||
+13:00,+13:00,+13,+13
|
||||
+13:00,+13:00,+13,+13
|
||||
+12:00,+12:00,+12,+12
|
||||
+12:00,+12:00,+12,+12
|
||||
?06:00,?06:00,-06,-06
|
||||
?09:00,?09:00,-09,-09
|
||||
+11:00,+11:00,+11,+11
|
||||
+10:00,+10:00,ChST,ChST
|
||||
?10:00,?10:00,HST,HST
|
||||
?10:00,?10:00,HST,HST
|
||||
+13:00,+13:00,+13,+13
|
||||
+14:00,+14:00,+14,+14
|
||||
+11:00,+11:00,+11,+11
|
||||
+12:00,+12:00,+12,+12
|
||||
+12:00,+12:00,+12,+12
|
||||
?09:30,?09:30,-0930,-0930
|
||||
?11:00,?11:00,SST,SST
|
||||
+12:00,+12:00,+12,+12
|
||||
?11:00,?11:00,-11,-11
|
||||
+11:00,+12:00,+11,+12
|
||||
+11:00,+11:00,+11,+11
|
||||
?11:00,?11:00,SST,SST
|
||||
+09:00,+09:00,+09,+09
|
||||
?08:00,?08:00,-08,-08
|
||||
+11:00,+11:00,+11,+11
|
||||
+11:00,+11:00,+11,+11
|
||||
+10:00,+10:00,+10,+10
|
||||
?10:00,?10:00,-10,-10
|
||||
+10:00,+10:00,ChST,ChST
|
||||
?11:00,?11:00,SST,SST
|
||||
?10:00,?10:00,-10,-10
|
||||
+12:00,+12:00,+12,+12
|
||||
+13:00,+13:00,+13,+13
|
||||
+10:00,+10:00,+10,+10
|
||||
+12:00,+12:00,+12,+12
|
||||
+12:00,+12:00,+12,+12
|
||||
+10:00,+10:00,+10,+10
|
||||
+01:00,+02:00,CET,CEST
|
||||
+00:00,+01:00,WET,WEST
|
||||
+08:00,+08:00,CST,CST
|
||||
?08:00,?07:00,PST,PDT
|
||||
+08:00,+08:00,CST,CST
|
||||
+09:00,+09:00,KST,KST
|
||||
+08:00,+08:00,+08,+08
|
||||
+03:00,+03:00,+03,+03
|
||||
+00:00,+00:00,UTC,UTC
|
||||
+00:00,+00:00,UTC,UTC
|
||||
?09:00,?08:00,AKST,AKDT
|
||||
?10:00,?09:00,HST,HDT
|
||||
?07:00,?07:00,MST,MST
|
||||
?06:00,?05:00,CST,CDT
|
||||
?05:00,?04:00,EST,EDT
|
||||
?05:00,?04:00,EST,EDT
|
||||
?10:00,?10:00,HST,HST
|
||||
?06:00,?05:00,CST,CDT
|
||||
?05:00,?04:00,EST,EDT
|
||||
?07:00,?06:00,MST,MDT
|
||||
?08:00,?07:00,PST,PDT
|
||||
?11:00,?11:00,SST,SST
|
||||
+00:00,+00:00,UTC,UTC
|
||||
+03:00,+03:00,MSK,MSK
|
||||
+00:00,+01:00,WET,WEST
|
||||
+00:00,+00:00,UTC,UTC
|
|
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
|
@ -64,6 +64,18 @@
|
|||
<li>Done! You can now view the event in your calendar</li>
|
||||
</ol>
|
||||
</p>
|
||||
<p>
|
||||
<h2>Mozilla Thunderbird:</h2>
|
||||
<ol>
|
||||
<li>Press Alt</li>
|
||||
<li>Click on File</li>
|
||||
<li>Hover on Open</li>
|
||||
<li>Click on Calendar File...</li>
|
||||
<li>Locate the ICS you just downloaded (if you didn't save it, click the link again and save it)</li>
|
||||
<li>Click Open</li>
|
||||
<li>Done! You can now view the event in your calendar</li>
|
||||
</ol>
|
||||
</p>
|
||||
</div>
|
||||
<script>
|
||||
if (window.location.search) {
|
||||
|
|
Loading…
Reference in New Issue