Compare commits

...

37 Commits

Author SHA1 Message Date
Ean Milligan ebe707ae26 !!BREAKING CHANGE!! This commit updates the Log4Deno dependency to support Deno 2.X, which means Deno 1.X is no longer supported. 2024-12-24 01:34:12 -05:00
Ean Milligan 6dab4b64b0 Add additional note to config.example.ts for discord IDs being bigints. 2024-12-24 01:13:45 -05:00
David Lau c7d12c25f4
Sanitize usernames for display (#4)
* Sanitize away underscores and asterisks for display names

* Bump version

* Bump version and date

* Formatting and rename param

* Un-raw dogged formatting

* lmao I don't know TS

* V1.1.6 - properly fix the formatting issue

---------

Co-authored-by: Ean Milligan <ean.milligan@gmail.com>
2024-07-10 00:54:12 -04:00
Ean Milligan b2c821991d V1.1.5
update member lists to prevent usernames with underscores from creating italics
2024-06-28 21:26:30 -04:00
Ean Milligan 841382df3d V1.1.4 - add activity 2024-06-18 23:47:49 -04:00
David Lau 51db47c828
Add Salvation's Edge (#3)
* Update README.md

* Update config.example.ts

* Update activities.ts
2024-06-09 23:24:04 -04:00
Ean Milligan 1a29eb7f33
Create CONTRIBUTING.md 2024-05-21 17:52:22 -04:00
Ean Milligan 0b8ae5c4e7
Update CODEOWNERS 2024-05-21 17:40:50 -04:00
Ean Milligan 073f8339ee
Update CODEOWNERS 2024-05-21 17:39:15 -04:00
David Lau f0a3e7d0da
Add Presage (#2)
This change adds Presage
2024-05-21 17:34:34 -04:00
Ean Milligan 3d72e4b586 update deno.yml, remove test step for now 2024-05-21 17:30:58 -04:00
Ean Milligan ec6781afc9 Merge branch 'master' of https://github.com/Burn-E99/GroupUp 2024-05-21 17:29:43 -04:00
Ean Milligan ebcc7d4a31 deno fmt and lint fixes 2024-05-21 17:29:00 -04:00
Ean Milligan 434599e411
Create deno.yml
add deno checks
2024-05-21 17:21:02 -04:00
Ean Milligan ef70de972a update codeowners 2024-05-21 17:10:21 -04:00
Ean Milligan 0a2a1259be add codeowners 2024-05-21 16:53:00 -04:00
Ean Milligan 8f5dc6a1e6 Add Pantheon and Onslaught activities 2024-05-21 16:38:08 -04:00
Ean Milligan ee09ad8c9c deno fmt 2024-05-21 05:00:32 -04:00
Ean Milligan ee501e4333 bump vernum 2024-05-21 04:59:42 -04:00
Ean Milligan acc168673a Add a couple possible fixes to the somethingWentWrong function 2024-05-21 04:42:12 -04:00
Ean Milligan 9869a602af add missing params 2024-05-21 04:35:13 -04:00
Ean Milligan 87aed21868 update privacy policy to note guild and user id are now being saved 2024-05-21 04:22:53 -04:00
Ean Milligan a7ecac486b add guild and user id logging to report command to facilitate data deletion. 2024-05-21 04:22:26 -04:00
Ean Milligan 2e3435db51 refactor for sonar, deno fmt 2024-05-21 04:06:29 -04:00
Ean Milligan f4c10e372d change date/time modal from using constant to config for dateFormat 2024-05-21 03:55:34 -04:00
Ean Milligan 74c5c6e4e9 Drastically improve the date parsing, allowing the user to do things group up doesn't recommend, such as using ISO8601, or going against the defaultDateFormat (at least when we can detect, if the user want 5/2/2025 instead of 2/5/2025, they need to follow the rules or spell it out) 2024-05-21 03:52:32 -04:00
Ean Milligan cd95894691 Fix UTC/GMT parsing, what the heck was I thinking when I wrote that 2024-05-21 03:20:01 -04:00
Ean Milligan 123aaa89a0 Add Hawaii and Alaska time zones to US list, add warning for when user possibly doesn't know the difference between XDT and XST. 2024-05-21 03:08:36 -04:00
Ean Milligan 05ac1a79dd add deno lint comment for universal util function 2024-05-21 03:07:07 -04:00
Ean Milligan 5ccccae1f4 Add activity verification 2024-05-20 19:40:41 -04:00
Ean Milligan 0b8ac79e5c Readd zero hour and whisper 2024-05-20 18:39:43 -04:00
Ean Milligan 6a951dff06 Remove tzConverter, not required for use anymore since Discord handles that now. 2024-05-20 18:38:01 -04:00
Ean Milligan 69e9ba6a6e Add notice to final stage of event creation to make sure people don't forget to finalize the event. 2024-05-20 18:36:37 -04:00
Ean Milligan d36304a774 Add additional details for self hosting bot 2024-05-20 03:16:02 -04:00
Ean Milligan 5ed49c6cab Update my username 2024-05-20 03:07:53 -04:00
Ean Milligan 9677a886d9 fix links 2024-05-20 03:06:38 -04:00
Ean Milligan bc5f7a0473 Add Guild and CustomActivity auditing 2024-05-20 02:54:45 -04:00
26 changed files with 533 additions and 247 deletions

5
.github/CODEOWNERS vendored Normal file
View File

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

42
.github/workflows/deno.yml vendored Normal file
View File

@ -0,0 +1,42 @@
# 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

10
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,10 @@
# 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
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.
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.
_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.
* 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 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 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:
@ -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.
## 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.
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.

View File

@ -1,4 +1,4 @@
# Group Up - An Event Scheduling Discord Bot | V1.0.4 - 2024/05/04
# Group Up - An Event Scheduling Discord Bot | V2.0.0 - 2024/12/24
[![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)
@ -43,7 +43,12 @@ If you run into any errors or problems with the bot, or think you have a good id
---
## 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.
Group Up is built on [Deno](https://deno.land/) using [Discordeno](https://discordeno.mod.land/) `v17.0.1`.
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.
@ -54,6 +59,6 @@ Once everything is set up, starting the bot can simply be done with `deno run --
## 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).
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).
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).
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).

View File

@ -1,10 +1,10 @@
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
'version': '1.0.4', // Version of the bot
'version': '2.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': '/', // 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
'localToken': 'local_testing_token', // Discord API Token for a secondary OPTIONAL testing bot, THIS SHOULD BE DIFFERENT FROM "token"
'prefix': '/', // Prefix for all commands, as this bot uses slash commands, this needs to be '/'
'db': { // Settings for the MySQL database, this is required to keep track of the currently active events.
'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
@ -13,16 +13,17 @@ export const config = {
'name': '', // Name of the database Schema to use for the bot
},
'links': { // 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
'sourceCode': 'https://github.com/Burn-E99/GroupUp', // Link to the repository, OPTIONAL
'supportServer': '', // Invite link to the Discord support server, OPTIONAL
'addToCalendar': '', // Link to where the icsGenerator is hosted, OPTIONAL
'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
'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. 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. 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. 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. 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, OPTIONAL
{ // Bot List object, duplicate for each bot list
'name': 'Bot List Name', // Name of bot list, not used
'enabled': false, // Should statistics be posted to this list?

View File

@ -47,4 +47,4 @@ export type {
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/V1.1.0/mod.ts';
export { initLog, log, LogTypes as LT } from 'https://raw.githubusercontent.com/Burn-E99/Log4Deno/V2.0.0/mod.ts';

View File

@ -1,3 +1,5 @@
import { log, LT } from '../../../deps.ts';
// Activity should either have maxMembers or options specified, NOT both
export type Activity = {
name: string;
@ -14,7 +16,11 @@ export const Activities: Array<Activity> = [
name: 'Raids',
options: [
{
name: 'Crota\'s End',
name: "Salvation's Edge",
maxMembers: 6,
},
{
name: "Crota's End",
maxMembers: 6,
},
{
@ -22,7 +28,7 @@ export const Activities: Array<Activity> = [
maxMembers: 6,
},
{
name: 'King\'s Fall',
name: "King's Fall",
maxMembers: 6,
},
{
@ -51,7 +57,7 @@ export const Activities: Array<Activity> = [
name: 'Dungeons',
options: [
{
name: 'Warlord\'s Ruin',
name: "Warlord's Ruin",
maxMembers: 3,
},
{
@ -154,11 +160,52 @@ export const Activities: Array<Activity> = [
name: '//node.ovrd.AVALON//',
maxMembers: 3,
},
{
name: 'Zero Hour',
maxMembers: 3,
},
{
name: 'The Whisper',
maxMembers: 3,
},
{
name: 'Presage',
maxMembers: 3,
},
],
},
{
name: 'Miscellaneous/Seasonal',
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',
maxMembers: 3,
@ -233,3 +280,33 @@ 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,3 +1,10 @@
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'];
export const monthsShort: Array<string> = monthsLong.map((month) => month.slice(0, 3));
const tzMap: Map<string, string> = new Map([
@ -52,7 +59,8 @@ const tzMap: Map<string, string> = new Map([
['CHST', '+10:00'],
['SST', '-11:00'],
]);
const shorthandUSTZ: Array<string> = ['ET', 'CT', 'MT', 'PT'];
const shorthandUSTZ: Array<string> = ['ET', 'CT', 'MT', 'PT', 'HT', 'AKT'];
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
const parseEventTime = (preParsedEventTime: string): [string, string, string] => {
@ -93,14 +101,33 @@ export const isDSTActive = (): boolean => {
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
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`;
preParsedEventTimeZone = `${preParsedEventTimeZone.slice(0, -1)}DT`;
} else {
preParsedEventTimeZone = `${preParsedEventTimeZone.slice(0, 1)}ST`;
preParsedEventTimeZone = `${preParsedEventTimeZone.slice(0, -1)}ST`;
}
}
if (tzMap.has(preParsedEventTimeZone)) {
@ -113,8 +140,8 @@ const parseEventTimeZone = (preParsedEventTimeZone: string): [string, string] =>
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}`;
if (!preParsedEventTimeZone.startsWith('UTC') || preParsedEventTimeZone.startsWith('GMT')) {
preParsedEventTimeZone = `UTC${addPlusSign ? '+' : ''}${preParsedEventTimeZone.startsWith('GMT') ? preParsedEventTimeZone.slice(3) : preParsedEventTimeZone}`;
} else if (addPlusSign) {
preParsedEventTimeZone = `${preParsedEventTimeZone.slice(0, 3)}+${preParsedEventTimeZone.slice(3)}`;
}
@ -122,10 +149,49 @@ 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
const parseEventDate = (preParsedEventDate: string): [string, string, string] => {
const today = new Date();
let [parsedEventMonth, parsedEventDay, parsedEventYear] = preParsedEventDate.split(/[\s,\\/-]+/g);
const [parsedSlot1, parsedSlot2, parsedSlot3] = preParsedEventDate.split(/[\s,\\/-]+/g);
let [parsedEventMonth, parsedEventDay, parsedEventYear] = determineDateTimeOrder(parsedSlot1, parsedSlot2, parsedSlot3);
if (isNaN(parseInt(parsedEventDay))) {
// User only provided one word, we're assuming it was TOMORROW, and all others will be treated as today
@ -154,12 +220,13 @@ const parseEventDate = (preParsedEventDate: string): [string, string, string] =>
};
// 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] => {
export const getDateFromRawInput = (rawEventTime: string, rawEventTimeZone: string, rawEventDate: string, newEvent: boolean): [Date, string, boolean, boolean, string] => {
// Verify/Set Time
const [parsedEventTimeHours, parsedEventTimeMinutes, parsedEventTimePeriod] = parseEventTime(rawEventTime.replaceAll(':', '').toUpperCase());
// Verify/Set Time Zone
const [parsedEventTimeZone, userInputTimeZone] = parseEventTimeZone(rawEventTimeZone.replaceAll(' ', '').trim().toUpperCase());
const usTZWarning = usTZDSTCheck(userInputTimeZone, newEvent);
// Verify/Set Date
const [parsedEventYear, parsedEventMonth, parsedEventDay] = parseEventDate(rawEventDate.trim().toUpperCase());
@ -172,5 +239,6 @@ export const getDateFromRawInput = (rawEventTime: string, rawEventTimeZone: stri
} ${parsedEventDay}, ${parsedEventYear}`,
parsedDateTime.getTime() > new Date().getTime(),
!isNaN(parsedDateTime.getTime()),
usTZWarning,
];
};

View File

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

View File

@ -2,7 +2,7 @@ import { ApplicationCommandFlags, Bot, Interaction, InteractionResponseTypes, Me
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 { commonFixes, dmTestMessage, safelyDismissMsg, sendDirectMessage, somethingWentWrong, warnColor } from '../../commandUtils.ts';
import { dbClient } from '../../db/client.ts';
import { queries } from '../../db/common.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));
if (!eventMessage) {
somethingWentWrong(bot, interaction, 'creatingEventSendMessageFinalizeEventStep');
somethingWentWrong(bot, interaction, 'creatingEventSendMessageFinalizeEventStep', commonFixes.CANT_SEND_MESSAGE);
return;
}

View File

@ -55,7 +55,7 @@ export const generateActionRow = (baseValue: string, activities: Array<Activity>
const createEventBtnName = 'Create Event';
const createWhitelistedBtnName = 'Create Whitelisted Event';
const editEventDetailsBtnName = 'Edit Event Details';
export const editEventDetailsBtnName = 'Edit Event Details';
export const invalidDateTimeStr = '`Invalid Date/Time`';
const finalizeButtons = (idxPath: string, eventInFuture: boolean): [ButtonComponent, ButtonComponent, ButtonComponent] | [ButtonComponent] => {
const editButton: ButtonComponent = {
@ -129,6 +129,7 @@ export const createLFGPost = (
idxPath: string,
eventInFuture: boolean,
dateTimeValid: boolean,
usTZWarning: string,
): InteractionResponse => {
const icsDetails = `${category}: ${activity.name}`;
const dateTimePastFutureStr = dateTimeValid ? 'in the past' : 'with an invalid date/time';
@ -138,7 +139,7 @@ export const createLFGPost = (
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${
? `${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${
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.`,

View File

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

View File

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

View File

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

View File

@ -12,133 +12,133 @@ 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?.user) {
// Light Telemetry
dbClient.execute(queries.callIncCnt('btn-confirmDelEvent')).catch((e) => utils.commonLoggers.dbError('deleteConfirmed.ts@incCnt', 'call sproc INC_CNT on', e));
if (interaction.data?.customId && interaction.data?.components?.length && interaction.channelId && interaction.guildId && 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,
};
// 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));
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));
// 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));
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 if (tempDataMap.get(confirmedCustomId)) {
// User either did not type yes confirm field was missing, lets see which it was
// 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.
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 if (tempDataMap.get(confirmedCustomId)) {
// User either did not type yes confirm field was missing, lets see which it was
// 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');
}
},
],
},
})
.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,
customId,
execute,
};

View File

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

View File

@ -106,7 +106,7 @@ ${safelyDismissMsg}`,
timestamp: new Date().getTime(),
});
}).catch((e: Error) => {
somethingWentWrong(bot, interaction, 'failedToDMOwnerInRequestToJoinEventButton');
somethingWentWrong(bot, interaction, 'failedToDMOwnerInRequestToJoinEventButton', `${config.name} could not message <@${ownerId}>. This likely means <@${ownerId}> has turned off DMs.`);
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, {
embeds: [{
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(
bot,
evtGuildId,
@ -353,8 +353,8 @@ export const joinRequestResponseButtons = (disabled: boolean): ActionRow[] => [{
}];
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${
export const applyEditMessage = (currentTime: number, usTZWarning: string) =>
`${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${
selfDestructMessage(currentTime)
}`;
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
export const somethingWentWrong = (bot: Bot, interaction: Interaction, errorCode: string) =>
export const somethingWentWrong = (bot: Bot, interaction: Interaction, errorCode: string, possibleFix = 'No fix provided.') =>
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
type: InteractionResponseTypes.ChannelMessageWithSource,
data: {
@ -35,15 +35,24 @@ export const somethingWentWrong = (bot: Bot, interaction: Interaction, errorCode
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.`,
description:
`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: [{
name: 'Error Code:',
value: `\`${errorCode}\``,
}, {
name: 'Possible Fix:',
value: possibleFix,
}],
}],
},
}).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
export const stopThat = (bot: Bot, interaction: Interaction, stopWhat: string) =>
bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {

View File

@ -1,5 +1,5 @@
import config from '../../config.ts';
import { ApplicationCommandOptionTypes, ApplicationCommandTypes, Bot, DiscordEmbedField, Interaction, InteractionResponseTypes } from '../../deps.ts';
import { ApplicationCommandOptionTypes, ApplicationCommandTypes, BotWithCache, DiscordEmbedField, Interaction, InteractionResponseTypes } from '../../deps.ts';
import { infoColor2, isLFGChannel, somethingWentWrong } from '../commandUtils.ts';
import { dbClient } from '../db/client.ts';
import { queries } from '../db/common.ts';
@ -7,8 +7,20 @@ import { CommandDetails } from '../types/commandTypes.ts';
import utils from '../utils.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 auditCustomActivities = 'custom-activities';
const auditCustomActivitiesName = 'custom-activities';
const auditGuildName = 'guilds';
const details: CommandDetails = {
@ -23,7 +35,7 @@ const details: CommandDetails = {
description: `Developer Command: Checks ${config.name}'s DB size.`,
},
{
name: auditCustomActivities,
name: auditCustomActivitiesName,
type: ApplicationCommandOptionTypes.SubCommand,
description: 'Developer Command: Checks for duplicate custom activities.',
},
@ -35,18 +47,18 @@ const details: CommandDetails = {
],
};
const execute = async (bot: Bot, interaction: Interaction) => {
const execute = async (bot: BotWithCache, 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));
const auditQuery: Array<DBSizeTable> = 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) => {
auditQuery.forEach((row) => {
embedFields.push({
name: `${row.table}`,
value: `**Size:** ${row.size} MB
@ -54,27 +66,129 @@ const execute = async (bot: Bot, interaction: Interaction) => {
inline: true,
});
});
bot.helpers.sendInteractionResponse(
interaction.id,
interaction.token,
{
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 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,
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.',
title: 'Guilds Audit',
description: `Shows details of the guilds that ${config.name} serves.
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(),
fields: embedFields.slice(0, 25),
}],
file: {
'blob': b.size > 8388290 ? tooBig : b,
'name': 'auditDetails.txt',
},
},
},
).catch((e: Error) => utils.commonLoggers.interactionSendError('audit.ts@dbSize', interaction, e));
})
.catch((e: Error) => utils.commonLoggers.interactionSendError('audit.ts@guilds', interaction, e));
break;
}
case auditCustomActivities:
case auditGuildName:
default:
somethingWentWrong(bot, interaction, `auditNameNotHandled@${auditName}`);
break;

View File

@ -31,6 +31,9 @@ const execute = (bot: Bot, interaction: Interaction) => {
color: infoColor2,
title: 'USER REPORT:',
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));
bot.helpers.sendInteractionResponse(

View File

@ -3,9 +3,9 @@ import { Client } from '../../deps.ts';
import { LOCALMODE } from '../../flags.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,
hostname: LOCALMODE ? config.db.localhost : config.db.host,
port: config.db.port,
db: config.db.name,
username: config.db.username,
password: config.db.password,
});

View File

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

View File

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

View File

@ -1,54 +0,0 @@
<!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>