Compare commits

..

No commits in common. "master" and "V1.0.4" have entirely different histories.

27 changed files with 296 additions and 587 deletions

5
.github/CODEOWNERS vendored
View File

@ -1,5 +0,0 @@
* @burn-e99
config.example.ts @davidopluslau
README.md @davidopluslau
src/buttons/event-creation/activities.ts @davidopluslau

View File

@ -1,42 +0,0 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will install Deno then run `deno lint` and `deno test`.
# For more information see: https://github.com/denoland/setup-deno
name: Deno
on:
push:
branches: ["master"]
pull_request:
branches: ["master"]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Setup repo
uses: actions/checkout@v4
- name: Setup Deno
# uses: denoland/setup-deno@v1
uses: denoland/setup-deno@61fe2df320078202e33d7d5ad347e7dcfa0e8f31 # v1.1.2
with:
deno-version: v1.x
- name: Verify formatting
run: deno fmt --check
- name: Run linter
run: deno lint
# Add this back in when tests are added
# - name: Run tests
# run: deno test -A

View File

@ -1,10 +0,0 @@
# CONTRIBUTING TO GROUP UP
## Things to check before committing a change
- Formatting and linting
- Run `deno fmt` to set all formatting correct
- Run `deno lint` to check for any issues that need fixed
- Are you making a change that will be updating the version number?
- Update the version number in `README.md` and `config.example.ts`, and update the date in `README.md`
- Create a tag on your commit marking this version (name it Vx.x.x)
## Things to check after committing a change
- Check in on Sonar to see if your commit caused new issues to appear. If it did, please fix them

View File

@ -3,7 +3,7 @@
### Public Bot Information ### 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. 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` (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. 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_ 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.
@ -11,7 +11,7 @@ _The Bot_ does not read any user messages sent in the past, but does read its ow
* 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. * 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). * 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 stores the Discord Guild ID, Discord User ID, and 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_, Discord Guild ID, and Discord User ID are 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 Discord Guild ID and Discord User ID are only used to determine which reports need to be deleted as detailed in the _Deleting Your Data_ section below. * 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. * 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. * 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 Create New Event command stores the following data for every event that is created:
@ -38,4 +38,4 @@ Due to the nature of open source code, _Rehosts_ may not use the same codebase t
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. 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 ## 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`) 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. 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.

View File

@ -1,4 +1,4 @@
# Group Up - An Event Scheduling Discord Bot | V2.0.1 - 2024/12/24 # Group Up - An Event Scheduling Discord Bot | V1.0.4 - 2024/05/04
[![SonarCloud](https://sonarcloud.io/images/project_badges/sonarcloud-orange.svg)](https://sonarcloud.io/summary/new_code?id=GroupUp) [![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) [![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)
@ -43,12 +43,7 @@ If you run into any errors or problems with the bot, or think you have a good id
--- ---
## Self Hosting Group Up ## Self Hosting Group Up
Group Up is built on [Deno](https://deno.land/) using [Discordeno](https://discordeno.mod.land/) `v17.0.1`. 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.
Group Up `V1.1.6` and lower requires Deno `V.33.1`.
Group Up `V2.0.0` and up requires Deno `V2.0.0`.
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. 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.
@ -59,6 +54,6 @@ Once everything is set up, starting the bot can simply be done with `deno run --
## Privacy Policy and Terms of Service ## 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`). 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/GroupUp/blob/master/PRIVACY.md). 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/GroupUp/blob/master/TERMS.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).

View File

@ -1,49 +1,40 @@
export const config = { export const config = {
// !! NOTICE !! All fields below are required unless they are explicitly noted as OPTIONAL. If a field is OPTIONAL, do not remove it from this file, just leave it at the default value 'name': 'Group Up', // Name of the bot
name: 'Group Up', // Name of the bot 'version': '1.0.4', // Version of the bot
version: '2.0.2', // Version of the bot 'token': 'the_bot_token', // Discord API Token for this 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"
localToken: 'local_testing_token', // Discord API Token for a secondary OPTIONAL testing bot, THIS SHOULD BE DIFFERENT FROM "token" 'prefix': '/', // Prefix for all commands
prefix: '/', // Prefix for all commands, as this bot uses slash commands, this needs to be '/' '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
db: { 'host': '', // IP address for the db, usually localhost
// Settings for the MySQL database, this is required to keep track of the currently active events. 'localhost': '', // IP address for a secondary OPTIONAL local testing DB, usually also is localhost, but depends on your dev environment
host: '', // IP address for the db, usually localhost 'port': 3306, // Port for the db
localhost: '', // IP address for a secondary OPTIONAL local testing DB, usually also is localhost, but depends on your dev environment 'username': '', // Username for the account that will access your DB, this account will need "DB Manager" admin rights and "REFERENCES" Global Privileges
port: 3306, // Port for the db '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
username: '', // Username for the account that will access your DB, this account will need "DB Manager" admin rights and "REFERENCES" Global Privileges 'name': '', // Name of the database Schema to use for the bot
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
}, },
links: { 'links': { // Links to various sites
// Links to various sites 'sourceCode': 'https://github.com/Burn-E99/GroupUp', // Link to the repository
sourceCode: 'https://github.com/Burn-E99/GroupUp', // Link to the repository, OPTIONAL 'supportServer': '', // Invite link to the Discord support server
supportServer: '', // Invite link to the Discord support server, OPTIONAL 'addToCalendar': '', // Link to where the icsGenerator is hosted
addToCalendar: '', // Link to where the icsGenerator is hosted, OPTIONAL 'creatorIcon': '', // Link to where the GroupUpSinglePerson.png (or similar image) is hosted
creatorIcon: '', // Link to where the GroupUpSinglePerson.png (or similar image) is hosted
}, },
defaultDateFormat: 'MONTH/DAY/YEAR', // Default format that Group Up will suggest to the user. Must match one of the options in the 'DateTimeFormats' enum inside 'src/buttons/event-creation/dateTimeUtils.ts' 'logChannel': 0n, // Discord channel ID where the bot should put startup messages and other error messages needed
logChannel: 0n, // Discord channel ID where the bot should put startup messages and other error messages needed. This value is a bigint, so please ensure you have a `n` after the ID you get from Discord. OPTIONAL 'reportChannel': 0n, // Discord channel ID where reports will be sent when using the built-in report command
reportChannel: 0n, // Discord channel ID where reports will be sent when using the built-in report command. This value is a bigint, so please ensure you have a `n` after the ID you get from Discord. OPTIONAL 'devServer': 0n, // Discord guild ID where testing of indev features/commands will be handled, used in conjunction with the DEVMODE bool in mod.ts
devServer: 0n, // Discord guild ID where testing of indev features/commands will be handled, used in conjunction with the DEVMODE bool in mod.ts. This value is a bigint, so please ensure you have a `n` after the ID you get from Discord. OPTIONAL 'owner': 0n, // Discord user ID of the bot admin
owner: 0n, // Discord user ID of the bot admin. This value is a bigint, so please ensure you have a `n` after the ID you get from Discord. 'botLists': [ // Array of objects containing all bot lists that stats should be posted to
botLists: [ { // Bot List object, duplicate for each bot list
// Array of objects containing all bot lists that stats should be posted to, OPTIONAL 'name': 'Bot List Name', // Name of bot list, not used
{ 'enabled': false, // Should statistics be posted to this list?
// Bot List object, duplicate for each bot list 'apiUrl': 'https://example.com/api/bots/?{bot_id}/stats', // API URL, use ?{bot_id} in place of the bot id so that it can be dynamically replaced
name: 'Bot List Name', // Name of bot list, not used 'headers': [ // Array of headers that need to be added to the request
enabled: false, // Should statistics be posted to this list? { // Header Object, duplicate for every header needed
apiUrl: 'https://example.com/api/bots/?{bot_id}/stats', // API URL, use ?{bot_id} in place of the bot id so that it can be dynamically replaced 'header': 'header_name', // Name of header needed, usually Authorization is needed
headers: [ 'value': 'header_value', // Value for the header
// Array of headers that need to be added to the request
{
// Header Object, duplicate for every header needed
header: 'header_name', // Name of header needed, usually Authorization is needed
value: 'header_value', // Value for the header
}, },
], ],
body: { 'body': { // Data payload to send to the bot list, will be turned into a string and any ?{} will be replaced with the required value, currently only has ?{server_count}
// Data payload to send to the bot list, will be turned into a string and any ?{} will be replaced with the required value, currently only has ?{server_count} 'param_name': '?{param_value}', // Add more params as needed
param_name: '?{param_value}', // Add more params as needed
}, },
}, },
], ],

View File

@ -1,19 +1,42 @@
{ {
"compilerOptions": { "compilerOptions": {
"lib": ["deno.window"], "allowJs": true,
"lib": [
"deno.window"
],
"strict": true "strict": true
}, },
"lint": { "lint": {
"include": ["src/", "db/", "mod.ts", "deps.ts", "config.ts", "config.example.ts"], "files": {
"exclude": [], "include": [
"src/",
"db/",
"mod.ts",
"deps.ts",
"config.ts",
"config.example.ts"
],
"exclude": []
},
"rules": { "rules": {
"tags": ["recommended"], "tags": [
"include": ["ban-untagged-todo"], "recommended"
],
"include": [
"ban-untagged-todo"
],
"exclude": [] "exclude": []
} }
}, },
"fmt": { "fmt": {
"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": [], "exclude": [],
"useTabs": true, "useTabs": true,
"lineWidth": 200, "lineWidth": 200,
@ -21,4 +44,4 @@
"singleQuote": true, "singleQuote": true,
"proseWrap": "preserve" "proseWrap": "preserve"
} }
} }

View File

@ -47,4 +47,4 @@ export type {
export { Client } from 'https://deno.land/x/mysql@v2.11.0/mod.ts'; export { Client } from 'https://deno.land/x/mysql@v2.11.0/mod.ts';
export { initLog, log, LogTypes as LT } from 'https://raw.githubusercontent.com/Burn-E99/Log4Deno/V2.0.0/mod.ts'; export { initLog, log, LogTypes as LT } from 'https://raw.githubusercontent.com/Burn-E99/Log4Deno/V1.1.0/mod.ts';

View File

@ -1,5 +1,3 @@
import { log, LT } from '../../../deps.ts';
// Activity should either have maxMembers or options specified, NOT both // Activity should either have maxMembers or options specified, NOT both
export type Activity = { export type Activity = {
name: string; name: string;
@ -16,19 +14,7 @@ export const Activities: Array<Activity> = [
name: 'Raids', name: 'Raids',
options: [ options: [
{ {
name: 'The Desert Perpetual (Epic)', name: 'Crota\'s End',
maxMembers: 6,
},
{
name: 'The Desert Perpetual',
maxMembers: 6,
},
{
name: "Salvation's Edge",
maxMembers: 6,
},
{
name: "Crota's End",
maxMembers: 6, maxMembers: 6,
}, },
{ {
@ -36,7 +22,7 @@ export const Activities: Array<Activity> = [
maxMembers: 6, maxMembers: 6,
}, },
{ {
name: "King's Fall", name: 'King\'s Fall',
maxMembers: 6, maxMembers: 6,
}, },
{ {
@ -65,19 +51,7 @@ export const Activities: Array<Activity> = [
name: 'Dungeons', name: 'Dungeons',
options: [ options: [
{ {
name: 'Equilibrium', name: 'Warlord\'s Ruin',
maxMembers: 3,
},
{
name: 'Sundered Doctrine',
maxMembers: 3,
},
{
name: "Vesper's Host",
maxMembers: 3,
},
{
name: "Warlord's Ruin",
maxMembers: 3, maxMembers: 3,
}, },
{ {
@ -180,52 +154,11 @@ export const Activities: Array<Activity> = [
name: '//node.ovrd.AVALON//', name: '//node.ovrd.AVALON//',
maxMembers: 3, maxMembers: 3,
}, },
{
name: 'Zero Hour',
maxMembers: 3,
},
{
name: 'The Whisper',
maxMembers: 3,
},
{
name: 'Presage',
maxMembers: 3,
},
], ],
}, },
{ {
name: 'Miscellaneous/Seasonal', name: 'Miscellaneous/Seasonal',
options: [ options: [
{
name: 'Excision',
maxMembers: 12,
},
{
name: 'Pantheon',
options: [
{
name: 'Atraks Sovereign (Week 1)',
maxMembers: 6,
},
{
name: 'Oryx Exalted (Week 2)',
maxMembers: 6,
},
{
name: 'Rhulk Indomitable (Week 3)',
maxMembers: 6,
},
{
name: 'Nezarec Sublime (Week 4)',
maxMembers: 6,
},
],
},
{
name: 'Onslaught',
maxMembers: 3,
},
{ {
name: 'Fishing', name: 'Fishing',
maxMembers: 3, maxMembers: 3,
@ -300,33 +233,3 @@ export const Activities: Array<Activity> = [
], ],
}, },
]; ];
// Activities Verification, verifies fields are proper lengths and amount of activities will actually fit in Discord
const actVerification = (currentAct: Activity, currentDepth = 0) => {
if (currentDepth > 4) {
log(LT.ERROR, `'${currentAct.name}' is too deep (${currentDepth} > 4)!`);
}
if (currentAct.name.length > 100) {
log(LT.ERROR, `'${currentAct.name}' is too long (${currentAct.name.length} > 100)!`);
}
if (currentAct.options && currentAct.maxMembers) {
log(LT.ERROR, `'${currentAct.name}' has both maxMembers and options specified (ONLY ONE ALLOWED)!`);
}
if (!currentAct.options && !currentAct.maxMembers) {
log(LT.ERROR, `'${currentAct.name}' is missing both maxMembers and options specified (ONE IS NEEDED)!`);
}
if (currentAct.options) {
if (currentAct.options.length > 25) {
log(LT.ERROR, `'${currentAct.name}' has too many options (${currentAct.options.length} > 25)!`);
}
for (const act of currentAct.options) {
actVerification(act, currentDepth + 1);
}
}
};
// Use a fake root activity to allow testing to occur simply
actVerification({
name: 'root',
options: Activities,
});

View File

@ -1,10 +1,3 @@
import config from '../../../config.ts';
import { editEventDetailsBtnName } from './utils.ts';
enum DateTimeFormats {
MMDDYYYY = 'MONTH/DAY/YEAR',
DDMMYYYY = 'DAY/MONTH/YEAR',
}
const monthsLong: Array<string> = ['JANUARY', 'FEBRUARY', 'MARCH', 'APRIL', 'MAY', 'JUNE', 'JULY', 'AUGUST', 'SEPTEMBER', 'OCTOBER', 'NOVEMBER', 'DECEMBER']; 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)); export const monthsShort: Array<string> = monthsLong.map((month) => month.slice(0, 3));
const tzMap: Map<string, string> = new Map([ const tzMap: Map<string, string> = new Map([
@ -59,8 +52,7 @@ const tzMap: Map<string, string> = new Map([
['CHST', '+10:00'], ['CHST', '+10:00'],
['SST', '-11:00'], ['SST', '-11:00'],
]); ]);
const shorthandUSTZ: Array<string> = ['ET', 'CT', 'MT', 'PT', 'HT', 'AKT']; const shorthandUSTZ: Array<string> = ['ET', 'CT', 'MT', 'PT'];
const allUSTZ: Array<string> = ['EST', 'CST', 'MST', 'PST', 'HST', 'AKST', 'EDT', 'CDT', 'MDT', 'PDT', 'HDT', 'AKDT'];
// Takes user input Time and makes it actually usable // Takes user input Time and makes it actually usable
const parseEventTime = (preParsedEventTime: string): [string, string, string] => { const parseEventTime = (preParsedEventTime: string): [string, string, string] => {
@ -101,33 +93,14 @@ export const isDSTActive = (): boolean => {
return today.getTimezoneOffset() < Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset()); return today.getTimezoneOffset() < Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset());
}; };
const editButtonMessage = `click \`${editEventDetailsBtnName}\` and change`;
const warningText = (incorrectTZ: string, correctTZ: string, newEvent: boolean) =>
`⚠️⚠️⚠️ WARNING! Did you mean to enter \`${incorrectTZ}\` for the time zone? ⚠️⚠️⚠️\nCurrently (as of the posting of this message), Daylight Savings Time is ${
isDSTActive() ? '' : 'not '
}active in most of the United States. If DST is ${isDSTActive() ? '' : 'not '}in effect for you (and will be when this event is scheduled to happen), ${
newEvent ? editButtonMessage : 'please dismiss this message and start over, using'
} the time zone ${newEvent ? 'to ' : ''}\`${correctTZ}\`, or shorten it to \`${correctTZ.slice(0, -2)}T\` to let ${config.name} automatically use the correct time zone.\n\n`;
const usTZDSTCheck = (timeZone: string, newEvent: boolean): string => {
if (allUSTZ.includes(timeZone)) {
if (isDSTActive() && timeZone.endsWith('ST')) {
return warningText(timeZone, `${timeZone.slice(0, -2)}DT`, newEvent);
} else if (!isDSTActive() && timeZone.endsWith('DT')) {
return warningText(timeZone, `${timeZone.slice(0, -2)}ST`, newEvent);
}
}
return '';
};
// Takes user input Time Zone and makes it actually usable // Takes user input Time Zone and makes it actually usable
const parseEventTimeZone = (preParsedEventTimeZone: string): [string, string] => { const parseEventTimeZone = (preParsedEventTimeZone: string): [string, string] => {
if (shorthandUSTZ.includes(preParsedEventTimeZone)) { if (shorthandUSTZ.includes(preParsedEventTimeZone)) {
// Handle shorthand US timezones, adding S for standard time and D for Daylight Savings // Handle shorthand US timezones, adding S for standard time and D for Daylight Savings
if (isDSTActive()) { if (isDSTActive()) {
preParsedEventTimeZone = `${preParsedEventTimeZone.slice(0, -1)}DT`; preParsedEventTimeZone = `${preParsedEventTimeZone.slice(0, 1)}DT`;
} else { } else {
preParsedEventTimeZone = `${preParsedEventTimeZone.slice(0, -1)}ST`; preParsedEventTimeZone = `${preParsedEventTimeZone.slice(0, 1)}ST`;
} }
} }
if (tzMap.has(preParsedEventTimeZone)) { if (tzMap.has(preParsedEventTimeZone)) {
@ -140,8 +113,8 @@ const parseEventTimeZone = (preParsedEventTimeZone: string): [string, string] =>
addPlusSign = true; addPlusSign = true;
} }
// Determine if we need to prepend UTC/GMT, handle adding the + into the string // Determine if we need to prepend UTC/GMT, handle adding the + into the string
if (!preParsedEventTimeZone.startsWith('UTC') || preParsedEventTimeZone.startsWith('GMT')) { if (!preParsedEventTimeZone.startsWith('UTC') && preParsedEventTimeZone.startsWith('GMT')) {
preParsedEventTimeZone = `UTC${addPlusSign ? '+' : ''}${preParsedEventTimeZone.startsWith('GMT') ? preParsedEventTimeZone.slice(3) : preParsedEventTimeZone}`; preParsedEventTimeZone = `UTC${addPlusSign && '+'}${preParsedEventTimeZone}`;
} else if (addPlusSign) { } else if (addPlusSign) {
preParsedEventTimeZone = `${preParsedEventTimeZone.slice(0, 3)}+${preParsedEventTimeZone.slice(3)}`; preParsedEventTimeZone = `${preParsedEventTimeZone.slice(0, 3)}+${preParsedEventTimeZone.slice(3)}`;
} }
@ -149,49 +122,10 @@ const parseEventTimeZone = (preParsedEventTimeZone: string): [string, string] =>
} }
}; };
const determineDateTimeOrder = (parsedSlot1: string, parsedSlot2: string, parsedSlot3: string): [string, string, string] => {
// Default these to MMDDYYYY in case something goes wrong and does not properly override them
let parsedEventMonth = parsedSlot1;
let parsedEventDay = parsedSlot2;
let parsedEventYear = parsedSlot3;
const slot1AsInt = parseInt(parsedSlot1);
const slot2AsInt = parseInt(parsedSlot2);
if (!isNaN(slot1AsInt) && slot1AsInt > 999) {
// First parsing slot appears to be a year, assume user used ISO8601 format
parsedEventYear = parsedSlot1;
parsedEventMonth = parsedSlot2;
parsedEventDay = parsedSlot3;
} else if (!isNaN(slot1AsInt) && slot1AsInt > 12) {
// First parsing slot appears to be a day, assume user used DDMMYYYY format
parsedEventDay = parsedSlot1;
parsedEventMonth = parsedSlot2;
parsedEventYear = parsedSlot3;
} else if (!isNaN(slot2AsInt) && slot2AsInt > 12) {
// Second parsing slot appears to be a day, assume user used MMDDYYYY format
parsedEventMonth = parsedSlot1;
parsedEventDay = parsedSlot2;
parsedEventYear = parsedSlot3;
} else if (config.defaultDateFormat === DateTimeFormats.DDMMYYYY) {
// Year was not first, and cannot locate a day from the string, fall back to bot's default setting
parsedEventDay = parsedSlot1;
parsedEventMonth = parsedSlot2;
parsedEventYear = parsedSlot3;
} else if (config.defaultDateFormat === DateTimeFormats.MMDDYYYY) {
// Year was not first, and cannot locate a day from the string, fall back to bot's default setting
parsedEventMonth = parsedSlot1;
parsedEventDay = parsedSlot2;
parsedEventYear = parsedSlot3;
}
return [parsedEventMonth, parsedEventDay, parsedEventYear];
};
// Takes user input Date and makes it actually usable // Takes user input Date and makes it actually usable
const parseEventDate = (preParsedEventDate: string): [string, string, string] => { const parseEventDate = (preParsedEventDate: string): [string, string, string] => {
const today = new Date(); const today = new Date();
const [parsedSlot1, parsedSlot2, parsedSlot3] = preParsedEventDate.split(/[\s,\\/-]+/g); let [parsedEventMonth, parsedEventDay, parsedEventYear] = preParsedEventDate.split(/[\s,\\/-]+/g);
let [parsedEventMonth, parsedEventDay, parsedEventYear] = determineDateTimeOrder(parsedSlot1, parsedSlot2, parsedSlot3);
if (isNaN(parseInt(parsedEventDay))) { if (isNaN(parseInt(parsedEventDay))) {
// User only provided one word, we're assuming it was TOMORROW, and all others will be treated as today // User only provided one word, we're assuming it was TOMORROW, and all others will be treated as today
@ -220,13 +154,12 @@ const parseEventDate = (preParsedEventDate: string): [string, string, string] =>
}; };
// Take full raw Date/Time input and convert it to a proper Date // Take full raw Date/Time input and convert it to a proper Date
export const getDateFromRawInput = (rawEventTime: string, rawEventTimeZone: string, rawEventDate: string, newEvent: boolean): [Date, string, boolean, boolean, string] => { export const getDateFromRawInput = (rawEventTime: string, rawEventTimeZone: string, rawEventDate: string): [Date, string, boolean, boolean] => {
// Verify/Set Time // Verify/Set Time
const [parsedEventTimeHours, parsedEventTimeMinutes, parsedEventTimePeriod] = parseEventTime(rawEventTime.replaceAll(':', '').toUpperCase()); const [parsedEventTimeHours, parsedEventTimeMinutes, parsedEventTimePeriod] = parseEventTime(rawEventTime.replaceAll(':', '').toUpperCase());
// Verify/Set Time Zone // Verify/Set Time Zone
const [parsedEventTimeZone, userInputTimeZone] = parseEventTimeZone(rawEventTimeZone.replaceAll(' ', '').trim().toUpperCase()); const [parsedEventTimeZone, userInputTimeZone] = parseEventTimeZone(rawEventTimeZone.replaceAll(' ', '').trim().toUpperCase());
const usTZWarning = usTZDSTCheck(userInputTimeZone, newEvent);
// Verify/Set Date // Verify/Set Date
const [parsedEventYear, parsedEventMonth, parsedEventDay] = parseEventDate(rawEventDate.trim().toUpperCase()); const [parsedEventYear, parsedEventMonth, parsedEventDay] = parseEventDate(rawEventDate.trim().toUpperCase());
@ -239,6 +172,5 @@ export const getDateFromRawInput = (rawEventTime: string, rawEventTimeZone: stri
} ${parsedEventDay}, ${parsedEventYear}`, } ${parsedEventDay}, ${parsedEventYear}`,
parsedDateTime.getTime() > new Date().getTime(), parsedDateTime.getTime() > new Date().getTime(),
!isNaN(parsedDateTime.getTime()), !isNaN(parsedDateTime.getTime()),
usTZWarning,
]; ];
}; };

View File

@ -67,7 +67,7 @@ const execute = async (bot: Bot, interaction: Interaction) => {
} }
// Get Date Object from user input // Get Date Object from user input
const [eventDateTime, eventDateTimeStr, eventInFuture, dateTimeValid, usTZWarning] = getDateFromRawInput(rawEventTime, rawEventTimeZone, rawEventDate, true); const [eventDateTime, eventDateTimeStr, eventInFuture, dateTimeValid] = getDateFromRawInput(rawEventTime, rawEventTimeZone, rawEventDate);
addTokenToMap(bot, interaction, interaction.guildId, interaction.channelId, interaction.member.id); addTokenToMap(bot, interaction, interaction.guildId, interaction.channelId, interaction.member.id);
bot.helpers.sendInteractionResponse( bot.helpers.sendInteractionResponse(
@ -89,7 +89,6 @@ const execute = async (bot: Bot, interaction: Interaction) => {
customIdIdxPath, customIdIdxPath,
eventInFuture, eventInFuture,
dateTimeValid, dateTimeValid,
usTZWarning,
), ),
).catch((e: Error) => utils.commonLoggers.interactionSendError('step2-finalize.ts', interaction, e)); ).catch((e: Error) => utils.commonLoggers.interactionSendError('step2-finalize.ts', interaction, e));
} else { } else {

View File

@ -2,7 +2,7 @@ import { ApplicationCommandFlags, Bot, Interaction, InteractionResponseTypes, Me
import { generateLFGButtons } from './utils.ts'; import { generateLFGButtons } from './utils.ts';
import { idSeparator, LfgEmbedIndexes } from '../eventUtils.ts'; import { idSeparator, LfgEmbedIndexes } from '../eventUtils.ts';
import { deleteTokenEarly } from '../tokenCleanup.ts'; import { deleteTokenEarly } from '../tokenCleanup.ts';
import { commonFixes, dmTestMessage, safelyDismissMsg, sendDirectMessage, somethingWentWrong, warnColor } from '../../commandUtils.ts'; import { dmTestMessage, safelyDismissMsg, sendDirectMessage, somethingWentWrong, warnColor } from '../../commandUtils.ts';
import { dbClient } from '../../db/client.ts'; import { dbClient } from '../../db/client.ts';
import { queries } from '../../db/common.ts'; import { queries } from '../../db/common.ts';
import utils from '../../utils.ts'; import utils from '../../utils.ts';
@ -52,7 +52,7 @@ const execute = async (bot: Bot, interaction: Interaction) => {
}], }],
}).catch((e: Error) => utils.commonLoggers.messageSendError('step3-createEvent.ts', 'createEvent', e)); }).catch((e: Error) => utils.commonLoggers.messageSendError('step3-createEvent.ts', 'createEvent', e));
if (!eventMessage) { if (!eventMessage) {
somethingWentWrong(bot, interaction, 'creatingEventSendMessageFinalizeEventStep', commonFixes.CANT_SEND_MESSAGE); somethingWentWrong(bot, interaction, 'creatingEventSendMessageFinalizeEventStep');
return; return;
} }

View File

@ -55,7 +55,7 @@ export const generateActionRow = (baseValue: string, activities: Array<Activity>
const createEventBtnName = 'Create Event'; const createEventBtnName = 'Create Event';
const createWhitelistedBtnName = 'Create Whitelisted Event'; const createWhitelistedBtnName = 'Create Whitelisted Event';
export const editEventDetailsBtnName = 'Edit Event Details'; const editEventDetailsBtnName = 'Edit Event Details';
export const invalidDateTimeStr = '`Invalid Date/Time`'; export const invalidDateTimeStr = '`Invalid Date/Time`';
const finalizeButtons = (idxPath: string, eventInFuture: boolean): [ButtonComponent, ButtonComponent, ButtonComponent] | [ButtonComponent] => { const finalizeButtons = (idxPath: string, eventInFuture: boolean): [ButtonComponent, ButtonComponent, ButtonComponent] | [ButtonComponent] => {
const editButton: ButtonComponent = { const editButton: ButtonComponent = {
@ -129,7 +129,6 @@ export const createLFGPost = (
idxPath: string, idxPath: string,
eventInFuture: boolean, eventInFuture: boolean,
dateTimeValid: boolean, dateTimeValid: boolean,
usTZWarning: string,
): InteractionResponse => { ): InteractionResponse => {
const icsDetails = `${category}: ${activity.name}`; const icsDetails = `${category}: ${activity.name}`;
const dateTimePastFutureStr = dateTimeValid ? 'in the past' : 'with an invalid date/time'; const dateTimePastFutureStr = dateTimeValid ? 'in the past' : 'with an invalid date/time';
@ -139,7 +138,7 @@ export const createLFGPost = (
data: { data: {
flags: ApplicationCommandFlags.Ephemeral, flags: ApplicationCommandFlags.Ephemeral,
content: eventInFuture content: eventInFuture
? `${usTZWarning}🛑🛑🛑 HEY! ONE MORE THING! 🛑🛑🛑\n\nPlease 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${ ? `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()) selfDestructMessage(new Date().getTime())
}` }`
: `You cannot create an event ${dateTimePastFutureStr}. Please change the event's \`Date/Time\` to be ${dateTimeValidStr} with the \`${editEventDetailsBtnName}\` button below.`, : `You cannot create an event ${dateTimePastFutureStr}. Please change the event's \`Date/Time\` to be ${dateTimeValidStr} with the \`${editEventDetailsBtnName}\` button below.`,

View File

@ -1,4 +1,3 @@
import config from '../../config.ts';
import { ActionRow, MessageComponentTypes, TextStyles } from '../../deps.ts'; import { ActionRow, MessageComponentTypes, TextStyles } from '../../deps.ts';
import { LFGMember } from '../types/commandTypes.ts'; import { LFGMember } from '../types/commandTypes.ts';
import { isDSTActive } from './event-creation/dateTimeUtils.ts'; import { isDSTActive } from './event-creation/dateTimeUtils.ts';
@ -27,12 +26,10 @@ export const alternateEventBtnStr = 'Join as Alternate';
export const noDescProvided = 'No description provided.'; export const noDescProvided = 'No description provided.';
// Member List generators // Member List generators
const escapeMemberNameForDisplay = (memberName: string): string => memberName.replaceAll('\\', '').replaceAll('_', '\\_');
export const generateMemberTitle = (memberList: Array<LFGMember>, maxMembers: number): string => `Members Joined: ${memberList.length}/${maxMembers}`; export const generateMemberTitle = (memberList: Array<LFGMember>, maxMembers: number): string => `Members Joined: ${memberList.length}/${maxMembers}`;
export const generateMemberList = (memberList: Array<LFGMember>): string => export const generateMemberList = (memberList: Array<LFGMember>): string => memberList.length ? memberList.map((member) => `${member.name} - <@${member.id}>`).join('\n') : noMembersStr;
memberList.length ? memberList.map((member) => `${escapeMemberNameForDisplay(member.name)} - <@${member.id}>`).join('\n') : noMembersStr;
export const generateAlternateList = (alternateList: Array<LFGMember>): string => export const generateAlternateList = (alternateList: Array<LFGMember>): string =>
alternateList.length ? alternateList.map((member) => `${escapeMemberNameForDisplay(member.name)} - <@${member.id}>${member.joined ? ' *' : ''}`).join('\n') : noMembersStr; alternateList.length ? alternateList.map((member) => `${member.name} - <@${member.id}>${member.joined ? ' *' : ''}`).join('\n') : noMembersStr;
// Fields for event creation and editing modals // Fields for event creation and editing modals
export const eventTimeId = 'eventTime'; export const eventTimeId = 'eventTime';
@ -87,7 +84,7 @@ export const dateTimeFields = (prefillTime = '', prefillTimeZone = '', prefillDa
type: MessageComponentTypes.InputText, type: MessageComponentTypes.InputText,
customId: eventDateId, customId: eventDateId,
label: 'Start Date:', label: 'Start Date:',
placeholder: `Enter date as "${config.defaultDateFormat}" or "Month Day, Year"`, placeholder: 'Enter date as "MONTH/DAY/YEAR" or "Month Day, Year"',
style: TextStyles.Short, style: TextStyles.Short,
minLength: 1, minLength: 1,
maxLength: 20, maxLength: 20,

View File

@ -33,7 +33,7 @@ const execute = async (bot: Bot, interaction: Interaction) => {
} }
// Get Date Object from user input // Get Date Object from user input
const [eventDateTime, eventDateTimeStr, eventInFuture, dateTimeValid, usTZWarning] = getDateFromRawInput(newTime, newTimeZone, newDate, false); const [eventDateTime, eventDateTimeStr, eventInFuture, dateTimeValid] = getDateFromRawInput(newTime, newTimeZone, newDate);
if (!eventInFuture || !dateTimeValid) { if (!eventInFuture || !dateTimeValid) {
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, { bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
type: InteractionResponseTypes.ChannelMessageWithSource, type: InteractionResponseTypes.ChannelMessageWithSource,
@ -70,7 +70,7 @@ const execute = async (bot: Bot, interaction: Interaction) => {
type: InteractionResponseTypes.ChannelMessageWithSource, type: InteractionResponseTypes.ChannelMessageWithSource,
data: { data: {
flags: ApplicationCommandFlags.Ephemeral, flags: ApplicationCommandFlags.Ephemeral,
content: applyEditMessage(new Date().getTime(), usTZWarning), content: applyEditMessage(new Date().getTime()),
embeds: [eventMessage.embeds[0]], embeds: [eventMessage.embeds[0]],
components: applyEditButtons(interaction.data.customId.split(idSeparator)[1] || ''), components: applyEditButtons(interaction.data.customId.split(idSeparator)[1] || ''),
}, },

View File

@ -31,7 +31,7 @@ const execute = async (bot: Bot, interaction: Interaction) => {
type: InteractionResponseTypes.ChannelMessageWithSource, type: InteractionResponseTypes.ChannelMessageWithSource,
data: { data: {
flags: ApplicationCommandFlags.Ephemeral, flags: ApplicationCommandFlags.Ephemeral,
content: applyEditMessage(new Date().getTime(), ''), content: applyEditMessage(new Date().getTime()),
embeds: [eventMessage.embeds[0]], embeds: [eventMessage.embeds[0]],
components: applyEditButtons(interaction.data.customId.split(idSeparator)[1] || ''), components: applyEditButtons(interaction.data.customId.split(idSeparator)[1] || ''),
}, },

View File

@ -12,133 +12,133 @@ export const confirmedCustomId = 'confirmedCustomId';
export const confirmStr = 'yes'; export const confirmStr = 'yes';
const execute = async (bot: Bot, interaction: Interaction) => { const execute = async (bot: Bot, interaction: Interaction) => {
if (interaction.data?.customId && interaction.data?.components?.length && interaction.channelId && interaction.guildId && interaction?.member?.user) { if (interaction.data?.customId && interaction.data?.components?.length && interaction.channelId && interaction.guildId && interaction?.member?.user) {
// Light Telemetry // Light Telemetry
dbClient.execute(queries.callIncCnt('btn-confirmDelEvent')).catch((e) => utils.commonLoggers.dbError('deleteConfirmed.ts@incCnt', 'call sproc INC_CNT on', e)); dbClient.execute(queries.callIncCnt('btn-confirmDelEvent')).catch((e) => utils.commonLoggers.dbError('deleteConfirmed.ts@incCnt', 'call sproc INC_CNT on', e));
// Parse out our data // Parse out our data
const tempDataMap: Map<string, string> = new Map(); const tempDataMap: Map<string, string> = new Map();
for (const row of interaction.data.components) { for (const row of interaction.data.components) {
if (row.components?.[0]) { if (row.components?.[0]) {
const textField = row.components[0]; const textField = row.components[0];
tempDataMap.set(textField.customId || 'missingCustomId', textField.value || 'missingValue'); tempDataMap.set(textField.customId || 'missingCustomId', textField.value || 'missingValue');
} }
} }
const actionByManager = interaction.data.customId.endsWith(pathIdxEnder); 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 [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)) || { const lfgChannelSetting = lfgChannelSettings.get(generateGuildSettingKey(interaction.guildId, interaction.channelId)) || {
managed: false, managed: false,
managerRoleId: 0n, managerRoleId: 0n,
logChannelId: 0n, logChannelId: 0n,
}; };
if (tempDataMap.get(confirmedCustomId)?.toLowerCase() === confirmStr) { if (tempDataMap.get(confirmedCustomId)?.toLowerCase() === confirmStr) {
const guildName = await getGuildName(bot, interaction.guildId); 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 eventMessage = await bot.helpers.getMessage(evtChannelId, evtMessageId).catch((e: Error) => utils.commonLoggers.messageGetError('deleteConfirmed.ts', 'get eventMessage', e));
const userId = interaction.member.id; const userId = interaction.member.id;
const userName = interaction.member.user.username; const userName = interaction.member.user.username;
// Delete event // Delete event
bot.helpers bot.helpers
.deleteMessage(evtChannelId, evtMessageId, 'User deleted event') .deleteMessage(evtChannelId, evtMessageId, 'User deleted event')
.then(() => { .then(() => {
dbClient.execute(queries.deleteEvent, [evtChannelId, evtMessageId]).catch((e) => utils.commonLoggers.dbError('deleteConfirmed.ts@deleteEvent', 'delete event from', e)); 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 // Acknowledge user so discord doesn't get annoyed
bot.helpers bot.helpers
.sendInteractionResponse(interaction.id, interaction.token, { .sendInteractionResponse(interaction.id, interaction.token, {
type: InteractionResponseTypes.ChannelMessageWithSource, type: InteractionResponseTypes.ChannelMessageWithSource,
data: { data: {
flags: ApplicationCommandFlags.Ephemeral, flags: ApplicationCommandFlags.Ephemeral,
embeds: [ embeds: [
{ {
color: successColor, color: successColor,
title: 'Event successfully deleted.', title: 'Event successfully deleted.',
description: safelyDismissMsg, description: safelyDismissMsg,
}, },
], ],
}, },
}) })
.catch((e: Error) => utils.commonLoggers.interactionSendError('deleteConfirmed.ts', interaction, e)); .catch((e: Error) => utils.commonLoggers.interactionSendError('deleteConfirmed.ts', interaction, e));
if (actionByManager) { if (actionByManager) {
const ownerId = BigInt(eventMessage?.embeds[0].footer?.iconUrl?.split('#')[1] || '0'); const ownerId = BigInt(eventMessage?.embeds[0].footer?.iconUrl?.split('#')[1] || '0');
const eventEmbed = eventMessage?.embeds[0] || { title: 'Event not found', color: failColor }; const eventEmbed = eventMessage?.embeds[0] || { title: 'Event not found', color: failColor };
bot.helpers bot.helpers
.sendMessage(lfgChannelSetting.logChannelId, { .sendMessage(lfgChannelSetting.logChannelId, {
embeds: [ embeds: [
{ {
color: infoColor2, color: infoColor2,
title: `Event deleted by a ${config.name} Manager`, title: `Event deleted by a ${config.name} Manager`,
description: `The following event was deleted by ${userName} - <@${userId}>.`, description: `The following event was deleted by ${userName} - <@${userId}>.`,
timestamp: new Date().getTime(), timestamp: new Date().getTime(),
}, },
eventEmbed, eventEmbed,
], ],
}) })
.catch((e: Error) => utils.commonLoggers.messageSendError('deleteConfirmed.ts', 'send log message', e)); .catch((e: Error) => utils.commonLoggers.messageSendError('deleteConfirmed.ts', 'send log message', e));
sendDirectMessage(bot, ownerId, { sendDirectMessage(bot, ownerId, {
embeds: [ embeds: [
{ {
color: infoColor2, color: infoColor2,
title: `Notice: A ${config.name} Manager has deleted one of your events in ${guildName}`, title: `Notice: A ${config.name} Manager has deleted one of your events in ${guildName}`,
description: 'The deleted event is listed below.', description: 'The deleted event is listed below.',
fields: [ fields: [
{ {
name: `${config.name} Manager:`, name: `${config.name} Manager:`,
value: generateMemberList([ value: generateMemberList([
{ {
id: userId, id: userId,
name: userName, name: userName,
}, },
]), ]),
inline: true, inline: true,
}, },
{ {
name: 'Are you unhappy with this action?', 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}.`, value: `Please reach out to the ${config.name} Manager that performed this action, or the moderators/administrators of ${guildName}.`,
}, },
], ],
}, },
eventEmbed, eventEmbed,
], ],
}).catch((e: Error) => utils.commonLoggers.messageSendError('deleteConfirmed.ts', 'send DM fail', e)); }).catch((e: Error) => utils.commonLoggers.messageSendError('deleteConfirmed.ts', 'send DM fail', e));
} }
}) })
.catch((e) => { .catch((e) => {
utils.commonLoggers.messageDeleteError('deleteConfirmed.ts', 'deleteEventFailedDB', e); utils.commonLoggers.messageDeleteError('deleteConfirmed.ts', 'deleteEventFailedDB', e);
somethingWentWrong(bot, interaction, 'deleteEventMessageInDeleteConfirmedButton'); somethingWentWrong(bot, interaction, 'deleteEventMessageInDeleteConfirmedButton');
}); });
} else if (tempDataMap.get(confirmedCustomId)) { } else if (tempDataMap.get(confirmedCustomId)) {
// User either did not type yes confirm field was missing, lets see which it was // User either did not type yes confirm field was missing, lets see which it was
// User did not type yes. // User did not type yes.
bot.helpers bot.helpers
.sendInteractionResponse(interaction.id, interaction.token, { .sendInteractionResponse(interaction.id, interaction.token, {
type: InteractionResponseTypes.ChannelMessageWithSource, type: InteractionResponseTypes.ChannelMessageWithSource,
data: { data: {
flags: ApplicationCommandFlags.Ephemeral, flags: ApplicationCommandFlags.Ephemeral,
embeds: [ embeds: [
{ {
color: infoColor1, color: infoColor1,
title: 'Event not deleted.', title: 'Event not deleted.',
description: `If you are trying to delete the event, please make sure you type \`${confirmStr}\` into the field provided. description: `If you are trying to delete the event, please make sure you type \`${confirmStr}\` into the field provided.
${safelyDismissMsg}`, ${safelyDismissMsg}`,
}, },
], ],
}, },
}) })
.catch((e: Error) => utils.commonLoggers.interactionSendError('deleteConfirmed.ts', interaction, e)); .catch((e: Error) => utils.commonLoggers.interactionSendError('deleteConfirmed.ts', interaction, e));
} else { } else {
// Field was missing // Field was missing
somethingWentWrong(bot, interaction, 'noIdsFromDeleteConfirmedButton'); somethingWentWrong(bot, interaction, 'noIdsFromDeleteConfirmedButton');
} }
} else { } else {
somethingWentWrong(bot, interaction, 'noDataFromDeleteConfirmedButton'); somethingWentWrong(bot, interaction, 'noDataFromDeleteConfirmedButton');
} }
}; };
export const deleteConfirmedButton = { export const deleteConfirmedButton = {
customId, customId,
execute, execute,
}; };

View File

@ -143,7 +143,7 @@ const execute = async (bot: Bot, interaction: Interaction) => {
type: InteractionResponseTypes.ChannelMessageWithSource, type: InteractionResponseTypes.ChannelMessageWithSource,
data: { data: {
flags: ApplicationCommandFlags.Ephemeral, flags: ApplicationCommandFlags.Ephemeral,
content: applyEditMessage(new Date().getTime(), ''), content: applyEditMessage(new Date().getTime()),
embeds: [eventMessage.embeds[0]], embeds: [eventMessage.embeds[0]],
components: applyEditButtons(interaction.data.customId.replaceAll(fillerChar, '').split(idSeparator)[1] || ''), components: applyEditButtons(interaction.data.customId.replaceAll(fillerChar, '').split(idSeparator)[1] || ''),
}, },

View File

@ -106,7 +106,7 @@ ${safelyDismissMsg}`,
timestamp: new Date().getTime(), timestamp: new Date().getTime(),
}); });
}).catch((e: Error) => { }).catch((e: Error) => {
somethingWentWrong(bot, interaction, 'failedToDMOwnerInRequestToJoinEventButton', `${config.name} could not message <@${ownerId}>. This likely means <@${ownerId}> has turned off DMs.`); somethingWentWrong(bot, interaction, 'failedToDMOwnerInRequestToJoinEventButton');
utils.commonLoggers.messageSendError('joinEvent.ts@dmOwner', 'failed to DM owner for join request', e); utils.commonLoggers.messageSendError('joinEvent.ts@dmOwner', 'failed to DM owner for join request', e);
}); });
} }

View File

@ -194,7 +194,7 @@ export const removeMemberFromEvent = async (
await sendDirectMessage(bot, memberToPromote.id, { await sendDirectMessage(bot, memberToPromote.id, {
embeds: [{ embeds: [{
color: successColor, color: successColor,
title: "Good news, you've been promoted!", title: 'Good news, you\'ve been promoted!',
description: `A member left [the full event](${utils.idsToMessageUrl(urlIds)}) in \`${await getGuildName( description: `A member left [the full event](${utils.idsToMessageUrl(urlIds)}) in \`${await getGuildName(
bot, bot,
evtGuildId, evtGuildId,
@ -353,8 +353,8 @@ export const joinRequestResponseButtons = (disabled: boolean): ActionRow[] => [{
}]; }];
export const applyEditButtonName = 'Apply Edit'; export const applyEditButtonName = 'Apply Edit';
export const applyEditMessage = (currentTime: number, usTZWarning: string) => export const applyEditMessage = (currentTime: number) =>
`${usTZWarning}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${ `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) selfDestructMessage(currentTime)
}`; }`;
export const applyEditButtons = (idxPath: string): ActionRow[] => [{ export const applyEditButtons = (idxPath: string): ActionRow[] => [{

View File

@ -27,7 +27,7 @@ export const isLFGChannel = (guildId: bigint, channelId: bigint) => {
}; };
// Tell user to try again or report issue // Tell user to try again or report issue
export const somethingWentWrong = (bot: Bot, interaction: Interaction, errorCode: string, possibleFix = 'No fix provided.') => export const somethingWentWrong = (bot: Bot, interaction: Interaction, errorCode: string) =>
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, { bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
type: InteractionResponseTypes.ChannelMessageWithSource, type: InteractionResponseTypes.ChannelMessageWithSource,
data: { data: {
@ -35,24 +35,15 @@ export const somethingWentWrong = (bot: Bot, interaction: Interaction, errorCode
embeds: [{ embeds: [{
color: failColor, color: failColor,
title: 'Something went wrong...', title: 'Something went wrong...',
description: 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.`,
`You should not be able to get here. If ${config.name} has seen this error before, the developer may have a possible fix for you to try. If one is provided, please attempt it before \`/${reportSlashName}\`ing it. If the issue continues, please \`/${reportSlashName}\` this issue to the developer with the error code below.`,
fields: [{ fields: [{
name: 'Error Code:', name: 'Error Code:',
value: `\`${errorCode}\``, value: `\`${errorCode}\``,
}, {
name: 'Possible Fix:',
value: possibleFix,
}], }],
}], }],
}, },
}).catch((e: Error) => utils.commonLoggers.interactionSendError('commandUtils.ts@somethingWentWrong', interaction, e)); }).catch((e: Error) => utils.commonLoggers.interactionSendError('commandUtils.ts@somethingWentWrong', interaction, e));
// Possible fixes for the user to try before reporting.
export const commonFixes = {
CANT_SEND_MESSAGE: `Please verify ${config.name} has permission to send messages in this channel.`,
};
// Smack the user for trying to modify an event that isn't theirs // Smack the user for trying to modify an event that isn't theirs
export const stopThat = (bot: Bot, interaction: Interaction, stopWhat: string) => export const stopThat = (bot: Bot, interaction: Interaction, stopWhat: string) =>
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, { bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {

View File

@ -1,5 +1,5 @@
import config from '../../config.ts'; import config from '../../config.ts';
import { ApplicationCommandOptionTypes, ApplicationCommandTypes, BotWithCache, DiscordEmbedField, Interaction, InteractionResponseTypes } from '../../deps.ts'; import { ApplicationCommandOptionTypes, ApplicationCommandTypes, Bot, DiscordEmbedField, Interaction, InteractionResponseTypes } from '../../deps.ts';
import { infoColor2, isLFGChannel, somethingWentWrong } from '../commandUtils.ts'; import { infoColor2, isLFGChannel, somethingWentWrong } from '../commandUtils.ts';
import { dbClient } from '../db/client.ts'; import { dbClient } from '../db/client.ts';
import { queries } from '../db/common.ts'; import { queries } from '../db/common.ts';
@ -7,20 +7,8 @@ import { CommandDetails } from '../types/commandTypes.ts';
import utils from '../utils.ts'; import utils from '../utils.ts';
import { auditSlashName } from './slashCommandNames.ts'; import { auditSlashName } from './slashCommandNames.ts';
type DupeAct = {
upperActTitle?: string;
upperActSubtitle?: string;
dupeCount: number;
};
type DBSizeTable = {
table: string;
size: number;
rows: number;
};
const auditDbName = 'database'; const auditDbName = 'database';
const auditCustomActivitiesName = 'custom-activities'; const auditCustomActivities = 'custom-activities';
const auditGuildName = 'guilds'; const auditGuildName = 'guilds';
const details: CommandDetails = { const details: CommandDetails = {
@ -35,7 +23,7 @@ const details: CommandDetails = {
description: `Developer Command: Checks ${config.name}'s DB size.`, description: `Developer Command: Checks ${config.name}'s DB size.`,
}, },
{ {
name: auditCustomActivitiesName, name: auditCustomActivities,
type: ApplicationCommandOptionTypes.SubCommand, type: ApplicationCommandOptionTypes.SubCommand,
description: 'Developer Command: Checks for duplicate custom activities.', description: 'Developer Command: Checks for duplicate custom activities.',
}, },
@ -47,18 +35,18 @@ const details: CommandDetails = {
], ],
}; };
const execute = async (bot: BotWithCache, interaction: Interaction) => { const execute = async (bot: Bot, interaction: Interaction) => {
if (interaction.member && interaction.guildId && interaction.data?.options?.[0].options) { 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)); 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; const auditName = interaction.data.options[0].name;
switch (auditName) { switch (auditName) {
case auditDbName: { case auditDbName: {
// Get DB statistics // Get DB statistics
const auditQuery: Array<DBSizeTable> = await dbClient.query(`SELECT * FROM db_size;`).catch((e) => utils.commonLoggers.dbError('audit.ts@dbSize', 'query', e)); 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 // 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> = []; const embedFields: Array<DiscordEmbedField> = [];
auditQuery.forEach((row) => { auditQuery.forEach((row: any) => {
embedFields.push({ embedFields.push({
name: `${row.table}`, name: `${row.table}`,
value: `**Size:** ${row.size} MB value: `**Size:** ${row.size} MB
@ -66,129 +54,27 @@ const execute = async (bot: BotWithCache, interaction: Interaction) => {
inline: true, inline: true,
}); });
}); });
bot.helpers bot.helpers.sendInteractionResponse(
.sendInteractionResponse(interaction.id, interaction.token, { interaction.id,
type: InteractionResponseTypes.ChannelMessageWithSource, interaction.token,
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 auditCustomActivitiesName: {
const dupActTitles: Array<DupeAct> = await dbClient.query(
`SELECT UPPER(activityTitle) as upperActTitle, COUNT(*) as dupeCount FROM custom_activities GROUP BY upperActTitle HAVING dupeCount > 1;`,
).catch((e) => utils.commonLoggers.dbError('audit.ts@customActTitle', 'query', e));
const dupActSubTitles: Array<DupeAct> = await dbClient.query(
`SELECT UPPER(activitySubtitle) as upperActSubtitle, COUNT(*) as dupeCount FROM custom_activities GROUP BY upperActSubtitle HAVING dupeCount > 1;`,
).catch((e) => utils.commonLoggers.dbError('audit.ts@customActSubTitle', 'query', e));
const dupActs: Array<DupeAct> = await dbClient
.query(
`SELECT UPPER(activityTitle) as upperActTitle, UPPER(activitySubtitle) as upperActSubtitle, COUNT(*) as dupeCount FROM custom_activities GROUP BY upperActTitle, upperActSubtitle HAVING dupeCount > 1;`,
)
.catch((e) => utils.commonLoggers.dbError('audit.ts@customAct', 'query', e));
bot.helpers
.sendInteractionResponse(interaction.id, interaction.token, {
type: InteractionResponseTypes.ChannelMessageWithSource,
data: {
flags: isLFGChannel(interaction.guildId || 0n, interaction.channelId || 0n),
content: 'Duplicate Custom Activity Titles, Subtitles, and Activities:',
embeds: [
{
color: infoColor2,
title: 'Duplicate Activity Titles:',
description: dupActTitles.map((dupAct) => `${dupAct.upperActTitle}: ${dupAct.dupeCount}`).join('\n'),
timestamp: new Date().getTime(),
},
{
color: infoColor2,
title: 'Duplicate Activity Subtitles:',
description: dupActSubTitles.map((dupAct) => `${dupAct.upperActSubtitle}: ${dupAct.dupeCount}`).join('\n'),
timestamp: new Date().getTime(),
},
{
color: infoColor2,
title: 'Duplicate Activities (Title/Subtitle):',
description: dupActs.map((dupAct) => `${dupAct.upperActTitle}/${dupAct.upperActSubtitle}: ${dupAct.dupeCount}`).join('\n'),
timestamp: new Date().getTime(),
},
],
},
})
.catch((e: Error) => utils.commonLoggers.interactionSendError('audit.ts@dbSize', interaction, e));
break;
}
case auditGuildName: {
let totalCount = 0;
let auditText = '';
bot.guilds.forEach((guild) => {
totalCount += guild.memberCount;
auditText += `Guild: ${guild.name} (${guild.id})
Owner: ${guild.ownerId}
Tot mem: ${guild.memberCount}
`;
});
const b = await new Blob([auditText as BlobPart], { 'type': 'text' });
const tooBig = await new Blob(['tooBig' as BlobPart], { 'type': 'text' });
bot.helpers
.sendInteractionResponse(interaction.id, interaction.token, {
type: InteractionResponseTypes.ChannelMessageWithSource, type: InteractionResponseTypes.ChannelMessageWithSource,
data: { data: {
flags: isLFGChannel(interaction.guildId || 0n, interaction.channelId || 0n), flags: isLFGChannel(interaction.guildId || 0n, interaction.channelId || 0n),
embeds: [{ embeds: [{
color: infoColor2, color: infoColor2,
title: 'Guilds Audit', title: 'Database Audit',
description: `Shows details of the guilds that ${config.name} serves. description: 'Lists all tables with their current size and row count.',
Please see attached file for audit details on cached guilds and members.`,
fields: [
{
name: 'Total Guilds:',
value: `${bot.guilds.size}`,
inline: true,
},
{
name: 'Uncached Guilds:',
value: `${bot.dispatchedGuildIds.size}`,
inline: true,
},
{
name: 'Total Members\n(may be artificially higher if 1 user is in multiple guilds the bot is in):',
value: `${totalCount}`,
inline: true,
},
{
name: 'Average members per guild:',
value: `${(totalCount / bot.guilds.size).toFixed(2)}`,
inline: true,
},
],
timestamp: new Date().getTime(), timestamp: new Date().getTime(),
fields: embedFields.slice(0, 25),
}], }],
file: {
'blob': b.size > 8388290 ? tooBig : b,
'name': 'auditDetails.txt',
},
}, },
}) },
.catch((e: Error) => utils.commonLoggers.interactionSendError('audit.ts@guilds', interaction, e)); ).catch((e: Error) => utils.commonLoggers.interactionSendError('audit.ts@dbSize', interaction, e));
break; break;
} }
case auditCustomActivities:
case auditGuildName:
default: default:
somethingWentWrong(bot, interaction, `auditNameNotHandled@${auditName}`); somethingWentWrong(bot, interaction, `auditNameNotHandled@${auditName}`);
break; break;

View File

@ -31,9 +31,6 @@ const execute = (bot: Bot, interaction: Interaction) => {
color: infoColor2, color: infoColor2,
title: 'USER REPORT:', title: 'USER REPORT:',
description: interaction.data.options[0].value as string, description: interaction.data.options[0].value as string,
footer: {
text: `${interaction.guildId}-${interaction.user.id}`,
},
}], }],
}).catch((e: Error) => utils.commonLoggers.interactionSendError('report.ts:28', interaction, e)); }).catch((e: Error) => utils.commonLoggers.interactionSendError('report.ts:28', interaction, e));
bot.helpers.sendInteractionResponse( bot.helpers.sendInteractionResponse(

View File

@ -3,9 +3,9 @@ import { Client } from '../../deps.ts';
import { LOCALMODE } from '../../flags.ts'; import { LOCALMODE } from '../../flags.ts';
export const dbClient = await new Client().connect({ export const dbClient = await new Client().connect({
hostname: LOCALMODE ? config.db.localhost : config.db.host, hostname: LOCALMODE ? config.db.localhost : config.db.host,
port: config.db.port, port: config.db.port,
db: config.db.name, db: config.db.name,
username: config.db.username, username: config.db.username,
password: config.db.password, password: config.db.password,
}); });

View File

@ -1,4 +1,4 @@
import { ApplicationCommandOption, ApplicationCommandTypes, Bot, BotWithCache, Interaction, PermissionStrings } from '../../deps.ts'; import { ApplicationCommandOption, ApplicationCommandTypes, PermissionStrings } from '../../deps.ts';
export type CommandDetails = { export type CommandDetails = {
name: string; name: string;
@ -11,12 +11,12 @@ export type CommandDetails = {
export type Command = { export type Command = {
details: CommandDetails; details: CommandDetails;
execute: ((bot: Bot, interaction: Interaction) => void) | ((bot: BotWithCache, interaction: Interaction) => void); execute: Function;
}; };
export type Button = { export type Button = {
customId: string; customId: string;
execute: ((bot: Bot, interaction: Interaction) => void) | ((bot: BotWithCache, interaction: Interaction) => void); execute: Function;
}; };
export type LfgChannelSetting = { export type LfgChannelSetting = {

View File

@ -1,7 +1,6 @@
import { CreateMessage, Interaction, log, LT, Message } from '../deps.ts'; import { CreateMessage, Interaction, log, LT, Message } from '../deps.ts';
import { UrlIds } from './types/commandTypes.ts'; import { UrlIds } from './types/commandTypes.ts';
// deno-lint-ignore no-explicit-any
const jsonStringifyBig = (input: any) => { const jsonStringifyBig = (input: any) => {
return JSON.stringify(input, (_key, value) => typeof value === 'bigint' ? value.toString() + 'n' : value); return JSON.stringify(input, (_key, value) => typeof value === 'bigint' ? value.toString() + 'n' : value);
}; };

View File

@ -0,0 +1,54 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
<meta name="HandheldFriendly" content="true"/>
<meta name="author" content="Ean Milligan (ean@milligan.dev)">
<meta name="designer" content="Ean Milligan (ean@milligan.dev)">
<meta name="publisher" content="Ean Milligan (ean@milligan.dev)">
<title>Group Up Time Checker</title>
<meta name="description" content="The Group Up Discord Bot is a fancy event scheduler. This page converts the time of an event to the user's local time.">
<meta name="robots" content="index, follow">
<meta name="revisit-after" content="7 days">
<meta name="distribution" content="web">
<meta name="robots" content="noodp, noydir">
<meta name="distribution" content="web">
<meta name="web_author" content="Ean Milligan (ean@milligan.dev)">
<style>
body {
font-family: sans-serif;
font-size: 5rem;
height: 100vh;
padding: 0;
margin: 0;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
color: #dcddde;
background-color: #2f3136;
}
</style>
</head>
<body>
<div id="message">
I don't know why you are here, the URL should have a timestamp in it.
</div>
<script>
if (window.location.hash) {
var groupDate = new Date(parseInt(window.location.hash.substr(1)));
document.getElementById("message").innerText = "Your event is happening at:\n\n" + groupDate.toLocaleString();
}
</script>
</body>
</html>