From a14bceac56ef50a5183f0323ee91fe93f556af53 Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 9 Jan 2026 14:33:03 +0100 Subject: [PATCH 01/27] update to discord.js v14 --- .github/FUNDING.yml | 2 - LICENSE | 6 +- config-generator/config.json | 9 +- config-generator/strings.json | 21 +- locales/de.json | 986 -------- main.js | 223 +- modules/admin-tools/commands/admin.js | 8 +- modules/admin-tools/commands/roles.js | 190 ++ modules/admin-tools/commands/stealemote.js | 6 +- modules/admin-tools/config.json | 10 +- modules/admin-tools/events/botReady.js | 6 + .../admin-tools/models/TemporaryRoleChange.js | 26 + modules/admin-tools/module.json | 6 +- modules/admin-tools/temporaryRoles.js | 52 + modules/afk-system/config.json | 9 +- modules/auto-delete/channels.json | 15 - modules/auto-delete/events/botReady.js | 4 - .../auto-delete/events/voiceStateUpdate.js | 5 +- modules/auto-delete/voice-channels.json | 15 - modules/auto-messager/cronjob.json | 4 +- modules/auto-messager/daily.json | 4 +- modules/auto-messager/hourly.json | 4 +- modules/auto-publisher/config.json | 7 +- .../auto-publisher/events/messageCreate.js | 6 +- modules/auto-react/configs/config.json | 101 - modules/auto-react/configs/replies.json | 42 - modules/auto-react/events/messageCreate.js | 94 - modules/auto-react/module.json | 25 - modules/betterstatus/config.json | 13 +- modules/betterstatus/events/botReady.js | 19 +- modules/betterstatus/events/guildMemberAdd.js | 15 +- modules/birthday/birthday.js | 18 +- modules/birthday/commands/birthday.js | 3 +- modules/birthday/config.json | 6 +- modules/channel-stats/channels.json | 2 +- modules/channel-stats/events/botReady.js | 7 +- modules/color-me/commands/color-me.js | 4 +- modules/connect-four/commands/connect-four.js | 16 +- modules/counter/config.json | 106 +- modules/counter/events/messageCreate.js | 42 +- modules/counter/events/messageDelete.js | 25 + modules/counter/milestones.json | 34 +- modules/duel/commands/duel.js | 8 +- .../economy-system/commands/economy-system.js | 10 +- modules/economy-system/commands/shop.js | 6 +- modules/economy-system/configs/config.json | 5 +- modules/economy-system/configs/strings.json | 8 +- modules/economy-system/economy-system.js | 80 +- .../events/interactionCreate.js | 3 +- modules/fun/commands/hug.js | 32 +- modules/fun/commands/kiss.js | 32 +- modules/fun/commands/pat.js | 8 +- modules/fun/commands/random.js | 11 +- modules/fun/commands/slap.js | 8 +- modules/fun/config.json | 197 +- modules/giveaways/commands/giveaway.js | 196 -- modules/giveaways/commands/gmessages.js | 34 - modules/giveaways/configs/config.json | 157 -- modules/giveaways/configs/strings.json | 529 ----- modules/giveaways/events/botReady.js | 30 - modules/giveaways/events/interactionCreate.js | 154 -- modules/giveaways/events/messageCreate.js | 31 - modules/giveaways/giveaways.js | 276 --- modules/giveaways/models/Giveaway.js | 48 - modules/giveaways/module.json | 27 - modules/guess-the-number/commands/manage.js | 30 +- modules/guess-the-number/configs/channel.json | 79 + modules/guess-the-number/configs/config.json | 155 ++ modules/guess-the-number/events/botReady.js | 17 + .../guess-the-number/events/messageCreate.js | 13 +- modules/guess-the-number/guessTheNumber.js | 38 + modules/guess-the-number/module.json | 4 +- .../commands/hunt-the-code-admin.js | 121 - .../hunt-the-code/commands/hunt-the-code.js | 114 - modules/hunt-the-code/models/Code.js | 27 - modules/hunt-the-code/models/User.js | 29 - modules/hunt-the-code/module.json | 25 - modules/hunt-the-code/strings.json | 207 -- modules/info-commands/commands/info.js | 66 +- modules/info-commands/module.json | 3 +- modules/info-commands/strings.json | 6 +- .../invite-tracking/commands/trace-invites.js | 75 - modules/invite-tracking/config.json | 28 - .../invite-tracking/events/guildMemberJoin.js | 79 - .../events/guildMemberRemove.js | 60 - .../events/interactionCreate.js | 48 - modules/invite-tracking/models/UserInvite.js | 30 - modules/invite-tracking/module.json | 26 - modules/invite-tracking/onLoad.js | 18 - modules/levels/commands/leaderboard.js | 29 +- modules/levels/commands/manage-levels.js | 65 +- modules/levels/commands/profile.js | 39 +- modules/levels/configs/config.json | 248 +- modules/levels/events/botReady.js | 14 + modules/levels/events/interactionCreate.js | 9 +- modules/levels/events/messageCreate.js | 136 +- modules/levels/events/voiceStateUpdate.js | 53 + modules/levels/leaderboardChannel.js | 49 +- modules/levels/models/LiveLeaderboard.js | 25 + modules/massrole/commands/massrole.js | 3 + modules/moderation/commands/moderate.js | 11 +- modules/moderation/commands/report.js | 8 +- modules/moderation/configs/antiGrief.json | 6 +- modules/moderation/configs/antiJoinRaid.json | 4 +- modules/moderation/configs/antiSpam.json | 8 +- modules/moderation/configs/config.json | 19 +- modules/moderation/events/botReady.js | 12 +- modules/moderation/events/guildMemberAdd.js | 83 +- .../moderation/events/guildMemberUpdate.js | 10 + modules/moderation/events/messageCreate.js | 9 +- modules/moderation/moderationActions.js | 20 +- modules/nicknames/events/botReady.js | 1 - modules/nicknames/events/guildMemberUpdate.js | 2 +- modules/nicknames/models/User.js | 2 +- modules/nicknames/module.json | 4 +- modules/nicknames/renameMember.js | 17 +- modules/partner-list/commands/partner.js | 231 -- modules/partner-list/config.json | 218 -- modules/partner-list/events/botReady.js | 5 - modules/partner-list/models/Partner.js | 27 - modules/partner-list/module.json | 26 - modules/partner-list/partnerlist.js | 57 - modules/ping-on-vc-join/config.json | 3 + .../events/voiceStateUpdate.js | 3 +- modules/ping-on-vc-join/module.json | 1 + modules/polls/commands/poll.js | 19 +- modules/polls/configs/strings.json | 2 +- modules/polls/module.json | 3 +- modules/polls/polls.js | 10 +- modules/quiz/commands/quiz.js | 24 +- modules/quiz/configs/strings.json | 4 +- modules/quiz/quizUtil.js | 16 +- .../commands/rock-paper-scissors.js | 80 +- modules/serverinfo/configs/config.json | 55 - modules/serverinfo/configs/fields.json | 166 -- modules/serverinfo/events/botReady.js | 94 - modules/serverinfo/module.json | 25 - modules/starboard/handleStarboard.js | 4 +- modules/status-roles/configs/config.json | 15 + modules/status-roles/events/presenceUpdate.js | 30 +- modules/suggestions/suggestion.js | 4 +- modules/team-list/config.json | 16 + modules/team-list/events/botReady.js | 24 +- modules/team-list/module.json | 5 +- modules/temp-channels/config.json | 23 +- modules/temp-channels/events/botReady.js | 2 +- .../temp-channels/events/interactionCreate.js | 50 +- .../temp-channels/events/voiceStateUpdate.js | 20 +- modules/temp-channels/module.json | 2 +- modules/tic-tak-toe/commands/tic-tac-toe.js | 9 +- modules/tic-tak-toe/module.json | 3 +- modules/tickets/config.json | 10 +- modules/tickets/events/botReady.js | 3 +- modules/tickets/events/interactionCreate.js | 12 +- .../configs/streamers.json | 4 +- .../twitch-notifications/events/botReady.js | 2 - modules/uno/commands/uno.js | 97 +- modules/welcomer/configs/channels.json | 34 +- modules/welcomer/configs/config.json | 21 +- modules/welcomer/events/guildMemberAdd.js | 33 +- modules/welcomer/events/guildMemberRemove.js | 8 +- modules/welcomer/events/guildMemberUpdate.js | 14 +- modules/welcomer/events/interactionCreate.js | 3 +- package-lock.json | 2064 ++++++++++++----- package.json | 33 +- src/cli.js | 2 +- src/commands/help.js | 34 +- src/commands/reload.js | 2 +- src/discordjs-fix.js | 152 ++ src/events/botReady.js | 2 + src/events/interactionCreate.js | 34 +- src/functions/configuration.js | 60 +- src/functions/helpers.js | 126 +- src/functions/localize.js | 4 +- 174 files changed, 4134 insertions(+), 6166 deletions(-) delete mode 100644 .github/FUNDING.yml delete mode 100644 locales/de.json create mode 100644 modules/admin-tools/commands/roles.js create mode 100644 modules/admin-tools/events/botReady.js create mode 100644 modules/admin-tools/models/TemporaryRoleChange.js create mode 100644 modules/admin-tools/temporaryRoles.js delete mode 100644 modules/auto-react/configs/config.json delete mode 100644 modules/auto-react/configs/replies.json delete mode 100644 modules/auto-react/events/messageCreate.js delete mode 100644 modules/auto-react/module.json create mode 100644 modules/counter/events/messageDelete.js delete mode 100644 modules/giveaways/commands/giveaway.js delete mode 100644 modules/giveaways/commands/gmessages.js delete mode 100644 modules/giveaways/configs/config.json delete mode 100644 modules/giveaways/configs/strings.json delete mode 100644 modules/giveaways/events/botReady.js delete mode 100644 modules/giveaways/events/interactionCreate.js delete mode 100644 modules/giveaways/events/messageCreate.js delete mode 100644 modules/giveaways/giveaways.js delete mode 100644 modules/giveaways/models/Giveaway.js delete mode 100644 modules/giveaways/module.json create mode 100644 modules/guess-the-number/configs/channel.json create mode 100644 modules/guess-the-number/configs/config.json create mode 100644 modules/guess-the-number/events/botReady.js create mode 100644 modules/guess-the-number/guessTheNumber.js delete mode 100644 modules/hunt-the-code/commands/hunt-the-code-admin.js delete mode 100644 modules/hunt-the-code/commands/hunt-the-code.js delete mode 100644 modules/hunt-the-code/models/Code.js delete mode 100644 modules/hunt-the-code/models/User.js delete mode 100644 modules/hunt-the-code/module.json delete mode 100644 modules/hunt-the-code/strings.json delete mode 100644 modules/invite-tracking/commands/trace-invites.js delete mode 100644 modules/invite-tracking/config.json delete mode 100644 modules/invite-tracking/events/guildMemberJoin.js delete mode 100644 modules/invite-tracking/events/guildMemberRemove.js delete mode 100644 modules/invite-tracking/events/interactionCreate.js delete mode 100644 modules/invite-tracking/models/UserInvite.js delete mode 100644 modules/invite-tracking/module.json delete mode 100644 modules/invite-tracking/onLoad.js create mode 100644 modules/levels/events/voiceStateUpdate.js create mode 100644 modules/levels/models/LiveLeaderboard.js create mode 100644 modules/moderation/events/guildMemberUpdate.js delete mode 100644 modules/partner-list/commands/partner.js delete mode 100644 modules/partner-list/config.json delete mode 100644 modules/partner-list/events/botReady.js delete mode 100644 modules/partner-list/models/Partner.js delete mode 100644 modules/partner-list/module.json delete mode 100644 modules/partner-list/partnerlist.js delete mode 100644 modules/serverinfo/configs/config.json delete mode 100644 modules/serverinfo/configs/fields.json delete mode 100644 modules/serverinfo/events/botReady.js delete mode 100644 modules/serverinfo/module.json create mode 100644 src/discordjs-fix.js diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 025f9520..00000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,2 +0,0 @@ -github: [ "ScootKit", "scderox" ] -custom: ["https://membership.sc-network.net", "https://www.buymeacoffee.com/scderox"] \ No newline at end of file diff --git a/LICENSE b/LICENSE index 8ceb9732..55ca2671 100644 --- a/LICENSE +++ b/LICENSE @@ -3,12 +3,12 @@ License text copyright (c) 2020 MariaDB Corporation Ab, All Rights Reserved. Parameters -Licensor: Simon Csaba ("ScootKit") -Licensed Work: ScootKit/CustomDCBot. The Licensed Work is (c) 2023 Simon Csaba +Licensor: ScootKit UG (haftungsbeschränkt) +Licensed Work: ScootKit/CustomDCBot. The Licensed Work is (c) 2025 ScootKit UG (haftungsbeschränkt) Additional Use Grant: You may make production use of the Licensed Work, provided such use does not include offering the Licensed Work to third parties on a hosted or embedded basis which is - competitive with Simon Csaba ("ScootKit")'s products. + competitive with ScootKit UG (haftungsbeschränkt)'s products. Change Date: Six years from the date the Licensed Work is published. Change License: MIT License diff --git a/config-generator/config.json b/config-generator/config.json index fef94470..8ef98cf5 100644 --- a/config-generator/config.json +++ b/config-generator/config.json @@ -34,6 +34,7 @@ "en": "Set the prefix of your bot here", "de": "Dein eigener Prefix - Wir empfehlen ihn einfach bei ! zu belassen." }, + "hidden": true, "type": "string" }, { @@ -82,8 +83,8 @@ "en": "Bot-Status" }, "default": { - "en": "Change this in your Bot-Configuration on scnx.app", - "de": "Ändere das in deiner Bot-Konfiguration auf scnx.app" + "en": "Change this in your Bot-Configuration on scnx.app: https://scootk.it/change-status", + "de": "Ändere das in deiner Bot-Konfiguration auf scnx.app: https://scootk.it/change-status" }, "description": { "en": "This will show up in Discord as \"Playing \"", @@ -103,6 +104,7 @@ "en": "Log-Level of the bot. Leave it as it is, if you don't know what this means", "de": "Log-Level des Bots. Belasse es wie es ist, wenn du nicht weißt, was das bedeutet." }, + "hidden": true, "type": "select", "content": [ "debug", @@ -157,8 +159,7 @@ "en": "Allows @everyone and @here pings for messages configurable in the dashboard", "de": "Erlaubt @everyone und @here pings in im Dashboard anpassbaren Nachrichten" }, - "type": "boolean", - "pro": true + "type": "boolean" }, { "name": "syncCommandGlobally", diff --git a/config-generator/strings.json b/config-generator/strings.json index 371fa39b..5fbdd4af 100644 --- a/config-generator/strings.json +++ b/config-generator/strings.json @@ -48,11 +48,12 @@ "default": { "en": "https://scnx.xyz/favicon.png" }, + "allowNull": true, "description": { "en": "Footer-Image of every embed", "de": "Footer-Bild von jedem Embed" }, - "type": "string", + "type": "imgURL", "pro": true }, { @@ -66,7 +67,7 @@ "de": "This command needs more arguments - you passed %count%, but you need to provide at least %neededCount%." }, "description": { - "en": "This message gets send if there are not enough arguments specified", + "en": "This message gets sent if there are not enough arguments specified", "de": "Diese Nachricht wird versendet, wenn eine oder mehrere Argumente für einen Befehl fehlen" }, "type": "string", @@ -99,7 +100,7 @@ "de": "✅ Rollen-Änderungen übernommen" }, "description": { - "en": "This message gets send after a user selects self-roles on a self-role-element.", + "en": "This message gets sent after a user selects self-roles on a self-role-element.", "de": "Diese Nachricht wird gesendet, wenn ein Nutzer eine Self-Rolle auswählt." }, "type": "string", @@ -116,7 +117,7 @@ "de": "✅ Rolle %role% erfolgreich hinzugefügt" }, "description": { - "en": "This message gets send when a user adds a role to themselves.", + "en": "This message gets sent when a user adds a role to themselves.", "de": "Diese Nachricht wird gesendet, wenn ein Nutzer sich selbst eine Self-Rolle hinzufügt." }, "type": "string", @@ -142,7 +143,7 @@ "de": "✅ Rolle %role% erfolgreich entfernt" }, "description": { - "en": "This message gets send when a user removes a role from themselves.", + "en": "This message gets sent when a user removes a role from themselves.", "de": "Diese Nachricht wird gesendet, wenn ein Nutzer sich selbst eine Self-Rolle entfernt." }, "type": "string", @@ -168,7 +169,7 @@ "de": "Scheint als hättest du nicht genügend Rechte." }, "description": { - "en": "This message gets send if an user don't hase enough permissions", + "en": "This message gets sent if an user don't hase enough permissions", "de": "Diese Nachricht wird versendet, wenn der Nutzer nicht genügen Rechte hat" }, "type": "string", @@ -238,15 +239,15 @@ { "name": "putBotInfoOnLastSite", "humanName": { - "en": "Put Bot-Info on the last site of the Help-Embed", - "de": "Verschiebt die Bot-Info auf die letzte Seite im Hilfe-Embed" + "en": "Hides the Bot-Info in the Help-Embed", + "de": "Verbergt die Bot-Info Sektion im Hilfe-Embed" }, "default": { "en": false }, "description": { - "en": "If enabled, the Bot-Info-Section of the Help-Embed will be moved to the last site", - "de": "Wenn aktiviert, wird der Bot-Info-Bereich im Help-Embed auf die letzte Seite verschoben" + "en": "If enabled, the Bot-Info-Section of the Help-Embed will be hidden.", + "de": "Wenn aktiviert, wird der Bot-Info-Bereich im Help-Embed verborgen." }, "type": "boolean", "pro": true diff --git a/locales/de.json b/locales/de.json deleted file mode 100644 index 320d9b3e..00000000 --- a/locales/de.json +++ /dev/null @@ -1,986 +0,0 @@ -{ - "main": { - "startup-info": "SCNX-CustomBot v2 - Log-Level: %l", - "missing-moduleconf": "Es fehlt die Modul-Konfiguration. Alle Module werden deaktiviert und die Datei wird generiert", - "sync-db": "Datenbank erfolgreich synchronisiert", - "login-error": "Der Bot konnte sich nicht anmelden. Fehler: %e", - "not-invited": "Der Bot ist nicht auf deinem Server, bitte lade ihn ein: %inv", - "logged-in": "Bot ist nun als %tag eingeloggt und ist online.", - "logchannel-wrong-type": "Es wurde kein Log-Kanal gesetzt oder der Log-Kanal hat den falschen Type (nur Text-Kanäle werden unterstützt).", - "config-check-failed": "Konfiguration-Überprüfung ist fehlgeschlagen. Du findest weitere Information in deinem Log. Der Bot stoppt sich nun automatisch.", - "bot-ready": "Der Bot wurde erfolgreich gestartet und ist nun bereit, Commands zu empfangen", - "no-command-permissions": "Die Befehle des Bots konnten nicht übernommen werden, bitte hier klicken: %inv", - "perm-sync": "Rechte für /%c wurden synchronisiert", - "perm-sync-failed": "Rechte für /%c konnten nicht synchronisiert werden: %e", - "loading-module": "Modul %m wird geladen", - "module-disabled": "Module %m ist deaktiviert", - "command-loaded": "Befehl %d/%f wurde geladen", - "command-dir": "Befehle in %d/%f werden geladen", - "command-sync": "Befehle wurden synchronisiert", - "command-no-sync-required": "Befehle sind aktuell, es ist keine Synchronisation notwendig", - "event-loaded": "Event %d/%f wurde geladen", - "event-dir": "Befehle in %d/%f werden geladen", - "model-loaded": "Datenbank-Model %d/%f wurde geladen", - "model-dir": "Datenbank-Modele in %d/%f werden geladen", - "loaded-cli": "API-Aktion %c in %p wurde geladen", - "channel-lock": "Kanal gesperrt", - "channel-unlock": "Kanal entsperrt", - "channel-unlock-data-not-found": "Kanal mit ID %c konnte nicht entsperrt werden, weil dieser nie gesperrt wurde (was merkwürdig ist).", - "login-error-token": "Der Bot konnte sich nicht anmelden, da der angegebene Token ungültig ist. Bitte ändere deinen Token.", - "login-error-intents": "Der Bot konnte sich nicht anmelden, da die Intents nicht korrekt aktiviert wurden. Bitte aktiviere \"PRESENCE INTENT\", \"SERVER MEMBERS INTENT\" und \"MESSAGE CONTENT INTENT\" in deinem Discord-Developer-Dashboard: %url", - "module-disable": "Das Modul %m wurde deaktiviert, da %r", - "migrate-success": "Migration von %o zu %m erfolgreich abgeschlossen.", - "migrate-start": "Migration von %o zu %m gestarted... Bitte den Bot nicht anhalten", - "global-command-sync": "Synchronisierte globale Anwendungsbefehle", - "guild-command-sync": "Synchronisierte Serveranwendungsbefehle", - "guild-command-no-sync-required": "Serveranwendungsbefehle sind auf dem neuesten Stand – keine Synchronisierung erforderlich", - "global-command-no-sync-required": "Globale Anwendungsbefehle sind auf dem neuesten Stand – keine Synchronisierung erforderlich" - }, - "scnx": { - "activating": "Aktiviere SCNX-Integration...", - "notLongerInSCNX": "Dieser Server wurde deaktiviert oder ist nicht länger bei SCNX. Beende Prozess.", - "activated": "SCNX Integration erfolgreich aktiviert", - "loggedInAs": "Eigener Bot %b auf Server %s mit Version %v und dem Plan %p eingeloggt", - "choose-roles": "Rollen auswählen", - "localeUpdate": "Locales aktualisiert", - "localeUpdateSkip": "Locales-Aktualisierung übersprungen", - "localeFetchFailed": "Locales zur Aktualisierung konnten nicht geladen werden", - "issueTrackingActivated": "Automatische Fehlermeldungen erfolgreich aktiviert. Dein Bot wird automatisch Fehler, die du nicht beheben kannst, an die Entwickler melden.", - "newVersion": "**⬆ Neue Version verfügbar: %v 🎉**\n\nUm diese Änderungen zu übernehmen, starte bitte deinen Bot in deinem SCNX-Dashboard neu.\nUpdates wie dieses sollten so schnell wie möglich angewandt werden, da sie Bug-Fixes, Verbesserungen und Optimierungen enthalten. Einen detaillierten Change-Log findest du auf unserem Discord-Server." - }, - "reload": { - "reloading-config": "Konfiguration wird neu geladen...", - "reloading-config-with-name": "Nutzer %tag lädt die Konfiguration neu...", - "reloaded-config": "Konfiguration erfolgreich neu geladen.\nVon %totalModules Modulen sind %enabled aktiviert und %configDisabled wurden deaktiviert, weil ihre Konfiguration fehlerhaft war.", - "reload-failed": "Das Neuladen der Konfiguration ist fehlgeschlagen. Bot fährt herunter.", - "reload-successful-syncing-commands": "Erfolgreich neu geladen, Befehle werden synchronisiert...", - "reload-failed-message": "**Fehlgeschlagen**\n```%r```\n**Lies bitte deinen Log, um mehr zu erfahren**\nDer Bot fährt nun herunter, bye :wave:", - "command-description": "Lädt die Konfiguration neu" - }, - "config": { - "checking-config": "Konfiguration wird überprüft...", - "done-with-checking": "Konfiguration erfolgreich überprüft.", - "creating-file": "Konfiguration %m/%f existiert aktuell nicht, wird aber gleich erstellt...", - "checking-of-field-failed": "Ein Fehler bei der Überprüfung von %fieldName in %m/%f ist aufgetreten", - "saved-file": "Konfiguration %f in %m wurde erfolgreich generiert.", - "moduleconf-regeneration": "Modul-Konfiguration wird regeniert, es werden keine Einstellungen überschrieben.", - "moduleconf-regeneration-success": "Module-Konfiguration wurde regeniert.", - "channel-not-found": "Kanal mit ID \"%id\" wurde nicht gefunden", - "user-not-found": "Discord-Account mit ID \"%id\" wurde nicht gefunden", - "channel-not-on-guild": "Kanal mit ID \"%id\" ist nicht auf deinem Server", - "role-not-found": "Rolle mit ID \"%id\" wurde nicht auf deinem Server gefunden", - "config-reload": "Gesamte Konfiguration wird neugeladen...", - "channel-invalid-type": "Kanal mit ID \"%id\" hat einen Typen, der nicht für dieses Feld verwendet werden darf" - }, - "helpers": { - "timestamp": "%dd.%mm.%yyyy um %hh:%min", - "you-did-not-run-this-command": "Du hast diesen Befehl nicht ausgeführt. Führe den Command selber aus, um Navigations-Knöpfe benutzen zu können.", - "next": "Weiter", - "back": "Zurück", - "toggle-data-fetch-error": "SC Network Release: Toggle-Daten konnten nicht geladen werden", - "toggle-data-fetch": "SC Network Release: Toggle-Daten geladen" - }, - "command": { - "startup": "Der Bot startet gerade. Bitte warte einige Minuten, bevor du diesen Befehl ausführst.", - "not-found": "Befehl nicht gefunden", - "used": "%tag (%id) hat den Befehl /%c %g %s ausgeführt", - "message-used": "%tag (%id) hat den Nachrichten-Befehl %p%c ausgeführt", - "execution-failed": "Die Ausführung von /%c %g %s ist fehlgeschlagen: %e", - "message-execution-failed": "Die Ausführung von %p%c ist fehlgeschlagen: %e", - "autcomplete-execution-failed": "Die Ausführung der Autovervollständigung des Befehls /%c %g %s mit der Option %f ist fehlgeschlagen: %e", - "execution-failed-message": "**🔴 Ein Fehler bei Ausführung des Befehls ist aufgetreten 🔴**\nDies sollte nicht passieren und kann verschiedene Gründe haben.\n\nDer Bot hat automatisch, außer diese Funktion wurde in der Konfiguration deaktiviert, den Fehler an den zuständigen Entwickler gemeldet. Unter Umständen wird dich ein Entwickler bei Rückfragen anschreiben, wenn du Mitglied des [SC Network Discords]() bist.", - "error-giving-role": "Es ist ein Fehler aufgetreten als ich versucht habe dir deine Rollen zu geben ):", - "module-disabled": "Dieser Befehl ist Teil von \"%m\", welches deaktiviert ist. Dies kann entweder von den Server-Admins gewollt (und slash-commands wurden noch nicht synchronisiert) oder durch einen Konfigurationsfehler bedingt sein. Bitte prüfe (oder frag die Admins) die Konfiguration und Logs des Bots zu überprüfen um Details zu erhalten.", - "wrong-guild": "Dieser Befehl ist nur auf den Server **%g** verfügbar." - }, - "help": { - "bot-info-titel": "ℹ️ Bot-Infos", - "bot-info-description": "Dieser Bot wurde vom [ScootKit](https://scootkit.net)-Team und unseren geliebten [Open-Source-Beitragenden](https://github.com/ScootKit/CustomDCBot/graphs/contributors) entwickelt und ist unter der [Business Source License](https://github.com/ScootKit/CustomDCBot/blob/main/LICENSE] lizenziert.", - "stats-title": "📊 Statistiken", - "stats-content": "Aktive Module: %am\nRegistrierte Befehle: %rc\nBot-Version: %v\nLäuft auf Server %si\n[Server-Plan](https://scnx.xyz/plan): %pl\nLetzter Neustart: %lr\nLetze Neuladung: %lrl", - "command-description": "Zeigt dir alle Befehle an", - "slash-commands-title": "Slash-Commands", - "slash-commands-description": "Du hast wahrscheinlich **%c Befehle** durch Nichtnutzung von Slash-Commands verpasst - bitte nutze `/help` als Slash-Command um alle verfügbaren Befehle zu sehen." - }, - "bot-feedback": { - "command-description": "Schick Feedback über den Bot an den Bot Entwickler", - "submitted-successfully": "Vielen Dank für dein Feedback! Wir haben es empfangen und unser Team wird es sich bald anschauen. Sollten wir Rückfragen haben, werden wir dich eventuell per Privatnachricht anschreiben (falls du auf unserem [Support-Server]() bist öffnen wir ein Ticket). Vielen Dank, dass du durch dein Feedback [Eigen Bots]() für alle besser machst.\n\nDein Feedback unterliegt unseren [Nutzungsbedingungen]() und unserer [Datenschutzerklärung]().", - "failed-to-submit": "Ich konnte dein Feedback leider nicht an den Entwickler senden. Dies kann entweder daran legen, das sdu gesperrt wurdest, oder daran, dass es technische Schwierigkeiten gibt. Du kannst weiterhin Vorschläge und Bugs im [Feature-Board]() melden bzw. vorschlagen. Vielen Dank.", - "feedback-description": "Dein Feedback. Es sollte neutral, konstruktiv und hilfreich sein" - }, - "admin-tools": { - "position": "%i hat die Position %p.", - "position-changed": "Die Position von %i wurde zu %p aktualisiert.", - "category-can-not-have-category": "Eine Kategorie kann keine Kategorie haben", - "not-category": "Die Kategorie eines Kanals kann nicht zu einem anderen Nicht-Kategorie-Kanals geändert werden", - "changed-category": "Die Kategorie von %c wurde zu %cat aktualisiert", - "command-description": "Führe Admin-Aktionen als Command aus", - "new-position-description": "Neue Position", - "movechannel-description": "Zeigt oder ändert die Position eines Kanals", - "moverole-description": "Zeigt oder ändert die Position einer Rolle", - "setcategory-description": "Setzt die Kategorie eines Kanals", - "channel-description": "Kanal auf welchem diese Aktion ausgeführt werden soll", - "role-description": "Rolle auf welchem diese Aktion ausgeführt werden soll", - "category-description": "Neue Kategorie des Kanals", - "emoji-too-much-data": "Bitte wähle **nur einen** Emote aus und schreibe nichts anders", - "emoji-import": "Der Emote \"%e\" wurde erfolgreich importiert.", - "stealemote-description": "Leiht einen Emote von einem anderen Server permanent aus", - "emote-description": "Der Emote, der permanent geliehen werden soll" - }, - "welcomer": { - "channel-not-found": "[welcomer] Kanal nicht gefunden: %c" - }, - "birthdays": { - "channel-not-found": "[Geburtstage] Kanal nicht gefunden: %c", - "sync-error": "[Geburtstage] Der Status von %u war auf \"Aktiviert\" gesetzt, aber es gab keinen Synchronisationskandidaten, deswegen wurde die Synchronisation deaktiviert", - "age-hover": "%a Jahre alt", - "sync-enabled-hover": "Geburtstag synchronisiert", - "verified-hover": "Geburtstag verifiziert", - "no-bd-this-month": "In diesem Monat hat niemand Geburtstag ):", - "no-birthday-set": "Du hast aktuell keinen Geburtstag auf diesem Server registriert. Wenn du autoSync aktiviert hast, kann es bis zu 24 Stunden dauern, bis dein Geburtstag auf jedem Server synchronisiert wurde. [Erfahre mehr über Geburtstagssynchronisation]().", - "birthday-status": "Dein Geburtstag ist aktuell auf **%dd.%mm%yyyy** eingestellt%age. %syncstatus", - "your-age": "was bedeutet, dass du **%age%** alt bist", - "sync-on": "Dein Geburtstag wird mit deinem [SC Network-Konto](https://sc-network.net/dashboard) synchronisiert.", - "sync-off": "Dein Geburtstag ist lokal für diesen Server eingestellt und wird nicht synchronisiert", - "no-sync-account": "Es scheint, als hättest du kein [SC Network-Konto](https://sc-network.net/dashboard) oder keine Informationen zu deinem Geburtstag in diesem eingetragen.", - "auto-sync-on": "Es scheint als hättest du automatische Synchronisierung in deinem [SC Network-Konto](https://sc-network.net/dashboard) aktiviert. Das bedeutet, dass dein Geburtstag immer auf jedem Server synchronisiert wird. [Erfahre mehr]().\nDein Geburtstag wird nicht angezeigt? Es kann bis zu 24 Stunden dauern (normalerweise sind es weniger als zwei Stunden), damit dieser synchronisiert wird. Hab also noch etwas Geduld und warte einfach noch ein Bisschen.", - "enabled-sync": "Die Synchronisierung wurde erfolgreich aktiviert :+1:", - "disabled-sync": "Die Synchronisierung wurde erfolgreich deaktiviert. Du kannst nun deinen Geburtstag auf diesem Server ändern oder entfernen.", - "delete-but-sync-is-on": "Du hast Synchronisierung aktiviert. Bitte deaktiviere Synchronisierung um deinen Geburtstag zu entfernen.", - "deleted-successfully": "Geburtstag erfolgreich entfernt.", - "only-sync-allowed": "Dieser Server erlaubt nur mit einem [SC Network-Konto](https://sc-network.net/dashboard) synchronisierte Geburtstage", - "invalid-date": "Ungültiges Datum eingegeben", - "against-tos": "Du musst mindestens 13 Jahre alt sein um Discord zu benutzen. Bitte lies die [Nutzungsbedingungen]() der Plattform und [lösche dein Konto](), wenn du unter 13 bist um nicht gegen die [Nutzungsbedingungen]() von Discord zu verstoßen und warte %waitTime (oder bis zum entsprechenden Alter, sollte dein Land [hier]() gelistet sein) Jahre bevor du ein neues Konto erstellst.", - "too-old": "Du scheinst zu alt für eine lebende Person zu sein", - "command-description": "Bearbeite, sehe und entferne deinen Geburtstag", - "status-command-description": "Zeigt den aktuellen Status deines Geburtstags", - "sync-command-description": "Bearbeite die Synchronisationseinstellungen für diesen Server", - "sync-command-action-description": "Aktion, welche mit deinen Synchronisationseinstellungen ausgeführt werden soll", - "sync-command-action-enable-description": "Synchronisation aktivieren", - "sync-command-action-disable-description": "Synchronisation deaktivieren", - "set-command-description": "Setzt deinen Geburtstag", - "set-command-day-description": "Tag deines Geburtstags", - "set-command-month-description": "Monat deines Geburtstags", - "set-command-year-description": "Jahr deines Geburtstags", - "delete-command-description": "Entfernt deinen Geburtstag von diesem Server", - "migration-happening": "Datenbank-Schema nicht aktuell. Datenbank wird migriert. Starte deinen Bot nicht neu, um Datenverlust zu vermeiden.", - "migration-done": "Datenbank wurde erfolgreich migriert." - }, - "months": { - "1": "Januar", - "2": "Februar", - "3": "März", - "4": "April", - "5": "Mai", - "6": "Juni", - "7": "Juli", - "8": "August", - "9": "September", - "10": "Oktober", - "11": "November", - "12": "Dezember" - }, - "giveaways": { - "no-link": "Kein", - "no-winners": "Keine", - "not-supported-for-news-channel": "Nicht unterstützt für Ankündigungs-Kanäle", - "required-messages": "Du brauchst %mc neue Nachrichten (überprüfe deine Nachrichten mit \\`/gmessages\\`)", - "required-messages-user": "Du musst mindestens %mc neue Nachrichten haben; zum anschauen: (%um/%mc Nachrichten)", - "roles-required": "Du musst mindestens eine dieser Rollen zum beitreten: %r", - "giveaway-ended-successfully": "Gewinnspiel erfolgreich beendet.", - "no-giveaways-found": "Kein Gewinnspiel gefunden", - "gmessages-description": "Überprüfe deine benötigten Nachrichten für ein Gewinnspiel", - "jump-to-message-hover": "Zur Nachricht springen", - "messages": "Nachrichten", - "giveaway-messages": "Gewinnspiel-Nachrichten", - "duration-parsing-failed": "Die Länge des Gewinnspieles konnte nicht interpretiert werden.", - "channel-type-not-supported": "Kanal-Typ nicht unterstützt", - "parameter-parsing-failed": "Ein(ige) Parameter deiner Angaben konnten nicht übernommen werden", - "started-successfully": "Gewinnspiel erfolgreich in %c gestartet.", - "reroll-done": "Erledigt :+1:", - "select-menu-description": "Wird in #%c am %d enden", - "no-giveaways-for-reroll": "Huch, aktuell laufen keine Gewinnspiele. Suchst du eventuell /reroll?", - "select-giveaway-to-end": "Bitte wähle das Gewinnspiel welches du beenden willst.", - "please-select": "Bitte wähle aus", - "gmanage-description": "Verwalte Gewinnspiele", - "gmanage-start-description": "Starte ein neues Gewinnspiel", - "gmanage-channel-description": "Kanal zum Starten des Gewinnspiels", - "gmanage-price-description": "Preis der gewonnen werden kann", - "gmanage-duration-description": "Länge des Gewinnspiels? (z.b: \"2h 40m\" oder \"7d 2h 3m\")", - "gmanage-winnercount-description": "Anzahl der Gewinner, die ausgewählt werden sollen", - "gmanage-requiredmessages-description": "Anzahl neuer (!) Nachrichten, die ein Benutzer vor dem Betreten haben muss", - "gmanage-requiredroles-description": "Rolle, die der Benutzer haben muss, um an der Verlosung teilzunehmen", - "gmanage-sponsor-description": "Setzt einen anderen Gewinnspiel-Starter, hilfreich bei Sponsoren", - "gmanage-sponsorlink-description": "Link zu einem Sponsor, falls zutreffend", - "gend-description": "Beendet ein Gewinnspiel", - "gereroll-description": "Rollt ein beendetes Giveaway erneut aus", - "gereroll-msgid-description": "Nachrichten-ID des Gewinnspiels", - "gereroll-winnercount-description": "Wie viele neue Gewinner sollen ausgewählt werden?", - "migration-happening": "Datenbank-Schema nicht aktuell. Datenbank wird migriert. Starte deinen Bot nicht neu, um Datenverlust zu vermeiden.", - "migration-done": "Datenbank wurde erfolgreich migriert." - }, - "levels": { - "leaderboard-channel-not-found": "Ranglisten-Kanal wurde nicht gefunden, oder hat den falschen Typ", - "leaderboard-notation": "**%p. %u**: Level %l - %xp XP", - "leaderboard": "Rangliste", - "no-user-on-leaderboard": "Es kann keine Rangliste generiert werden, da niemand Erfahrungspunkte hat. Das ist zwar komisch, aber ist halt so ¯\\_(ツ)_/¯", - "and-x-other-users": "und %uc andere Nutzer", - "level": "Level %l", - "users": "Nutzer", - "leaderboard-command-description": "Zeigt die Rangliste des Servers", - "leaderboard-sortby-description": "Ranglistensortierung (Standardwert: %d)", - "no-bio": "Keine Bio gesetzt.", - "no-bio-author": "Keine Bio gesetzt. [Setze Bio](https://sc-network.net/auth?action=set-bio)", - "profile-command-description": "Zeigt das Profil von dir oder einem anderen Nutzer", - "profile-user-description": "Nutzer von dem das Profil angezeigt werden soll (Standardwert: Du)", - "please-send-a-message": "Bitte sende einige Nachrichten, bevor ich Daten über dich anzeigen kann", - "no-role": "Keine Rolle", - "are-you-sure-you-want-to-delete-user-xp": "Okay, willst du dich wirklich mit %u anlegen? Wenn du ihn/sie so sehr hasst kannst du gerne `/manage-levels reset-xp confirm:True user:%ut` ausführen, um diese endgültige nicht umkehrbare Aktion auszuführen.", - "are-you-sure-you-want-to-delete-server-xp": "Willst du wirklich alle Erfahrungspunkte und Levels dieses Servers zurücksetzen? Diese Aktion ist nicht umkehrbar und jeder auf diesem Server wird dich hassen. Hast du dich entschieden, ob es das wert ist? Führe `/manage-levels reset-xp confirm:True` aus", - "user-not-found": "Nutzer nicht gefunden", - "user-deleted-users-xp": "%t hat die Erfahrungspunkte vom Nutzer mit der ID %u zurückgesetzt", - "removed-xp-successfully": "`Erfahrungspunkte und Level von %u erfolgreich zurückgesetzt.`", - "deleted-server-xp": "%u hat die Erfahrungspunkte von allen Nutzern zurückgesetzt", - "successfully-deleted-all-xp-of-users": "Erfolgreich alle Erfahrungspunkte aller Nutzer zurückgesetzt", - "cheat-no-profile": "Dieser Nutzer hat (noch) kein Profil, bitte zwinge ihn eine Nachricht zuschreiben bevor du deine Community betrügst, indem du Levels manipulierst.", - "abuse-detected": "%u versuchte seine Rechte durch Bearbeitung seiner eigenen Erfahrungspunkte zu seinem eigenen Vorteil zu missbrauchen. Dies ist offensichtlicher Missbrauch, ich erwarte, dass Disziplinarmaßnahmen gegenüber dieses Nutzers ergriffen werden.", - "cant-change-your-level-1": "Warte... du meinst das nicht ernst? Doch, oder? Du meinst das ernst... Ich bin sehr enttäuscht von dir, %un... Ich dachte du seist ein fairer Admin, aber wie ich es heute sehen kann bist du es nicht. Du wolltest diesen Befehl zu deinem eigenen Vorteil nutzen und alle Nutzer betrügen. Ich bin wirklich sehr sehr enttäuscht von dir, Ich habe mehr von dir erwartet. Ich werde diesen Zwischenfall an deinen Vorgesetzten melden müssen und - wie ich bereits sagte - Ich bin sehr von dir enttäuscht und ehrlich gesagt - wenn ich die Rechte dazu hätte - würde ich dich von diesem Server bannen, da dieser Zwischenfall beweist, dass du deine Rechte zu deinem eigenen Vorteil nutzt.", - "cant-change-your-level-2": "Und du versuchst es wieder... Das ist sehr sehr traurig, Ich werde dich nochmal melden und erneut betonen, dass das offensichtlich Rechteausnutzung ist. Hab noch einen schönen Tag.", - "manipulated": "%u manipulierte die Erfahrungspunkte von %u auf %v", - "successfully-changed": "Erfolgreich die Erfahrungspunkte dieses Nutzers bearbeitet. Bedenke, da jede Änderung die du am Levelsystem machst das Erlebnis anderer Nutzer auf diesem Server zerstört, da das Levelsystem nicht mehr fair ist.", - "edit-xp-command-description": "Verwalte die Level deines Servers", - "reset-xp-description": "Setze die Erfahrungspunkte eines Nutzers oder des ganzen Servers zurück", - "reset-xp-user-description": "Nutzer zum Erfahrungspunkte zurücksetzen (Standardwert: Ganzer Server)", - "reset-xp-confirm-description": "Willst du wirklich die Daten löschen?", - "edit-xp-user-description": "Nutzer zum Bearbeiten (kannst *nicht* du selbst sein!)", - "edit-xp-value-description": "Neue Erfahrungspunktemenge des Nutzers", - "edit-xp-description": "Betrüge deine Community und bearbeite die Erfahrungspunkte eines Nutzers", - "random-messages-enabled-but-non-configured": "Zufällige Nachrichten sind aktiviert, allerdings wurden keine zufälligen Nachrichten festgelegt. Ignoriere Anweisung.", - "granted-rewards-audit-log": "Rollen aktualisiert um sicherzustellen, dass der/die Nutzer:in die benötigten Rollen hat" - }, - "partner-list": { - "could-not-give-role": "%u konnte keine Rolle gegeben werden", - "could-not-remove-role": "%u konnte keine Rolle entfernt werden", - "partner-not-found": "Der Partner konnte nicht gefunden werden. Bitte überprüfe ob du die richtige Partner-ID verwendest. Die Partner-ID ist nicht mit der Server-ID des Partners identisch. Die Partner-ID findest du [hier](https://gblobscdn.gitbook.com/assets%2F-MNyHzQ4T8hs4m6x1952%2F-MWDvDO9-_JwAGqtD6at%2F-MWDxIcOHB9VcWhjsWt7%2Fscreen_20210320-102628.png?alt=media&token=2f9ac1f7-1a14-445c-b34e-83057789578e) im Partner-Embed.", - "successful-edit": "Partner-Liste erfolgreich bearbeitet.", - "channel-not-found": "Kanal mit der ID %c konnte nicht gefunden werden, oder hat den falschen Typ (es werden nur Textkanäle unterstützt)", - "no-partners": "Es gibt aktuell keine Partner. Das ist komisch, aber es ist halt so ¯\\_(ツ)_/¯\n\nUm einen Partner hinzuzufügen, nutze den Slash-Befehl `/partner add`.", - "information": "Information", - "command-description": "Verwaltet die Partner-Liste des Servers", - "padd-description": "Fügt einen neuen Partner hinzu", - "padd-name-description": "Name des Partners", - "padd-category-description": "Bitte wähle eine der Kategorien, die du in deiner Konfiguration gesetzt hast", - "padd-owner-description": "Inhaber des Partnerservers", - "padd-inviteurl-description": "Einladung zum Partnerserver", - "pedit-description": "Bearbeitet einen existierenden Partner", - "pedit-id-description": "ID des Partners", - "pedit-name-description": "Neuer Name des Partners", - "pedit-inviteurl-description": "Neue Einladung zum Partnerserver", - "pedit-category-description": "Neue Kategorie des Partnerservers", - "pdelete-description": "Entfernt einen existierenden Partner", - "pdelete-id-description": "ID des Partners", - "pedit-owner-description": "Neuer Besitzer des Partner-Servers", - "pedit-staff-description": "Neues zugewiese Teammitglied für diesen Partner-Server" - }, - "ping-on-vc-join": { - "channel-not-found": "Kanal für Benachrichtigungen %c nicht gefunden", - "could-not-send-pn": "Es konnte keine PN an %m gesendet werden" - }, - "suggestions": { - "approved": "Angenommen", - "denied": "Abgelehnt", - "admin-answer": "%status von %u mit folgendem Grund: \"%r\"", - "suggestion-not-found": "Vorschlag nicht gefunden", - "updated-suggestion": "Vorschlag erfolgreich aktualisiert", - "manage-suggestion-command-description": "Verwalte Vorschläge als Admin", - "manage-suggestion-accept-description": "Akzeptiert einen Vorschlag", - "manage-suggestion-deny-description": "Lehnt einen Vorschlag ab", - "manage-suggestion-id-description": "ID des Vorschlags", - "manage-suggestion-comment-description": "Erkläre, warum du diese Entscheidung getroffen hast" - }, - "auto-delete": { - "could-not-fetch-channel": "Der Kanal mit der ID %c konnte nicht gefunden werden", - "could-not-fetch-messages": "Die Nachrichten im Kanal mit der ID %c konnten nicht gefunden werden" - }, - "auto-thread": { - "thread-create-reason": "Dieser Thread wurde aufgrund der Einstellungen von auto-thread erstellt" - }, - "auto-messager": { - "channel-not-found": "Der Kanal mit der ID %id konnte nicht gefunden werden" - }, - "polls": { - "what-have-i-votet": "Für was habe ich abgestimmt?", - "vote": "Abstimmen!", - "vote-this": "Wähle diese Option um hierfür abzustimmen", - "voted-successfully": "Erfolgreich abgestimmt. Danke für deine Teilnahme.", - "not-voted-yet": "Du hast noch nicht abgestimmt, also kann ich dir nicht zeigen für was du abgestimmt hast?", - "you-voted": "Du hast für **%o** abgestimmt.", - "change-opinion": "Du kannst deine Meinung jeder Zeit ändern, indem du einfach etwas anderes über dem Knopf den du gerade angeklickt hast auswählst.", - "command-poll-description": "Erstelle und beende Umfragen", - "command-poll-create-description": "Erstelle eine neue Umfrage", - "command-poll-end-description": "Beende eine existierende Umfrage", - "command-poll-end-msgid-description": "ID der Umfrage", - "command-poll-create-description-description": "Thema / Beschreibung dieser Umfrage", - "command-poll-create-channel-description": "Kanal, in welchem diese Umfrage erstellt werden soll", - "command-poll-create-option-description": "Option Nummer %o", - "command-poll-create-endAt-description": "Dauer der Umfrage (wenn nicht gesetzt ist, wird die Umfrage nicht automatisch beendet)", - "created-poll": "Umfrage erfolgreich in %c erstellt.", - "not-found": "Umfrage konnte nicht gefunden werden", - "ended-poll": "Umfrage erfolgreich beendet", - "not-text-channel": "Du musst einen Textkanal auswählen, der kein Ankündigungskanal ist." - }, - "channel-stats": { - "audit-log-reason-interval": "Dieser Kanal wurde aufgrund des in channel-stats eingestellten Intervalls aktualisiert", - "audit-log-reason-startup": "Dieser Kanal wurde von channel-stats aktualisiert, da der Bot neu gestartet wurde", - "not-voice-channel-info": "Der Kanal ist kein Sprachkanal" - }, - "activities": { - "hook-installed": "Hook um spezielle Aktivitätseinladungen zu erstellen wurde installiert", - "command-description": "Voice-Aktivität auf Discord erstellt", - "type-description": "Typ der Voice-Aktivität" - }, - "info-commands": { - "info-command-description": "Finde Informationen über Teile dieses Servers heraus", - "command-userinfo-description": "Finde mehr Informationen über einen Nutzer auf diesem Server heraus", - "argument-userinfo-user-description": "Benutzer, über den Du Informationen sehen möchten (Standard: Du)", - "command-roleinfo-description": "Weitere Informationen zu einer Rolle auf diesem Server finden", - "argument-roleinfo-role-description": "Rolle, zu der Du Informationen sehen möchten", - "command-channelinfo-description": "Weitere Informationen zu einem Kanal auf diesem Server finden", - "argument-channelinfo-channel-description": "Kanal, über den Du Informationen sehen möchten", - "command-serverinfo-description": "Weitere Informationen zu diesem Server finden", - "information-about-role": "Informationen zur Rolle %r", - "hoisted": "Rechts gelistet", - "mentionable": "Erwähnbar", - "managed": "Verwaltet", - "information-about-channel": "Informationen über den Kanal %c", - "information-about-user": "Information über den Nutzer %u", - "information-about-server": "Informationen über %s", - "boostLevel": "Level", - "boostCount": "Boosts", - "userCount": "Nutzer", - "memberCount": "Mitglieder", - "onlineCount": "Online", - "textChannel": "Text", - "voiceChannel": "Sprach", - "categoryChannel": "Kategorien", - "otherChannel": "Anderes", - "total-invites": "Gesamt", - "active-invites": "Aktiv", - "left-invites": "Übrig" - }, - "channelType": { - "GUILD_TEXT": "Text-Kanal", - "GUILD_VOICE": "Sprach-Kanal", - "GUILD_CATEGORY": "Kategorie", - "GUILD_NEWS": "Ankündigungs-Kanal", - "GUILD_STORE": "Store-Kanal", - "GUILD_NEWS_THREAD": "News-Kanal-Thread", - "GUILD_PUBLIC_THREAD": "Öffentlicher Thread", - "GUILD_PRIVATE_THREAD": "Privater Thread", - "GUILD_STAGE_VOICE": "Bühnen-Kanal", - "DM": "Direkt-Nachricht", - "GROUP_DM": "Gruppen-Direkt-Nachricht", - "UNKNOWN": "Unbekannt" - }, - "stagePrivacy": { - "PUBLIC": "Öffentlich zugänglich", - "GUILD_ONLY": "Nur Servermitglieder können beitreten" - }, - "guildVerification": { - "NONE": "Keins", - "LOW": "Niedrig", - "MEDIUM": "Mittel", - "HIGH": "Hoch", - "VERY_HIGH": "Sehr Hoch" - }, - "boostTier": { - "NONE": "Keins", - "TIER_1": "Level 1", - "TIER_2": "Level 2", - "TIER_3": "Level 3" - }, - "temp-channels": { - "removed-audit-log-reason": "Temp-Channel entfernt, da niemand darin war", - "permission-update-audit-log-reason": "Berechtigungen aktualisiert, um sicherzustellen, dass nur Personen im Sprachkanal den No-Mic-Kanal sehen können", - "created-audit-log-reason": "Temp-Channel für %u erstellt", - "move-audit-log-reason": "Nutzer in seinen/ihren Sprachkanal verschoben", - "no-mic-channel-topic": "Willkommen in %us No-Mic-Kanal. Du wirst diesen Kanal so lang sehen, wie du mit dem Temp-Channel verbunden bist.", - "disconnect-audit-log-reason": "Der alte Kanal des Nutzers konnte nicht gefunden werden - Verbindung wird getrennt - Hoffentlich joint er/sie wieder", - "command-description": "Verwalte deinen Temp-channel", - "mode-subcommand-description": "Ändere den Modus deines Kanals", - "public-option-description": "local public-option-description", - "add-subcommand-description": "Füge Nutzer hinzu, die deinem Channel beitreten können sollen, während er privat ist", - "remove-subcommand-description": "Entferne Nutzer von deinem Kanal", - "add-user-option-description": "Der Nutzer der hinzugefügt werden soll", - "remove-user-option-description": "Der Nutzer der entfernt werden soll", - "list-subcommand-description": "Liste der Nutzer mit Zugang zu deinem Kanal", - "edit-subcommand-description": "Bearbeite deinen Kanal", - "user-limit-option-description": "Ändere die maximale Nutzeranzahl deines Kanals", - "bitrate-option-description": "Ändere die Bitrate deines Kanals (min. 8000)", - "name-option-description": "Ändere den Namen deines Kanals", - "nsfw-option-description": "Ändere ob dein Kanal altersbeschränkt (NSFW) ist oder nicht", - "no-added-user": "Es gibt keine Nutzer die hier angezeigt werden können", - "nothing-changed": "Dein Kanal hatte bereits diese Einstellungen", - "no-disconnect": "Trennen der Verbindung des Nutzers nicht möglich. Dies kann an fehlenden Rechten liegen, oder daran, dass der Nutzer nicht in deinem Kanal ist", - "edit-error": "Beim Bearbeiten deines Kanals ist ein Fehler aufgetreten. Eine oder mehrere deiner Einstellungen konnten nicht angewendet werden. Dies kann an fehlenden Rechten oder einem ungültigen Eingabewert liegen.", - "add-user": "Nutzer hinzufügen", - "remove-user": "Nutzer entfernen", - "list-users": "Nutzer anzeigen", - "private-channel": "Privat", - "public-channel": "Öffentlich", - "edit-channel": "Kanal Bearbeiten", - "add-modal-title": "Füge einen Nutzer zu deinem Kanal hinzu", - "add-modal-prompt": "Der Nutzer den du hinzufügen willst", - "remove-modal-title": "Entferne einen Nutzer von deinem Temp-channel", - "remove-modal-prompt": "Der Nutzer den du entfernen willst", - "edit-modal-title": "Kanal bearbeiten", - "edit-modal-nsfw-prompt": "Temp-channel als altersbeschränkt markieren?", - "edit-modal-nsfw-placeholder": "\"true\" (ja) oder \"false\" (nein)", - "edit-modal-bitrate-prompt": "Bitrate deines Temp-channels", - "edit-modal-bitrate-placeholder": "Eine Zahl; Mindestens 8000", - "edit-modal-limit-prompt": "Limit an Nutzern in deinem Temp-channel", - "edit-modal-limit-placeholder": "Zahl zwischen 0 und 99; 0 = beliebig viele", - "edit-modal-name-prompt": "Wie soll dein Kanal heißen?", - "edit-modal-name-placeholder": "Ein Super Kanalname", - "user-not-found": "Nutzer nicht gefunden" - }, - "guess-the-number": { - "command-description": "Verwalte den Status deines Errate-die-Zahl-Spiels", - "status-command-description": "Zeigt den aktuellen Status eines Errate-die-Zahl-Spiels", - "create-command-description": "Erstelle ein neues Errate-die-Zahl-Spiel in diesem Kanal", - "create-min-description": "Niedrigster Wert, den Nutzer raten können", - "create-max-description": "Höchster Wert, den Nutzer raten können", - "create-number-description": "Zahl, welche Nutzer erraten sollen um zu gewinnen", - "end-command-description": "Beendet das aktuelle Spiel", - "session-already-running": "In diesem Kanal läuft bereits eine Runde. Bitte beende es zuerst mit /guess-the-number end", - "session-not-running": "Aktuell läuft keine Runde.", - "session-ended-successfully": "Runde erfolgreich beendet. Kanal erfolgreich gesperrt.", - "current-session": "Aktuelle Runde", - "number": "Zahl", - "min-val": "Niedrigster Wert", - "max-val": "Höchster Wert", - "owner": "Eigentümer", - "guess-count": "Anzahl der Versuche", - "min-max-discrepancy": "`min` kann nicht größer oder gleich groß wie `max` sein", - "emoji-guide-button": "Was bedeutet die Reaktion unter meinem Versuch?", - "emoji-guide-link": "https://docs.sc-network.net/de/custom-bot-v2/module/guess-the-number#was-bedeuten-die-reaktionen-unter-meinen-nachrichten", - "created-successfully": "Runde erfolgreich erstellt. Nutzer können nun anfangen in diesem Kanal zu raten. Die gewinnende Zahl ist **%n**. Du kannst den Status immer mit `/guess-the-number-status` abfragen. Beachte, dass du als Admin nicht mitraten kannst.", - "game-ended": "Spiel beendet", - "game-started": "Spiel gestartet" - }, - "twitch-notifications": { - "channel-not-found": "Der Kanal mit der ID %c konnte nicht gefunden werden", - "user-not-on-twitch": "Nutzer %u konnte auf Twitch nicht gefunden werden", - "user-not-found": "Der Benutzer mit der ID %u konnte nicht gefunden werden" - }, - "fun": { - "slap-command-description": "Schlägt einen Nutzer ins Gesicht", - "user-argument-description": "Nutzer, auf den diese Aktion auszuführen ist", - "no-no-not-slapping-yourself": "Du kannst dich nicht selbst schlagen lol (also theoretisch schon, aber unsere GIFs können das nicht, also Akzeptiere es einfach ¯\\_(ツ)_/¯)", - "pat-command-description": "Tätschle jemanden nett", - "no-no-not-patting-yourself": "Guter Versuch, aber das machen wir hier nicht", - "no-no-not-kissing-yourself": "Uah, das ist eklig, du solltest versuchen jemanden anderen dafür zu bezahlen (also solltest du nicht, aber es ist besser als dich selbst zu küssen)", - "kiss-command-description": "Küsse jemanden", - "hug-command-description": "Umarme jemanden <3", - "no-no-not-hugging-yourself": "Du bist ziemlich einsam, oder? Versuche einen Baum zu umarmen, das sollte funktionieren. Außer du lebst in einer Wüste. Dann umarme einen Kaktus. Das tut ein bisschen mehr weh, aber vertrau mir.", - "random-command-description": "Hilft dir zufällige Sachen auszusuchen", - "random-number-command-description": "Sagt dir eine zufällige Zahl", - "min-argument-description": "Niedrigste mögliche Zahl (Standard: 1)", - "max-argument-description": "Höchste mögliche Zahl (Standard: 42)", - "random-ikeaname-command-description": "Generiert einen zufälligen IKEA-Namen", - "syllable-count-argument-description": "Anzahl der Silben des generierten Namens (Standard: Zufällig)", - "random-dice-command-description": "Würfle", - "random-coinflip-command-description": "Wirf eine Münze!", - "random-8ball-command-description": "Generiert eine Antwort auf eine Ja/Nein-Frage", - "dice-site-1": "Kopf", - "dice-site-2": "Zahl" - }, - "moderation": { - "moderate-command-description": "Moderiert Nutzer auf deinem Server", - "moderate-notes-command-description": "Setze oder sehe die Kommentare eines Moderators über einen Nutzer", - "moderate-notes-command-view": "Zeige die Notizen zu einem Nutzer an", - "moderate-notes-command-create": "Erstelle eine neue Notiz zu einem Nutzer", - "moderate-notes-command-edit": "Bearbeite eine deiner existierenden Notizen zu einem Nutzer", - "moderate-notes-command-delete": "Lösche eine deiner existierenden Notizen zu einem Nutzer", - "moderate-ban-command-description": "Bannt einen Nutzer von deinem Server", - "moderate-reason-description": "Grund für die Aktion", - "moderate-duration-description": "Dauer der Aktion (Standard: Permanent)", - "mute-max-duration": "Discord begrenzt die Höchstdauer eines Timeouts auf 28 Tage. Bitte gib einen Wert an, der niedriger oder gleich ist", - "moderate-quarantine-command-description": "Versetzt einen Nurzer auf deinem Server in Quarantäne", - "moderate-unquarantine-command-description": "Entfernt einen Nutzer aus der Quarantäne", - "moderate-unban-command-description": "Hebt einen existierenden Bann auf", - "moderate-clear-command-description": "Löscht Nachrichten im aktuellen Kanal", - "moderate-clear-amount-description": "Wie viele Nachrichten sollen gelöscht werden?", - "moderate-kick-command-description": "Kickt einen Nutzer von deinem Server", - "moderate-unwarn-command-description": "Hebt eine Verwarnung auf", - "moderate-mute-command-description": "Schaltet einen Nutzer auf deinem Server stumm", - "moderate-unmute-command-description": "Hebt die Stummschaltung eines Nutzers wieder auf", - "moderate-warn-command-description": "Verwarnt einen Nutzer", - "moderate-lock-command-description": "Sperrt den aktuellen Kanal", - "moderate-unlock-command-description": "Entsperrt den aktuellen Kanal", - "moderate-user-description": "Nutzer, auf den diese Aktion auszuführen ist", - "moderate-userid-description": "ID eines Nutzers", - "moderate-days-description": "Anzahl der zu löschenden Nachrichten", - "invalid-days": "Tage können nur zwischen 0 und 7 sein", - "moderate-notes-description": "Deine Notiz setzen (leer lassen, um Notizen zu sehen)", - "moderate-note-id-description": "ID einer deiner Notizen, die du bearbeiten willst (leer lassen, um neu zu erstellen)", - "moderate-warnid-description": "ID einer Verwarnung (führe /moderate actions aus um diese herauszufinden)", - "moderate-actions-command-description": "Zeigt alle Aktionen gegen einen Nutzer", - "report-command-description": "Meldet einen Nutzer und sendet einen Ausschnitt des Chats an das Serverteam", - "report-reason-description": "Bitte beschreibe was der Nutzer falsch gemacht hat", - "report-user-description": "Nutzer, den du melden willst", - "no-reason": "Nicht gesetzt", - "muterole-not-found": "Die Stummschaltungsrolle konnte nicht gefunden werden. Aktion kann nicht ausgeführt werden", - "quarantinerole-not-found": "Die Quarantänerolle konnte nicht gefunden werden. Aktion kann nicht ausgeführt werden", - "mute-audit-log-reason": "Wurde von %u wegen \"%r\" stumm geschaltet", - "unmute-audit-log-reason": "Die Stummschaltung wurde von %u wegen \"%r\" aufgehoben", - "quarantine-audit-log-reason": "Wurde von %u wegen \"%r\" in Quarantäne versetzt", - "kicked-audit-log-reason": "Wurde von %u wegen \"%r\" gekickt", - "banned-audit-log-reason": "Wurde von %u wegen \"%r\" gebannt", - "unbanned-audit-log-reason": "Wurde von %u wegen \"%r\" entbannt", - "unquarantine-audit-log-reason": "Wurde von %u wegen \"%r\" aus der Quarantäne entfernt", - "action-expired": "Aktion ausgelaufen", - "auto-mod": "Auto-Mod", - "batch-role-remove-failed": "Es konnten nicht alle Rollen von %i entfernt werden (versuche eine nach der anderen zu entfernen): %e", - "batch-role-add-failed": "Es konnten %i nicht alle Rollen gegeben werden (versuche eine nach der anderen zu geben): %e", - "could-not-remove-role": "Rolle %r konnte %i nicht entfernt werden: %e", - "could-not-add-role": "Rolle %r konnte %i nicht gegeben werden: %e", - "reason": "Grund", - "join-gate": "Join-Gate", - "expires-at": "Aktion läuft aus am", - "action": "Aktion", - "case": "Fall", - "victim": "Betroffener Nutzer", - "missing-logchannel": "Log-Kanal konnte nicht gefunden werden", - "reached-warns": "%w Verwarnungen erreicht", - "restored-punishment-audit-log-reason": "Strafe wiederhergestellt", - "anti-join-raid": "ANTI-JOIN-RAID", - "raid-detected": "Raid erkannt", - "joingate-for-everyone": "Join-Gate-Modus: Alle Nutzer abfangen", - "account-age-to-low": "Accounterstellungsalter von %a Tagen ist zu niedrig (es werden mehr als %c Tage benötigt)", - "no-profile-picture": "Account hat kein Profilbild", - "join-gate-fail": "Account hat das Join-Gate nicht bestanden (%r)", - "blacklisted-word": "Hat ein verbotenes Wort in %c geschrieben", - "invite-sent": "Hat einen Einladungslink in %c geschrieben", - "anti-spam": "Anti-Spam", - "reached-messages-in-timeframe": "Hat %m (normale) Nachrichten in unter %t Sekunden erreicht", - "reached-duplicated-content-messages": "Hat %m mit dem selben Inhalt in unter %t Sekunden erreicht", - "reached-ping-messages": "Hat %m mit (Nutzer-) Erwähnungen in unter %t Sekunden erreicht", - "reached-massping-messages": "Hat %m mit Massenerwähnungen in unter %t Sekunden erreicht", - "action-done": "Aktion erfolgreich ausgeführt. AktionsID: #%i", - "expiring-action-done": "Fertig. Aktion wird am %d automatisch auslaufen. AktionsID: #%i", - "cleared-channel": "Kanal erfolgreich geleert.\nHinweis: Nachrichten, die älter als 14 Tage sind werden eventuell mit dieser Methode nicht gelöscht.", - "clear-failed": "Ein Fehler ist aufgetreten. Du kannst nur 100 Nachrichten auf ein Mal löschen.", - "no-quarantine-action-found": "Entschuldigung, aber ich kann keine Aufzeichnungen über Quarantäneaufenthalte dieses Nutzers finden.", - "locked-channel-successfully": "Kanal erfolgreich gesperrt. Nur Moderatoren (und Admins) können hier noch Nachrichten schreiben.", - "unlocked-channel-successfully": "Kanal erfolgreich entsperrt. Berechtigungen wurden auf den Status von vor der Sperrung wiederhergestellt.", - "unlock-audit-log-reason": "Nutzer %u hat diesen Kanal durch Ausführung von /moderate unlock entsperrt", - "warning-not-found": "Verwarnung konnte nicht gefunden werden. Bitte stelle sicher, dass du eine VerwarnungsID und keine NutzerID verwendest.", - "can-not-report-mod": "Du kannst Moderatoren nicht melden.", - "action-description-format": "%reason\nvon %u am %t", - "no-actions-title": "Nicht gefunden", - "no-actions-value": "Es wurden keine Aktionen gegen %u gefunden.", - "actions-embed-title": "Mod-Aktionen gegen %u - Seite %i", - "actions-embed-description": "Du kannst jede Aktion gegen %u hier sehen.", - "report-embed-title": "Neue Meldung", - "report-embed-description": "Ein Nutzer hat einen anderen Nutzer gemeldet. Bitte bearbeite den Fall und führe, wenn nötig, Aktionen aus.", - "reported-user": "Gemeldeter Nutzer", - "report-reason": "Grund der Meldung", - "report-user": "Nutzer, welcher die Meldung eingereicht hat", - "message-log": "Letzte 100 Nachrichten", - "message-log-description": "Du kannst einen verschlüsselten Nachrichten-Log [hier](%u) sehen.", - "channel": "Kanal", - "no-report-pings": "Keine Erwähnungen konfiguriert. Überprüfe deine Konfiguration um dein Team zu benachrichtigen.", - "not-allowed-to-see-own-notes": "Sorry, aber leider darfst du die Notizen über dich nicht selber sehen.", - "note-added": "Notiz erfolgreich hinzugefügt", - "note-edited": "Notiz erfolgreich bearbeitet", - "note-deleted": "Notiz erfolgreich gelöscht", - "note-not-found-or-no-permissions": "Notiz nicht gefunden oder keine Rechte, diese Notiz zu bearbeiten.", - "notes-embed-title": "Notizen über %u", - "info-field-title": "ℹ️ Information", - "no-notes-found": "Keine Notizen über diesen Nutzer gefunden. Erstelle eine neue Notiz mit `/moderate notes` und setze dabei das `notes`-Attribut.", - "more-notes": "%x weitere Moderatoren haben Notizen über diesen Nutzer hinterlassen. Notizen sind in entgegengesetzer Chronologie sortiert, du siehst die neuesten Notizen zuerst.", - "user-notes-field-title": "%t's Notizen", - "user-not-on-server": "Ich kann gegen diesen Nutzer keine Aktionen ausführen, da er gerade nicht auf deinem Server ist.", - "verification": "VERIFIKATION", - "verification-failed": "Verifikation fehlgeschlagen", - "verification-started": "Verifikation wurde begonnen", - "verification-completed": "Verifikation beendet", - "user": "Nutzer", - "manual-verification-needed": "Manuelle Verifikation benötigt", - "verification-deny": "Verifikation ablehnen", - "verification-approve": "Verifikation bestätigen", - "verification-skip": "Verifikation überspringen", - "captcha-verification-pending": "Captcha-Verification steht noch aus. Du kannst entweder warten, bis der Nutzer diese beendet hat, oder sie manuell überspringen.", - "verification-update-proceeded": "Verifikationsstatus erfolgreich aktualisiert", - "verify-channel-set-but-not-found-or-wrong-type": "Der eingestellte Verifikationskanal wurde nicht gefunden oder hat den falschen Typ.", - "generating-message": "Wir bereiten einiges vor; diese Nachricht wird bald bearbeitet...", - "restart-verification-button": "Verifikation neustarten", - "member-not-found": "Dieser Nutzer konnte nicht gefunden werden, vielleicht ist er schon gegangen?", - "already-verified": "Es sieht so aus als wärst du bereits verifiziert... Warum würdest du das wiederholen wollen?", - "restarted-verification": "Ich habe dir eine neue PN zu deinem Verifikationsvorgang gesendet. Bitte lese sie gründlich und folge den darin beschriebenen Anweisungen. Bitte beachte, dass dies nicht die mauelle Verifikation neugestartet hat (wenn sie aktiviert ist), deshalb bringt es nichts diesen Knopf zu spammen.", - "dms-still-disabled": "Es scheint als wären deine PNs immer noch deaktiviert. Bitte aktiviere deine PNs um den Verifikationsprozess zu starten. Dies ist nicht optional, du musst das tun, um Zugriff auf %g zu erhalten.", - "dms-not-enabled-ping": "%p, es scheint als hättest du deine DMs deaktiviert. Bitte aktiviere sie und klicke auf den Knopf unter dieser Nachricht um dich zu verifizieren. Um dies zu tun hast du zwei Minuten Zeit.", - "scam-url-sent": "Gesendete Betrugs-URL in %c" - }, - "counter": { - "created-db-entry": "Datenbankeintrag für %i initialisiert", - "not-a-number": "Das ist keine Zahl. Du kannst hier nicht chatten. Versuche einen Thread zu erstellen, wenn deine Nachricht so wichtig ist.", - "banned-because-of-improper-use": "Ich musste dir den Zugriff auf diesen Kanal verbieten, da du ihn mehrmals falsch verwendet hast.", - "restriction-audit-log": "Dieser Nutzer hat nach fünf Warnungen weiterhin den Zähl-Kanal missbraucht, also habe ich ihn ausgesperrt.", - "only-one-message-per-person": "Nutzer müssen abwechseld zählen: Du kannst nicht zwei mal hintereinander zählen.", - "not-the-next-number": "Das ist nicht die nächste Zahl. Diese wäre **%n**, bitte stelle sicher immer eine Zahl nach der anderen zu zählen.", - "channel-topic-change-reason": "Jemand hat gezählt, also haben wir die Kanalbeschreibung wie in der Konfiguration angegeben bearbeitet" - }, - "tickets": { - "channel-not-found": "Ticket-Erstellungs-Kanal konnte nicht gefunden werden", - "existing-ticket": "Du hast bereits ein geöffnetes Ticket: %c", - "ticket-created-audit-log": "%u hat ein neues Ticket durch klicken des Knopfes erstellt", - "ticket-created": "Das Ticket wurde erfolgreich erstellt und das Team benachrichtigt. Du findest es hier: %c", - "new-ticket-embed-title": "📥 Neues Ticket #%i", - "new-ticket-embed-user": "👤 Nutzer", - "new-ticket-embed-info": "ℹ️ Information", - "close-info": "Dein Problem wurde behoben? Klicke den Knopf unten. Du kannst diese Nachricht immer in den angepinnten Nachrichten finden.", - "no-admin-pings": "Keine Erwähnungen konfiguriert. Überprüfe deine Konfiguration um dein Team zu benachrichtigen.", - "ticket-closed-successfully": "Ticket erfolgreich geschlossen. Dieser Kanal wird in wenigen Sekunden gelöscht. Danke, dass du unseren Support benachrichtigt hast.", - "ticket-closed-audit-log": "%u hat das Ticket geschlossen", - "closing-ticket": "Schließe Ticket, wie von %u angefragt...", - "could-not-dm": "Es konnte keine PN an %u gesendet werden: %r", - "no-log-channel": "Log-Kanal nicht gefunden", - "ticket-log-embed-title": "📎 Ticket %i geschlossen", - "ticket-with-user": "👤 Ticket-Nutzer", - "ticket-log": "Ticket-Protokoll", - "ticket-log-value": "Transkript mit %n Nachrichten kann [hier](%u) gefunden werden.", - "closed-by": "👷 Ticket geschlossen von", - "ticket-type": "☕ Ticket-Thema" - }, - "custom-commands": { - "not-a-booster": "Kein Booster", - "no-nickname": "Kein Nickname", - "ub-parameters-missing": "Einige Parameter für den Filter \"ub\" fehlen", - "filter-not-supported": "Filter \"%t\" wird auf dieser Version nicht unterstützt. Versuche deinen Bot zu updaten.", - "action-not-supported": "Aktion \"%t\" wird auf dieser Version nicht unterstützt. Versuche deinen Bot zu updaten.", - "audit-log": "Eigener Befehl %c wurde ausgeführt (ID: %i)", - "ub-error": "SCNX <-> UnbeliveaBoat hat mit %s und dem Inhalt \"%c\" geantwortet", - "ub-parse-error": "SCNX <-> UnbeliveaBoat hat mit %s geantwortet, allerdings konnte der Inhalt nicht geparst werden.", - "error-executing-action": "Ein Fehler ist bei Ausführung von Aktion %a im eigenen Befehl %c (ID: %i) aufgetreten: %e", - "not-found": "Dieser benutzerdefinierte Befehl existiert nicht mehr. Möglicherweise wurde es gelöscht oder deaktiviert.", - "parameter-not-set": "Dieser Parameter wurde nicht angegeben", - "true": "Richtig", - "false": "Falsch" - }, - "logging": { - "hook-installed": "Installierter Haken für den Empfang von Sonderveranstaltungen" - }, - "akinator": { - "command-description": "Lass akinator einen Charakter / ein Objekt oder ein Tier erraten", - "type-description": "Wähle aus, was Akinator erraten soll (Standart: Charakter)", - "character-name": "Charakter", - "object-name": "Objekt", - "animal-name": "Tier" - }, - "afk-system": { - "command-description": "Verwalte deinen AFK-Status auf diesem Server", - "end-command-description": "Beendet eine laufende AFK-Sitzung", - "start-command-description": "Startet eine neue AFK-Sitzung", - "reason-option-description": "Erkläre, warum du AFK gehst", - "autoend-option-description": "Der Bot wird automatisch dein AFK-Status beenden, sobald du eine Nachricht schreibst (Standart: an)", - "no-running-session": "Es scheint, als wärst du aktuell keine AFK-Sitzung am Laufen.", - "already-running-session": "Du hast bereits eine AFK-Sitzung am Laufen, du kannst sie jederzeit mit `/afk-system end` beenden.", - "afk-nickname-change-audit-log": "Der Nickname des Nutzers wurde aktualisiert, weil er eine AFK-Sitzung gestartet hat", - "can-not-edit-nickname": "Der Nickname von %u konnte nicht geändert werden: %e" - }, - "invite-tracking": { - "hook-installed": "Integration initialisiert, um mehr Informationen über Einladungen zu erhalten", - "log-channel-not-found-but-set": "Der angegebene Log-Kanal %c wurde nicht gefunden.", - "new-member": "Neues Mitglied beigetreten", - "member-leave": "Ein Mitglied hat den Server verlassen", - "invite-type": "Einladungs-Typ", - "member": "Mitglied", - "invite": "Einladung", - "invite-code": "Einladungscode: [%c](%u)", - "invite-channel": "Kanal: %c", - "expires-at": "Läuft ab am: %t", - "created-at": "Erstellt: am %t", - "inviter": "Einlader: %u (%a/%i aktive Einladungen)", - "uses": "Verwendungen: %u", - "createdAt": "Erstellt am: %t", - "max-uses": "Maximal-Verwendungen: %u", - "normal-invite": "Normaler Invite", - "vanity-invite": "Vanity-Einladung", - "missing-permissions": "Ich habe nicht die Rechte, den Inviter zu ermitteln", - "unknown-invite": "Sorry, ich kann nicht herausfinden, welchen Invite dieser Nutzer verwendet hat", - "joined-for-the-x-time": "%u ist dem Server schon %x-zuvor beigetreten, letztes mal am %t.", - "revoke-invite": "Dieser Invite entfernen", - "invite-not-found": "Dieser Invite wurde nicht gefunden... Vielleicht wurde er schon gelöscht?", - "invite-revoked": "Invite wurde erfolgreich entfernt.", - "missing-revoke-permissions": "Sorry, du kannst diesen Invite nicht entfernen: Du brauchst `MANAGE_GUILD` Rechte um diese Aktion durchzuführen.", - "invite-revoke-audit-log": "Dieser Invite wurde von %u entfernt", - "invite-revoked-error": "Invite konnte nicht gelöscht werden %c: %e", - "trace-command-description": "Verfolge die Einladungen eines Nutzers nach", - "argument-user-description": "Nutzer, dessen Einladungen zu verfolgen sind", - "invited-by": "Eingeladen von", - "invited-users": "Eingeladene Nutzer", - "inviter-not-found": "Ich konnte nicht ermitteln, wer diesen Nutzer eingeladen hat.", - "no-users-invited": "Dieses Mitglied hat keine anderen Nutzer eingeladen.", - "and-x-more-users": "und %x Nutzer mehr", - "and-x-more-invites": "und %x mehr Einladungen", - "created-invites": "Erstellte Einladungen", - "not-showing-left-users": "Eingeladene Nutzer, die den Server verlassen haben, werden hier nicht angezeigt.", - "no-invites": "Dieses Mitglied hat keine Einladungen erstellt", - "revoke-user-invite": "Alle Einladungen dieses Nutzers entfernen", - "revoked-invites-successfully": "Alle Einladungen dieses Nutzers wurden entfernt" - }, - "2022-countdown": { - "channel-not-found": "Countdown-Channel wurde nicht gefunden ):", - "days-left": "Noch %x Tage bis 2022", - "hours-left": "Noch %x Stunden bis 2022", - "2022-is-here": "2022 ist hier 🎉", - "channel-edit-audit-log": "Um die Aktualität des Countdowns sicherzustellen, wurde dieser Channel geupdatet." - }, - "tic-tac-toe": { - "command-description": "Spiele tic-tac-toe gegen jemanden im Chat", - "user-description": "Nutzer, gegen den du spielen willst", - "challenge-message": "%t, %u hat dich zu einer Runde tic-tac-toe herausgefordert! Klicke auf den Knopf unter dieser Nachricht um beizutreten! Diese Einladung wird in etwa 2 Minuten ablaufen, also zögere nicht sie anzunehmen.", - "accept-invite": "Spiel beitreten", - "deny-invite": "Nein, danke", - "self-invite-not-possible": "Bist du wirklich so einsam? Selbst Simon, ein total introvertierter Mensch ohne Freunde und Entwickler dieses Bots, kann einen Nutzer zum tic-tac-toe spielen finden... Du solltest das auch schaffen, versuche zum Beispiel %r einzuladen, vielleicht will er/sie eine Runde spielen?", - "invite-expired": "Entschuldigung, %u, %i hat deine Einladung tic-tac-toe zu spielen nicht rechtzeitig angenommen ):", - "invite-denied": "Entschuldigung, %u, aber %i hat deine Einladung tic-tac-toe zu spielen abgelehnt ):", - "you-are-not-the-invited-one": "Entschuldigung, aber diese Einladung gehört dir nicht. Du kannst dein eigenes Spiel mit `/tic-tac-toe` starten.", - "playing-header": "**TIC-TAC-TOE SPIEL LÄUFT GERADE**\n\n%u (🟢) VS %i (🟡)\nAktuell am Zug: %t\n\n%t, klicke einen Knopf mit einem weißen Kreis unter dieser Nachricht um deine Markierung zu setzen", - "win-header": "**TIC-TAC-TOE-GAME BEENDET**\n\n%u (🟢) VS %i (🟡)\n\n%w hat das Spiel gewonnen - GG!\n\n*Du kannst mit `/tic-tac-toe` eine neue Runde starten*", - "draw-header": "**TIC-TAC-TOE-GAME BEENDET**\n\n%u (🟢) VS %i (🟡)\n\nUnentschieden - niemand hat diese Runde gewonnen.", - "not-your-turn": "Du bist gerade nicht dran, hol dir einen Kaffee und versuche es später nochmal" - }, - "economy-system": { - "work-earned-money": "Der Benutzer %u hat %m %c durch Arbeit bekommen", - "crime-earned-money": "Der Benutzer %u hat %m %c durch ein Verbrechen bekommen", - "crime-loose-money": "Der Benutzer %u hat %m %c durch ein Verbrechen bekommen", - "message-drop-earned-money": "Der Benutzer %u hat %m %c gewonnen, indem er eine Nachricht geschrieben hat", - "rob-earned-money": "Der Benutzer %u hat %m %c durch den Raub von %v bekommen", - "weekly-earned-money": "Der Benutzer %u hat %m %c bekommen, indem er seine wöchentliche Belohnung einlöste", - "daily-earned-money": "Der Benutzer %u hat %m %c bekommen, indem er seine wöchentliche Belohnung einlöste", - "admin-self-abuse": "Der Admin %a wollte seine Berechtigungen missbrauchen, indem er sich selbst noch mehr Geld gab! Das kann und darf nicht ignoriert werden!", - "admin-self-abuse-answer": "Was für ein schlechter Administrator du bist, %u. Ich bin enttäuscht von dir! Ich muss das melden. Wenn ich wollte, könnte ich dich bannen!", - "added-money": "%i %c wurde dem Konto von %u hinzugefügt", - "removed-money": "%i %c wurde aus dem Konto von %u entfernt", - "set-money": "Der Kontostand von %u wurde auf %i gesetzt.", - "added-money-log": "Der Benutzer %u hat %i %c zum Konto von %v hinzugefügt", - "removed-money-log": "Der Benutzer %u hat %i %c aus dem Konto von %v entfernt", - "set-money-log": "Der Benutzer %u hat den Kontostand von %v auf %i %c gesetzt", - "command-description-main": "Verwende das Economy-System", - "command-description-work": "Verdiene etwas Geld, indem du arbeiten gehst", - "command-description-crime": "Verdiene etwas Geld, indem du ein Verbrechen begehst", - "command-description-rob": "Einem anderen Mitglied Geld klauen", - "option-description-rob-user": "Mitglied zum Ausrauben", - "command-description-daily": "Löse deinen täglichen Bonus ein", - "command-description-weekly": "Löse deinen wöchentlichen Bonus ein", - "command-description-balance": "Zeige dir den Kontostand von einem Mitglied", - "option-description-user": "Mitglied zum Ausführen einer Aktion", - "command-description-add": "Füge einem Nutzer Geld hinzu", - "command-description-remove": "Entferne Geld von einem Nutzer", - "option-description-amount": "Zu manipulierender Betrag", - "command-description-set": "Kontostand eines Mitglieds einstellen", - "option-description-balance": "Kontostand, welches das Mitglied bekommt", - "message-drop": "Nachrichten-Drop: Du hast %m %c einfach durch Chatten verdient!", - "created-item": "Der Nutzer %u hat einen neuen Shopartikel erstellt: Name: %n, ID: %i", - "item-duplicate": "Das Item existiert schon", - "role-to-high" : "Die angegebene Rolle ist höher, als die höchste Rolle des Bots. Deshalb kann der Bot die Rolle nicht vergeben. Das Item wurde **nicht** erstellt.", - "delete-item": "Der Nutzer %u hat einen Shopartikel entfernt: %i", - "user-purchase": "Der Nutzer %u hat den Shopartikel %i für %p gekauft.", - "shop-command-description": "Benutze das Shop-System", - "shop-command-description-add": "Erstelle ein neuen Artikel im Shop (nur für Administratoren)", - "shop-option-description-itemName": "Name des Artikels", - "shop-option-description-itemID": "ID des Artikels", - "shop-option-description-price": "Preis des Artikels", - "shop-option-description-role": "Rolle, die dem Nutzer, die den Artikel kaufen, zugewiesen wird", - "shop-command-description-buy": "Kaufe einen Artikel", - "shop-command-description-list": "Alle Artikel im Shop auflisten", - "shop-command-description-delete": "Entferne einen Artikel aus dem Shop", - "channel-not-found": "Kann den Ranglisten-Kanal mit der ID %c nicht finden", - "command-description-deposit": "Zahle xyz auf dein Bankkonto ein", - "option-description-amount-deposit": "Einzuzahlender Betrag", - "command-description-withdraw": "Hebe xyz von deinem Bankkonto ab", - "option-description-amount-withdraw": "Auszuzahlender Betrag", - "command-group-description-msg-drop-msg": "Aktiviere/Deaktiviere die Nachrichten-Drop-Nachricht", - "command-description-msg-drop-msg-enable": "Aktiviere die Nachrichten-Drop-Nachricht", - "command-description-msg-drop-msg-disable": "Deaktiviere die Nachrichten-Drop-Nachricht", - "command-description-destroy": "Die gesamte Wirtschaft zerstören (löscht alle Datenbankeinträge)", - "option-description-confirm": "Bitte bestätige, dass du wirklich die gesamte Wirtschaft zerstören willst", - "destroy-cancel-reply": "Glück gehabt. Du hast mich im letzten Moment gestoppt, bevor ich die Wirtschaft zerstört habe", - "destroy-reply": "Ok... Ich werde die gesamte Wirtschaft zerstören", - "destroy": "%u hat die Wirtschaft zerstört", - "migration-happening": "Datenbank-Schema nicht aktuell. Datenbank wird migriert. Starte deinen Bot nicht neu, um Datenverlust zu vermeiden.", - "migration-done": "Datenbank wurde erfolgreich migriert.", - "nothing-selected": "Nichts ausgewählt", - "select-menu-price": "Preis: %p" - }, - "team-list": { - "channel-not-found": "Kanal mit der ID %c konnte nicht gefunden werden, oder hat den falschen Typ (es werden nur Textkanäle unterstützt)", - "role-not-found": "Rolle mit ID %r konnte nicht gefunden werden", - "no-users-with-role": "Kein Mitglied des Servers hat die %r Rolle.", - "no-roles-selected": "Es wurden noch keine Rollen gelistet ):" - }, - "massrole": { - "command-description": "Verwalte die Rollen deiner Mitglieder", - "role-option-remove-description": "Die Rolle, die entfernt werden soll von allen Mitgliedern", - "remove-subcommand-description": "Entferne eine Rolle von allen Mitgliedern", - "remove-all-subcommand-description": "Entferne alle Rollen von allen Mitgliedern", - "role-option-add-description": "Die Rolle, die an alle Mitglieder vergeben wird", - "target-option-description": "Lege fest, ob Bots miteinbezogen werden sollen oder nicht", - "all-users": "Alle Mitglieder", - "bots": "Bots", - "humans": "Mitglieder (keine Bots)", - "add-subcommand-description": "Füge eine Rolle zu allen Mitgliedern hinzu", - "not-admin": "⚠️ Um diesen Befehl zu verwenden musst du zur adminRoles option im SCNX-Dashboard hinzugefügt werden. Falls du der Eigentümer dieses Bots bist, denk daran in deinen Servereinstellungen ebenfalls einen entsprechenden Override einzustellen um Missbrauch dieses Commands zu verhindern.", - "add-reason": "Massen-Rollenvergabe durch %u", - "remove-reason": "Massen-Rollenentfernung durch %u" - }, - "hunt-the-code": { - "display-name-description": "Name des Codes, der dem Mitglied angezeigt wird, wenn er den Code einlöst", - "error-creating-code": "Fehler beim erstellen des Codes \"{{displayName}}\". Eventuell ist der eingegebene Code schon in der Datenbank?", - "code-redeem-description": "Den Code den du einlösen möchtest", - "report-header": "Bericht für das Jage den Code-Spiel am %s", - "admin-command-description": "Verwalte die derzeitige Code-Jagd", - "create-code-description": "Erstelle ein neuen Code für die derzeitige Code-Jagd", - "code-description": "Lege den Code fest, der zum Einlösen verwendet werden soll (Standard: zufällig generiert)", - "code-created": "Code \"%displayName\" erfolgreich erstellt: \"%code\"", - "successful-reset": "Erfolgreich das aktuelle Code-Hunt-Spiel beendet - [hier](%url) ist dein Bericht - speichere die URL, wenn du später darauf zugreifen willst.", - "end-description": "Beendet die derzeitige Code-Jagd (löscht Mitglieder und Codes und erstellt einen Bericht)", - "command-description": "Einlösen oder Daten über die aktuelle Code-Jagd einsehen", - "redeem-description": "Löse ein Code ein den du gefunden hast", - "leaderboard-description": "Sehe dir die Rangliste an", - "profile-description": "Aktuelle Anzahl deiner gefundenen Codes anzeigen", - "no-codes-found": "Keine Codes bis jetzt eingelöst ):", - "no-users": "Es haben noch keine Benutzer Codes eingelöst ):", - "user-header": "Teilnehmende Mitglieder", - "code-header": "Codes", - "report-description": "Erstellt einen Bericht", - "report": "Du kannst den Bericht [hier](%url) finden." - }, - "status-role": { - "fulfilled": "Status-Rollen-Bedingung ist erfüllt", - "not-fulfilled": "Status-Rollen-Bedingung ist nicht mehr erfüllt" - }, - "color-me": { - "create-log-reason": "%user hat seine Boosting-Vorteile durch das erstellen der Rolle eingelöst", - "edit-log-reason": "%user hat seine Boosting-Vorteil-Rolle editiert", - "delete-unboost-log-reason": "%user hat aufgehört zu boosten daher wurde seine Rolle gelöscht", - "delete-manual-log-reason": "%user hat seine Rolle manuell gelöscht", - "command-description": "Fordere eine benutzerdefinierte Rolle als Belohnung für das Boosten an. Cooldown: 24h", - "manage-subcommand-description": "Erstelle oder editiere deine Custom Rolle", - "name-option-description": "Der Name deiner Custom Rolle", - "color-option-description": "Die Farbe deiner Custom Rolle", - "remove-subcommand-description": "Entferne deine Custom Rolle", - "confirm-option-remove-description": "Willst du deine Custom Rolle wirklich löschen? Dies wird keine laufenden Cooldowns zurücksetzen" - }, - "rock-paper-scissors": { - "stone": "Stein", - "paper": "Papier", - "scissors": "Schere", - "won": "gewonnen", - "lost": "verloren", - "tie": "Unentschieden", - "play-again": "Erneut spielen", - "challenge-message": "%t, %u hat dich zu einer Runde Schere Stein Papier herausgefordert! Klicke auf den Knopf unter dieser Nachricht, um beizutreten! Diese Einladung wird in etwa 2 Minuten ablaufen, also zögere nicht, sie anzunehmen.", - "invite-expired": "Entschuldigung, %u, %i hat deine Einladung, Schere Stein Papier zu spielen, nicht rechtzeitig angenommen ):", - "invite-denied": "Entschuldigung, %u, aber %i hat deine Einladung, Schere Stein Papier zu spielen, abgelehnt ):", - "rps-title": "Schere Stein Papier", - "rps-description": "Wähle deine Waffe!", - "its-a-tie-try-again": "Unentschieden! Versuch's nochmal!", - "command-description": "Spiele Schere Stein Papier gegen den Bot oder jemanden im Chat" - }, - "connect-four": { - "tie": "Unentschieden!", - "win": "%u hat das Spiel gewonnen!", - "not-turn": "Entschuldigung, aber du bist nicht an der Reihe!", - "game-message": "Vier-gewinnt-Spiel von %u1 und %u2\nAktuell spielt: %c %t.\n\n%g", - "challenge-message": "%t, %u hat dich zu einer Runde Vier gewinnt herausgefordert! Klicke auf den Knopf unter dieser Nachricht, um beizutreten! Diese Einladung wird in etwa 2 Minuten ablaufen, also zögere nicht, sie anzunehmen.", - "invite-expired": "Entschuldigung, %u, %i hat deine Einladung, Vier gewinnt zu spielen, nicht rechtzeitig angenommen ):", - "invite-denied": "Entschuldigung, %u, aber %i hat deine Einladung, Vier gewinnt zu spielen, abgelehnt ):", - "command-description": "Spiele Vier gewinnt gegen jemanden im Chat", - "field-size-description": "Die Größe des Spielfelds (Standard: 7)", - "challenge-yourself": "Du kannst dich nicht selbst herausfordern!", - "challenge-bot": "Du kannst Bots nicht herausfordern!" - }, - "uno": { - "command-description": "Spiele Uno gegen jemanden im Chat", - "challenge-message": "%u lädt zu einer Runde Uno ein! Klicke auf den Knopf unter dieser Nachricht, um beizutreten! Das Spiel startet %timestamp mit %count Spielern.", - "not-enough-players": "Es sind nicht genug Spieler für eine Runde Uno beigetreten!", - "user-cards": "%u: %cards Karten", - "already-joined": "Du bist bereits beigetreten!", - "view-deck": "Eigene Karten ansehen", - "draw": "Karte ziehen", - "uno": "Uno!", - "turn": "%u ist an der Reihe!", - "update-button": "Aktualisieren", - "use-drawn": "Möchtest du die gezogene Karte verwenden?", - "dont-use-drawn": "Nicht verwenden", - "win": "%u hat das Spiel gewonnen! Es wurden %turns Karten gespielt.", - "win-you": "Du hast das Spiel gewonnen!", - "missing-uno": "⚠️️ Du musst den Uno!-Button nutzen, bevor du die vorletzte Karte legst!", - "choose-color": "Wähle eine Farbe aus:", - "pending-draws": "Lege eine Ziehe 2/4-Karte, sonst musst du %count Karten ziehen!", - "not-ingame": "Du bist nicht in dem Uno-Spiel!", - "skip": "Überspringen", - "reverse": "Reverse", - "color": "Farbwahl", - "draw2": "Ziehe 2", - "colordraw4": "Farbwahl und ziehe 4", - "cant-uno": "Du kannst Uno aktuell nicht nutzen.", - "done-uno": "Du hast Uno gerufen!", - "auto-drawn-skip": "Dein Zug wurde übersprungen, da du die Karten sowieso hättest ziehen müssen.", - "start-game": "Spiel sofort starten", - "not-host": "Du bist nicht der Ersteller des Spiels!", - "max-players": "Das Spiel ist voll!", - "previous-cards": "Vorherige Karten: ", - "used-card": "Du hast die Karte %c bereits verwendet! Nutze den Aktualisieren-Button und spiele eine zulässige Karte.", - "invalid-card": "Du kannst die Karte %c momentan nicht spielen! Bitte spiele eine zulässige Karte.", - "inactive-warn": "%u, du bist bei Uno am Zug!", - "inactive-win": "Das Uno-Spiel wurde beendet. %u hat gewonnen, da alle anderen ausgeschieden sind!" - }, - "quiz": { - "what-have-i-voted": "Was habe ich gewählt?", - "vote": "Abstimmen!", - "vote-this": "Wähle diese Option, wenn du denkst, dass diese richtig ist.", - "voted-successfully": "Erfolgreich ausgewählt. Danke für deine Teilnahme.", - "not-voted-yet": "Du hast noch keine Antwort ausgewählt, also kann ich dir nicht zeigen, für was du abgestimmt hast?", - "you-voted": "Du hast **%o** als Antwort ausgewählt.", - "change-opinion": "Du kannst deine Auswahl jederzeit ändern, indem du einfach etwas anderes über dem Knopf, den du gerade angeklickt hast, auswählst.", - "cannot-change-opinion": "Du kannst deine Auswahl nicht ändern, da der Ersteller diese Funktion deaktiviert hat.", - "select-correct": "Wähle alle richtigen Antworten aus", - "this-correct": "Diese Antwort als richtig markieren", - "cmd-description": "Erstelle oder spiele Quiz", - "cmd-create-normal-description": "Erstelle ein Quiz mit bis zu 10 Antworten", - "cmd-create-bool-description": "Erstelle ein Quiz, bei dem Nutzer nur Ja oder Nein auswählen können", - "cmd-play-description": "Spiele ein Server-Quiz", - "cmd-leaderboard-description": "Zeigt das Quiz-Leaderboard des Servers", - "cmd-create-description-description": "Thema / Beschreibung des Quiz", - "cmd-create-channel-description": "Kanal, in welchem dieses Quiz erstellt werden soll", - "cmd-create-endAt-description": "Relative Dauer des Quiz", - "cmd-create-option-description": "Option Nummer %o", - "cmd-create-canchange-description": "Ob die Teilnehmer ihre Auswahl nachträglich ändern können (Standard: Nein)", - "daily-quiz-limit": "Du hast das Limit von **%l** täglichen Quiz erreicht. Du kannst %timestamp wieder Quiz spielen.", - "created": "Quiz erfolgreich in %c erstellt.", - "correct-highlighted": "Alle richtigen Antworten wurden hervorgehoben.", - "answer-correct": "✅ Deine Antwort war richtig, du hast einen Punkt fürs Leaderboard erhalten!", - "answer-wrong": "❌ Deine Antwort war falsch!", - "bool-true": "Aussage stimmt", - "bool-false": "Aussage stimmt nicht", - "leaderboard-channel-not-found": "Der Leaderboard-Kanal wurde nicht gefunden oder sein Typ ist nicht erlaubt.", - "leaderboard-notation": "**%p. %u**: %xp XP", - "your-rank": "Du hast **%xp** Punkte in Quiz gesammelt!", - "no-rank": "Du hast noch nie ein Quiz erfolgreich beendet!", - "no-quiz": "Es wurden noch keine Quiz erstellt. Serveradmins können auf https://scnx.app/glink?page=bot/configuration?query=quiz&file=quiz%7Cconfigs%2FquizList Quiz hinzufügen.", - "no-permission": "Du hast keine Berechtigung, um Quiz mit dem Befehl zu erstellen." - }, - "starboard": { - "invalid-minstars": "Ungültige Mindestanzahl an Sternen %stars", - "star-limit": "Du hast das stündliche Starboard-Limit von %limitEmoji auf dem Server erreicht, deswegen kannst du nicht auf die Nachricht %msgUrl reagieren.\nProbiers doch %time nochmal!" - }, - "nicknames": { - "owner-cannot-be-renamed": "Der Serverbesitzer (%u) kann nicht umbenannt werden.", - "nickname-error": "Fehler beim Ändern des Nicknamens von %u: %e" - } -} diff --git a/main.js b/main.js index 9ca9b0e8..a42405bc 100644 --- a/main.js +++ b/main.js @@ -1,8 +1,18 @@ -const Discord = require('discord.js'); +const Discord = require('./src/discordjs-fix'); +const { + ApplicationCommandOptionType, + ApplicationCommandType, + ChannelType, + GatewayIntentBits, + Partials, + PermissionFlagsBits, + PermissionsBitField +} = Discord; const client = new Discord.Client({ - partials: ['MESSAGE', 'GUILD_MEMBER', 'GUILD_SCHEDULED_EVENT', 'MESSAGE', 'REACTION', 'USER', 'CHANNEL'], // Most of these are not needed, but enabling them does not increase CPU / RAM usage and does not introduce problems, as we handle them in the event emitter system + partials: [Partials.Message, Partials.GuildMember, Partials.GuildScheduledEvent, Partials.Reaction, Partials.User, Partials.Channel], // Most of these are not needed, but enabling them does not increase CPU / RAM usage and does not introduce problems, as we handle them in the event emitter system allowedMentions: {parse: ['users', 'roles']}, // Disables @everyone mentions because everyone hates them - intents: [Discord.Intents.FLAGS.GUILDS, 'GUILD_BANS', 'DIRECT_MESSAGES', 'GUILD_MESSAGES', 'MESSAGE_CONTENT', 'GUILD_VOICE_STATES', 'GUILD_PRESENCES', 'GUILD_INVITES', 'GUILD_EMOJIS_AND_STICKERS', 'GUILD_MESSAGE_REACTIONS', 'GUILD_EMOJIS_AND_STICKERS', 'GUILD_MEMBERS', 'GUILD_WEBHOOKS'] + intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildBans, GatewayIntentBits.DirectMessages, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, + GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.GuildPresences, GatewayIntentBits.GuildInvites, GatewayIntentBits.GuildEmojisAndStickers, GatewayIntentBits.GuildMessageReactions, GatewayIntentBits.GuildEmojisAndStickers, GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildWebhooks] }); client.intervals = []; client.jobs = []; @@ -10,8 +20,36 @@ const fs = require('fs'); const {Sequelize} = require('sequelize'); const log4js = require('log4js'); const jsonfile = require('jsonfile'); +const centra = require('centra'); const readline = require('readline'); +const optionTypeMap = { + SUB_COMMAND: ApplicationCommandOptionType.Subcommand, + SUB_COMMAND_GROUP: ApplicationCommandOptionType.SubcommandGroup, + STRING: ApplicationCommandOptionType.String, + INTEGER: ApplicationCommandOptionType.Integer, + BOOLEAN: ApplicationCommandOptionType.Boolean, + USER: ApplicationCommandOptionType.User, + CHANNEL: ApplicationCommandOptionType.Channel, + ROLE: ApplicationCommandOptionType.Role, + MENTIONABLE: ApplicationCommandOptionType.Mentionable, + NUMBER: ApplicationCommandOptionType.Number, + ATTACHMENT: ApplicationCommandOptionType.Attachment +}; +const channelTypeMap = { + GUILD_TEXT: ChannelType.GuildText, + GUILD_VOICE: ChannelType.GuildVoice, + GUILD_NEWS: ChannelType.GuildAnnouncement, + GUILD_STAGE_VOICE: ChannelType.GuildStageVoice, + GUILD_CATEGORY: ChannelType.GuildCategory +}; +const permissionMap = { + ADMINISTRATOR: PermissionFlagsBits.Administrator, + MANAGE_EMOJIS_AND_STICKERS: PermissionFlagsBits.ManageGuildExpressions, + MODERATE_MEMBERS: PermissionFlagsBits.ModerateMembers, + MANAGE_MESSAGES: PermissionFlagsBits.ManageMessages +}; + // Parsing parameters let config; let confDir = `${__dirname}/config`; @@ -27,6 +65,7 @@ if (args[0] && args[1]) { confDir = args[0]; dataDir = args[1]; } + client.locale = process.argv.find(a => a.startsWith('--lang')) ? (process.argv.find(a => a.startsWith('--lang')).split('--lang=')[1] || 'de') : 'en'; module.exports.client = client; log4js.configure({ @@ -39,7 +78,8 @@ log4js.configure({ level: 'debug' }, output: { - type: 'stdout', layout: { + type: 'stdout', + layout: { type: 'pattern', pattern: '[%p] %m' } @@ -50,14 +90,18 @@ log4js.configure({ level: 'error' }, erroutput: { - type: 'stderr', layout: { + type: 'stderr', + layout: { type: 'pattern', pattern: '[%p] %m' } } }, categories: { - default: {appenders: ['out', 'err'], level: 'debug'} + default: { + appenders: ['out', 'err'], + level: 'debug' + } } }); const logger = log4js.getLogger(); @@ -68,7 +112,7 @@ try { config = jsonfile.readFileSync(`${confDir}/config.json`); } catch (e) { logger.fatal('Missing config.json! Run "npm run generate-config " (Parameter ConfDir is optional) to generate it'); - process.exit(1); + process.exit(0); } const models = {}; // Object with all models @@ -83,7 +127,12 @@ logger.level = config.logLevel || process.env.LOGLEVEL || 'debug'; client.logger = logger; module.exports.logger = logger; const configChecker = require('./src/functions/configuration'); -const {compareArrays, checkForUpdates, formatDiscordUserName} = require('./src/functions/helpers'); +const { + compareArrays, + checkForUpdates, + formatDiscordUserName, + truncate +} = require('./src/functions/helpers'); const {localize} = require('./src/functions/localize'); logger.info(localize('main', 'startup-info', {l: logger.level})); @@ -98,21 +147,26 @@ try { const db = new Sequelize({ dialect: 'sqlite', storage: `${dataDir}/database.sqlite`, + transactionType: 'IMMEDIATE', logging: false }); const commands = []; +let modulesLoaded = false; async function startUp() { if (config.timezone !== process.env.TZ) { process.env.TZ = config.timezone; - logger.info(`Successfully set timezone to ${config.timezone}. The time is ${new Date().toLocaleString(client.locale)}.`); + logger.info(`Successfully set timezone to ${config.timezone}. The time is ${new Date().toLocaleString(client.locale.split('_')[0])}.`); } if (scnxSetup) client.scnxHost = client.config.scnxHostOverwirde || 'https://scnx.app'; - await loadModelsInDir('/src/models'); - await loadModules(); - await loadEventsInDir('./src/events'); - await db.sync(); + if (!modulesLoaded) { + modulesLoaded = true; + await loadModelsInDir('/src/models'); + await loadModules(); + await loadEventsInDir('./src/events'); + await db.sync(); + } logger.info(localize('main', 'sync-db')); if (scnxSetup) await require('./src/functions/scnx-integration').beforeInit(client); await client.login(config.token).catch(async (e) => { @@ -131,7 +185,8 @@ async function startUp() { } else logger.fatal(localize('main', 'login-error', {e})); process.exit(); }); - if ((await client.application.fetch()).botRequireCodeGrant) { + const app = JSON.parse((await centra(`https://discord.com/api/applications/@me`, 'GET').header('Authorization', `Bot ${client.token}`).send()).body.toString()); + if (app.bot_require_code_grant) { if (scnxSetup) await require('./src/functions/scnx-integration').reportIssue(client, { type: 'CORE_ISSUE', errorDescription: 'require_code_grant_active', @@ -139,6 +194,15 @@ async function startUp() { }); logger.error(localize('main', 'require-code-grant-active', {d: `https://discord.com/developers/applications/${client.user.id}/bot`})); } + if (app.interactions_endpoint_url) { + if (scnxSetup) await require('./src/functions/scnx-integration').reportIssue(client, { + type: 'CORE_FAILURE', + errorDescription: 'interactions_endpoint_set', + errorData: {settingsURL: `https://discord.com/developers/applications/${client.user.id}`} + }); + logger.error(localize('main', 'interactions-endpoint-active', {d: `https://discord.com/developers/applications/${client.user.id}/bot`})); + process.exit(); + } client.guild = await client.guilds.fetch(config.guildID).catch(() => { }); if (!client.guild) { @@ -152,7 +216,7 @@ async function startUp() { console.log('Waiting for being added to server…'); client.once('guildCreate', () => startUp()); return; - } else process.exit(1); + } else process.exit(0); } logger.info(localize('main', 'logged-in', {tag: formatDiscordUserName(client.user)})); loadCLIFile('/src/cli.js'); @@ -160,9 +224,14 @@ async function startUp() { client.moduleConf = moduleConf; client.logChannel = await client.channels.fetch(config.logChannelID).catch(() => { }); - if (!client.logChannel || client.logChannel.type !== 'GUILD_TEXT') { + if (!client.logChannel || client.logChannel.type !== ChannelType.GuildText) { logger.warn(localize('main', 'logchannel-wrong-type')); client.logChannel = null; + config.logChannelID = null; + jsonfile.writeFileSync(`${confDir}/config.json`, { + ...jsonfile.readFileSync(`${confDir}/config.json`), + logChannelID: null + }); if (scnxSetup) { const {reportIssue} = require('./src/functions/scnx-integration'); await reportIssue(client, { @@ -175,7 +244,7 @@ async function startUp() { if (client.logChannel) await client.logChannel.send('⚠️ ' + localize('main', 'config-check-failed')); console.log(e); logger.fatal(localize('main', 'config-check-failed')); - process.exit(1); + process.exit(0); }); await loadCommandsInDir('./src/commands'); if (client.scnxSetup) { @@ -247,14 +316,55 @@ async function syncCommandsIfNeeded() { errorData: {inviteURL: `https://discord.com/oauth2/authorize?client_id=${client.user.id}&guild_id=${config.guildID}&disable_guild_select=true&permissions=8&scope=bot%20applications.commands`} }); logger.fatal(localize('main', 'no-command-permissions', {inv: `https://discord.com/oauth2/authorize?client_id=${client.user.id}&guild_id=${config.guildID}&disable_guild_select=true&permissions=8&scope=bot%20applications.commands`})); - process.exit(1); + process.exit(0); } + const oldGuildCommands = await (await client.guilds.fetch(config.guildID)).commands.fetch().catch(handleSyncFailure); const oldGlobalCommands = await client.application.commands.fetch().catch(handleSyncFailure); + + function normalizePermission(permission) { + if (typeof permission === 'string') { + if (permissionMap[permission]) return permissionMap[permission]; + const pascal = permission.toLowerCase().split('_').map(p => p.charAt(0).toUpperCase() + p.slice(1)).join(''); + return PermissionFlagsBits[pascal] || permission; + } + return permission; + } + + function normalizeOption(option) { + const newOption = {...option}; + if (typeof newOption.type === 'string') { + const upper = newOption.type.toUpperCase(); + const pascal = newOption.type.charAt(0).toUpperCase() + newOption.type.slice(1); + newOption.type = optionTypeMap[upper] || ApplicationCommandOptionType[upper] || ApplicationCommandOptionType[pascal] || newOption.type; + } + if (newOption.channelTypes) newOption.channelTypes = newOption.channelTypes.map(t => { + if (typeof t !== 'string') return t; + const upper = t.toUpperCase(); + return channelTypeMap[upper] || ChannelType[upper] || ChannelType[t] || t; + }); + if (newOption.options) newOption.options = newOption.options.map(normalizeOption); + return newOption; + } + + function normalizeCommand(command) { + const newCommand = {...command}; + if (!newCommand.type) newCommand.type = ApplicationCommandType.ChatInput; + else if (typeof newCommand.type === 'string') { + const upper = newCommand.type.toUpperCase(); + const pascal = newCommand.type.charAt(0).toUpperCase() + newCommand.type.slice(1); + newCommand.type = ApplicationCommandType[upper] || ApplicationCommandType[pascal] || newCommand.type; + } + if (newCommand.options) newCommand.options = newCommand.options.map(normalizeOption); + if (newCommand.defaultMemberPermissions) newCommand.defaultMemberPermissions = new PermissionsBitField(newCommand.defaultMemberPermissions.map(normalizePermission)).bitfield.toString(); + return newCommand; + } + const ranCommands = []; // Commands with all functions run for (const orgCmd of enabledCommands) { - const command = {...orgCmd}; + let command = {...orgCmd}; + if (typeof command.options === 'function') command.options = await command.options(client); if (command.options) { const options = []; @@ -264,6 +374,33 @@ async function syncCommandsIfNeeded() { } command.options = options; } + + function fixObjectDescriptionLength(ob) { + if (typeof ob !== 'object') return ob; + const newObject = {}; + for (const key in ob) { + if (Array.isArray(ob[key])) { + const b = []; + for (const o of ob[key]) { + b.push(fixObjectDescriptionLength(o)); + } + newObject[key] = b; + continue; + } + if (key === 'description' && ob[key].length >= 100) { + logger.error(localize('command', 'description-too-long', { + c: command.name, + s: ob[key] + })); + newObject[key] = truncate(ob[key], 100); + } else newObject[key] = ob[key]; + } + return newObject; + } + + command = fixObjectDescriptionLength(command); + command = normalizeCommand(command); + ranCommands.push(command); } @@ -288,17 +425,9 @@ async function syncCommandsIfNeeded() { break; } - if (oldCommand.defaultMemberPermissions) oldCommand.defaultMemberPermissions = oldCommand.defaultMemberPermissions.toArray(); - if ((command.defaultMemberPermissions || []).length !== (oldCommand.defaultMemberPermissions || []).length) { - needSync = true; - break; - } - for (const permission of (command.defaultMemberPermissions || [])) { - if (!(oldCommand.defaultMemberPermissions || []).includes(permission)) { - needSync = true; - break; - } - } + const newPerms = new PermissionsBitField(command.defaultMemberPermissions || []).bitfield; + const oldPerms = new PermissionsBitField(oldCommand.defaultMemberPermissions || []).bitfield; + if (newPerms !== oldPerms) needSync = true; for (const option of (command.options || [])) { const oldOptionOption = (oldCommand.options || []).find(o => o.name === option.name); @@ -336,7 +465,7 @@ async function syncCommandsIfNeeded() { let guildCommands = config.syncCommandGlobally ? [] : ranCommands; const globalCommands = config.syncCommandGlobally ? ranCommands : []; - if (scnxSetup) guildCommands = [...guildCommands, ...await require('./src/functions/scnx-integration').generateCustomSlashCommands(client, guildCommands)]; + if (scnxSetup) guildCommands = [...guildCommands, ...((await require('./src/functions/scnx-integration').generateCustomSlashCommands(client, guildCommands)).map(f => normalizeCommand(f)))]; if (commandsNeedSync(oldGuildCommands, guildCommands)) { await client.application.commands.set(guildCommands, config.guildID).catch(handleSyncFailure); logger.info(localize('main', 'guild-command-sync')); @@ -361,7 +490,7 @@ async function loadModelsInDir(dir, moduleName = null) { await fs.readdir(`${__dirname}/${dir}`, (async (err, files) => { if (err) { logger.fatal(err); - process.exit(1); + process.exit(0); } for await (const file of files) { const model = require(`${__dirname}/${dir}/${file}`); @@ -370,7 +499,10 @@ async function loadModelsInDir(dir, moduleName = null) { if (!models[moduleName]) models[moduleName] = {}; models[moduleName][model.config.name] = model; } else models[model.config.name] = model; - logger.debug(localize('main', 'model-loaded', {d: dir, f: file})); + logger.debug(localize('main', 'model-loaded', { + d: dir, + f: file + })); } resolve(); })); @@ -409,8 +541,8 @@ async function loadEventsInDir(dir, moduleName = null) { try { if (!client.botReadyAt && !eData.eventFunction.ignoreBotReadyCheck) continue; if (!eData.eventFunction.allowPartial && cArgs.filter(f => f && f.partial).length !== 0) continue; - if (!eData.moduleName) return eData.eventFunction.run(client, ...cArgs); - if (client.modules[eData.moduleName].enabled) eData.eventFunction.run(client, ...cArgs); + if (!eData.moduleName) eData.eventFunction.run(client, ...cArgs); + else if (client.modules[eData.moduleName].enabled) eData.eventFunction.run(client, ...cArgs); } catch (e) { if (client.captureException) client.captureException(e, { module: eData.moduleName, @@ -421,10 +553,19 @@ async function loadEventsInDir(dir, moduleName = null) { } }); } - events[eventName].push({eventFunction, moduleName}); - logger.debug(localize('main', 'event-loaded', {d: dir, f: f})); + events[eventName].push({ + eventFunction, + moduleName + }); + logger.debug(localize('main', 'event-loaded', { + d: dir, + f: f + })); } else { - logger.debug(localize('main', 'event-dir', {d: dir, f: f})); + logger.debug(localize('main', 'event-dir', { + d: dir, + f: f + })); await loadEventsInDir(`${dir}/${f}/`); } }); @@ -446,7 +587,10 @@ function loadCLIFile(path, moduleName = null) { command.module = moduleName; cliCommands.push(command); command.command = command.command.toLowerCase(); - logger.debug(localize('main', 'loaded-cli', {c: command.command, p: path})); + logger.debug(localize('main', 'loaded-cli', { + c: command.command, + p: path + })); } } @@ -466,6 +610,7 @@ async function loadCommandsInDir(dir, moduleName = null) { const props = require(`${__dirname}/${dir}/${f}`); commands.push({ name: props.config.name, + forceAnonymous: props.config.forceAnonymous, description: props.config.description, restricted: props.config.restricted, defaultMemberPermissions: props.config.defaultMemberPermissions || null, diff --git a/modules/admin-tools/commands/admin.js b/modules/admin-tools/commands/admin.js index ae138fd4..a0d6ecda 100644 --- a/modules/admin-tools/commands/admin.js +++ b/modules/admin-tools/commands/admin.js @@ -1,3 +1,4 @@ +const {ChannelType} = require('discord.js'); const {localize} = require('../../../src/functions/localize'); module.exports.subcommands = { @@ -27,12 +28,12 @@ module.exports.subcommands = { }, 'setcategory': async function (interaction) { const channel = interaction.options.getChannel('channel', true); - if (channel.type === 'GUILD_CATEGORY') return interaction.reply({ + if (channel.type === ChannelType.GuildCategory) return interaction.reply({ content: '⚠️ ' + localize('admin-tools', 'category-can-not-have-category'), ephemeral: true }); const category = interaction.options.getChannel('category', true); - if (category.type !== 'GUILD_CATEGORY') return interaction.reply({ + if (category.type !== ChannelType.GuildCategory) return interaction.reply({ content: '⚠️ ' + localize('admin-tools', 'not-category'), ephemeral: true }); @@ -96,11 +97,12 @@ module.exports.config = { type: 'CHANNEL', required: true, name: 'channel', - channelTypes: ['GUILD_TEXT', 'GUILD_VOICE', 'GUILD_NEWS', 'GUILD_STAGE_VOICE'], + channelTypes: [ChannelType.GuildText, ChannelType.GuildVoice, ChannelType.GuildAnnouncement, ChannelType.GuildStageVoice], description: localize('admin-tools', 'channel-description') }, { type: 'CHANNEL', + channel_types: [ChannelType.GuildCategory], required: true, name: 'category', description: localize('admin-tools', 'category-description') diff --git a/modules/admin-tools/commands/roles.js b/modules/admin-tools/commands/roles.js new file mode 100644 index 00000000..f0317d7e --- /dev/null +++ b/modules/admin-tools/commands/roles.js @@ -0,0 +1,190 @@ +const {localize} = require('../../../src/functions/localize'); +const durationParser = require('parse-duration'); +const {createTemporaryRoleAction, createTemporaryRoleChangeAction} = require('../temporaryRoles'); +const {client} = require('../../../main'); +const {formatDate} = require('../../../src/functions/helpers'); + +module.exports.beforeSubcommand = async function (interaction) { + const member = await interaction.guild.members.fetch(interaction.options.getUser('user', true).id).catch(() => { + }); + if (!member) return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('admin-tools', 'user-not-found') + }); + const role = interaction.options.getRole('role'); + if (role) { + if (role.position >= interaction.guild.me.roles.highest.position) return interaction.reply({ + ephemeral: true, + allowedMentions: {parse: []}, + content: '⚠️ ' + localize('admin-tools', 'role-not-high-enough', {e: role.toString()}) + }); + if (interaction.guild.ownerId !== interaction.user.id && role.position >= interaction.member.roles.highest.position) return interaction.reply({ + ephemeral: true, + allowedMentions: {parse: []}, + content: '⚠️ ' + localize('admin-tools', 'users-trying-to-manage-higher-role', { + t: interaction.member.roles.highest.toString(), + e: role.toString() + }) + }); + if (interaction.options.getString('duration')) { + interaction.duration = durationParser(interaction.options.getString('duration')); + if (interaction.duration === 0 || !interaction.duration || interaction.duration < 20000) return interaction.reply({ + content: '⚠️ ' + localize('admin-tools', 'duration-wrong'), + ephemeral: true + }); + interaction.removeDate = new Date(new Date().getTime() + interaction.duration); + } + } + await interaction.deferReply({ephemeral: true}); +}; + +module.exports.subcommands = { + give: async function (interaction) { + if (interaction.replied) return; + const member = interaction.options.getMember('user'); + member.roles.add(interaction.options.getRole('role'), localize('admin-tools', `audit-log-add${interaction.removeDate ? '-duration' : ''}`, { + u: interaction.user.username, + t: interaction.removeDate?.toLocaleString(interaction.client.locale.split('_')[0]) + })).then(() => { + if (interaction.removeDate) createTemporaryRoleChangeAction(client, 'remove', interaction.removeDate, interaction.options.getRole('role').id, interaction.options.getUser('user').id); + interaction.editReply({ + allowedMentions: {parse: []}, + content: '✅ ' + localize('admin-tools', `role-add${interaction.removeDate ? '-duration' : ''}`, { + u: member.toString(), + t: interaction.removeDate ? formatDate(interaction.removeDate) : '', + r: interaction.options.getRole('role').toString() + }) + }); + }).catch(e => { + interaction.editReply({ + allowedMentions: {parse: []}, + content: '⚠️ ' + localize('admin-tools', 'unable-to-change-roles', { + r: interaction.options.getRole('role').toString(), + u: member.toString(), + e: e.toString() + }) + }); + }); + }, + remove: async function (interaction) { + if (interaction.replied) return; + const member = interaction.options.getMember('user'); + member.roles.remove(interaction.options.getRole('role'), localize('admin-tools', `audit-log-remove${interaction.removeDate ? '-duration' : ''}`, { + u: interaction.user.username, + t: interaction.removeDate?.toLocaleString(interaction.client.locale.split('_')[0]) + })).then(() => { + if (interaction.removeDate) createTemporaryRoleChangeAction(client, 'add', interaction.removeDate, interaction.options.getRole('role').id, interaction.options.getUser('user').id); + interaction.editReply({ + allowedMentions: {parse: []}, + content: '✅ ' + localize('admin-tools', `role-remove${interaction.removeDate ? '-duration' : ''}`, { + u: member.toString(), + t: interaction.removeDate ? formatDate(interaction.removeDate) : '', + r: interaction.options.getRole('role').toString() + }) + }); + }).catch(e => { + interaction.editReply({ + allowedMentions: {parse: []}, + content: '⚠️ ' + localize('admin-tools', 'unable-to-change-roles', { + r: interaction.options.getRole('role').toString(), + u: member.toString(), + e: e.toString() + }) + }); + }); + }, + status: async function (interaction) { + if (interaction.replied) return; + const roles = await client.models['admin-tools']['TemporaryRoleChange'].findAll({ + where: { + userID: interaction.options.getMember('user').id + } + }); + if (roles.length === 0) return interaction.editReply({ + allowedMentions: {parse: []}, + content: '⚠️ ' + localize('admin-tools', 'user-without-temporary-action', {u: interaction.options.getMember('user').toString()}) + }); + let answerString = ''; + for (const role of roles) { + answerString = answerString + '\n* ' + localize('admin-tools', `status-${role.type}`, { + r: `<@&${role.roleID}>`, + t: formatDate(new Date(parseInt(role.changeDate))) + }); + } + interaction.editReply({ + allowedMentions: {parse: []}, + content: `## ${localize('admin-tools', 'user-temporary-action-header', {u: interaction.options.getMember('user').toString()})}\n\n${answerString}` + }); + } +}; + +module.exports.config = { + name: 'roles', + description: localize('admin-tools', 'command-description'), + defaultMemberPermissions: ['ADMINISTRATOR'], + options: [ + { + type: 'SUB_COMMAND', + name: 'give', + description: localize('admin-tools', 'role-give-description'), + options: [ + { + type: 'USER', + required: true, + name: 'user', + description: localize('admin-tools', 'role-user-add-description') + }, + { + type: 'ROLE', + required: true, + name: 'role', + description: localize('admin-tools', 'role-add-role-description') + }, + { + type: 'STRING', + name: 'duration', + required: false, + description: localize('admin-tools', 'role-add-duration-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'remove', + description: localize('admin-tools', 'role-remove-description'), + options: [ + { + type: 'USER', + required: true, + name: 'user', + description: localize('admin-tools', 'role-user-remove-description') + }, + { + type: 'ROLE', + required: true, + name: 'role', + description: localize('admin-tools', 'role-remove-role-description') + }, + { + type: 'STRING', + name: 'duration', + required: false, + description: localize('admin-tools', 'role-remove-duration-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'status', + description: localize('admin-tools', 'role-status-description'), + options: [ + { + type: 'USER', + required: true, + name: 'user', + description: localize('admin-tools', 'role-user-status-description') + } + ] + } + ] +}; \ No newline at end of file diff --git a/modules/admin-tools/commands/stealemote.js b/modules/admin-tools/commands/stealemote.js index 2211c4fd..47130c35 100644 --- a/modules/admin-tools/commands/stealemote.js +++ b/modules/admin-tools/commands/stealemote.js @@ -9,7 +9,11 @@ module.exports.run = async function (interaction) { content: '⚠️ ' + localize('admin-tools', 'emoji-too-much-data'), ephemeral: true }); - emote = await interaction.guild.emojis.create(`https://cdn.discordapp.com/emojis/${emote[2]}`, emote[1], {reason: `Emoji imported by ${formatDiscordUserName(interaction.user)}`}); + emote = await interaction.guild.emojis.create({ + attachment: `https://cdn.discordapp.com/emojis/${emote[2]}`, + name: emote[1], + reason: `Emoji imported by ${formatDiscordUserName(interaction.user)}` + }); await interaction.reply({ content: localize('admin-tools', 'emoji-import', {e: emote.toString()}), ephemeral: true diff --git a/modules/admin-tools/config.json b/modules/admin-tools/config.json index 9be995c6..c34fdcf6 100644 --- a/modules/admin-tools/config.json +++ b/modules/admin-tools/config.json @@ -1,5 +1,8 @@ { - "description": {}, + "description": { + "en": "Configure the behaviour of the module here", + "de": "Stelle hier die Funktionen des Modules ein" + }, "humanName": { "en": "Configuration", "de": "Konfiguration" @@ -8,8 +11,9 @@ "commandsWarnings": { "normal": [ "/admin", - "/stealemote" + "/stealemote", + "/roles" ] }, "content": [] -} +} \ No newline at end of file diff --git a/modules/admin-tools/events/botReady.js b/modules/admin-tools/events/botReady.js new file mode 100644 index 00000000..aa148028 --- /dev/null +++ b/modules/admin-tools/events/botReady.js @@ -0,0 +1,6 @@ +const {scheduleAllTemporaryRoleJobs} = require('../temporaryRoles'); + +module.exports.run = async function (client) { + scheduleAllTemporaryRoleJobs(client).then(() => { + }); +}; \ No newline at end of file diff --git a/modules/admin-tools/models/TemporaryRoleChange.js b/modules/admin-tools/models/TemporaryRoleChange.js new file mode 100644 index 00000000..9e11c49a --- /dev/null +++ b/modules/admin-tools/models/TemporaryRoleChange.js @@ -0,0 +1,26 @@ +const {DataTypes, Model} = require('sequelize'); + +module.exports = class AdminToolsTemporaryRoleChange extends Model { + static init(sequelize) { + return super.init({ + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + userID: DataTypes.STRING, + roleID: DataTypes.STRING, + type: DataTypes.STRING, + changeDate: DataTypes.STRING + }, { + tableName: 'admin_tools-TemporaryRoleChange', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'TemporaryRoleChange', + 'module': 'admin-tools' +}; \ No newline at end of file diff --git a/modules/admin-tools/module.json b/modules/admin-tools/module.json index c2e2f30d..65542085 100644 --- a/modules/admin-tools/module.json +++ b/modules/admin-tools/module.json @@ -7,6 +7,8 @@ }, "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/admin-tools", "commands-dir": "/commands", + "models-dir": "/models", + "events-dir": "/events", "config-example-files": [ "config.json" ], @@ -17,7 +19,7 @@ "en": "Admin-Tools" }, "description": { - "en": "Simple tools for admins - move channels and roles via commands or copy an emoji from another server to your server.", - "de": "Einfache Tools für Admins, um Channel und Rollen per Command zu verschieben und Emojis zu leihen." + "en": "Simple tools for admins - move channels and roles via commands, assign temporary roles or copy an emoji from another server to your server.", + "de": "Einfache Tools für Admins, um Channel und Rollen per Command zu verschieben, temporäre Rollen zu vergeben und Emojis zu leihen." } } \ No newline at end of file diff --git a/modules/admin-tools/temporaryRoles.js b/modules/admin-tools/temporaryRoles.js new file mode 100644 index 00000000..04ee8b65 --- /dev/null +++ b/modules/admin-tools/temporaryRoles.js @@ -0,0 +1,52 @@ +const {scheduleJob} = require('node-schedule'); +const {localize} = require('../../src/functions/localize'); +const jobCache = new Map(); + +module.exports.scheduleAllTemporaryRoleJobs = async function (client) { + jobCache.clear(); + const temporaryRoleActions = await client.models['admin-tools']['TemporaryRoleChange'].findAll(); + for (const role of temporaryRoleActions) planTemporaryRoleChangeAction(client, role); +}; + +module.exports.createTemporaryRoleChangeAction = async function (client, type, changeDate, roleID, userID) { + const duplicate = await client.models['admin-tools']['TemporaryRoleChange'].findOne({ + where: { + userID, + roleID + } + }); + if (duplicate) { + duplicate.destroy(); + if (jobCache.has(duplicate.id)) jobCache.get(duplicate.id).cancel(); + } + const res = await client.models['admin-tools']['TemporaryRoleChange'].create({ + userID, + roleID, + changeDate: changeDate.getTime(), + type + }); + planTemporaryRoleChangeAction(client, res); +}; + +function planTemporaryRoleChangeAction(client, changeItem) { + const job = scheduleJob(new Date(parseInt(changeItem.changeDate)), async () => { + doChange().then(() => { + }); + }); + + async function doChange() { + await changeItem.destroy(); + const member = await client.guild.members.fetch(changeItem.userID).catch(() => { + }); + if (!member) return; + await member.roles[changeItem.type](changeItem.roleID, localize('admin-tools', `audit-log-temporary-${changeItem.type}`)); + } + + if (!job) { + doChange().then(() => { + }); + return; + } + jobCache.set(changeItem.id, job); + client.jobs.push(job); +} \ No newline at end of file diff --git a/modules/afk-system/config.json b/modules/afk-system/config.json index 8245760a..d8447340 100644 --- a/modules/afk-system/config.json +++ b/modules/afk-system/config.json @@ -1,5 +1,8 @@ { - "description": {}, + "description": { + "en": "Configure the behaviour of the module here", + "de": "Stelle hier die Funktionen des Modules ein" + }, "humanName": { "en": "Configuration", "de": "Konfiguration" @@ -34,8 +37,8 @@ "de": "✅ Dein Status wurde auf \"AFK\" aktualisiert. Wenn dich ein anderer Nutzer erwähnt, während du AFK bist, werden wir ihn über deinen Status informieren." }, "description": { - "de": "This message gets send if a user started their session successfully.", - "en": "Diese Nachricht wird Nutzern angezeigt, wenn sie ihren Status auf AFK wechseln." + "en": "This message gets send if a user started their session successfully.", + "de": "Diese Nachricht wird Nutzern angezeigt, wenn sie ihren Status auf AFK wechseln." }, "type": "string", "allowEmbed": true diff --git a/modules/auto-delete/channels.json b/modules/auto-delete/channels.json index 2bbc3530..14888788 100644 --- a/modules/auto-delete/channels.json +++ b/modules/auto-delete/channels.json @@ -44,21 +44,6 @@ }, "type": "integer" }, - { - "name": "purgeOnStart", - "humanName": { - "en": "Purge On Start", - "de": "Kanal leeren beim Bot Start" - }, - "default": { - "en": false - }, - "description": { - "en": "If enabled every (excluding pinned) message (max 100) in this channel gets deleted when the bot starts.", - "de": "Wenn aktiviert, werden alle (außer angepinnte) Nachrichten (max 100) aus dem gewälten Kanal, beim Start des Bots, gelöscht." - }, - "type": "boolean" - }, { "name": "keepMessageCount", "default": { diff --git a/modules/auto-delete/events/botReady.js b/modules/auto-delete/events/botReady.js index 9f9a4d5c..5a3a11b8 100644 --- a/modules/auto-delete/events/botReady.js +++ b/modules/auto-delete/events/botReady.js @@ -12,8 +12,6 @@ module.exports.run = async function (client) { }); for (const channel of client.modules['auto-delete'].uniqueChannels) { - if (!channel.purgeOnStart) continue; - const dcChannel = await client.channels.fetch(channel.channelID).catch(() => { }); if (!dcChannel) return client.logger.error(`[auto-delete] ${localize('auto-delete', 'could-not-fetch-channel', {c: channel.channelID})}`); @@ -35,8 +33,6 @@ module.exports.run = async function (client) { } for (const voiceChannel of uniqueConfigVoiceChannels) { - if (!voiceChannel.purgeOnStart) continue; - const dcVoiceChannel = await client.channels.fetch(voiceChannel.channelID).catch(() => { }); if (!dcVoiceChannel) return client.logger.error(`[auto-delete] ${localize('auto-delete', 'could-not-fetch-channel', {c: voiceChannel.channelID})}`); diff --git a/modules/auto-delete/events/voiceStateUpdate.js b/modules/auto-delete/events/voiceStateUpdate.js index c4616057..4c1c24b2 100644 --- a/modules/auto-delete/events/voiceStateUpdate.js +++ b/modules/auto-delete/events/voiceStateUpdate.js @@ -1,3 +1,4 @@ +const {ChannelType} = require('discord.js'); const {localize} = require('../../../src/functions/localize'); module.exports.run = async function (client, oldState) { if (!client.botReadyAt) return; @@ -12,7 +13,7 @@ module.exports.run = async function (client, oldState) { if (!channel) { return client.logger.error(`[auto-delete] ${localize('auto-delete', 'could-not-fetch-channel', {c: channelConfigEntry.channelID})}`); } - if (channel.type !== 'GUILD_VOICE') return; + if (channel.type !== ChannelType.GuildVoice) return; if (channel.members.size > 0) return; const channelMessages = await channel.messages.fetch().catch(() => { @@ -26,4 +27,4 @@ module.exports.run = async function (client, oldState) { channel.bulkDelete(channelMessages, true).catch(() => { }); }, parseInt(channelConfigEntry.timeout) * 1000 * 60); -}; \ No newline at end of file +}; diff --git a/modules/auto-delete/voice-channels.json b/modules/auto-delete/voice-channels.json index 1d116f33..688205a2 100644 --- a/modules/auto-delete/voice-channels.json +++ b/modules/auto-delete/voice-channels.json @@ -42,21 +42,6 @@ "de": "Timeout (in Minuten), nachdem die Nachrichten gelöscht werden, wenn das letzte Mitglied den Sprachkanal verlassen hat. Wenn du eine '0' verwendest, werden die Nachrichten sofort gelöscht." }, "type": "integer" - }, - { - "name": "purgeOnStart", - "humanName": { - "en": "Purge On Start", - "de": "Kanal leeren beim Bot Start" - }, - "default": { - "en": false - }, - "description": { - "en": "If enabled every message (max 100) in this channel gets deleted when the bot starts and no members are left in the channel", - "de": "Wenn aktiviert, werden alle Nachrichten (max 100) aus dem gewälten Sprachkanal gelöscht (beim Start des Bots), sofern keine Mitglieder in dem Sprachkanal sind." - }, - "type": "boolean" } ] } \ No newline at end of file diff --git a/modules/auto-messager/cronjob.json b/modules/auto-messager/cronjob.json index 6a9c020c..18e9373b 100644 --- a/modules/auto-messager/cronjob.json +++ b/modules/auto-messager/cronjob.json @@ -6,7 +6,9 @@ "elementLimits": { "STARTER": 2, "ACTIVE_GUILD": 5, - "PRO": 15 + "PRO": 15, + "UNLIMITED": 5, + "PROFESSIONAL": 15 }, "humanName": { "en": "Cronjob (advanced)", diff --git a/modules/auto-messager/daily.json b/modules/auto-messager/daily.json index e07a440d..4e32ae27 100644 --- a/modules/auto-messager/daily.json +++ b/modules/auto-messager/daily.json @@ -6,7 +6,9 @@ "elementLimits": { "STARTER": 2, "ACTIVE_GUILD": 5, - "PRO": 15 + "PRO": 15, + "UNLIMITED": 5, + "PROFESSIONAL": 15 }, "configElementName": { "de": { diff --git a/modules/auto-messager/hourly.json b/modules/auto-messager/hourly.json index 3237ce96..9b9c2882 100644 --- a/modules/auto-messager/hourly.json +++ b/modules/auto-messager/hourly.json @@ -10,7 +10,9 @@ "elementLimits": { "STARTER": 1, "ACTIVE_GUILD": 4, - "PRO": 14 + "PRO": 14, + "UNLIMITED": 4, + "PROFESSIONAL": 14 }, "configElementName": { "de": { diff --git a/modules/auto-publisher/config.json b/modules/auto-publisher/config.json index 2535e581..f067cfde 100644 --- a/modules/auto-publisher/config.json +++ b/modules/auto-publisher/config.json @@ -1,5 +1,8 @@ { - "description": {}, + "description": { + "en": "Configure the behaviour of the module here", + "de": "Stelle hier die Funktionen des Modules ein" + }, "humanName": { "en": "Configuration", "de": "Konfiguration" @@ -72,4 +75,4 @@ "type": "boolean" } ] -} +} \ No newline at end of file diff --git a/modules/auto-publisher/events/messageCreate.js b/modules/auto-publisher/events/messageCreate.js index 8c10f69b..77f1a4fe 100644 --- a/modules/auto-publisher/events/messageCreate.js +++ b/modules/auto-publisher/events/messageCreate.js @@ -1,9 +1,11 @@ +const {ChannelType} = require('discord.js'); + module.exports.run = async (client, msg) => { if (!msg.guild) return; if (!client.botReadyAt) return; if (msg.guild.id !== client.guildID) return; if (msg.content.startsWith(client.config.prefix)) return; - if (msg.channel.type === 'GUILD_NEWS') { + if (msg.channel.type === ChannelType.GuildAnnouncement) { const config = client.configurations['auto-publisher']['config']; if (config.ignoreBots && msg.author.bot) return; if (!config.blacklist) config.blacklist = []; @@ -18,4 +20,4 @@ module.exports.run = async (client, msg) => { }, 2500); }); } -}; \ No newline at end of file +}; diff --git a/modules/auto-react/configs/config.json b/modules/auto-react/configs/config.json deleted file mode 100644 index 6e6dad18..00000000 --- a/modules/auto-react/configs/config.json +++ /dev/null @@ -1,101 +0,0 @@ -{ - "description": {}, - "humanName": { - "en": "Configuration", - "de": "Konfiguration" - }, - "filename": "config.json", - "content": [ - { - "name": "channels", - "humanName": { - "en": "Channels", - "de": "Kanäle" - }, - "default": { - "en": {} - }, - "description": { - "en": "Here you can add channels and the reactions in it (you can add multiple emojis with | between each one)", - "de": "Du kannst hier Kanal-IDs und die dazugehörigen Emojis eintragen (mehrere Emojis müssen mit einem | getrennt werden)" - }, - "type": "keyed", - "content": { - "key": "userID", - "value": "string" - } - }, - { - "name": "members", - "humanName": { - "en": "Mentions", - "de": "Erwähnungen" - }, - "default": { - "en": {} - }, - "description": { - "en": "Here you can add members and the reactions on their mentions of them (you can add multiple emojis with | between each one)", - "de": "Du kannst hier NutzerIDs und die dazugehörigen Emojis auf Erwähnungen dieser eintragen (mehrere Emojis müssen mit einem | getrennt werden" - }, - "type": "keyed", - "content": { - "key": "userID", - "value": "string" - } - }, - { - "name": "authors", - "humanName": { - "en": "Authors", - "de": "Autoren" - }, - "default": { - "en": {} - }, - "description": { - "en": "Here you can add members and the reactions on their messages in it (you can add multiple emojis with | between each one)", - "de": "Du kannst hier NutzerIDs und die dazugehörigen Emojis auf deren Nachrichten eintragen (mehrere Emojis müssen mit einem | getrennt werden" - }, - "type": "keyed", - "content": { - "key": "userID", - "value": "string" - } - }, - { - "name": "categories", - "humanName": { - "en": "Categories", - "de": "Kategorien" - }, - "default": { - "en": {} - }, - "description": { - "en": "Here you can add categories and the reactions in it (you can add multiple emojis with | between each one)", - "de": "Du kannst hier Kategorien und die dazugehörigen Emojis eintragen (mehrere Emojis müssen mit einem | getrennt werden)" - }, - "type": "keyed", - "content": { - "key": "channelID", - "value": "string" - } - }, - { - "name": "forcedMentionMatching", - "default": { - "en": true - }, - "type": "boolean", - "humanName": { - "en": "Only react to @mentions?", - "de": "Nur auf @Erwähnungen reagieren?" - }, - "description": { - "en": "If disabled, the bot will also react to mentions in inline-replies or otherwise in addition to conventional @mentions.", - "de": "Wenn deaktiviert, wird der Bot auch auf Erwähnungen in Inline-Antworten oder anderweitigen Erwähnungen, zusätzlich zu den gewöhnlichen @Erwähnungen, reagieren." - } - } - ] -} \ No newline at end of file diff --git a/modules/auto-react/configs/replies.json b/modules/auto-react/configs/replies.json deleted file mode 100644 index 713075cc..00000000 --- a/modules/auto-react/configs/replies.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "description": {}, - "humanName": { - "en": "Replies", - "de": "Antworten" - }, - "filename": "replies.json", - "configElements": true, - "content": [ - { - "name": "members", - "humanName": { - "en": "User", - "de": "Nutzer" - }, - "default": { - "en": "" - }, - "description": { - "en": "Here you can add a member to be replied on mentions of them", - "de": "Du kannst hier einen Nutzer für Antworten auf Erwähnungen dessen eintragen" - }, - "type": "string" - }, - { - "name": "reply", - "humanName": { - "en": "Reply", - "de": "Antwort" - }, - "default": { - "en": "" - }, - "description": { - "en": "Here you can add the reply message", - "de": "Du kannst hier die Antwort eintragen" - }, - "type": "string", - "allowEmbed": true - } - ] -} \ No newline at end of file diff --git a/modules/auto-react/events/messageCreate.js b/modules/auto-react/events/messageCreate.js deleted file mode 100644 index 25dc6702..00000000 --- a/modules/auto-react/events/messageCreate.js +++ /dev/null @@ -1,94 +0,0 @@ -const {embedType} = require('../../../src/functions/helpers'); -module.exports.run = async (client, msg) => { - if (!client.botReadyAt) return; - if (msg.interaction || msg.system || !msg.author || !msg.guild || msg.guild.id !== client.config.guildID) return; - await checkChannel(msg); - await checkMembers(msg); - await checkCategory(msg); - await checkAuthor(msg); - await checkMembersReply(msg); -}; - -/** - * Checks for member pings on a message and reacts with the configured emotes - * @private - * @param msg [Message](https://discord.js.org/#/docs/main/stable/class/Message) - * @returns {Promise} - */ -async function checkMembers(msg) { - const moduleConfig = msg.client.configurations['auto-react']['config']; - if (!msg.mentions.members) return; - for (const m of msg.mentions.members.values()) { - if (!msg.content.replaceAll('!', '').includes(`<@${m.id}>`) && moduleConfig.forcedMentionMatching) continue; - if (moduleConfig.members[m.id]) moduleConfig.members[m.id].split('|').forEach(emoji => { - msg.react(emoji).catch(() => { - }); - }); - } -} - -/** - * Checks if a message need reactions (and reacts if needed) because it was send in a configured channel - * @private - * @param msg [Message](https://discord.js.org/#/docs/main/stable/class/Message) - * @returns {Promise} - */ -async function checkChannel(msg) { - const moduleConfig = msg.client.configurations['auto-react']['config']; - if (!moduleConfig.channels[msg.channel.id]) return; - moduleConfig.channels[msg.channel.id].split('|').forEach(emoji => { - msg.react(emoji).catch(() => { - }); - }); -} - -/** - * Checks if a message need reactions (and reacts if needed) because it was send in a configured category - * @private - * @param msg [Message](https://discord.js.org/#/docs/main/stable/class/Message) - * @returns {Promise} - */ -async function checkCategory(msg) { - const moduleConfig = msg.client.configurations['auto-react']['config']; - if (!moduleConfig.categories[msg.channel.parentId]) return; - moduleConfig.categories[msg.channel.parentId].split('|').forEach(emoji => { - msg.react(emoji).catch(() => { - }); - }); -} - -/** - * Checks for member pings in a message and replys with the configured message - * @private - * @param msg - * @returns {Promise} - */ -async function checkMembersReply(msg) { - const moduleConfig = msg.client.configurations['auto-react']['replies']; - if (!msg.mentions.users) return; - if (msg.author.id === msg.client.user.id) return; - for (const m of msg.mentions.users.values()) { - if (!msg.content.replaceAll('!', '').includes(`<@${m.id}>`) && msg.client.configurations['auto-react']['config'].forcedMentionMatching) continue; - const matches = moduleConfig.filter(c => c.members === m.id); - for (const element of matches) { - await msg.reply(embedType(element.reply, {}, {ephemeral: true})).catch(() => { - }); - } - } -} - - -/** - * Checks if a message need reactions (and reacts if needed) because it was send in a configured channel - * @private - * @param msg [Message](https://discord.js.org/#/docs/main/stable/class/Message) - * @returns {Promise} - */ -async function checkAuthor(msg) { - const moduleConfig = msg.client.configurations['auto-react']['config']; - if (!moduleConfig.authors[msg.author.id]) return; - moduleConfig.authors[msg.author.id].split('|').forEach(emoji => { - msg.react(emoji).catch(() => { - }); - }); -} \ No newline at end of file diff --git a/modules/auto-react/module.json b/modules/auto-react/module.json deleted file mode 100644 index c97ca445..00000000 --- a/modules/auto-react/module.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "auto-react", - "author": { - "scnxOrgID": "1", - "name": "SCDerox (SC Network Team)", - "link": "https://github.com/SCDerox" - }, - "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/auto-react", - "events-dir": "/events", - "config-example-files": [ - "configs/config.json", - "configs/replies.json" - ], - "tags": [ - "fun" - ], - "humanReadableName": { - "en": "Automatic reactions", - "de": "Automatisches Reagieren" - }, - "description": { - "en": "Automatically reacts with selected emojis in selected channels or if a user gets pinged", - "de": "Reagiert automatisch mit ausgewählten Emojs in einem ausgewählten Channel und bei Pings" - } -} \ No newline at end of file diff --git a/modules/betterstatus/config.json b/modules/betterstatus/config.json index 65919e83..95e9da39 100644 --- a/modules/betterstatus/config.json +++ b/modules/betterstatus/config.json @@ -5,16 +5,6 @@ "de": "Konfiguration" }, "filename": "config.json", - "categories": [ - { - "name": "interval", - "humanname-de": "Intervalle", - "humanname-en": "Intervall", - "description-de": "Intervalle erlauben es dir, den Status des Bots alle paar Sekunden automatisch zu ändern!", - "description-en": "You can use intervalls to automatically change the Status of the bot", - "categoryToggle": "enableInterval" - } - ], "content": [ { "name": "enableInterval", @@ -148,6 +138,7 @@ "en": "The interval in seconds (at least 10 seconds)", "de": "Das Intervall der Statusänderungen in Sekunden (mindestens 10 Sekunden)" }, + "minValue": 10, "type": "integer" }, { @@ -208,7 +199,7 @@ "name": "streamingLink", "type": "string", "humanName": { - "en": "Steaming-Link", + "en": "Streaming Link", "de": "Stream-Link" }, "default": { diff --git a/modules/betterstatus/events/botReady.js b/modules/betterstatus/events/botReady.js index b73caff1..2dab5e48 100644 --- a/modules/betterstatus/events/botReady.js +++ b/modules/betterstatus/events/botReady.js @@ -1,4 +1,15 @@ const {formatDiscordUserName} = require('../../../src/functions/helpers'); +const {ActivityType} = require('discord.js'); + +const activityTypes = { + 'PLAYING': ActivityType.Playing, + 'STREAMING': ActivityType.Streaming, + 'WATCHING': ActivityType.Watching, + 'COMPETING': ActivityType.Competing, + 'LISTENING': ActivityType.Listening, + 'CUSTOM': ActivityType.Custom +}; + module.exports.run = async function (client) { const moduleConf = client.configurations['betterstatus']['config']; @@ -10,7 +21,7 @@ module.exports.run = async function (client) { const interval = setInterval(async () => { await client.user.setActivity(await replaceStatusString(moduleConf['intervalStatuses'][moduleConf['intervalStatuses'].length * Math.random() | 0]), { - type: moduleConf['activityType'], + type: activityTypes[moduleConf['activityType']], url: (moduleConf['streamingLink'] && moduleConf.activityType === 'STREAMING') ? moduleConf['streamingLink'] : null }); }, moduleConf.interval < 5 ? 5000 : moduleConf.interval * 1000); // At least 5 seconds to prevent rate limiting @@ -23,7 +34,7 @@ module.exports.run = async function (client) { if (moduleConf.activityType !== 'PLAYING' && !moduleConf.enableInterval) { await client.user.setActivity(client.config.user_presence, { - type: moduleConf.activityType, + type: activityTypes[moduleConf['activityType']], url: (moduleConf['streamingLink'] && moduleConf.activityType === 'STREAMING') ? moduleConf['streamingLink'] : null }); } @@ -36,8 +47,8 @@ module.exports.run = async function (client) { */ async function replaceStatusString(statusString) { if (!statusString) return 'Invalid status'; - const members = await (await client.guild.fetch()).members.fetch({withPresences: true, force: true}); - const randomOnline = members.filter(m => m.presence && !m.user.bot).random(); + const members = client.guild.members.cache; + const randomOnline = members.filter(m => ['online', 'dnd'].includes(m.presence?.status) && !m.user.bot).random(); const random = members.filter(m => !m.user.bot).random(); return statusString.replaceAll('%memberCount%', client.guild.memberCount) .replaceAll('%onlineMemberCount%', members.filter(m => m.presence && !m.user.bot).size) diff --git a/modules/betterstatus/events/guildMemberAdd.js b/modules/betterstatus/events/guildMemberAdd.js index d48806e1..3a2d5c96 100644 --- a/modules/betterstatus/events/guildMemberAdd.js +++ b/modules/betterstatus/events/guildMemberAdd.js @@ -1,5 +1,16 @@ const {formatDiscordUserName} = require('../../../src/functions/helpers'); -exports.run = async (client, member) => { +const {ActivityType} = require('discord.js'); + +const activityTypes = { + 'PLAYING': ActivityType.Playing, + 'STREAMING': ActivityType.Streaming, + 'WATCHING': ActivityType.Watching, + 'COMPETING': ActivityType.Competing, + 'LISTENING': ActivityType.Listening, + 'CUSTOM': ActivityType.Custom +}; + +module.exports.run = async (client, member) => { const moduleConf = client.configurations['betterstatus']['config']; /** @@ -16,7 +27,7 @@ exports.run = async (client, member) => { if (moduleConf['changeOnUserJoin']) { await client.user.setActivity(replaceMemberJoinStatusString(moduleConf['userJoinStatus']), { - type: moduleConf['activityType'] + type: activityTypes[moduleConf['activityType']] }); } }; \ No newline at end of file diff --git a/modules/birthday/birthday.js b/modules/birthday/birthday.js index 01d454c6..3cf18822 100644 --- a/modules/birthday/birthday.js +++ b/modules/birthday/birthday.js @@ -3,7 +3,14 @@ * @module Birthdays * @author Simon Csaba */ -const {embedType, disableModule, truncate, embedTypeV2, formatDiscordUserName} = require('../../src/functions/helpers'); +const { + embedType, + disableModule, + truncate, + embedTypeV2, + formatDiscordUserName, + parseEmbedColor +} = require('../../src/functions/helpers'); const {MessageEmbed} = require('discord.js'); const {AgeFromDate} = require('age-calculator'); const {localize} = require('../../src/functions/localize'); @@ -25,10 +32,9 @@ generateBirthdayEmbed = async function (client, notifyUsers = false) { return; } const messages = (await channel.messages.fetch()).filter(msg => msg.author.id === client.user.id); - await channel.guild.members.fetch({force: true}); if (notifyUsers && !moduleConf.notificationChannelOverwriteID) { - for (const m of messages.filter(msg => msg.id !== messages.last().id)) { + for (const m of messages.filter(msg => msg.id !== messages.last().id).values()) { if (m.deletable) await m.delete(); // Removing old messages } } @@ -37,7 +43,7 @@ generateBirthdayEmbed = async function (client, notifyUsers = false) { new MessageEmbed() .setTitle(moduleConf['birthdayEmbed']['title']) .setDescription(moduleConf['birthdayEmbed']['description']) - .setColor(moduleConf['birthdayEmbed']['color']) + .setColor(parseEmbedColor(moduleConf['birthdayEmbed']['color'])) .setAuthor({name: client.user.username, iconURL: client.user.avatarURL()}) .setFooter({text: client.strings.footer, iconURL: client.strings.footerImgUrl}) .addFields([ @@ -126,7 +132,7 @@ generateBirthdayEmbed = async function (client, notifyUsers = false) { if (!birthdayUsers) return; if (moduleConf['birthday_role']) { - const guildMembers = await channel.guild.members.fetch(); + const guildMembers = client.guild.members.cache; for (const member of guildMembers.values()) { if (!member) return; if (member.roles.cache.has(moduleConf['birthday_role'])) { @@ -188,7 +194,7 @@ async function getUserStringForMonth(client, channel, month) { await user.destroy(); continue; } - dateString = `[${dateString}](https://sc-network.net/age?age=${age} "${localize('birthdays', 'age-hover', {a: age})}")`; + dateString = `[${dateString}](https://scnx.xyz/${client.locale === 'de' ? 'de/' : ''}custom-bot/age-calculator?age=${age} "${localize('birthdays', 'age-hover', {a: age})}")`; } if (channel.guild.members.cache.get(user.id)) string = string + `${dateString}: ${client.configurations['birthday']['config'].useTags ? formatDiscordUserName(channel.guild.members.cache.get(user.id).user) : channel.guild.members.cache.get(user.id).user.toString()}\n`; } diff --git a/modules/birthday/commands/birthday.js b/modules/birthday/commands/birthday.js index bea02177..9bceb1d0 100644 --- a/modules/birthday/commands/birthday.js +++ b/modules/birthday/commands/birthday.js @@ -17,13 +17,14 @@ module.exports.subcommands = { ephemeral: true, content: '⚠️️ ' + localize('birthdays', 'no-birthday-set') }); + const date = new Date(interaction.birthday.year, interaction.birthday.month - 1, interaction.birthday.day); interaction.reply({ ephemeral: true, content: localize('birthdays', 'birthday-status', { dd: interaction.birthday.day, mm: interaction.birthday.month, yyyy: (interaction.birthday.year ? `.${interaction.birthday.year}` : ''), - age: interaction.birthday.year ? ', ' + (localize('birthdays', 'your-age', {age: new AgeFromDateString(`${interaction.birthday.year}-${interaction.birthday.month - 1}-${interaction.birthday.day}`).age})) : '' + age: interaction.birthday.year ? ', ' + (localize('birthdays', 'your-age', {age: new AgeFromDate(date).age})) : '' }) }); diff --git a/modules/birthday/config.json b/modules/birthday/config.json index b2a4bec2..c1227e11 100644 --- a/modules/birthday/config.json +++ b/modules/birthday/config.json @@ -56,7 +56,7 @@ "name": "birthday_message", "allowGeneratedImage": true, "humanName": { - "en": "Giveaway-Message", + "en": "Birthday Message", "de": "Geburtstags-Nachricht" }, "default": { @@ -104,7 +104,7 @@ "name": "birthday_message_with_age", "allowGeneratedImage": true, "humanName": { - "en": "Giveaway-Message with age", + "en": "Birthday message with age", "de": "Geburtstags-Nachricht mit Alter" }, "default": { @@ -201,7 +201,7 @@ "color": "GREEN", "thumbnail": " ", "image": " ", - "description": "Here you can find every birthday - add yours with !birthday [Year]" + "description": "Here you can find every birthday - add yours with /birthday set [Year]" }, "de": { "title": "Geburtstage", diff --git a/modules/channel-stats/channels.json b/modules/channel-stats/channels.json index dde44a0b..89f67b7b 100644 --- a/modules/channel-stats/channels.json +++ b/modules/channel-stats/channels.json @@ -175,7 +175,7 @@ "en": "Update-Interval" }, "default": { - "en": "15", + "en": 15, "de": 15 }, "description": { diff --git a/modules/channel-stats/events/botReady.js b/modules/channel-stats/events/botReady.js index e321a87e..4d5f8532 100644 --- a/modules/channel-stats/events/botReady.js +++ b/modules/channel-stats/events/botReady.js @@ -1,3 +1,4 @@ +const {ChannelType} = require('discord.js'); const {formatDate} = require('../../../src/functions/helpers'); const {localize} = require('../../../src/functions/localize'); @@ -7,7 +8,7 @@ module.exports.run = async (client) => { const dcChannel = await client.channels.fetch(channel.channelID).catch(() => { }); if (!dcChannel) continue; - if (dcChannel.type !== 'GUILD_VOICE' && dcChannel.type !== 'GUILD_CATEGORY') client.logger.warn(`[channel-stats] ` + localize('channel-stats', 'not-voice-channel-info', { + if (dcChannel.type !== ChannelType.GuildVoice && dcChannel.type !== ChannelType.GuildCategory) client.logger.warn(`[channel-stats] ` + localize('channel-stats', 'not-voice-channel-info', { c: dcChannel.name, id: dcChannel.id, t: dcChannel.type @@ -17,7 +18,7 @@ module.exports.run = async (client) => { client.intervals.push(setInterval(async () => { const repName = await channelNameReplacer(client, dcChannel, channel.channelName); if (repName !== dcChannel.name) dcChannel.setName(repName, '[channel-stats] ' + localize('channel-stats', 'audit-log-reason-interval')); - }, (channel.updateInterval || 5) < 5 * 60000 ? 5 * 60000 : channel.updateInterval * 60000)); + }, (channel.updateInterval || 5) < 5 ? 5 * 60000 : (channel.updateInterval || 5) * 60000)); } }; @@ -30,7 +31,7 @@ module.exports.run = async (client) => { * @return {Promise} */ async function channelNameReplacer(client, channel, input) { - const users = await channel.guild.members.fetch(); + const users = client.guild.members.cache; const members = users.filter(u => !u.user.bot); /** diff --git a/modules/color-me/commands/color-me.js b/modules/color-me/commands/color-me.js index fa21a26f..168c7588 100644 --- a/modules/color-me/commands/color-me.js +++ b/modules/color-me/commands/color-me.js @@ -227,11 +227,11 @@ async function color(interaction, moduleStrings) { roleColor = '#' + roleColor; } if (!(/^#[0-9A-F]{6}$/i).test(roleColor)) { - await interaction.editReply(await embedType(moduleStrings['invalidColor'], {})); + await interaction.editReply(embedType(moduleStrings['invalidColor'], {})); cancel = true; } } else { - roleColor = 'DEFAULT'; + roleColor = 0xF1C40F; } } diff --git a/modules/connect-four/commands/connect-four.js b/modules/connect-four/commands/connect-four.js index 3b57a711..79fdc43c 100644 --- a/modules/connect-four/commands/connect-four.js +++ b/modules/connect-four/commands/connect-four.js @@ -1,5 +1,5 @@ const {localize} = require('../../../src/functions/localize'); -const {MessageActionRow, MessageButton} = require('discord.js'); +const {ActionRowBuilder, ButtonBuilder, ComponentType, ButtonStyle} = require('discord.js'); const footer = ['1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣', '6️⃣', '7️⃣', '8️⃣', '9️⃣', '🔟']; /** @@ -184,7 +184,7 @@ module.exports.run = async function (interaction) { }); const confirmed = await msg.awaitMessageComponent({ filter: i => i.user.id === member.id, - componentType: 'BUTTON', + componentType: ComponentType.Button, time: 120000 }).catch(() => { }); @@ -206,14 +206,14 @@ module.exports.run = async function (interaction) { const grid = new Array(fieldSize - 1).fill(); for (const i in grid) grid[i] = new Array(fieldSize).fill('⬜'); - const row1 = new MessageActionRow(); - const row2 = new MessageActionRow(); + const row1 = new ActionRowBuilder(); + const row2 = new ActionRowBuilder(); for (let i = 1; i < fieldSize + 1; i++) { (i <= 5 ? row1 : row2).addComponents( - new MessageButton() + new ButtonBuilder() .setCustomId('c4_' + i) .setLabel('' + i) - .setStyle('PRIMARY') + .setStyle(ButtonStyle.Primary) ); } @@ -224,11 +224,11 @@ module.exports.run = async function (interaction) { confirmed.update({ content: gameMessage(grid, fieldSize, color, user, member.user.username, interaction.user.username), - components: fieldSize > 5 ? [row1, row2] : [row1] + components: fieldSize > 5 ? [row1.toJSON(), row2.toJSON()] : [row1.toJSON()] }); const collector = msg.createMessageComponentCollector({ - componentType: 'BUTTON', + componentType: ComponentType.Button, filter: i => i.user.id === interaction.user.id || i.user.id === member.id }); collector.on('collect', i => { diff --git a/modules/counter/config.json b/modules/counter/config.json index 81ec6017..d70829d3 100644 --- a/modules/counter/config.json +++ b/modules/counter/config.json @@ -17,8 +17,8 @@ "de": [] }, "description": { - "en": "ID of channels with the counter game", - "de": "ID der Kanäle mit dem Zählspiel" + "en": "Channels in which users can participate in the counting game", + "de": "Kanäle, in welchem Nutzer am Zählspiel teilnehmen können." }, "type": "array", "content": "channelID" @@ -75,7 +75,7 @@ "en": "Restart game, if user miscounts" }, "description": { - "en": "If enabled, the game will restarts if a user sends a number that is not in ordner", + "en": "If enabled, the game will restarts if a user sends a number that is not in order", "de": "Wenn aktiviert, wird das Spiel neustarten, wenn ein Nutzer eine Zahl sendet, die nicht in die Reihenfolge passt" }, "type": "boolean" @@ -129,6 +129,70 @@ }, "type": "boolean" }, + { + "name": "protectAgainstDeletion", + "default": { + "en": true + }, + "humanName": { + "de": "Verhindern, dass Nutzer die letzte Zählungsnachricht löschen?", + "en": "Protect against users deleting the last counting message?" + }, + "description": { + "en": "If enabled, the bot will send a message when the last correct counting message gets deleted so that other counters can't be fooled into counting an already counted number again.", + "de": "Wenn aktiviert, wird der Bot eine Nachricht in den Kanal schicken, wenn die letzte korrekte Zählnachricht gelöscht wird - das verhindert, dass andere Nutzer nicht dazu gebracht werden können, eine korrekte Nummer erneut zu zählen." + }, + "type": "boolean" + }, + { + "name": "protectionMessage", + "dependsOn": "protectAgainstDeletion", + "humanName": { + "de": "Löschschutznachricht", + "en": "Deletion protection message" + }, + "default": { + "de": "Scheint als hätte %mention% seine letzte Nachricht gelöscht - die zuletzt gezählte Zahl ist **%number%**.", + "en": "It seems like %mention% deleted their last message - the last counted number is **%number%**." + }, + "description": { + "en": "Message that gets send if a user deletes the last correct counting message.", + "de": "Nachricht, welche verschickt wird, wenn die letzte korrekte Zahlnachricht gelöscht wird." + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "mention", + "description": { + "en": "Mention of the user who's message got removed", + "de": "Erwähnung des Nutzers, dessen Nachricht gelöscht wurde" + } + }, + { + "name": "number", + "description": { + "en": "Last counted number in this the channel", + "de": "Zuletzt gezählte Nummer in diesem Kanal" + } + } + ] + }, + { + "name": "removeReactions", + "default": { + "en": true + }, + "humanName": { + "de": "Reaktionen nach 5 Sekunden entfernen?", + "en": "Remove reactions after 5 seconds?" + }, + "description": { + "en": "If enabled, the reactions the bot gives will be removed after 5 seconds. This will free up space in the counting channel", + "de": "Wenn aktiviert, werden die Reaktionen des Bots nach 5 Sekunden entfernt. Das lässt mehr Platz im Kanal." + }, + "type": "boolean" + }, { "name": "wrong-input-message", "humanName": { @@ -160,8 +224,8 @@ "en": 5 }, "humanName": { - "de": "Amount of wrong messages to trigger action", - "en": "Anzahl von falschen Nachrichten, um eine Aktion auszulösen" + "en": "Amount of wrong messages to trigger action", + "de": "Anzahl von falschen Nachrichten, um eine Aktion auszulösen" }, "description": { "en": "This is the amount of wrong messages a user has to send to trigger action. Once this amount is reached, the bot will either, depending on your configuration, give a role or disable the SEND_MESSAGES permission for a user (set to 0 to disable)", @@ -214,7 +278,7 @@ "allowEmbed": true, "description": { "en": "This message will be sent when a user reach the configured amount of wrong messages and gets actioned", - "de": "Diese Rolle wird versendet, sobald die konfigurierte Anzahl von falschen Nachrichten erreicht wird und ein Nutzer bestraft wird" + "de": "Diese Nachricht wird versendet, sobald die konfigurierte Anzahl von falschen Nachrichten erreicht wird und ein Nutzer bestraft wird" }, "params": [ { @@ -225,6 +289,36 @@ } } ] + }, + { + "name": "allowCharactersInMessage", + "default": { + "en": false + }, + "type": "boolean", + "humanName": { + "en": "Allow text characters in messages?", + "de": "Textcharaktere in der Nachricht erlauben?" + }, + "description": { + "en": "If enabled, users may write additional content into their messages instead of forcing them to just write a number. Messages without a number will still lead to an error.", + "de": "Wenn aktiviert, können Nutzer weitere Inhalte in ihre Nachrichten schreiben, statt sie zu zwingen, nur eine Nachricht zu posten. Nachrichten ohne Zahlen werden weiterhin zu einem Fehler führen." + } + }, + { + "name": "allowMaths", + "default": { + "en": true + }, + "type": "boolean", + "humanName": { + "en": "Allow users to use maths in their messages?", + "de": "Nutzern erlauben, Mathematik in ihren Nachrichten zu verwenden?" + }, + "description": { + "en": "If enabled, users can use maths in messages, as long as the result of their formula is the correct next number.", + "de": "If enabled, können Nutzer Mathematik in ihren Nachrichten verwenden, solange das Ergebnis des Termes der korrekten nächsten Zahl entspricht." + } } ] } \ No newline at end of file diff --git a/modules/counter/events/messageCreate.js b/modules/counter/events/messageCreate.js index 1e51ecf0..f2915962 100644 --- a/modules/counter/events/messageCreate.js +++ b/modules/counter/events/messageCreate.js @@ -1,5 +1,6 @@ const {localize} = require('../../../src/functions/localize'); const {embedType} = require('../../../src/functions/helpers'); +let Formula; const invalidMessages = {}; @@ -19,10 +20,11 @@ module.exports.run = async function (client, msg) { }); if (!object) return; - if (!parseInt(msg.content)) return wrongMessage(localize('counter', 'not-a-number')); + const parsedNumber = await parseMessageNumber(msg.content, client); + if (!parsedNumber) return wrongMessage(localize('counter', 'not-a-number')); if (object.lastCountedUser === msg.author.id && moduleConfig.onlyOneMessagePerUser) return wrongMessage(localize('counter', 'only-one-message-per-person')); - if (parseInt(object.currentNumber) + 1 !== parseInt(msg.content)) { - if (parseInt(object.currentNumber) !== parseInt(msg.content) && moduleConfig.restartOnWrongCount) { + if (parseInt(object.currentNumber) + 1 !== parsedNumber) { + if (parseInt(object.currentNumber) !== parsedNumber && moduleConfig.restartOnWrongCount) { object.currentNumber = 0; object.lastCountedUser = null; object.userCounts = {}; @@ -48,10 +50,13 @@ module.exports.run = async function (client, msg) { for (const benefit of benefits.filter(b => parseInt(b.userMessageCount) === userCounts[msg.author.id])) { if (benefit.giveRoles.length !== 0) await msg.member.roles.add(benefit.giveRoles); if (benefit.sendMessage) { - const ben = await msg.reply(embedType(benefit.sendMessage)); + const ben = await msg.reply(embedType(benefit.sendMessage, { + '%mention%': msg.author.toString(), + '%milestone%': userCounts[msg.author.id] + })); setTimeout(() => { ben.delete(); - }, 5000); + }, 10000); } } @@ -59,10 +64,12 @@ module.exports.run = async function (client, msg) { if (msg.content === '42') reactions = [await msg.react('❓')]; else if (msg.content === '420') reactions = [await msg.react('🚬')]; else if (msg.content === '100') reactions = [await msg.react('💯')]; - else if (msg.content === '112' || msg.content === '911') reactions = [await msg.react('🚑')]; + else if (msg.content === '110') reactions = [await msg.react('🚓')]; + else if (msg.content === '112' || msg.content === '911') reactions = [await msg.react('🚑'), await msg.react('🚒')]; else if (msg.content === '69') reactions = [await msg.react('🇳'), await msg.react('🇮'), await msg.react('🇨'), await msg.react('🇪')]; else reactions = [await msg.react(moduleConfig['success-reaction'])]; - setTimeout(async () => { + + if (moduleConfig.removeReactions) setTimeout(async () => { for (const reaction of reactions) await reaction.remove(); }, 5000); if (moduleConfig.channelDescription) await msg.channel.setTopic(moduleConfig.channelDescription.split('%x%').join(object.currentNumber + 1), '[counter] ' + localize('counter', 'channel-topic-change-reason')); @@ -95,4 +102,23 @@ module.exports.run = async function (client, msg) { }, 8000); } } -}; \ No newline at end of file +}; + +async function parseMessageNumber(content, client) { + if (client.configurations['counter']['config'].allowCharactersInMessage) content = content.replace(/[^\d\+\-\*\+()\/\.^]/g, ''); + if (client.configurations['counter']['config'].allowMaths) { + if (!Formula) Formula = (await import('fparser')).default; + try { + const math = new Formula(content); + content = math.evaluate({}); + } catch (e) { + + } + } + + if (!parseInt(content)) return null; + + return parseInt(content); +} + +module.exports.countingGameParseContent = parseMessageNumber; \ No newline at end of file diff --git a/modules/counter/events/messageDelete.js b/modules/counter/events/messageDelete.js new file mode 100644 index 00000000..39d50710 --- /dev/null +++ b/modules/counter/events/messageDelete.js @@ -0,0 +1,25 @@ +const {countingGameParseContent} = require('./messageCreate'); +const {embedType} = require('../../../src/functions/helpers'); +module.exports.run = async function (client, msg) { + if (!client.botReadyAt) return; + if (!msg.guild) return; + if (msg.guild.id !== client.guildID) return; + if (!msg.member) return; + if (msg.author.bot) return; + + const moduleConfig = client.configurations['counter']['config']; + if (!moduleConfig.channels.includes(msg.channel.id) || !moduleConfig.protectAgainstDeletion) return; + const object = await client.models['counter']['CountChannel'].findOne({ + where: { + channelID: msg.channel.id + } + }); + if (!object) return; + + if (await countingGameParseContent(msg.content, client) === object.currentNumber && msg.author.id === object.lastCountedUser) { + msg.channel.send(embedType(moduleConfig.protectionMessage, { + '%mention%': msg.author.toString(), + '%number%': object.currentNumber + })); + } +}; \ No newline at end of file diff --git a/modules/counter/milestones.json b/modules/counter/milestones.json index c73c9e61..2ca1f83e 100644 --- a/modules/counter/milestones.json +++ b/modules/counter/milestones.json @@ -23,13 +23,14 @@ { "name": "userMessageCount", "humanName": { - "de": "Nachrichtenzahl" + "de": "Nachrichtenzahl", + "en": "Message count" }, "default": { "en": "" }, "description": { - "en": "Count of valid counter-messages the users has to archive this goal", + "en": "Count of valid counter-messages the users has to achieve this goal", "de": "Anzahl der gültigen Zähl-Nachrichten, die der Nutzer schreiben muss, um dieses Ziel zu erreichen" }, "type": "integer" @@ -37,14 +38,15 @@ { "name": "giveRoles", "humanName": { - "de": "Rollen" + "de": "Rollen", + "en": "Roles" }, "default": { "en": [], "de": [] }, "description": { - "en": "These roles are given to the user if they archive this goal (optional)", + "en": "These roles are given to the user if they achieve this goal (optional)", "de": "Diese Rollen werden an den Nutzer vergeben, wenn er dieses Ziel erreicht (optional)" }, "type": "array", @@ -53,13 +55,31 @@ { "name": "sendMessage", "humanName": { - "de": "Nachricht" + "de": "Nachricht", + "en": "Message" }, "default": { - "en": "" + "en": "Congrats %mention% for counting %milestone% times!", + "de": "Herzlichen Glückwunsch, %mention%, für %milestone%-mal zählen!!" }, + "params": [ + { + "name": "mention", + "description": { + "en": "Mention the user who achieved the milestone", + "de": "Erwähnt den Nutzer, der das Ziel erreicht hat" + } + }, + { + "name": "milestone", + "description": { + "en": "The milestone (the number of message) that was reached", + "de": "Das Ziel (also die Zahl der Nachrichten, die verschickt), das erreicht wurde" + } + } + ], "description": { - "en": "This message gets send when they archive this goal", + "en": "This message gets send when they achieve this goal", "de": "Diese Nachricht wird gesendet, wenn er dieses Ziel erreicht" }, "type": "string", diff --git a/modules/duel/commands/duel.js b/modules/duel/commands/duel.js index d5aedf78..d7af202d 100644 --- a/modules/duel/commands/duel.js +++ b/modules/duel/commands/duel.js @@ -1,11 +1,11 @@ const {localize} = require('../../../src/functions/localize'); -const {MessageEmbed} = require('discord.js'); +const {ComponentType, MessageEmbed} = require('discord.js'); module.exports.run = async function (interaction) { const member = interaction.options.getMember('user', true); if (member.user.id === interaction.user.id) return interaction.reply({ ephemeral: true, - content: '⚠️ ' + localize('duel', 'self-invite-not-possible', {r: `<@${((await interaction.guild.members.fetch({withPresences: true})).filter(u => u.presence && u.user.id !== interaction.user.id && !u.user.bot).random() || {user: {id: 'RickAstley'}}).user.id}>`}) + content: '⚠️ ' + localize('duel', 'self-invite-not-possible', {r: `<@${(interaction.guild.members.cache.filter(u => u.presence && u.user.id !== interaction.user.id && !u.user.bot).random() || {user: {id: 'RickAstley'}}).user.id}>`}) }); const rep = await interaction.reply({ content: localize('duel', 'challenge-message', { @@ -46,7 +46,7 @@ module.exports.run = async function (interaction) { bullets[member.user.id] = 0; guardAfterEachOther[interaction.user.id] = 0; guardAfterEachOther[member.user.id] = 0; - const a = rep.createMessageComponentCollector({componentType: 'BUTTON'}); + const a = rep.createMessageComponentCollector({componentType: ComponentType.Button}); setTimeout(() => { if (started || a.ended) return; endReason = localize('duel', 'invite-expired', {u: interaction.user.toString(), i: member.toString()}); @@ -191,4 +191,4 @@ module.exports.config = { description: localize('duel', 'user-description') } ] -}; \ No newline at end of file +}; diff --git a/modules/economy-system/commands/economy-system.js b/modules/economy-system/commands/economy-system.js index c444da32..5fe1b9f2 100644 --- a/modules/economy-system/commands/economy-system.js +++ b/modules/economy-system/commands/economy-system.js @@ -114,7 +114,7 @@ module.exports.subcommands = { id: user.id } }); - if (!robbedUser) return interaction.reply(embedType(interaction.str['userNotFound']), {'%user%': formatDiscordUserName(user)}, {ephemeral: !interaction.config['publicCommandReplies']}); + if (!robbedUser) return interaction.reply(embedType(interaction.str['userNotFound'], {'%user%': formatDiscordUserName(user)}), {ephemeral: !interaction.config['publicCommandReplies']}); if (!await cooldown('rob', interaction.config['robCooldown'] * 60000, interaction.user.id, interaction.client)) return interaction.reply(embedType(interaction.str['cooldown'], {}, {ephemeral: !interaction.config['publicCommandReplies']})); let toRob = parseInt(robbedUser.balance) * (parseInt(interaction.config['robPercent']) / 100); if (toRob >= parseInt(interaction.config['maxRobAmount'])) toRob = parseInt(interaction.config['maxRobAmount']); @@ -145,7 +145,7 @@ module.exports.subcommands = { if (interaction.options.getUser('user').id === interaction.user.id && !interaction.client.configurations['economy-system']['config']['selfBalance']) { if (interaction.client.logChannel) interaction.client.logChannel.send(localize('economy-system', 'admin-self-abuse')); return interaction.reply({ - content: localize('economy-system', 'admin-self-abuse-answer'), + content: localize('economy-system', 'admin-self-abuse-answer', {u: interaction.user.toString()}), ephemeral: !interaction.config['publicCommandReplies'] }); } @@ -177,7 +177,7 @@ module.exports.subcommands = { if (interaction.options.getUser('user').id === interaction.user.id && !interaction.client.configurations['economy-system']['config']['selfBalance']) { if (interaction.client.logChannel) interaction.client.logChannel.send(localize('economy-system', 'admin-self-abuse')); return interaction.reply({ - content: localize('economy-system', 'admin-self-abuse-answer'), + content: localize('economy-system', 'admin-self-abuse-answer', {u: interaction.user.toString()}), ephemeral: !interaction.config['publicCommandReplies'] }); } @@ -208,7 +208,7 @@ module.exports.subcommands = { if (interaction.options.getUser('user').id === interaction.user.id && !interaction.client.configurations['economy-system']['config']['selfBalance']) { if (interaction.client.logChannel) interaction.client.logChannel.send(localize('economy-system', 'admin-self-abuse')); return interaction.reply({ - content: localize('economy-system', 'admin-self-abuse-answer'), + content: localize('economy-system', 'admin-self-abuse-answer', {u: interaction.user.toString()}), ephemeral: !interaction.config['publicCommandReplies'] }); } @@ -272,7 +272,7 @@ module.exports.subcommands = { id: user.id } }); - if (!balanceV) return interaction.reply(embedType(interaction.str['userNotFound']), {'%user%': formatDiscordUserName(user)}, {ephemeral: !interaction.config['publicCommandReplies']}); + if (!balanceV) return interaction.reply(embedType(interaction.str['userNotFound'], {'%user%': formatDiscordUserName(user)}), {ephemeral: !interaction.config['publicCommandReplies']}); interaction.reply(embedType(interaction.str['balanceReply'], { '%user%': formatDiscordUserName(user), '%balance%': `${balanceV['balance']} ${interaction.client.configurations['economy-system']['config']['currencySymbol']}`, diff --git a/modules/economy-system/commands/shop.js b/modules/economy-system/commands/shop.js index 44751f37..a65eacc5 100644 --- a/modules/economy-system/commands/shop.js +++ b/modules/economy-system/commands/shop.js @@ -3,16 +3,14 @@ const {localize} = require('../../../src/functions/localize'); /** * @param {*} interaction Interaction - * @returns {boolean} Result + * @returns {Promise} Result */ async function checkPerms(interaction) { const result = interaction.client.configurations['economy-system']['config']['shopManagers'].includes(interaction.user.id) || interaction.client.config['botOperators'].includes(interaction.user.id); - if (!result) { - await interaction.reply({ + if (!result) await interaction.reply({ content: interaction.client.strings['not_enough_permissions'], ephemeral: !interaction.client.configurations['economy-system']['config']['publicCommandReplies'] }); - } return result; } diff --git a/modules/economy-system/configs/config.json b/modules/economy-system/configs/config.json index 892087bf..e37366e1 100644 --- a/modules/economy-system/configs/config.json +++ b/modules/economy-system/configs/config.json @@ -252,9 +252,10 @@ "default": { "en": "" }, + "allowNull": true, "description": { - "en": "The if of the channel for the leaderboard. On this leaderboard everyone can see who has the most money.", - "de": "Die ID des Kanals für das Leaderboard. Hier kann jeder sehen, wer das meiste Geld hat" + "en": "The channel for the leaderboard. On this leaderboard everyone can see who has the most money.", + "de": "Der Kanals für das Leaderboard. Hier kann jeder sehen, wer das meiste Geld hat" }, "type": "channelID" }, diff --git a/modules/economy-system/configs/strings.json b/modules/economy-system/configs/strings.json index e2a81938..23be38be 100644 --- a/modules/economy-system/configs/strings.json +++ b/modules/economy-system/configs/strings.json @@ -78,8 +78,8 @@ "de": "Item Text" }, "default": { - "en": "**%id%** %itemName%, **price**: %price%, **sellcount**: %sellcount%\n", - "de": "**%id%** %itemName%: **Preis**: %price%, **Verkäufe**: %sellcount%\n" + "en": "**%id%** %itemName%, **price**: %price%, **sellcount**: %sellcount%", + "de": "**%id%** %itemName%: **Preis**: %price%, **Verkäufe**: %sellcount%" }, "description": { "en": "String for the items for the shop message", @@ -506,7 +506,7 @@ "content": "string", "params": [ { - "name": "erned", + "name": "earned", "description": {} } ] @@ -683,4 +683,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/modules/economy-system/economy-system.js b/modules/economy-system/economy-system.js index 5b9c4417..7871e9aa 100644 --- a/modules/economy-system/economy-system.js +++ b/modules/economy-system/economy-system.js @@ -3,10 +3,14 @@ * @module economy-system * @author jateute */ -const { MessageEmbed } = require('discord.js'); -const {embedType, inputReplacer} = require('../../src/functions/helpers'); +const {MessageEmbed} = require('discord.js'); +const { + embedType, + inputReplacer, + parseEmbedColor +} = require('../../src/functions/helpers'); const {localize} = require('../../src/functions/localize'); -const { Op } = require('sequelize'); +const {Op} = require('sequelize'); /** * add a User to DB @@ -240,16 +244,10 @@ async function buyShopItem(interaction, id, name) { ] } }); - if (item.length < 1) return await interaction.editReply({ - content: interaction.client.configurations['economy-system']['strings']['notFound'] - }); - else if (item.length > 1) return await interaction.editReply({ - content: interaction.client.configurations['economy-system']['strings']['multipleMatches'] - }); + if (item.length < 1) return await interaction.editReply(embedType(interaction.client.configurations['economy-system']['strings']['notFound'])); + else if (item.length > 1) return await interaction.editReply(embedType(interaction.client.configurations['economy-system']['strings']['multipleMatches'])); - if (interaction.member.roles.cache.has(item[0]['role'])) return await interaction.editReply({ - content: interaction.client.configurations['economy-system']['strings']['rebuyItem'] - }); + if (interaction.member.roles.cache.has(item[0]['role'])) return await interaction.editReply(embedType(interaction.client.configurations['economy-system']['strings']['rebuyItem'])); let user = await interaction.client.models['economy-system']['Balance'].findOne({ where: { id: interaction.user.id @@ -263,9 +261,7 @@ async function buyShopItem(interaction, id, name) { } }); } - if (user.balance < item[0]['price']) return await interaction.editReply({ - content: interaction.client.configurations['economy-system']['strings']['notEnoughMoney'] - }); + if (user.balance < item[0]['price']) return await interaction.editReply(embedType(interaction.client.configurations['economy-system']['strings']['notEnoughMoney'])); await interaction.member.roles.add(item[0]['role']); await editBalance(interaction.client, interaction.user.id, 'remove', item[0]['price']); leaderboard(interaction.client); @@ -331,6 +327,7 @@ async function deleteShopItem(interaction) { const nameOption = interaction.options.get('item-name'); const idOption = interaction.options.get('item-id'); let model; + if (nameOption && idOption) { model = await interaction.client.models['economy-system']['Shop'].findAll({ where: { @@ -340,32 +337,37 @@ async function deleteShopItem(interaction) { ] } }); - }else if (nameOption) { + } else if (nameOption) { model = await interaction.client.models['economy-system']['Shop'].findAll({ where: { name: nameOption['value'] } }); - } - else if (idOption) { + } else if (idOption) { model = await interaction.client.models['economy-system']['Shop'].findAll({ where: { id: idOption['value'] } }); } else { - await interaction.editReply("Please use the id or the name!") + await interaction.editReply('Please use the id or the name!'); } if (model.length > 1) { await interaction.editReply(embedType(interaction.client.configurations['economy-system']['strings']['multipleMatches'])); resolve(); } else if (model.length < 1) { - await interaction.editReply(embedType(interaction.client.configurations['economy-system']['strings']['noMatches'], {'%id%': idOption ? idOption['value'] : '-', '%name%': nameOption ? nameOption['value'] : '-'})); + await interaction.editReply(embedType(interaction.client.configurations['economy-system']['strings']['noMatches'], { + '%id%': idOption ? idOption['value'] : '-', + '%name%': nameOption ? nameOption['value'] : '-' + })); resolve(); } else { await model[0].destroy(); - await interaction.editReply(embedType(interaction.client.configurations['economy-system']['strings']['itemDelete'], {'%name%': model[0]['name'], '%id%': model[0]['id']})); + await interaction.editReply(embedType(interaction.client.configurations['economy-system']['strings']['itemDelete'], { + '%name%': model[0]['name'], + '%id%': model[0]['id'] + })); interaction.client.logger.info(`[economy-system] ` + localize('economy-system', 'delete-item', { u: interaction.user.tag, i: model.name @@ -393,7 +395,13 @@ async function createShopMsg(client, guild, ephemeral) { const options = []; for (let i = 0; i < items.length; i++) { const roles = await guild.roles.fetch(items[i].dataValues.role); - string = `${string}${inputReplacer({'%id%': items[i].dataValues.id, '%itemName%': items[i].dataValues.name, '%price%': `${items[i].dataValues.price} ${client.configurations['economy-system']['config']['currencySymbol']}`, '%sellcount%': roles ? roles.members.size : '0'}, client.configurations['economy-system']['strings']['itemString'])}`; + string = `${string}${inputReplacer({ + '%id%': items[i].dataValues.id, + '%itemName%': items[i].dataValues.name, + '%price%': `${items[i].dataValues.price} ${client.configurations['economy-system']['config']['currencySymbol']}`, + '%sellcount%': roles ? roles.members.size : '0', + '\n': '' + }, client.configurations['economy-system']['strings']['itemString'])}\n`; options.push({ label: items[i].dataValues.name, description: localize('economy-system', 'select-menu-price', { @@ -416,7 +424,10 @@ async function createShopMsg(client, guild, ephemeral) { }] }]; } - return embedType(client.configurations['economy-system']['strings']['shopMsg'], {'%shopItems%': string}, { ephemeral: ephemeral, components: components }); + return embedType(client.configurations['economy-system']['strings']['shopMsg'], {'%shopItems%': string}, { + ephemeral: ephemeral, + components: components + }); } /** @@ -471,14 +482,23 @@ async function leaderboard(client) { const messages = (await channel.messages.fetch()).filter(msg => msg.author.id === client.user.id); const embed = new MessageEmbed() - .setTitle(moduleStr['leaderboardEmbed']['title']) - .setDescription(moduleStr['leaderboardEmbed']['description']) - .setTimestamp() - .setColor(moduleStr['leaderboardEmbed']['color']) - .setAuthor({name: client.user.username, iconURL: client.user.avatarURL()}) - .setFooter({text: client.strings.footer, iconURL: client.strings.footerImgUrl}); + .setTitle(moduleStr['leaderboardEmbed']['title']) + .setDescription(moduleStr['leaderboardEmbed']['description']) + .setTimestamp() + .setColor(parseEmbedColor(moduleStr['leaderboardEmbed']['color'])) + .setAuthor({ + name: client.user.username, + iconURL: client.user.avatarURL() + }) + .setFooter({ + text: client.strings.footer, + iconURL: client.strings.footerImgUrl + }); - if (model.length !== 0) embed.addFields({name: 'Leaderboard:', value: await topTen(model, client)}); + if (model.length !== 0) embed.addFields({ + name: 'Leaderboard:', + value: await topTen(model, client) + }); if ((moduleStr['leaderboardEmbed']['thumbnail'] || '').replaceAll(' ', '')) embed.setThumbnail(moduleStr['leaderboardEmbed']['thumbnail']); if ((moduleStr['leaderboardEmbed']['image'] || '').replaceAll(' ', '')) embed.setImage(moduleStr['leaderboardEmbed']['image']); diff --git a/modules/economy-system/events/interactionCreate.js b/modules/economy-system/events/interactionCreate.js index db0b4399..7b40565b 100644 --- a/modules/economy-system/events/interactionCreate.js +++ b/modules/economy-system/events/interactionCreate.js @@ -1,9 +1,10 @@ -const { buyShopItem } = require('../economy-system'); +const {buyShopItem} = require('../economy-system'); module.exports.run = async function (client, interaction) { if (!client.botReadyAt) return; if (interaction.guild.id !== client.config.guildID) return; if (!interaction.isSelectMenu()) return; if (interaction.customId !== 'economy-system_shop-select') return; + await interaction.deferReply({ephemeral: true}); buyShopItem(interaction, interaction.values[0], null); }; \ No newline at end of file diff --git a/modules/fun/commands/hug.js b/modules/fun/commands/hug.js index b79878a0..5615b5d6 100644 --- a/modules/fun/commands/hug.js +++ b/modules/fun/commands/hug.js @@ -1,26 +1,32 @@ -const {embedType, randomElementFromArray} = require('../../../src/functions/helpers'); +const { + embedType, + randomElementFromArray +} = require('../../../src/functions/helpers'); const {localize} = require('../../../src/functions/localize'); +const {MessageAttachment} = require('discord.js'); module.exports.run = async function (interaction) { const moduleConfig = interaction.client.configurations['fun']['config']; const user = interaction.options.getUser('user', true); - if (user.id === interaction.user.id) return interaction.reply({content: localize('fun', 'no-no-not-hugging-yourself'), ephemeral: true}); - interaction.reply(embedType(moduleConfig.hugMessage, { + if (user.id === interaction.user.id) return interaction.reply({ + content: localize('fun', 'no-no-not-hugging-yourself'), + ephemeral: true + }); + await interaction.deferReply({}); + await interaction.editReply(embedType(moduleConfig.hugMessage, { '%authorID%': interaction.user.id, '%userID%': user.id, - '%imgUrl%': randomElementFromArray(moduleConfig.hugImages) - })); + '%imgUrl%': '' + }, {files: [new MessageAttachment(randomElementFromArray(moduleConfig.hugImages))]})); }; module.exports.config = { name: 'hug', description: localize('fun', 'hug-command-description'), - options: [ - { - type: 'USER', - name: 'user', - description: localize('fun', 'user-argument-description'), - required: true - } - ] + options: [{ + type: 'USER', + name: 'user', + description: localize('fun', 'user-argument-description'), + required: true + }] }; \ No newline at end of file diff --git a/modules/fun/commands/kiss.js b/modules/fun/commands/kiss.js index 29b4de58..ac4a7e29 100644 --- a/modules/fun/commands/kiss.js +++ b/modules/fun/commands/kiss.js @@ -1,26 +1,32 @@ -const {embedType, randomElementFromArray} = require('../../../src/functions/helpers'); +const { + embedType, + randomElementFromArray +} = require('../../../src/functions/helpers'); const {localize} = require('../../../src/functions/localize'); +const {MessageAttachment} = require('discord.js'); module.exports.run = async function (interaction) { const moduleConfig = interaction.client.configurations['fun']['config']; const user = interaction.options.getUser('user', true); - if (user.id === interaction.user.id) return interaction.reply({content: localize('fun', 'no-no-not-kissing-yourself'), ephemeral: true}); - interaction.reply(embedType(moduleConfig.kissMessage, { + if (user.id === interaction.user.id) return interaction.reply({ + content: localize('fun', 'no-no-not-kissing-yourself'), + ephemeral: true + }); + await interaction.deferReply({}); + await interaction.editReply(embedType(moduleConfig.kissMessage, { '%authorID%': interaction.user.id, '%userID%': user.id, - '%imgUrl%': randomElementFromArray(moduleConfig.kissImages) - })); + '%imgUrl%': '' + }, {files: [new MessageAttachment(randomElementFromArray(moduleConfig.kissImages))]})); }; module.exports.config = { name: 'kiss', description: localize('fun', 'kiss-command-description'), - options: [ - { - type: 'USER', - name: 'user', - description: localize('fun', 'user-argument-description'), - required: true - } - ] + options: [{ + type: 'USER', + name: 'user', + description: localize('fun', 'user-argument-description'), + required: true + }] }; \ No newline at end of file diff --git a/modules/fun/commands/pat.js b/modules/fun/commands/pat.js index d44dd825..619a9009 100644 --- a/modules/fun/commands/pat.js +++ b/modules/fun/commands/pat.js @@ -1,14 +1,18 @@ const {embedType, randomElementFromArray} = require('../../../src/functions/helpers'); const {localize} = require('../../../src/functions/localize'); +const {MessageAttachment} = require('discord.js'); module.exports.run = async function (interaction) { const moduleConfig = interaction.client.configurations['fun']['config']; const user = interaction.options.getUser('user', true); if (user.id === interaction.user.id) return interaction.reply({content: localize('fun', 'no-no-not-patting-yourself'), ephemeral: true}); - interaction.reply(embedType(moduleConfig.patMessage, { + await interaction.deferReply({}); + await interaction.editReply(embedType(moduleConfig.patMessage, { '%authorID%': interaction.user.id, '%userID%': user.id, - '%imgUrl%': randomElementFromArray(moduleConfig.patImages) + '%imgUrl%': '' + }, { + files: [new MessageAttachment(randomElementFromArray(moduleConfig.patImages))] })); }; diff --git a/modules/fun/commands/random.js b/modules/fun/commands/random.js index c613a469..feb652e5 100644 --- a/modules/fun/commands/random.js +++ b/modules/fun/commands/random.js @@ -9,25 +9,24 @@ module.exports.subcommands = { '%min%': interaction.options.getNumber('min') || 1, '%max%': interaction.options.getNumber('max') || 42, '%number%': randomIntFromInterval(interaction.options.getNumber('min') || 1, interaction.options.getNumber('max') || 42) - }, - {ephemeral: true} + } )); }, 'ikea-name': function (interaction) { let count = interaction.options.getNumber('syllable-count') || Math.floor(Math.random() * 4) + 1; if (count && count > 20) count = 20; - interaction.reply(embedType(interaction.client.configurations['fun']['config']['ikeaMessage'], {'%name%': generateIkeaName(count)}, {ephemeral: true})); + interaction.reply(embedType(interaction.client.configurations['fun']['config']['ikeaMessage'], {'%name%': generateIkeaName(count)})); }, 'dice': function (interaction) { - interaction.reply(embedType(interaction.client.configurations['fun']['config']['diceRollMessage'], {'%number%': randomIntFromInterval(1, 6)}, {ephemeral: true})); + interaction.reply(embedType(interaction.client.configurations['fun']['config']['diceRollMessage'], {'%number%': randomIntFromInterval(1, 6)})); }, 'coinflip': function (interaction) { - interaction.reply(embedType(interaction.client.configurations['fun']['config']['coinFlipMessage'], {'%site%': localize('fun', `dice-site-${randomIntFromInterval(1, 2)}`)}, {ephemeral: true})); + interaction.reply(embedType(interaction.client.configurations['fun']['config']['coinFlipMessage'], {'%site%': localize('fun', `dice-site-${randomIntFromInterval(1, 2)}`)})); }, '8ball': function (interaction) { interaction.reply(embedType(interaction.client.configurations['fun']['config']['8ballMessage'], { '%answer%': randomElementFromArray(interaction.client.configurations['fun']['config']['8BallMessages']) - }, {ephemeral: true})); + })); } }; diff --git a/modules/fun/commands/slap.js b/modules/fun/commands/slap.js index 36f5f5f7..ebddb8f2 100644 --- a/modules/fun/commands/slap.js +++ b/modules/fun/commands/slap.js @@ -1,15 +1,17 @@ const {embedType, randomElementFromArray} = require('../../../src/functions/helpers'); const {localize} = require('../../../src/functions/localize'); +const {MessageAttachment} = require('discord.js'); module.exports.run = async function (interaction) { const moduleConfig = interaction.client.configurations['fun']['config']; const user = interaction.options.getUser('user', true); if (user.id === interaction.user.id) return interaction.reply({content: localize('fun', 'no-no-not-slapping-yourself'), ephemeral: true}); - interaction.reply(embedType(moduleConfig.slapMessage, { + await interaction.deferReply({}); + await interaction.editReply(embedType(moduleConfig.slapMessage, { '%authorID%': interaction.user.id, '%userID%': user.id, - '%imgUrl%': randomElementFromArray(moduleConfig.slapImages) - })); + '%imgUrl%': '' + }, {files: [new MessageAttachment(randomElementFromArray(moduleConfig.slapImages))]})); }; module.exports.config = { diff --git a/modules/fun/config.json b/modules/fun/config.json index 0731b906..ef0f1bd7 100644 --- a/modules/fun/config.json +++ b/modules/fun/config.json @@ -9,15 +9,16 @@ { "name": "ikeaMessage", "humanName": { - "de": "IKEA-Nachricht" + "de": "IKEA-Nachricht", + "en": "IKEA Message" }, "default": { "en": "Here's a ikea-product-name: %name%", "de": "Hier ist ein IKEA-Produkt-Name: %name%" }, "description": { - "en": "Message that gets send when someone uses !ikea", - "de": "Nachricht welche gesendet wird, wenn jemand /random ikea benutzt" + "en": "Message that gets send when someone uses /random ikea-name", + "de": "Nachricht welche gesendet wird, wenn jemand /random ikea-name benutzt" }, "type": "string", "allowEmbed": true, @@ -34,14 +35,15 @@ { "name": "randomNumberMessage", "humanName": { - "de": "Zufallszahl-Nachricht" + "de": "Zufallszahl-Nachricht", + "en": "Random numer message" }, "default": { "en": "Here your random number between %min% and %max%: %number%", "de": "Hier ist deine Zufallszahl zwischen %min% und %max%: %number%" }, "description": { - "en": "Message that gets send when someone uses !random", + "en": "Message that gets send when someone uses /random number", "de": "Nachricht, welche gesendet wird, wenn jemand /random number benutzt" }, "type": "string", @@ -73,15 +75,16 @@ { "name": "diceRollMessage", "humanName": { - "de": "Würfel-Nachricht" + "de": "Würfel-Nachricht", + "en": "Dice Roll message" }, "default": { "en": "🎲 %number%", "de": "🎲 %number%" }, "description": { - "en": "Message that gets send when someone uses !dice", - "de": "Nachricht, welche gesendet wird, wenn jemand /random dice benutzt" + "en": "Message that gets send when someone uses /random dice", + "de": "Nachricht, welche gesendet wird, wenn jemand /random dice benutzt" }, "type": "string", "allowEmbed": true, @@ -98,7 +101,8 @@ { "name": "coinFlipMessage", "humanName": { - "de": "Münzwurf-Nachricht" + "de": "Münzwurf-Nachricht", + "en": "Coin toss message" }, "default": { "en": "\uD83E\uDE99 %site%", @@ -106,7 +110,7 @@ }, "description": { "en": "Message that gets send when someone uses /random coinfilp", - "de": "Nachricht, welche gesendet wird, wenn jemand /random coinfilp benutzt" + "de": "Nachricht, welche gesendet wird, wenn jemand /random coinfilp benutzt" }, "type": "string", "allowEmbed": true, @@ -123,14 +127,16 @@ { "name": "hugMessage", "humanName": { - "de": "Umarmungsnachricht" + "de": "Umarmungsnachricht", + "en": "Hug message" }, "default": { - "en": "<@%authorID%> hugs <@%userID%>\n%imgUrl%", - "de": "<@%authorID%> umarmt <@%userID%>\n%imgUrl%" + "en": "<@%authorID%> hugs <@%userID%>", + "de": "<@%authorID%> umarmt <@%userID%>" }, "description": { - "de": "Nachricht, welche gesendet wird, wenn jemand /hug benutzt" + "de": "Nachricht, welche gesendet wird, wenn jemand /hug benutzt", + "en": "Message that gets send when someone uses /hug" }, "type": "string", "allowEmbed": true, @@ -148,36 +154,25 @@ "en": "ID of the user that gets hugged", "de": "ID des umarmten Nutzers" } - }, - { - "name": "imgUrl", - "description": { - "en": "Randomly selected URL to a image", - "de": "Zufällig ausgewählte URL zu einem Bild" - }, - "isImage": true } ] }, { "name": "hugImages", "humanName": { - "de": "Umarmungsbilder" + "de": "Umarmungsbilder", + "en": "Hug images" }, "default": { "en": [ - "https://media1.tenor.com/images/94989f6312726739893d41231942bb1b/tenor.gif?itemid=14106856", - "https://media1.tenor.com/images/d7529f6003b20f3b21f1c992dffb8617/tenor.gif?itemid=4782499", - "https://media1.tenor.com/images/fd47e55dfb49ae1d39675d6eff34a729/tenor.gif?itemid=12687187" - ], - "de": [ - "https://media1.tenor.com/images/94989f6312726739893d41231942bb1b/tenor.gif?itemid=14106856", - "https://media1.tenor.com/images/d7529f6003b20f3b21f1c992dffb8617/tenor.gif?itemid=4782499", - "https://media1.tenor.com/images/fd47e55dfb49ae1d39675d6eff34a729/tenor.gif?itemid=12687187" + "https://scnx-cdn.scootkit.net/1723477011519-tjCfeHPcYYzFe3jRnoUVI7dn.gif", + "https://scnx-cdn.scootkit.net/1723477171157-3wGistN45zd9kwrP67YKfRgU.gif", + "https://scnx-cdn.scootkit.net/1753891037940-pdaiqed4ffL4XHbLe2N0j6fbW6zRvPDzy0ZCwKIRwmOz85yX.gif" ] }, "description": { - "de": "Bilder aus welchen, wenn jemand /hug ausführt, zufällig ausgewählt wird" + "de": "Bilder aus welchen, wenn jemand /hug ausführt, zufällig ausgewählt wird", + "en": "Images that one will be randomly selected from when someone uses /hug." }, "type": "array", "content": "imgURL" @@ -185,13 +180,15 @@ { "name": "kissMessage", "humanName": { + "en": "Kiss message", "de": "Kuss-Nachrichten" }, "default": { - "en": "<@%authorID%> kissed <@%userID%>\n%imgUrl%", - "de": "<@%authorID%> küsst <@%userID%>\n%imgUrl%" + "en": "<@%authorID%> kissed <@%userID%>", + "de": "<@%authorID%> küsst <@%userID%>" }, "description": { + "en": "Message that gets send when someone uses /kiss", "de": "Nachricht, welche gesendet wird, wenn jemand /kiss benutzt" }, "type": "string", @@ -210,35 +207,24 @@ "en": "ID of the user that gets kissed", "de": "ID des geküssten Nutzers" } - }, - { - "name": "imgUrl", - "description": { - "en": "Randomly selected URL to a image", - "de": "Zufällig ausgewählte URL zu einem Bild" - }, - "isImage": true } ] }, { "name": "kissImages", "humanName": { - "de": "Kussbilder" + "de": "Kussbilder", + "en": "Kiss images" }, "default": { "en": [ - "https://media1.tenor.com/images/ef9687b36e36605b375b4e9b0cde51db/tenor.gif?itemid=12498627", - "https://media1.tenor.com/images/2d2a1af1568277f2bc52467f984cb697/tenor.gif?itemid=14190535", - "https://media1.tenor.com/images/78095c007974aceb72b91aeb7ee54a71/tenor.gif?itemid=5095865" - ], - "de": [ - "https://media1.tenor.com/images/ef9687b36e36605b375b4e9b0cde51db/tenor.gif?itemid=12498627", - "https://media1.tenor.com/images/2d2a1af1568277f2bc52467f984cb697/tenor.gif?itemid=14190535", - "https://media1.tenor.com/images/78095c007974aceb72b91aeb7ee54a71/tenor.gif?itemid=5095865" + "https://scnx-cdn.scootkit.net/1743549285215-t9x4Fm9ZqE0f4vxyKfrTNo7JlGLO2hFHae8R8arRQHjQeylk.gif", + "https://scnx-cdn.scootkit.net/1695864480892-EVwr6ighEdpxY22G8jUweAPt.gif", + "https://scnx-cdn.scootkit.net/1743549267626-cSru5Kn1Dg2zv5KAefHMtRL5XuWqCW84hegW40aty4b8iFH7.gif" ] }, "description": { + "en": "Images that one will be randomly selected from when someone uses /kiss.", "de": "Bilder aus welchen, wenn jemand /kiss ausführt, zufällig ausgewählt wird" }, "type": "array", @@ -247,14 +233,16 @@ { "name": "slapMessage", "humanName": { - "de": "Schlag-Nachricht" + "de": "Schlag-Nachricht", + "en": "Slap message" }, "default": { - "en": "<@%authorID%> slapped <@%userID%>\n%imgUrl%", - "de": "<@%authorID%> schlägt <@%userID%>\n%imgUrl%" + "en": "<@%authorID%> slapped <@%userID%>", + "de": "<@%authorID%> schlägt <@%userID%>" }, "description": { - "de": "Nachricht, welche gesendet wird, wenn jemand /slap benutzt" + "de": "Nachricht, welche gesendet wird, wenn jemand /slap benutzt", + "en": "Message that gets send when someone uses /slap" }, "type": "string", "allowEmbed": true, @@ -272,40 +260,26 @@ "en": "ID of the user that gets slapped", "de": "ID des geschlagenen Nutzers" } - }, - { - "name": "imgUrl", - "description": { - "en": "Randomly selected URL to a image", - "de": "Zufällig ausgewählte URL zu einem Bild" - }, - "isImage": true } ] }, { "name": "slapImages", "humanName": { - "de": "Schlag-Bilder" + "de": "Schlag-Bilder", + "en": "Slap images" }, "default": { "en": [ - "https://media1.tenor.com/images/3c161bd7d6c6fba17bb3e5c5ecc8493e/tenor.gif?itemid=5196956", - "https://media1.tenor.com/images/73adef04dadf613cb96ed3b2c8a192b4/tenor.gif?itemid=9631495", - "https://media.tenor.com/images/bfda4a429071a7fa51c7e45685849f76/tenor.gif", - "https://media1.tenor.com/images/97624764cb41414ad2c60d2028c19394/tenor.gif?itemid=16739345", - "https://media1.tenor.com/images/03ea2379718496fbbd144c5bc50f8e96/tenor.gif?itemid=18908545" - ], - "de": [ - "https://media1.tenor.com/images/3c161bd7d6c6fba17bb3e5c5ecc8493e/tenor.gif?itemid=5196956", - "https://media1.tenor.com/images/73adef04dadf613cb96ed3b2c8a192b4/tenor.gif?itemid=9631495", - "https://media.tenor.com/images/bfda4a429071a7fa51c7e45685849f76/tenor.gif", - "https://media1.tenor.com/images/97624764cb41414ad2c60d2028c19394/tenor.gif?itemid=16739345", - "https://media1.tenor.com/images/03ea2379718496fbbd144c5bc50f8e96/tenor.gif?itemid=18908545" + "https://scnx-cdn.scootkit.net/1744620013783-xEkcviAsrCZulbhoVoPPWtTUWlJbQda6kk43eQb58CMLFvDU.gif", + "https://scnx-cdn.scootkit.net/1744620140479-qz6nc8xzCSW2TB6Yy40vj6WzCBi31ezRZVElFrKuKCIfc6vZ.gif", + "https://scnx-cdn.scootkit.net/1744620083811-RYado8KTb7E8AzCVfncyNgUxD2GyQFdhjH4YxzVc5aLkGvN4.gif", + "https://scnx-cdn.scootkit.net/1744620244031-0JO1dEMxvKBAz12dj08BIVw8njCxgj8CJ89SnUihMZxnzyDE.gif" ] }, "description": { - "de": "Bilder aus welchen, wenn jemand /slap ausführt, zufällig ausgewählt wird" + "de": "Bilder aus welchen, wenn jemand /slap ausführt, zufällig ausgewählt wird", + "en": "Images that one will be randomly selected from when someone uses /slap." }, "type": "array", "content": "imgURL" @@ -313,14 +287,16 @@ { "name": "patMessage", "humanName": { - "de": "Tätschel-Nachricht" + "de": "Tätschel-Nachricht", + "en": "Pat message" }, "default": { - "en": "<@%authorID%> patted <@%userID%>\n%imgUrl%", - "de": "<@%authorID%> tätschelt <@%userID%>\n%imgUrl%" + "en": "<@%authorID%> patted <@%userID%>", + "de": "<@%authorID%> tätschelt <@%userID%>" }, "description": { - "de": "Nachricht, welche gesendet wird, wenn jemand /pat benutzt" + "de": "Nachricht, welche gesendet wird, wenn jemand /pat benutzt", + "en": "Message that gets send when someone uses /pat" }, "type": "string", "allowEmbed": true, @@ -338,38 +314,26 @@ "en": "ID of the user that gets patted", "de": "ID des getätschelten Nutzers" } - }, - { - "name": "imgUrl", - "description": { - "en": "Randomly selected URL to a image", - "de": "Zufällig ausgewählte URL zu einem Bild" - }, - "isImage": true } ] }, { "name": "patImages", "humanName": { - "de": "Tätschel-Bilder" + "de": "Tätschel-Bilder", + "en": "Pat images" }, "default": { "en": [ - "https://media1.tenor.com/images/da8f0e8dd1a7f7db5298bda9cc648a9a/tenor.gif?itemid=12018819", - "https://media1.tenor.com/images/f5176d4c5cbb776e85af5dcc5eea59be/tenor.gif?itemid=5081286", - "https://media.tenor.com/images/0e5b7f4be25e309ecaafff8700438a72/tenor.gif", - "https://media1.tenor.com/images/be0c22e0af951aa7fa8753381663eb2c/tenor.gif?itemid=15824856" - ], - "de": [ - "https://media1.tenor.com/images/da8f0e8dd1a7f7db5298bda9cc648a9a/tenor.gif?itemid=12018819", - "https://media1.tenor.com/images/f5176d4c5cbb776e85af5dcc5eea59be/tenor.gif?itemid=5081286", - "https://media.tenor.com/images/0e5b7f4be25e309ecaafff8700438a72/tenor.gif", - "https://media1.tenor.com/images/be0c22e0af951aa7fa8753381663eb2c/tenor.gif?itemid=15824856" + "https://scnx-cdn.scootkit.net/1744619869697-AYVUENwLWjusxCOKvJLOnpdSiiiQZJC2dmSwnHMSOLr7eLbH.gif", + "https://scnx-cdn.scootkit.net/1744619643063-Iw3QdOJ9LsQLKv3Moe3zvMfakKu0NVfqlrmmd2ssrBqLEJai.gif", + "https://scnx-cdn.scootkit.net/1671631825485-6eaH1p3ngebQigoVjBicgaRy.gif", + "https://scnx-cdn.scootkit.net/1744619413990-auYiCEqSxZnp2QldAOgav77oVb2EiXnPS83icTlX7AkV1JzV.gif" ] }, "description": { - "de": "Bilder aus welchen, wenn jemand /pat ausführt, zufällig ausgewählt wird" + "de": "Bilder aus welchen, wenn jemand /pat ausführt, zufällig ausgewählt wird", + "en": "Images that one will be randomly selected from when someone uses /pat." }, "type": "array", "content": "imgURL" @@ -377,22 +341,25 @@ { "name": "8ballMessage", "humanName": { - "de": "8ball-Nachricht" + "de": "8ball-Nachricht", + "en": "8ball Message" }, "default": { - "en": "%answer%", + "en": "The oracle has spoken... %answer%", "de": "Das Orakel hat gesprochen... %answer%" }, "description": { - "de": "Nachricht, welche gesendet wird, wenn jemand /random 8ball benutzt" + "de": "Nachricht, welche gesendet wird, wenn jemand /random 8ball benutzt", + "en": "Message that gets send when someone uses /random 8ball" }, "type": "string", "allowEmbed": true, "params": [ { - "name": "The oracle has spoken... answer", + "name": "%answer", "description": { - "en": "Answer to the question" + "en": "Answer to the question", + "de": "Antwort auf die Frage" } } ] @@ -400,26 +367,28 @@ { "name": "8BallMessages", "humanName": { - "de": "8ball-Antworten" + "de": "8ball-Antworten", + "en": "8ball responses" }, "default": { "en": [ - "Yes", + "", "No", "Maybe", "Try again", "42 is the answer" ], "de": [ - "Yes", - "No", - "Maybe", - "Try again", - "42 is the answer" + "Ja", + "Nein", + "Vielleicht", + "Bitte versuche es erneut", + "42 ist die Antwort" ] }, "description": { - "de": "Mögliche Antworten für /random 8ball" + "de": "Mögliche Antworten für /random 8ball", + "en": "Possible answers for /random 8ball" }, "type": "array", "content": "string" diff --git a/modules/giveaways/commands/giveaway.js b/modules/giveaways/commands/giveaway.js deleted file mode 100644 index 1093aa30..00000000 --- a/modules/giveaways/commands/giveaway.js +++ /dev/null @@ -1,196 +0,0 @@ -const {truncate} = require('../../../src/functions/helpers'); -const {createGiveaway, endGiveaway} = require('../giveaways'); -const durationParser = require('parse-duration'); -const {localize} = require('../../../src/functions/localize'); - -module.exports.subcommands = { - 'start': async function (interaction) { - if (interaction.options.getString('duration') === 0) return interaction.reply({ - ephemeral: true, - content: '⚠️ ' + localize('giveaways', 'duration-parsing-failed') - }); - if (interaction.options.getChannel('channel').type !== 'GUILD_TEXT' && interaction.options.getChannel('channel').type !== 'GUILD_NEWS') return interaction.reply({ - ephemeral: true, - content: '⚠️ ' + localize('giveaways', 'duration-parsing-failed') - }); - if (interaction.options.getInteger('winner-count') < 1 || interaction.options.getString('prize').length < 2) return interaction.reply({ - ephemeral: true, - content: '⚠️ ' + localize('giveaways', 'parameter-parsing-failed') - }); - const requirements = []; - if (interaction.options.getInteger('required-messages')) requirements.push({ - type: 'messages', - messageCount: interaction.options.getInteger('required-messages') - }); - if (interaction.options.getRole('required-role')) requirements.push(({ - type: 'roles', - roles: [interaction.options.getRole('required-role').id] - })); - await createGiveaway(interaction.options.getUser('sponsor') || interaction.user, interaction.options.getChannel('channel'), interaction.options.getString('prize'), new Date(durationParser(interaction.options.getString('duration') + new Date().getTime())), interaction.options.getInteger('winner-count'), requirements, interaction.options.getString('sponsorlink')); - interaction.reply({ - ephemeral: true, - content: localize('giveaways', 'started-successfully', {c: interaction.options.getChannel('channel').toString()}) - }); - }, - 'reroll': async function (interaction) { - const giveaway = await interaction.client.models['giveaways']['Giveaway'].findOne({ - where: {messageID: interaction.options.getString('msg-id', true)} - }); - if (!giveaway) return interaction.reply({ - ephemeral: true, - content: '⚠️ ' + localize('giveaways', 'no-giveaways-found') - }); - await endGiveaway(giveaway.id, null, false, interaction.options.getInteger('winner-count')); - await interaction.reply({ - ephemeral: true, - content: localize('giveaways', 'reroll-done') - }); - }, - 'end': async function (interaction) { - const giveaway = await interaction.client.models['giveaways']['Giveaway'].findOne({ - where: {messageID: interaction.options.getString('msg-id', true)} - }); - if (!giveaway) return interaction.reply({ - ephemeral: true, - content: '⚠️ ' + localize('giveaways', 'no-giveaways-found') - }); - await endGiveaway(giveaway.id, null, true); - await interaction.reply({ - ephemeral: true, - content: localize('giveaways', 'giveaway-ended-successfully') - }); - } -}; - -module.exports.autoComplete = { - 'end': { - 'msg-id': autoCompleteMsgID - }, - 'reroll': { - 'msg-id': autoCompleteMsgID - } -}; - -/** - * @private - * Runs auto complete on the msg-id option - * @param {Interaction} interaction - * @return {Promise} - */ -async function autoCompleteMsgID(interaction) { - const giveaways = await interaction.client.models['giveaways']['Giveaway'].findAll({ - where: { - ended: !(interaction.options['_subcommand'] === 'end') - }, - order: [['createdAt', 'DESC']], - limit: 25 - }); - const matches = []; - interaction.value = interaction.value.toLowerCase(); - for (const match of giveaways.filter(g => g.messageID.includes(interaction.value) || g.prize.toLowerCase().includes(interaction.value) || ((interaction.client.guild.channels.cache.get(g.channelID) || {name: g.channelID}).name).includes(interaction.value))) { - matches.push({ - value: match.messageID, - name: truncate(`${(interaction.client.guild.channels.cache.get(match.channelID) || {name: match.channelID}).name}: ${match.prize}`, 100) - }); - } - interaction.respond(matches); -} - -module.exports.config = { - name: 'gmanage', - defaultMemberPermissions: ['MANAGE_MESSAGES'], - description: localize('giveaways', 'gmanage-description'), - - options: [ - { - type: 'SUB_COMMAND', - name: 'start', - description: localize('giveaways', 'gmanage-start-description'), - options: [ - { - type: 'CHANNEL', - name: 'channel', - required: true, - channelTypes: ['GUILD_TEXT', 'GUILD_NEWS'], - description: localize('giveaways', 'gmanage-channel-description') - }, - { - type: 'STRING', - name: 'prize', - required: true, - description: localize('giveaways', 'gmanage-price-description') - }, - { - type: 'STRING', - name: 'duration', - required: true, - description: localize('giveaways', 'gmanage-duration-description') - }, - { - type: 'INTEGER', - name: 'winner-count', - required: true, - description: localize('giveaways', 'gmanage-winnercount-description') - }, - { - type: 'INTEGER', - name: 'required-messages', - required: false, - description: localize('giveaways', 'gmanage-requiredmessages-description') - }, - { - type: 'ROLE', - name: 'required-role', - required: false, - description: localize('giveaways', 'gmanage-requiredroles-description') - }, - { - type: 'USER', - name: 'sponsor', - required: false, - description: localize('giveaways', 'gmanage-sponsor-description') - }, - { - type: 'STRING', - name: 'sponsorlink', - required: false, - description: localize('giveaways', 'gmanage-sponsorlink-description') - } - ] - }, - { - type: 'SUB_COMMAND', - name: 'end', - description: localize('giveaways', 'gend-description'), - options: [ - { - type: 'STRING', - name: 'msg-id', - required: true, - autocomplete: true, - description: localize('giveaways', 'gereroll-msgid-description') - } - ] - }, - { - type: 'SUB_COMMAND', - name: 'reroll', - description: localize('giveaways', 'gereroll-description'), - options: [ - { - type: 'STRING', - name: 'msg-id', - required: true, - autocomplete: true, - description: localize('giveaways', 'gereroll-msgid-description') - }, - { - type: 'INTEGER', - name: 'winner-count', - required: false, - description: localize('giveaways', 'gereroll-winnercount-description') - } - ] - } - ] -}; \ No newline at end of file diff --git a/modules/giveaways/commands/gmessages.js b/modules/giveaways/commands/gmessages.js deleted file mode 100644 index c48a1b37..00000000 --- a/modules/giveaways/commands/gmessages.js +++ /dev/null @@ -1,34 +0,0 @@ -const {localize} = require('../../../src/functions/localize'); -module.exports.run = async function (interaction) { - const giveaways = await interaction.client.models['giveaways']['Giveaway'].findAll({ - where: { - ended: false, - countMessages: true - }, - order: [['createdAt', 'DESC']], - limit: 15 - }); - if (giveaways.length === 0) return interaction.reply({ - ephemeral: true, - content: '⚠️ ' + localize('giveaways', 'no-giveaways-found') - }); - let gwMessages = ''; - for (const giveaway of giveaways) { - const channel = interaction.channel.guild.channels.cache.get(giveaway.channelID); - if (!channel) continue; - const message = await channel.messages.fetch(giveaway.messageID).catch(() => { - }); - if (!message) continue; - gwMessages = gwMessages + `[${giveaway.prize}](${message.url} "${localize('giveaways', 'jump-to-message-hover')}") in ${channel.toString()}: ${giveaway.messageCount[interaction.user.id] || 0}/${giveaway.requirements.find(r => r.type === 'messages').messageCount} ${localize('giveaways', 'messages')}`; - } - interaction.reply({ - ephemeral: true, - content: `**${localize('giveaways', 'giveaway-messages')}**\n\n${gwMessages}` - }); -}; - -module.exports.config = { - name: 'gmessages', - description: localize('giveaways', 'gmessages-description'), - defaultPermission: true -}; \ No newline at end of file diff --git a/modules/giveaways/configs/config.json b/modules/giveaways/configs/config.json deleted file mode 100644 index b9cf1977..00000000 --- a/modules/giveaways/configs/config.json +++ /dev/null @@ -1,157 +0,0 @@ -{ - "description": { - "en": "Configure the function of the module here", - "de": "Stelle hier die Funktionen des Modules ein" - }, - "humanName": { - "en": "Configuration", - "de": "Konfiguration" - }, - "filename": "config.json", - "commandsWarnings": { - "normal": [ - "/gmanage" - ] - }, - "content": [ - { - "name": "bypassRoles", - "humanName": { - "en": "Giveaway-Requirement-Bypass-Roles", - "de": "Gewinnspiel-Voraussetzungen-Ignorierung-Rollen" - }, - "default": { - "en": [], - "de": [] - }, - "description": { - "en": "Roles who can participate in giveaways even if they don't meet the requirements", - "de": "Rollen, die an Gewinnspielen teilnehmen können, ohne die Bedingungen erfüllen zu müssen" - }, - "type": "array", - "content": "roleID" - }, - { - "name": "messageCountMode", - "humanName": { - "en": "Message-Count-Mode", - "de": "Nachrichten-Zähl-Modus" - }, - "default": { - "en": "all", - "de": "all" - }, - "description": { - "en": "Modus in which messages should get counted", - "de": "Modus, in welchem Nachrichten gezählt werden sollen" - }, - "type": "select", - "content": [ - "all", - "blacklist", - "whitelist" - ] - }, - { - "name": "blacklist", - "humanName": { - "en": "Blacklist" - }, - "default": { - "en": [], - "de": [] - }, - "description": { - "en": "Channel in which messages should get ignored (only if messageCountMode = \"blacklist\")", - "de": "Channel in welchen Nachrichten nicht gezählt werden sollen (nur wenn messageCountMode = \"blacklist\")" - }, - "type": "array", - "content": "channelID" - }, - { - "name": "whitelist", - "humanName": { - "en": "Whitelist" - }, - "default": { - "en": [], - "de": [] - }, - "description": { - "en": "Channel in which messages should get counted (only if messageCountMode = \"whitelist\")", - "de": "Channel in welchen Nachrichten gezählt werden sollen (nur wenn messageCountMode = \"whitelist\")" - }, - "type": "array", - "content": "channelID" - }, - { - "name": "multipleEntries", - "humanName": { - "en": "Multiple Entries", - "de": "Zusätzliche Gewinnchancen" - }, - "default": { - "en": {}, - "de": {} - }, - "description": { - "en": "Allow certain users with a specified role to enter multiple times.\n⚠️ Please remember that allowing multiple entries for users who invited other users is against Discord's Terms of Service", - "de": "Erlaubt es, Nutzern mit einer bestimmten Rollen mehre Gewinnchancen zu geben.\n⚠️ Please remember that allowing multiple entries for users who invited other users is against Discord's Terms of Service" - }, - "type": "keyed", - "content": { - "key": "roleID", - "value": "integer" - } - }, - { - "name": "entryDeniedRoles", - "humanName": { - "en": "Entry denied roles", - "de": "Teilnahme verboten Rollen" - }, - "default": { - "en": [], - "de": [] - }, - "description": { - "en": "Members with these roles won't be able to join your giveaway.", - "de": "Mitglieder mit diesen Rollen werden nicht an Gewinnspielen teilnehmen können." - }, - "type": "array", - "content": "roleID" - }, - { - "name": "winRoles", - "humanName": { - "en": "Win roles", - "de": "Gewinner Rollen" - }, - "default": { - "en": [], - "de": [] - }, - "description": { - "en": "These roles will be assigned to the winners of giveaways, regardless of the giveaway price. These role will not be removed when rerolling winners.", - "de": "Rollen, die an die Gewinner von Gewinnspielen vergeben wird, egal was der Preis des Gewinnspiels ist. Die Rolle wird beim erneuten Auslösen nicht entfernt." - }, - "type": "array", - "content": "roleID" - }, - { - "name": "sendDMOnWin", - "humanName": { - "en": "Send DM-message to winner", - "de": "PN-Nachricht an Gewinner senden" - }, - "default": { - "en": false - }, - "description": { - "en": "If enabled the bot will send each winner a DM when they win.", - "de": "Wenn aktiviert wird der Bot eine Nachricht an den Gewinner senden, wenn diese gewinnen." - }, - "type": "boolean" - } - ] -} \ No newline at end of file diff --git a/modules/giveaways/configs/strings.json b/modules/giveaways/configs/strings.json deleted file mode 100644 index 3dd3d30a..00000000 --- a/modules/giveaways/configs/strings.json +++ /dev/null @@ -1,529 +0,0 @@ -{ - "description": { - "en": "Edit the messages and strings of the module here", - "de": "Stelle hier die Nachrichten des Modules ein" - }, - "humanName": { - "en": "Nachrichten", - "de": "Nachrichten" - }, - "filename": "strings.json", - "content": [ - { - "name": "giveaway_message", - "humanName": { - "en": "Giveaway-Message", - "de": "Gewinnspiel-Nachricht" - }, - "default": { - "en": { - "title": "Neues Gewinnspiel 🎉", - "description": "**Prize**: %prize%\n**Winners**: %winners%\n**Organiser**: %organiser%\n**Sponsor-Website**: <%sponsorLink%>\n\n**Currently valid entries**: %entryCount% (%enteredCount% users)\n**Ends at**: %endAtDiscordFormation%\n\nPress the big button below to participate!", - "color": "GREEN" - }, - "de": { - "title": "New Giveaway 🎉", - "description": "**Preis**: %prize%\n**Anzahl Gewinner**: %winners%\n**Veranstalter**: %organiser%\n**Sponsor-Webseite**: <%sponsorLink%>\n\n**Gültige Teilnahmen**: %entryCount% (%enteredCount% Nutzer)\n**Endet am**: %endAtDiscordFormation%\n\nKlicke auf den Knopf unten, um teilzunehmen!", - "color": "GREEN" - } - }, - "description": { - "en": "Message that gets send in the giveaway channel if a new giveaway gets created", - "de": "Diese Nachricht wird verschickt, wenn ein Gewinnspiel gestartet wird." - }, - "type": "string", - "allowEmbed": true, - "params": [ - { - "name": "prize", - "description": { - "en": "Prize of the giveaway", - "de": "Preis des Gewinnspiels" - } - }, - { - "name": "endAtDiscordFormation", - "description": { - "en": "When using this variable, Discord will automatically format the timestamp in the client of the user", - "de": "Beim Nutzen dieser Variable wird Discord direkt beim Nutzer die Zeit rendern (Beispiel: \"In 4 Stunden\")" - } - }, - { - "name": "endAt", - "description": { - "en": "Date of the end of the giveaway", - "de": "Datum und Uhrzeit, wenn das Gewinnspiel endet" - } - }, - { - "name": "winners", - "description": { - "en": "Count of possible winners", - "de": "Anzahl möglicher Gewinner" - } - }, - { - "name": "sponsorLink", - "description": { - "en": "Link of the sponsor, if specified.", - "de": "Link des Sponsoren, wenn angegeben." - } - }, - { - "name": "organiser", - "description": { - "en": "Mention of the organiser of the giveaway", - "de": "Erwähnung des Veranstalters des Gewinnspieles" - } - }, - { - "name": "enteredCount", - "description": { - "en": "Count of users who entered this giveaway already", - "de": "Anzahl an Teilnehmern am Gewinnspiel" - } - }, - { - "name": "entryCount", - "description": { - "en": "Count of valid entries", - "de": "Anzahl von gültigen Teilnahmen" - } - } - ] - }, - { - "name": "giveaway_message_with_requirements", - "humanName": { - "en": "Giveaway-Message with requirements", - "de": "Gewinnspiel-Nachricht mit Voraussetzungen" - }, - "default": { - "en": { - "title": "New Giveaway 🎉", - "description": "Prize: %prize%\nEnds at: %endAtDiscordFormation%\nWinners: %winners%\nOrganiser: %organiser%\nSponsor-Website: %sponsorLink%\n\n__Requirements__\n%requirements%\n\nCurrently valid entries: %entryCount% (%enteredCount% users)\nPress the big button under this message to join the giveaway!", - "color": "GREEN" - }, - "de": { - "title": "Neues Gewinnspiel 🎉", - "description": "Preis: %prize%\nLäuft bis: %endAtDiscordFormation%\nAnzahl Gewinner: %winners%\nVeranstalter: %organiser%\nSponsor-Website: %sponsorLink%\n__3aussetzungen__\n%requirements%\nGültige Teilnahmen: %entryCount% (von %enteredCount% Teilnehmern)\n\nDrücke auf den großen Knopf unten, um teilzunehmen.", - "color": "GREEN" - } - }, - "description": { - "en": "Message that gets send in the giveaway channel if a new giveaway gets created", - "de": "Diese Nachricht wird in den Gewinnspiel-Channel versendet, wenn ein Gewinnspiel mit Voraussetzungen gestartet wird" - }, - "type": "string", - "allowEmbed": true, - "params": [ - { - "name": "prize", - "description": { - "en": "Prize of the giveaway", - "de": "Preis des Gewinnspiels" - } - }, - { - "name": "endAtDiscordFormation", - "description": { - "en": "When using this variable, Discord will automatically format the timestamp in the client of the user", - "de": "Beim Nutzen dieser Variable wird Discord direkt beim Nutzer die Zeit rendern (Beispiel: \"In 4 Stunden\")" - } - }, - { - "name": "endAt", - "description": { - "en": "Date of the end of the giveaway", - "de": "Datum und Uhrzeit, wenn das Gewinnspiel endet" - } - }, - { - "name": "winners", - "description": { - "en": "Count of possible winners", - "de": "Anzahl möglicher Gewinner" - } - }, - { - "name": "sponsorLink", - "description": { - "en": "Link of the sponsor, if specified.", - "de": "Link des Sponsoren, wenn angegeben." - } - }, - { - "name": "organiser", - "description": { - "en": "Mention of the organiser of the giveaway", - "de": "Erwähnung des Veranstalters des Gewinnspieles" - } - }, - { - "name": "enteredCount", - "description": { - "en": "Count of users who entered this giveaway already", - "de": "Anzahl an Teilnehmern am Gewinnspiel" - } - }, - { - "name": "entryCount", - "description": { - "en": "Count of valid entries", - "de": "Anzahl von gültigen Teilnahmen" - } - }, - { - "name": "requirements", - "description": { - "en": "Requirements for this giveaway", - "de": "Voraussetzungen für dieses Gewinnspiel" - } - } - ] - }, - { - "name": "requirementsNotPassed", - "humanName": { - "en": "Requirement-Not-Passed-Message", - "de": "Gewinnspiel-Voraussetzungen-Nicht-Erfüllt-Nachricht" - }, - "default": { - "en": "I am sorry but you did not pass the requirement-check for this giveaway.\nYou need to fulfill these requirements:\n%requirements%", - "de": "Huch, scheint als würdest du die Voraussetzungen für dieses Gewinnspiel nicht erfüllen.\nDu musst folgende Voraussetzungen noch erfüllen:\n%requirements%" - }, - "description": { - "en": "Message that will be displayed to users when they try to join a giveaway even when they do not meet the requirements.", - "de": "Nachrichten, die angezeigt wird, wenn ein Nutzer an einem Gewinnspiel teilnehmen will, obwohl er die Bedingungen zur Teilnahme nicht erfüllt." - }, - "type": "string", - "allowEmbed": "true", - "params": [ - { - "name": "requirements", - "description": { - "en": "Requirements of this giveaway", - "de": "Voraussetzungen des Gewinnspieles, die der Nutzer noch erfüllen muss" - } - } - ] - }, - { - "name": "giveaway_message_edit_after_winning", - "humanName": { - "en": "Giveaway-Message after message ended", - "de": "Gewinnspiel-Nachricht nach Beendung des Gewinnspiels" - }, - "default": { - "en": { - "title": "Giveaway ended", - "description": "Price: %price%\nEnded at: %endAtDiscordFormation%\nWinners: %winners%\nCurrently valid entries: %entryCount% (%enteredCount% users)\nOrganiser: %organiser%\nSponsor-Website: %sponsorLink%", - "color": "RED" - }, - "de": { - "title": "Gewinnspiel beended", - "description": "Price: Preis: %prize%\nEnddatum: %endAtDiscordFormation%\nGewinner: %winners%\nVeranstalter: %organiser%\nGültige Teilnahmen: %entryCount% (von %enteredCount% Nutzern)\nSponsor-Website: %sponsorLink", - "color": "RED" - } - }, - "description": { - "en": "Message that gets send after a giveaway ended", - "de": "Wenn ein Gewinnspiel endet wird die Gewinnspiel-Nachricht zu dieser Nachricht editiert." - }, - "type": "string", - "allowEmbed": true, - "params": [ - { - "name": "prize", - "description": { - "en": "Prize of the giveaway", - "de": "Preis des Gewinnspieles" - } - }, - { - "name": "sponsorLink", - "description": { - "en": "Link of the sponsor, if specified.", - "de": "Link des Sponsoren, wenn angegeben." - } - }, - { - "name": "endAtDiscordFormation", - "description": { - "en": "When using this variable, Discord will automatically format the timestamp in the client of the user", - "de": "Beim Nutzen dieser Variable wird Discord direkt beim Nutzer die Zeit rendern (Beispiel: \"In 4 Stunden\")" - } - }, - { - "name": "endAt", - "description": { - "en": "Date of the end of the giveaway", - "de": "End-Datum des Gewinnspieles" - } - }, - { - "name": "winners", - "description": { - "en": "Winners of this giveaway", - "de": "Gewinner des Gewinnspieles" - } - }, - { - "name": "organiser", - "description": { - "en": "Mention of the organiser of the giveaway", - "de": "Erwähnung des Veranstalters" - } - }, - { - "name": "enteredCount", - "description": { - "en": "Count of users who entered this giveaway already", - "de": "Anzahl von Teilnahmen" - } - }, - { - "name": "entryCount", - "description": { - "en": "Count of valid entries", - "de": "Anzahl von Teilnehmern" - } - } - ] - }, - { - "name": "winner_message", - "humanName": { - "en": "Win-Message", - "de": "Gewinn-Nachricht" - }, - "default": { - "en": "%winners% won this giveaway. Shoot a DM at %organiser% to claim your prize!", - "de": "%winners% haben **%prize%** in folgendem Gewinnspiel gewonnen: %url%. Schreib %organiser% eine PN, um den Preis zu erhalten!" - }, - "description": { - "en": "Message that gets send when the giveaway ends.", - "de": "Diese Nachricht wird verschickt, wenn das Gewinnspiel endet" - }, - "type": "string", - "allowEmbed": true, - "params": [ - { - "name": "winners", - "description": { - "en": "Winners of the giveaway", - "de": "Gewinner des Gewinnspieles" - } - }, - { - "name": "sponsorLink", - "description": { - "en": "Link of the sponsor, if specified.", - "de": "Link des Sponsoren, wenn angegeben." - } - }, - { - "name": "organiser", - "description": { - "en": "Mention of the organiser of the giveaway", - "de": "Erwähnung des Veranstalters des Gewinnspieles" - } - }, - { - "name": "prize", - "description": { - "en": "Prize of the giveaway", - "de": "Preis des Gewinnspieles" - } - } - ] - }, - { - "name": "no_winner_message", - "humanName": { - "en": "No-Winner-Message", - "de": "Kein-Gewinner-Nachricht" - }, - "default": { - "en": "No winner could be determined for this giveaway ):", - "de": "Dieses Gewinnspiel hatte keinen Gewinner ): ):" - }, - "description": { - "en": "Message that gets send when the giveaway ends and no winner could be determined.", - "de": "Diese Nachricht wird gesendet, wenn ein Gewinnspiel endet, aber kein Gewinner gefunden wurde" - }, - "type": "string", - "allowEmbed": true, - "params": [ - { - "name": "sponsorLink", - "description": { - "en": "Link of the sponsor, if specified.", - "de": "Link zum Sponsor, falls angeben" - } - }, - { - "name": "organiser", - "description": { - "en": "Mention of the organiser of the giveaway", - "de": "Erwähnung des Veranstalters" - } - }, - { - "name": "prize", - "description": { - "en": "Prize of the giveaway", - "de": "Preis des Gewinnspiels" - } - } - ] - }, - { - "name": "confirmationMessage", - "humanName": { - "en": "Confirmation-Message", - "de": "Teilnahme-Bestätigung-Nachricht" - }, - "default": { - "en": "Giveaway entered successfully with **%entries% entry(s)**.", - "de": "Gewinnspiel mit **%entries% Teilnahme(n)** beigetreten." - }, - "description": { - "en": "Message that gets shown to the user after they enter the giveaway successfully.", - "de": "Nachricht die angezeigt wird, wenn ein Nutzer teilnimmt" - }, - "type": "string", - "allowEmbed": true, - "params": [ - { - "name": "price", - "description": { - "en": "Price of the giveaway", - "de": "Preis des Gewinnspiels" - } - }, - { - "name": "entries", - "description": { - "en": "Count of entries the user has to this giveaway", - "de": "Anzahl an Teilnahmen am Gewinnspiel" - } - } - ] - }, - { - "name": "alreadyEnteredMessage", - "humanName": { - "en": "Already Entered Message", - "de": "Bereits teilgenommen-Nachricht" - }, - "default": { - "en": "You are already in this giveaway with **%entries% entry(s)**.", - "de": "Du nimmst bereits mit **%entries% Teilnahme(n)** teil." - }, - "description": { - "en": "Message that gets shown to the user when someone tries to enter when they already are in", - "de": "Nachricht, die angezeigt wird, wenn ein Nutzer teilnehmen will, obwohl er bereits teilgenommen hat" - }, - "type": "string", - "allowEmbed": true, - "params": [ - { - "name": "price", - "description": { - "en": "Price of the giveaway", - "de": "Preis des Gewinnspiels" - } - }, - { - "name": "entries", - "description": { - "en": "Count of entries the user has to this giveaway", - "de": "Anzahl an Teilnahmen am Gewinnspiel" - } - } - ] - }, - { - "name": "deniedRoleMessage", - "humanName": { - "en": "User has forbidden roles message", - "de": "Nutzer mit verbotenen Rollen Nachricht" - }, - "default": { - "en": "⚠\uFE0F You can't participate in giveaways on this server because you have one or more forbidden roles.", - "de": "⚠\uFE0F Du kannst an keinem Gewinnspiel auf diesem Server teilnehmen, da du eine oder mehrere Rollen hast, die die Teilnahme verbieten." - }, - "description": { - "en": "Message that users with one of the configured entry denied roles will see when they try to join a giveaway.", - "de": "Nachricht, die angezeigt wird, wenn ein Nutzer teilnehmen will, obwohl er eine Rolle hat, die nicht teilnehmen dürfen" - }, - "type": "string", - "allowEmbed": true, - "params": [] - }, - { - "name": "buttonContent", - "humanName": { - "en": "Button-Content", - "de": "Knopf-Inhalt" - }, - "default": { - "en": "Join giveaway 🎉", - "de": "Gewinnspiel beitreten 🎉" - }, - "description": { - "en": "Content of the button under giveaways", - "de": "Inhalt des Teilnehmen-Knopfes" - }, - "type": "string" - }, - { - "name": "winner_DM_message", - "humanName": { - "en": "Winner-DM-Message", - "de": "Gewinner-PN-Nachricht" - }, - "default": { - "en": "Congrats, you won this giveaway: %url%", - "de": "Herzlichen Glückwunsch, du hast folgendes Gewinnspiel gewonnen: %url%" - }, - "description": { - "en": "Nachricht, die an den Nutzer gesendet wird, wenn er gewinnt (wenn aktiviert).", - "de": "Message that gets send when to the winner when they win (if enabled)." - }, - "type": "string", - "allowEmbed": true, - "params": [ - { - "name": "sponsorLink", - "description": { - "en": "Link of the sponsor, if specified.", - "de": "Link des Sponsoren, wenn angegeben." - } - }, - { - "name": "organiser", - "description": { - "en": "Mention of the organiser of the giveaway", - "de": "Erwähnung des Veranstalters des Gewinnspieles" - } - }, - { - "name": "prize", - "description": { - "en": "Prize of the giveaway", - "de": "Preis des Gewinnspiels" - } - }, - { - "name": "url", - "description": { - "en": "Url to the giveaway", - "de": "Url zum Gewinnspiel" - } - } - ] - } - ] -} \ No newline at end of file diff --git a/modules/giveaways/events/botReady.js b/modules/giveaways/events/botReady.js deleted file mode 100644 index 90a86b5e..00000000 --- a/modules/giveaways/events/botReady.js +++ /dev/null @@ -1,30 +0,0 @@ -const {endGiveaway} = require('../giveaways'); -const {scheduleJob} = require('node-schedule'); -const {localize} = require('../../../src/functions/localize'); - -module.exports.run = async (client) => { - // Migration - const dbVersion = await client.models['DatabaseSchemeVersion'].findOne({where: {model: 'giveaways_Giveaway'}}); - if (!dbVersion) { - client.logger.info('[giveaways] ' + localize('giveaways', 'migration-happening')); - await client.models['giveaways']['Giveaway'].sync({force: true}); - client.logger.info('[giveaways] ' + localize('giveaways', 'migration-done')); - await client.models['DatabaseSchemeVersion'].create({model: 'giveaways_Giveaway', version: 'V1'}); - } - - const giveaways = await client.models['giveaways']['Giveaway'].findAll({ - where: { - ended: false - } - }); - for (const g of giveaways) { - if (parseInt(g.endAt) < new Date().getTime()) { - await endGiveaway(g.id, null, true); - continue; - } - const job = scheduleJob(new Date(parseInt(g.endAt)), async () => { - await endGiveaway(g.id, job, true); - }); - client.jobs.push(job); - } -}; \ No newline at end of file diff --git a/modules/giveaways/events/interactionCreate.js b/modules/giveaways/events/interactionCreate.js deleted file mode 100644 index 8685f96a..00000000 --- a/modules/giveaways/events/interactionCreate.js +++ /dev/null @@ -1,154 +0,0 @@ -const {calculateUserEntries, checkRequirements} = require('../giveaways'); -const {embedType, formatDate} = require('../../../src/functions/helpers'); -const {localize} = require('../../../src/functions/localize'); - -const toBeProcessed = []; - -exports.run = async (client, interaction) => { - if (!interaction.client.botReadyAt) return; - if (!interaction.isButton()) return; - if (!interaction.customId.startsWith('giveaway-l') && interaction.customId !== 'giveaway') return; - await interaction.deferReply({ephemeral: true}); - toBeProcessed.push(interaction); - startProcessing(); -}; - -let processing = false; - -/** - * This is here to prevent race conditions leading to unregistered entries. It's bad I know, but it gets the job done. I should rewrite the whole system - */ -async function startProcessing() { - if (processing) return; - for (const k in toBeProcessed) { - await processReply(toBeProcessed[k]); - delete toBeProcessed[k]; - } - processing = false; - if (toBeProcessed.filter(f => f !== null).length !== 0) await startProcessing(); -} - -async function processReply(interaction) { - const client = interaction.client; - const moduleStrings = interaction.client.configurations['giveaways']['strings']; - if (interaction.customId.startsWith('giveaway-l')) { - const giveaway = await client.models['giveaways']['Giveaway'].findOne({ - where: { - id: interaction.customId.replaceAll('giveaway-l-', '') - } - }); - if (!giveaway) return; - const entries = {...giveaway.entries}; - delete entries[interaction.user.id]; - giveaway.entries = {...entries}; - await giveaway.save(); - interaction.editReply({content: localize('giveaways', 'giveaway-left')}).then(() => { - }); - interaction.channel.messages.fetch(giveaway.messageID).then(m => updateGiveaway(giveaway, m).then(() => { - })); - - return; - } - if (interaction.customId !== 'giveaway') return; - - const giveaway = await client.models['giveaways']['Giveaway'].findOne({ - where: { - messageID: interaction.message.id - } - }); - - if (interaction.member.roles.cache.find(r => (interaction.client.configurations['giveaways']['config'].entryDeniedRoles || []).includes(r.id))) return interaction.editReply(embedType(moduleStrings['deniedRoleMessage'], {})); - - if (giveaway.requirements.length === 0) return await enterUser(); - - const [failedRequirements, notPassedRequirementsString] = await checkRequirements(interaction.member, giveaway); - if (failedRequirements) { - interaction.editReply(embedType(moduleStrings['requirementsNotPassed'], { - '%requirements%': notPassedRequirementsString - })); - } else await enterUser(); - - /** - * Enters this user to this giveaway - * @private - * @returns {Promise} - */ - async function enterUser() { - if (giveaway.entries[interaction.user.id]) return interaction.editReply(embedType(moduleStrings.alreadyEnteredMessage, { - '%price%': giveaway.price, - '%entries%': calculateUserEntries(interaction.member) - }, { - components: [{ - type: 'ACTION_ROW', - components: [{ - type: 'BUTTON', - style: 'DANGER', - label: 'Leave giveaway', - customId: `giveaway-l-${giveaway.id}` - }] - }] - })); - const entries = giveaway.entries; - giveaway.entries = {}; // Thx sequelize - entries[interaction.user.id] = calculateUserEntries(interaction.member); - giveaway.entries = entries; - await giveaway.save(); - interaction.editReply(embedType(moduleStrings.confirmationMessage, { - '%price%': giveaway.price, - '%entries%': calculateUserEntries(interaction.member) - })).then(() => { - }); - - interaction.channel.messages.fetch(giveaway.messageID).then(m => updateGiveaway(giveaway, m).then(() => { - })); - } - - async function updateGiveaway(giveaway, message) { - const enteredUsers = []; - let totalEntries = 0; - for (const userID in giveaway.entries) { - totalEntries = totalEntries + giveaway.entries[userID]; - if (!enteredUsers.includes(userID)) enteredUsers.push(userID); - } - const components = [{ - type: 'ACTION_ROW', - components: [{type: 'BUTTON', label: moduleStrings.buttonContent, style: 'PRIMARY', customId: 'giveaway'}] - }]; - const endAt = new Date(parseInt(giveaway.endAt)); - - if (giveaway.requirements.length !== 0) { - let requirementString = ''; - giveaway.requirements.forEach((r) => { - if (r.type === 'messages') requirementString = requirementString + `• ${localize('giveaways', 'required-messages', {mc: r.messageCount})}\n`; - if (r.type === 'roles') { - let rolesString = ''; // Surely there is a better way to to this kind of stuff, but I am to stupid to find it - r.roles.forEach(rID => rolesString = rolesString + `<@&${rID}> `); - requirementString = rolesString + `• ${localize('giveaways', 'roles-required', {r: rolesString})}\n`; - } - }); - - await message.edit(embedType(moduleStrings['giveaway_message_with_requirements'], { - '%prize%': giveaway.prize, - '%winners%': giveaway.winnerCount, - '%requirements%': requirementString, - '%sponsorLink%': giveaway.sponsorWebsite || localize('giveaways', 'no-link'), - '%endAt%': formatDate(endAt), - '%endAtDiscordFormation%': ``, - '%organiser%': `<@${giveaway.organiser}>`, - '%entryCount%': interaction.channel.type === 'GUILD_NEWS' ? localize('giveaways', 'not-supported-for-news-channel') : totalEntries, - '%enteredCount%': interaction.channel.type === 'GUILD_NEWS' ? localize('giveaways', 'not-supported-for-news-channel') : enteredUsers.length - }, {components})); - } else { - await message.edit(embedType(moduleStrings['giveaway_message'], { - '%prize%': giveaway.prize, - '%winners%': giveaway.winnerCount, - '%endAtDiscordFormation%': ``, - '%endAt%': formatDate(endAt), - '%sponsorLink%': giveaway.sponsorWebsite || localize('giveaways', 'no-link'), - '%organiser%': `<@${giveaway.organiser}>`, - '%entryCount%': interaction.channel.type === 'GUILD_NEWS' ? localize('giveaways', 'not-supported-for-news-channel') : totalEntries, - '%enteredCount%': interaction.channel.type === 'GUILD_NEWS' ? localize('giveaways', 'not-supported-for-news-channel') : enteredUsers.length - }, {components})); - } - } -} \ No newline at end of file diff --git a/modules/giveaways/events/messageCreate.js b/modules/giveaways/events/messageCreate.js deleted file mode 100644 index 0137f6c3..00000000 --- a/modules/giveaways/events/messageCreate.js +++ /dev/null @@ -1,31 +0,0 @@ -module.exports.run = async function (client, message) { - if (!client.botReadyAt) return; - if (!message.guild) return; - if (message.author.bot) return; - if (message.guild.id !== client.config.guildID) return; - const config = client.configurations['giveaways']['config']; - - if (!config.blacklist) config.blacklist = []; - if (!config.whitelist) config.blacklist = []; - if (!config.messageCountMode) config.messageCountMode = 'all'; - if (config.messageCountMode === 'blacklist' && config.blacklist.includes(message.channel.id)) return; - if (config.messageCountMode === 'whitelist' && !config.whitelist.includes(message.channel.id)) return; - - const giveaways = await client.models['giveaways']['Giveaway'].findAll({ - where: { - ended: false, - countMessages: true - } - }); - - for (const giveaway of giveaways) { - if (giveaway.requirements.find(r => r.type === 'messages')) { - const messages = giveaway.messageCount; - giveaway.messageCount = null; - if (!messages[message.author.id]) messages[message.author.id] = 0; - messages[message.author.id] = (parseInt(messages[message.author.id]) + 1).toString(); - giveaway.messageCount = messages; - await giveaway.save(); - } - } -}; \ No newline at end of file diff --git a/modules/giveaways/giveaways.js b/modules/giveaways/giveaways.js deleted file mode 100644 index 781776e6..00000000 --- a/modules/giveaways/giveaways.js +++ /dev/null @@ -1,276 +0,0 @@ -/** - * Manages giveaways - * @module Giveaways - * @author Simon Csaba - */ -const {formatDate, randomElementFromArray} = require('../../src/functions/helpers'); -const {scheduleJob} = require('node-schedule'); -const {embedType} = require('../../src/functions/helpers'); -const {localize} = require('../../src/functions/localize'); - -/** - * Create a new giveaway - * @param {User} organiser User who organized this giveaway - * @param {Channel} channel Channel in which this giveaway should take place - * @param {String} prize Prize which should be given away - * @param {Date} endAt Date on which the giveaway should end - * @param {Number} winners Count of winners the bot should select - * @param {Array} requirements Array of requirements - * @param {String} sponsorLink Link to the sponsor's website (if applicable) - * @returns {Promise} - */ -module.exports.createGiveaway = async function (organiser, channel, prize, endAt, winners, requirements = [], sponsorLink = null) { - const moduleStrings = channel.client.configurations['giveaways']['strings']; - let m; - const components = [{ - type: 'ACTION_ROW', - components: [{type: 'BUTTON', label: moduleStrings.buttonContent, style: 'PRIMARY', customId: 'giveaway'}] - }]; - if (requirements.length === 0) m = await channel.send(embedType(moduleStrings['giveaway_message'], { - '%prize%': prize, - '%winners%': winners, - '%endAtDiscordFormation%': ``, - '%endAt%': formatDate(endAt), - '%sponsorLink%': sponsorLink || localize('giveaways', 'no-link'), - '%organiser%': `<@${organiser.id}>`, - '%entryCount%': channel.type === 'GUILD_NEWS' ? localize('giveaways', 'not-supported-for-news-channel') : 0, - '%enteredCount%': channel.type === 'GUILD_NEWS' ? localize('giveaways', 'not-supported-for-news-channel') : 0 - }, {components})); - else { - let requirementString = ''; - requirements.forEach((r) => { - if (r.type === 'messages') requirementString = requirementString + `* ${localize('giveaways', 'required-messages', {mc: r.messageCount})}\n`; - if (r.type === 'roles') { - let rolesString = ''; // Surely there is a better way to to this kind of stuff, but I am to stupid to find it - r.roles.forEach(rID => rolesString = rolesString + `<@&${rID}> `); - requirementString = requirementString + `* ${localize('giveaways', 'roles-required', {r: rolesString})}\n`; - } - }); - m = await channel.send(embedType(moduleStrings['giveaway_message_with_requirements'], { - '%prize%': prize, - '%winners%': winners, - '%requirements%': requirementString, - '%sponsorLink%': sponsorLink || localize('giveaways', 'no-link'), - '%endAt%': formatDate(endAt), - '%endAtDiscordFormation%': ``, - '%organiser%': `<@${organiser.id}>`, - '%entryCount%': channel.type === 'GUILD_NEWS' ? localize('giveaways', 'not-supported-for-news-channel') : 0, - '%enteredCount%': channel.type === 'GUILD_NEWS' ? localize('giveaways', 'not-supported-for-news-channel') : 0 - }, {components})); - } - const dbItem = await channel.client.models['giveaways']['Giveaway'].create({ - endAt: endAt.getTime(), - winnerCount: winners, - prize: prize, - requirements: requirements, - countMessages: !!requirements.find(e => e.type === 'messages'), - messageCount: {}, - sponsorWebsite: sponsorLink, - organiser: organiser.id, - messageID: m.id, - channelID: channel.id - }); - const job = scheduleJob(endAt, async () => { - await endGiveaway(dbItem.id, job, true); - }); - channel.client.jobs.push(job); -}; - -/** - * Ends a giveaway - * @param {Number} gID ID of the giveaway to end - * @param {Job} job Job which should get canceled after the giveaway ends - * @param {Boolean} checkIfGiveawayEnded If enabled the function will return early when this giveaway already ended - * @param {Number} maxWinCount Number of persons who can win this giveaway (overwrites Giveaway.winner) - * @returns {Promise} - */ -async function endGiveaway(gID, job = null, checkIfGiveawayEnded = false, maxWinCount = null) { - const {client} = require('../../main'); - const moduleStrings = client.configurations['giveaways']['strings']; - const moduleConfig = client.configurations['giveaways']['config']; - - const giveaway = await client.models['giveaways']['Giveaway'].findOne({ - where: { - id: gID - } - }); - if (!giveaway) return; - if (job) job.cancel(); - if (checkIfGiveawayEnded && giveaway.ended) return; - - const channel = await client.channels.fetch(giveaway.channelID).catch(() => { - }); - if (!channel) return; - const message = await channel.messages.fetch(giveaway.messageID).catch(() => { - }); - if (!message) return; - giveaway.ended = true; - await giveaway.save(); - if (job) job.cancel(); - - const winners = []; - let userEntries = []; - let enteredUsers = 0; - - for (const id in giveaway.entries) { - const member = await channel.guild.members.fetch(id).catch(() => { - }); - if (!member) continue; - const [failedReqCheck] = await checkRequirements(member, giveaway); - if (failedReqCheck) continue; - enteredUsers++; - for (let i = 0; i < calculateUserEntries(member); i++) userEntries.push(id); - } - - const entries = userEntries.length; - if (userEntries.length === 0) { - await editMessage(localize('giveaways', 'no-winners')); - return await message.reply(embedType(moduleStrings['no_winner_message'], { - '%prize%': giveaway.prize, - '%sponsorLink%': giveaway.sponsorWebsite || localize('giveaways', 'no-link'), - '%organiser%': `<@${giveaway.organiser}>` - }, {})); - } - - - if (maxWinCount) giveaway.winnerCount = maxWinCount; - if (enteredUsers < giveaway.winnerCount) giveaway.winnerCount = enteredUsers; - - for (let winnerCount = 0; winnerCount < giveaway.winnerCount; winnerCount++) { - const winner = randomElementFromArray(userEntries); - winners.push(winner); - userEntries = userEntries.filter(u => u !== winner); - } - - - let winnersstring = ''; - for (const winner of winners) { - winnersstring = winnersstring + `<@${winner}> `; - } - - await message.reply(embedType(moduleStrings['winner_message'], { - '%prize%': giveaway.prize, - '%winners%': winnersstring, - '%sponsorLink%': giveaway.sponsorWebsite || localize('giveaways', 'no-link'), - '%organiser%': `<@${giveaway.organiser}>` - })); - - await editMessage(winnersstring); - - for (const winnerID of winners) { - const member = channel.guild.members.cache.get(winnerID); - if (member) { - if (moduleConfig.winRoles) member.roles.add(moduleConfig.winRoles).then(() => { - }).catch(() => { - }); - if (moduleConfig.sendDMOnWin) { - member.send(embedType(moduleStrings['winner_DM_message'], { - '%prize%': giveaway.prize, - '%winners%': winnersstring, - '%sponsorLink%': giveaway.sponsorWebsite || localize('giveaways', 'no-link'), - '%organiser%': `<@${giveaway.organiser}>`, - '%url%': message.url - })).then(() => { - }).catch(() => { - }); - } - } - } - - /** - * Edits the message if needed - * @private - * @param {String} winners Winnerstring - * @returns {Promise} - */ - async function editMessage(winnerString) { - const endAt = new Date(parseInt(giveaway.endAt)); - if (!maxWinCount) { - const components = [{ - type: 'ACTION_ROW', - components: [{ - type: 'BUTTON', - label: moduleStrings.buttonContent, - style: 'PRIMARY', - customId: 'giveaway', - disabled: true - }] - }]; - await message.edit( - embedType(moduleStrings['giveaway_message_edit_after_winning'], { - '%prize%': giveaway.prize, - '%endAt%': formatDate(endAt), - '%endAtDiscordFormation%': ``, - '%winners%': winnerString, - '%sponsorLink%': giveaway.sponsorWebsite || localize('giveaways', 'no-link'), - '%organiser%': `<@${giveaway.organiser}>`, - '%entryCount%': entries, - '%enteredCount%': enteredUsers - }, {components}) - ); - } - } -} - -module.exports.endGiveaway = endGiveaway; - -/** - * Checks if a [GuildMember](https://discord.js.org/#/docs/main/stable/class/GuildMember) passes the requirements for a giveaway - * @param {GuildMember} member Guild member - * @param {Object} giveaway Giveaway in which the user has to pass the requiremetns - * @returns {Promise} Returns array with these values: 1. if the users passes the requirements 2. Which requirements where not passed in a human-readable string - */ -async function checkRequirements(member, giveaway) { - let failedRequirements = false; - let notPassedRequirementsString = ''; - const moduleConfig = member.client.configurations['giveaways']['config']; - if (member.roles.cache.find(r => (moduleConfig.entryDeniedRoles || []).includes(r.id))) return [true, '']; - if (member.roles.cache.find(r => (moduleConfig.bypassRoles || []).includes(r.id))) { - return [failedRequirements, notPassedRequirementsString]; - } - for (const requirement of giveaway.requirements) { - switch (requirement.type) { - case 'roles': - let passedRoleRequirement = false; - let rolesString = ''; - for (const r of requirement.roles) { - rolesString = rolesString + `<@&${r}> `; - if (member.roles.cache.get(r)) passedRoleRequirement = true; - } - if (!passedRoleRequirement) { - notPassedRequirementsString = notPassedRequirementsString + `\t• ${localize('giveaways', 'roles-required', {r: rolesString})}\n`; - failedRequirements = true; - } - break; - case 'messages': - if (!giveaway.messageCount[member.user.id]) giveaway.messageCount[member.user.id] = 0; - if (parseInt(giveaway.messageCount[member.user.id]) < parseInt(requirement.messageCount)) { - notPassedRequirementsString = notPassedRequirementsString + `\t• ${localize('giveaways', 'required-messages-user', { - um: giveaway.messageCount[member.user.id], - mc: requirement.messageCount - })}\n`; - failedRequirements = true; - } - break; - } - } - return [failedRequirements, notPassedRequirementsString]; -} - -module.exports.checkRequirements = checkRequirements; - -/** - * Calculate the entries of a GuildMember - * @param {GuildMember} member [GuildMember](https://discord.js.org/#/docs/main/stable/class/GuildMember) - * @returns {number} Entries this user has - */ -function calculateUserEntries(member) { - const moduleConfig = member.client.configurations['giveaways']['config']; - let entries = 1; - for (const rID in moduleConfig.multipleEntries) { - if (member.roles.cache.get(rID)) entries = entries + parseFloat(moduleConfig.multipleEntries[rID]); - } - return entries; -} - -module.exports.calculateUserEntries = calculateUserEntries; \ No newline at end of file diff --git a/modules/giveaways/models/Giveaway.js b/modules/giveaways/models/Giveaway.js deleted file mode 100644 index cdb2806e..00000000 --- a/modules/giveaways/models/Giveaway.js +++ /dev/null @@ -1,48 +0,0 @@ -const {DataTypes, Model} = require('sequelize'); - -module.exports = class Giveaway extends Model { - static init(sequelize) { - return super.init({ - id: { - type: DataTypes.INTEGER, - autoIncrement: true, - primaryKey: true - }, - endAt: { - type: DataTypes.STRING - }, - ended: { - type: DataTypes.BOOLEAN, - defaultValue: false - }, - prize: DataTypes.STRING, - requirements: { - type: DataTypes.JSON, - defaultValue: [] - }, - countMessages: { // Yeah, I could get that from the requirements, but it's easier to fetch giveaways this way - type: DataTypes.BOOLEAN, - defaultValue: false - }, - messageCount: DataTypes.JSON, - entries: { - type: DataTypes.JSON, - defaultValue: {} - }, - sponsorWebsite: DataTypes.STRING, - winnerCount: DataTypes.INTEGER, - organiser: DataTypes.STRING, - messageID: DataTypes.STRING, - channelID: DataTypes.STRING - }, { - tableName: 'giveaways_giveaways', - timestamps: true, - sequelize - }); - } -}; - -module.exports.config = { - 'name': 'Giveaway', - 'module': 'giveaways' -}; \ No newline at end of file diff --git a/modules/giveaways/module.json b/modules/giveaways/module.json deleted file mode 100644 index 17ecee72..00000000 --- a/modules/giveaways/module.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "giveaways", - "author": { - "scnxOrgID": "1", - "name": "SCDerox (SC Network Team)", - "link": "https://github.com/SCDerox" - }, - "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/giveaways", - "commands-dir": "/commands", - "models-dir": "/models", - "events-dir": "/events", - "config-example-files": [ - "configs/strings.json", - "configs/config.json" - ], - "tags": [ - "community" - ], - "humanReadableName": { - "en": "Giveaways", - "de": "Gewinnspiele" - }, - "description": { - "en": "Easily create a giveaway in your server", - "de": "Erstelle einfach Gewinnspiele auf deinem Server" - } -} \ No newline at end of file diff --git a/modules/guess-the-number/commands/manage.js b/modules/guess-the-number/commands/manage.js index 162fe28e..faac36f1 100644 --- a/modules/guess-the-number/commands/manage.js +++ b/modules/guess-the-number/commands/manage.js @@ -1,11 +1,16 @@ const {localize} = require('../../../src/functions/localize'); const {randomIntFromInterval, embedType, lockChannel, unlockChannel} = require('../../../src/functions/helpers'); +const {startGame} = require('../guessTheNumber'); module.exports.beforeSubcommand = async function (interaction) { if (interaction.member.roles.cache.filter(m => interaction.client.configurations['guess-the-number']['config'].adminRoles.includes(m.id)).size === 0) return interaction.reply({ ephemeral: true, content: '⚠️ To use this command, you need to be added to the adminRoles option in the SCNX-Dashboard.' }); + if (interaction.client.configurations['guess-the-number']['channel'].enabled && interaction.client.configurations['guess-the-number']['channel'].channel === interaction.channel.id) return interaction.reply({ + content: '⚠️ ' + localize('guess-the-number', 'gamechannel-modus'), + ephemeral: true + }); }; module.exports.subcommands = { @@ -55,31 +60,8 @@ module.exports.subcommands = { ephemeral: true, content: '⚠️ ' + localize('guess-the-number', 'min-discrepancy') }); - await interaction.client.models['guess-the-number']['Channel'].create({ - channelID: interaction.channel.id, - number, - min: interaction.options.getInteger('min'), - max: interaction.options.getInteger('max'), - ownerID: interaction.user.id, - ended: false - }); - const pins = await interaction.channel.messages.fetchPinned(); - for (const pin of pins.values()) { - if (pin.author.id !== interaction.client.user.id) continue; - await pin.unpin(); - } - const m = await interaction.channel.send(embedType(interaction.client.configurations['guess-the-number']['config'].startMessage, {'%min%': interaction.options.getInteger('min'), '%max%': interaction.options.getInteger('max')}, {components: [{ - type: 'ACTION_ROW', - components: [{ - type: 'BUTTON', - label: localize('guess-the-number', 'emoji-guide-button'), - style: 'SECONDARY', - customId: 'gtn-reaction-meaning' - }] - }]})); - await m.pin(); - await unlockChannel(interaction.channel, '[guess-the-number] ' + localize('guess-the-number', 'game-started')); + await startGame(interaction.channel, number, interaction.options.getInteger('min'), interaction.options.getInteger('max'), interaction.user.id); await interaction.reply({ ephemeral: true, diff --git a/modules/guess-the-number/configs/channel.json b/modules/guess-the-number/configs/channel.json new file mode 100644 index 00000000..58ecea0f --- /dev/null +++ b/modules/guess-the-number/configs/channel.json @@ -0,0 +1,79 @@ +{ + "description": { + "en": "Enable the Gamechannel mode to automatically re-start games", + "de": "Aktiviere den Spielkanalmodus, um das Spiel automatisch neuzustarten" + }, + "humanName": { + "en": "Gamechannel Mode", + "de": "Spielkanal-Modus" + }, + "filename": "channel.json", + "content": [ + { + "default": { + "en": false + }, + "name": "enabled", + "description": { + "en": "If enabled, you can configure a game channel, in which a new guess the number game will be started if a number got guessed correctly. You still will be able to manually start games in other channels. Everyone, including admins, can guess in game channels.", + "de": "Wenn aktiviert, kannst du einen Spielkanal konfigurieren, in welchem neue Nummer-Erraten-Spiele gestartet werden, sobald eine Zahl korrekt erraten wurde. Du kannst auch weiterhin manuell Spiele in anderen Kanälen starten. In Spielkanälen kann jeder, also auch Admins, raten." + }, + "humanName": { + "en": "Enable Gamechannel mode?", + "de": "Spielkanalmodus aktivieren?" + }, + "type": "boolean" + }, + { + "default": { + "en": "" + }, + "dependsOn": "enabled", + "description": { + "en": "In this channel, games will be automatically started if a game ends or no game is currently running", + "de": "In diesem Kanal werden Spiele automatisch gestartet, wenn ein Spiel endet oder gerade kein Spiel läuft." + }, + "humanName": { + "en": "Gamechannel", + "de": "Spielkanal" + }, + "content": [ + "GUILD_TEXT" + ], + "type": "channelID", + "name": "channel" + }, + { + "type": "integer", + "dependsOn": "enabled", + "default": { + "en": 1 + }, + "name": "minInt", + "humanName": { + "en": "Minimum number", + "de": "Kleinste Nummer" + }, + "description": { + "en": "A number between this and the highest number will be selected at random when a game starts.", + "de": "Eine Nummer zwischen dieser under der höchsten Nummer wird automatisch ausgewählt, wenn das Spiel startet." + } + }, + { + "type": "integer", + "dependsOn": "enabled", + "default": { + "en": 1000 + }, + "name": "maxInt", + "humanName": { + "en": "Highest number", + "de": "Höchste Nummer" + }, + "description": { + "en": "A number between this and the minimum number will be selected at random when a game starts.", + "de": "Eine Nummer zwischen dieser under der kleinsten Nummer wird automatisch ausgewählt, wenn das Spiel startet." + } + } + ] +} \ No newline at end of file diff --git a/modules/guess-the-number/configs/config.json b/modules/guess-the-number/configs/config.json new file mode 100644 index 00000000..3c583908 --- /dev/null +++ b/modules/guess-the-number/configs/config.json @@ -0,0 +1,155 @@ +{ + "description": { + "en": "Adjust messages and permissions here", + "de": "Passe Nachrichten und Rechte hier an" + }, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "filename": "config.json", + "commandsWarnings": { + "special": [ + { + "name": "/guess-the-number", + "info": { + "en": "You need to first set the permissions in your server settings for this command and after that add them under \"adminRoles\" here.", + "de": "Du musst zuerst die Rechte in deinen Server-Einstellungen einstellen und danach diese unter \"AdminRollen\" hinzufügen." + } + } + ] + }, + "content": [ + { + "name": "adminRoles", + "humanName": { + "de": "Adminrollen", + "en": "Admin-Roles" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "Every role that can manage game sessions.", + "de": "Jede Rolle, welche Spielrunden verwalten kann" + }, + "type": "array", + "content": "roleID" + }, + { + "name": "startMessage", + "humanName": { + "de": "Startnachricht", + "en": "Start-Message" + }, + "default": { + "en": { + "title": "Guess the Number - Game started", + "description": "Guess a number between %min% and %max%. Good luck!" + }, + "de": { + "title": "Errate die Zahl - Das Spiel beginnt", + "description": "Errate eine Zahl zwischen %min% und %max%. Viel Glück!" + } + }, + "description": { + "de": "Nachricht, die am Anfang einer Runde gesendet wird", + "en": "Message that gets send when a new round gets started" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "min", + "description": { + "en": "Minimal value to guess", + "de": "Niedrigester möglichster Wert" + } + }, + { + "name": "max", + "description": { + "en": "Maximal value to guess", + "de": "Höchster möglichster Wert" + } + } + ] + }, + { + "name": "endMessage", + "humanName": { + "de": "Endnachricht", + "en": "End-Message" + }, + "default": { + "en": { + "title": "Guess the Number - Game ended", + "description": "Good game everyone!\nThe winner is %winner%.\nThe number was **%number%**.\nThere were around **%guessCount% guesses** in total." + }, + "de": { + "title": "Errate die Zahl - Das Spiel ist beendet", + "description": "Gutes Spiel!\nDer Gewinner ist %winner%.\nDie Zahl war **%number%**.\nInsgesamt wurde **%guessCount% mal** geraten." + } + }, + "description": { + "de": "Nachricht, die am Ende einer Runde gesendet wird", + "en": "Message that gets send when a round ends" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "min", + "description": { + "en": "Minimal value to guess", + "de": "Niedrigester möglichster Wert" + } + }, + { + "name": "max", + "description": { + "en": "Maximal value to guess", + "de": "Höchster möglichster Wert" + } + }, + { + "name": "winner", + "description": { + "en": "@-mention of the winner", + "de": "@-Erwähnung des Gewinners" + } + }, + { + "name": "guessCount", + "description": { + "en": "Count of guesses in this game session", + "de": "Anzahl der Versuche in dieser Runde" + } + }, + { + "name": "number", + "description": { + "en": "Winning number", + "de": "Nummer, die gesucht wurde" + } + } + ] + }, + { + "name": "higherLowerReactions", + "type": "boolean", + "humanName": { + "de": "Reagiere mit Höher / Geringer Emojis", + "en": "React with Lower / Higher reactions" + }, + "default": { + "en": false + }, + "description": { + "de": "Wenn aktiviert, reagiert der Bot bei falschen Versuchen mit ⬇ (wenn die gesuchte Zahl unter der gesendeten Zahl ist) oder mit ⬆ (wenn die gesuchte Zahl größer als die gesendete Zahl ist). Falls deaktiviert, wird der Bot nur mit ❌ bei falschen Versuchen reagieren.", + "en": "If enabled, the bot will react with ⬇ (if the guess is higher than the correct number) or with ⬆ (if the guess is lower than the correct number) on wrong guesses. If disabled, the bot will just react with ❌ on wrong guesses." + } + } + ] +} \ No newline at end of file diff --git a/modules/guess-the-number/events/botReady.js b/modules/guess-the-number/events/botReady.js new file mode 100644 index 00000000..77f36bc6 --- /dev/null +++ b/modules/guess-the-number/events/botReady.js @@ -0,0 +1,17 @@ +const {startGame} = require('../guessTheNumber'); +const {randomIntFromInterval} = require('../../../src/functions/helpers'); +module.exports.run = async function (client) { + if (client.configurations['guess-the-number']['channel'].enabled && client.configurations['guess-the-number']['channel']) { + const channel = await client.guild.channels.fetch(client.configurations['guess-the-number']['channel'].channel).catch(() => { + }); + if (!channel) return; + const game = await client.models['guess-the-number']['Channel'].findOne({ + where: { + channelID: channel.id, + ended: false + } + }); + if (game) return; + await startGame(channel, randomIntFromInterval(client.configurations['guess-the-number']['channel'].minInt, client.configurations['guess-the-number']['channel'].maxInt), client.configurations['guess-the-number']['channel'].minInt, client.configurations['guess-the-number']['channel'].maxInt); + } +}; \ No newline at end of file diff --git a/modules/guess-the-number/events/messageCreate.js b/modules/guess-the-number/events/messageCreate.js index 5ffaa251..582aaabe 100644 --- a/modules/guess-the-number/events/messageCreate.js +++ b/modules/guess-the-number/events/messageCreate.js @@ -1,5 +1,10 @@ -const {embedType, lockChannel} = require('../../../src/functions/helpers'); +const { + embedType, + lockChannel, + randomIntFromInterval +} = require('../../../src/functions/helpers'); const {localize} = require('../../../src/functions/localize'); +const {startGame} = require('../guessTheNumber'); module.exports.run = async (client, msg) => { if (!client.botReadyAt) return; @@ -13,7 +18,7 @@ module.exports.run = async (client, msg) => { } }); if (!game) return; - if (msg.member.roles.cache.filter(m => m.client.configurations['guess-the-number']['config'].adminRoles.includes(m.id)).size !== 0) return msg.react('⛔'); + if (msg.member.roles.cache.filter(m => m.client.configurations['guess-the-number']['config'].adminRoles.includes(m.id)).size !== 0 && !(client.configurations['guess-the-number']['channel'].enabled && client.configurations['guess-the-number']['channel'].channel === msg.channel.id)) return msg.react('⛔'); const parsedInt = parseInt(msg.content); if (isNaN(parsedInt)) return msg.react('🚫'); if (parsedInt < game.min || parsedInt > game.max) return msg.react('🚫'); @@ -21,8 +26,7 @@ module.exports.run = async (client, msg) => { await game.save(); if (parsedInt !== game.number) { if (client.configurations['guess-the-number']['config']['higherLowerReactions']) { - if (game.number < parsedInt) await msg.react('⬇'); - else await msg.react('⬆'); + if (game.number < parsedInt) await msg.react('⬇'); else await msg.react('⬆'); return; } return msg.react('❌'); @@ -38,4 +42,5 @@ module.exports.run = async (client, msg) => { '%guessCount%': game.guessCount, '%number%': game.number })); + if (client.configurations['guess-the-number']['channel'].enabled && client.configurations['guess-the-number']['channel'].channel === msg.channel.id) await startGame(msg.channel, randomIntFromInterval(client.configurations['guess-the-number']['channel'].minInt, client.configurations['guess-the-number']['channel'].maxInt), client.configurations['guess-the-number']['channel'].minInt, client.configurations['guess-the-number']['channel'].maxInt); }; \ No newline at end of file diff --git a/modules/guess-the-number/guessTheNumber.js b/modules/guess-the-number/guessTheNumber.js new file mode 100644 index 00000000..9ffd8e03 --- /dev/null +++ b/modules/guess-the-number/guessTheNumber.js @@ -0,0 +1,38 @@ +const {localize} = require('../../src/functions/localize'); +const { + embedType, + unlockChannel +} = require('../../src/functions/helpers'); + +module.exports.startGame = async function (channel, number, min, max, ownerID = null) { + await channel.client.models['guess-the-number']['Channel'].create({ + channelID: channel.id, + number, + min, + max, + ownerID, + ended: false + }); + const pins = await channel.messages.fetchPinned(); + for (const pin of pins.values()) { + if (pin.author.id !== channel.client.user.id) continue; + await pin.unpin(); + } + const m = await channel.send(embedType(channel.client.configurations['guess-the-number']['config'].startMessage, { + '%min%': min, + '%max%': max + }, { + components: [{ + type: 'ACTION_ROW', + components: [{ + type: 'BUTTON', + label: localize('guess-the-number', 'emoji-guide-button'), + style: 'SECONDARY', + customId: 'gtn-reaction-meaning' + }] + }] + })); + await m.pin(); + + await unlockChannel(channel, '[guess-the-number] ' + localize('guess-the-number', 'game-started')); +}; \ No newline at end of file diff --git a/modules/guess-the-number/module.json b/modules/guess-the-number/module.json index c5230e4f..0bc9ae44 100644 --- a/modules/guess-the-number/module.json +++ b/modules/guess-the-number/module.json @@ -6,10 +6,12 @@ "link": "https://github.com/SCDerox" }, "commands-dir": "/commands", + "fa-icon": "fas fa-dice-five", "models-dir": "/models", "events-dir": "/events", "config-example-files": [ - "config.json" + "configs/config.json", + "configs/channel.json" ], "tags": [ "fun" diff --git a/modules/hunt-the-code/commands/hunt-the-code-admin.js b/modules/hunt-the-code/commands/hunt-the-code-admin.js deleted file mode 100644 index 2df97745..00000000 --- a/modules/hunt-the-code/commands/hunt-the-code-admin.js +++ /dev/null @@ -1,121 +0,0 @@ -const {localize} = require('../../../src/functions/localize'); -const {randomString, postToSCNetworkPaste, formatDiscordUserName} = require('../../../src/functions/helpers'); - -module.exports.subcommands = { - 'create-code': function (interaction) { - interaction.client.models['hunt-the-code']['Code'].create({ - code: (interaction.options.getString('code') || (randomString(3) + '-' + randomString(3) + '-' + randomString(3))).toUpperCase(), - displayName: interaction.options.getString('display-name') - }).then((codeObject) => { - interaction.reply({ - ephemeral: true, - content: '✅ ' + localize('hunt-the-code', 'code-created', { - displayName: interaction.options.getString('display-name'), - code: codeObject.code - }) - }); - }).catch(() => { - interaction.reply({ - ephemeral: true, - content: '⚠️ ' + localize('hunt-the-code', 'error-creating-code', {displayName: interaction.options.getString('display-name')}) - }); - }); - }, - 'end': async function (interaction) { - await interaction.deferReply({ephemeral: true}); - const url = await generateReport(interaction.client); - await interaction.client.models['hunt-the-code']['Code'].destroy({ - truncate: true - }); - await interaction.client.models['hunt-the-code']['User'].destroy({ - truncate: true - }); - await interaction.editReply({ - content: '✅ ' + localize('hunt-the-code', 'successful-reset', {url}) - }); - }, - 'report': async function (interaction) { - await interaction.deferReply({ephemeral: true}); - const url = await generateReport(interaction.client); - await interaction.editReply({ - content: localize('hunt-the-code', 'report', {url}) - }); - } -}; - -/** - * Generate a report of the current Code-Hunt-Session - * @param {Client} client Client - * @returns {Promise} URL to Report - */ -async function generateReport(client) { - let reportString = `# ${localize('hunt-the-code', 'report-header', {s: client.guild.name})}\n`; - const codes = await client.models['hunt-the-code']['Code'].findAll({ - order: [ - ['foundCount', 'DESC'] - ] - }); - const users = await client.models['hunt-the-code']['User'].findAll({ - order: [ - ['foundCount', 'DESC'] - ] - }); - reportString = reportString + `\n## ${localize('hunt-the-code', 'user-header')}\n| Rank | Tag | ID | Amount found | Codes |\n| --- | --- | --- | --- | --- |\n`; - for (const i in users) { - const user = users[i]; - const u = await client.users.fetch(user.id); - reportString = reportString + `| ${parseInt(i) + 1}. | ${formatDiscordUserName(u)} | ${u.id} | ${user.foundCount} | ${user.foundCodes.join(', ')} |\n`; - } - reportString = reportString + `\n## ${localize('hunt-the-code', 'code-header')}\n| Rank | Code | Display-Name | Times found |\n| --- | --- | --- | --- |\n`; - for (const i in codes) { - const code = codes[i]; - reportString = reportString + `| ${parseInt(i) + 1}. | ${code.code} | ${code.displayName} | ${code.foundCount} |\n`; - } - reportString = reportString + `\n



Generated at ${new Date().toLocaleString(client.locale)}.`; - return await postToSCNetworkPaste(reportString, { - expire: '1month', - burnafterreading: 0, - opendiscussion: 1, - textformat: 'markdown', - output: 'text', - compression: 'zlib' - }); -} - -module.exports.config = { - name: 'hunt-the-code-admin', - defaultMemberPermissions: ['MANAGE_MESSAGES'], - description: localize('hunt-the-code', 'admin-command-description'), - - options: [ - { - type: 'SUB_COMMAND', - name: 'create-code', - description: localize('hunt-the-code', 'create-code-description'), - options: [ - { - type: 'STRING', - name: 'display-name', - required: true, - description: localize('hunt-the-code', 'display-name-description') - }, - { - type: 'STRING', - name: 'code', - required: false, - description: localize('hunt-the-code', 'code-description') - } - ] - }, - { - type: 'SUB_COMMAND', - name: 'end', - description: localize('hunt-the-code', 'end-description') - }, - { - type: 'SUB_COMMAND', - name: 'report', - description: localize('hunt-the-code', 'report-description') - } - ] -}; \ No newline at end of file diff --git a/modules/hunt-the-code/commands/hunt-the-code.js b/modules/hunt-the-code/commands/hunt-the-code.js deleted file mode 100644 index 11f344d7..00000000 --- a/modules/hunt-the-code/commands/hunt-the-code.js +++ /dev/null @@ -1,114 +0,0 @@ -const {localize} = require('../../../src/functions/localize'); -const {embedType} = require('../../../src/functions/helpers'); -const {MessageEmbed} = require('discord.js'); - -module.exports.subcommands = { - 'redeem': async function (interaction) { - const moduleStrings = interaction.client.configurations['hunt-the-code']['strings']; - const codeObject = await interaction.client.models['hunt-the-code']['Code'].findOne({ - where: { - code: interaction.options.getString('code').toUpperCase() - } - }); - if (!codeObject) return interaction.reply(embedType(moduleStrings.codeNotFoundMessage, {}, {ephemeral: true})); - const [user] = await interaction.client.models['hunt-the-code']['User'].findOrCreate({ - where: { - id: interaction.user.id - } - }); - if (user.foundCodes.includes(codeObject.code)) return interaction.reply(embedType(moduleStrings.codeAlreadyRedeemed, { - '%userCodesCount%': user.foundCount, - '%displayName%': codeObject.displayName, - '%codeUseCount%': codeObject.foundCount - }, {ephemeral: true})); - user.foundCount++; - user.foundCodes = [...user.foundCodes, codeObject.code]; - await user.save(); - codeObject.foundCount++; - interaction.reply(embedType(moduleStrings.codeRedeemed, { - '%displayName%': codeObject.displayName, - '%codeUseCount%': codeObject.foundCount, - '%userCodesCount%': user.foundCount - }, {ephemeral: true})); - await codeObject.save(); - }, - 'profile': async function (interaction) { - const [user] = await interaction.client.models['hunt-the-code']['User'].findOrCreate({ - where: { - id: interaction.user.id - } - }); - const codes = await interaction.client.models['hunt-the-code']['Code'].findAll({ - attributes: ['displayName', 'code'] - }); - let foundCodes = ''; - for (const code of user.foundCodes) { - const codeObject = codes.find(c => c.code === code); - if (!codeObject) continue; - foundCodes = foundCodes + `\n• ${codeObject.displayName}`; - } - if (!foundCodes) foundCodes = localize('hunt-the-code', 'no-codes-found'); - interaction.reply(embedType(interaction.client.configurations['hunt-the-code']['strings'].profileMessage, { - '%username%': interaction.user.username, - '%foundCount%': user.foundCount, - '%allCodesCount%': codes.length, - '%foundCodes%': foundCodes - }, {ephemeral: true})); - }, - 'leaderboard': async function (interaction) { - const moduleStrings = interaction.client.configurations['hunt-the-code']['strings']; - const users = await interaction.client.models['hunt-the-code']['User'].findAll({ - attributes: ['id', 'foundCount'], - order: [ - ['foundCount', 'DESC'] - ], - limit: 20 - }); - let userString = ''; - for (const user of users) { - userString = userString + `\n<@${user.id}>: ${user.foundCount}`; - } - if (userString === '') userString = localize('hunt-the-code', 'no-users'); - const embed = new MessageEmbed() - .setDescription(userString) - .setTitle(moduleStrings.leaderboardMessage.title) - .setImage(moduleStrings.leaderboardMessage.image || null) - .setThumbnail(moduleStrings.leaderboardMessage.thumbnail || null) - .setColor(moduleStrings.leaderboardMessage.color); - interaction.reply({ - ephemeral: true, - embeds: [embed] - }); - } -}; - -module.exports.config = { - name: 'hunt-the-code', - description: localize('hunt-the-code', 'command-description'), - - options: [ - { - type: 'SUB_COMMAND', - name: 'redeem', - description: localize('hunt-the-code', 'redeem-description'), - options: [ - { - type: 'STRING', - name: 'code', - required: true, - description: localize('hunt-the-code', 'code-redeem-description') - } - ] - }, - { - type: 'SUB_COMMAND', - name: 'leaderboard', - description: localize('hunt-the-code', 'leaderboard-description') - }, - { - type: 'SUB_COMMAND', - name: 'profile', - description: localize('hunt-the-code', 'profile-description') - } - ] -}; \ No newline at end of file diff --git a/modules/hunt-the-code/models/Code.js b/modules/hunt-the-code/models/Code.js deleted file mode 100644 index 39fc1580..00000000 --- a/modules/hunt-the-code/models/Code.js +++ /dev/null @@ -1,27 +0,0 @@ -const {DataTypes, Model} = require('sequelize'); - -module.exports = class HuntTheCodeCode extends Model { - static init(sequelize) { - return super.init({ - code: { - type: DataTypes.STRING, - primaryKey: true - }, - creatorID: DataTypes.STRING, - displayName: DataTypes.STRING, - foundCount: { - type: DataTypes.INTEGER, - defaultValue: 0 - } - }, { - tableName: 'hunt-the-code_Code', - timestamps: true, - sequelize - }); - } -}; - -module.exports.config = { - 'name': 'Code', - 'module': 'hunt-the-code' -}; \ No newline at end of file diff --git a/modules/hunt-the-code/models/User.js b/modules/hunt-the-code/models/User.js deleted file mode 100644 index 929872c0..00000000 --- a/modules/hunt-the-code/models/User.js +++ /dev/null @@ -1,29 +0,0 @@ -const {DataTypes, Model} = require('sequelize'); - -module.exports = class HuntTheCodeUser extends Model { - static init(sequelize) { - return super.init({ - id: { - type: DataTypes.STRING, - primaryKey: true - }, - foundCount: { - type: DataTypes.INTEGER, - defaultValue: 0 - }, - foundCodes: { - type: DataTypes.JSON, - defaultValue: [] - } - }, { - tableName: 'hunt-the-code_User', - timestamps: true, - sequelize - }); - } -}; - -module.exports.config = { - 'name': 'User', - 'module': 'hunt-the-code' -}; \ No newline at end of file diff --git a/modules/hunt-the-code/module.json b/modules/hunt-the-code/module.json deleted file mode 100644 index e77b5926..00000000 --- a/modules/hunt-the-code/module.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "hunt-the-code", - "author": { - "scnxOrgID": "1", - "name": "SCDerox (SC Network Team)", - "link": "https://github.com/SCDerox" - }, - "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/hunt-the-code", - "commands-dir": "/commands", - "models-dir": "/models", - "config-example-files": [ - "strings.json" - ], - "tags": [ - "community" - ], - "humanReadableName": { - "en": "Hunt the code", - "de": "Sammel die Codes" - }, - "description": { - "en": "Hide codes and let your users collect them", - "de": "Verstecke Codes und lasse sie von deinen Nutzern sammeln" - } -} \ No newline at end of file diff --git a/modules/hunt-the-code/strings.json b/modules/hunt-the-code/strings.json deleted file mode 100644 index aa0f1986..00000000 --- a/modules/hunt-the-code/strings.json +++ /dev/null @@ -1,207 +0,0 @@ -{ - "description": { - "en": "Edit the messages and strings of the module here", - "de": "Stelle hier die Nachrichten des Modules ein" - }, - "humanName": { - "en": "Messages", - "de": "Nachrichten" - }, - "filename": "strings.json", - "content": [ - { - "name": "codeNotFoundMessage", - "humanName": { - "en": "Code-not-found Message", - "de": "Code-nicht-gefunden Nachricht" - }, - "default": { - "en": "⚠️ Sorry, this code is invalid ):", - "de": "⚠️ Dieser Code ist leider ungültig" - }, - "description": { - "en": "This message gets send, when an invalid code is being redeemed", - "de": "Diese Nachricht wird verschickt, wenn ein ungültiger Code eingelöst wird" - }, - "type": "string", - "allowEmbed": true - }, - { - "name": "codeAlreadyRedeemed", - "humanName": { - "en": "Code-already-Redeemed Message", - "de": "Code-bereits-eingelöst Nachricht" - }, - "default": { - "en": "Good news, you have already redeemed this code", - "de": "Gute Nachrichten, du hast diesen Code bereits eingelöst" - }, - "description": { - "en": "This message gets send, when a user tries to redeem a code that is already in their inventory", - "de": "Diese Nachricht wird verschickt, wenn ein Nutzer einen Code einlösen will, den er bereits eingelöst hat" - }, - "type": "string", - "allowEmbed": true, - "params": [ - { - "name": "displayName", - "description": { - "en": "Display-Name of the code that the user wants to redeem", - "de": "Anzeige-Name des Codes, denn der Nutzer einlösen möchte" - } - }, - { - "name": "codeUseCount", - "description": { - "en": "Count of times this code has already been redeemed", - "de": "Anzahl von Nutzer, die diesen Code bereits eingelöst haben" - } - }, - { - "name": "userCodesCount", - "description": { - "en": "Count of codes this user already has redeemed", - "de": "Anzahl der Codes, die dieser Nutzer bereits eingelöst hat" - } - } - ] - }, - { - "name": "codeRedeemed", - "humanName": { - "en": "Code-Redeemed Message", - "de": "Code-eingelöst Nachricht" - }, - "default": { - "en": "Good job, you have successfully redeemed the code **%displayName%**", - "de": "Gute Arbeit, du hast erfolgreich den Code **%displayName%** eingelöst." - }, - "description": { - "en": "This message gets send, when a user tries redeems a code", - "de": "Diese Nachricht wird verschickt, wenn ein Nutzer einen Code einlöst" - }, - "type": "string", - "allowEmbed": true, - "params": [ - { - "name": "displayName", - "description": { - "en": "Display-Name of the code", - "de": "Anzeige-Name des Codes, denn der Nutzer einlöst" - } - }, - { - "name": "codeUseCount", - "description": { - "en": "Count of times this code has already been redeemed", - "de": "Anzahl von Nutzer, die diesen Code bereits eingelöst haben" - } - }, - { - "name": "userCodesCount", - "description": { - "en": "Count of codes this user already has redeemed", - "de": "Anzahl der Codes, die dieser Nutzer bereits eingelöst hat" - } - } - ] - }, - { - "name": "profileMessage", - "humanName": { - "en": "Profile-Message", - "de": "Profil-Nachricht" - }, - "default": { - "en": { - "title": "Your profile, %username%!", - "fields": [ - { - "name": "Found codes", - "value": "%foundCodes%", - "inline": true - }, - { - "name": "Progress", - "value": "%foundCount%/%allCodesCount% found", - "inline": true - } - ] - }, - "de": { - "title": "Dein Profil %username%!", - "fields": [ - { - "name": "Gefunde Codes", - "value": "%foundCodes%", - "inline": true - }, - { - "name": "Fortschritt", - "value": "%foundCount%/%allCodesCount% gefunden", - "inline": true - } - ] - } - }, - "description": { - "en": "This message gets send, when a user opens their profile", - "de": "Diese Nachricht wird versendet, wenn ein Nutzer sein Profil öffnet" - }, - "type": "string", - "allowEmbed": true, - "params": [ - { - "name": "foundCodes", - "description": { - "en": "All codes that this user has found", - "de": "Alle Codes, die der Nutzer gefunden hat" - } - }, - { - "name": "username", - "description": { - "en": "Username of the user running the command", - "de": "Nutzername des Nutzers, der den Befehl ausführt" - } - }, - { - "name": "foundCount", - "description": { - "en": "Count of found codes", - "de": "Anzahl aller gefunden Codes" - } - }, - { - "name": "allCodesCount", - "description": { - "en": "Count of all available codes", - "de": "Anzahl aller verfügbaren Codes" - } - } - ] - }, - { - "name": "leaderboardMessage", - "humanName": { - "en": "Leaderboard-Message", - "de": "Leaderboard-Nachricht" - }, - "default": { - "en": { - "title": "Leaderboard", - "color": "GREEN", - "thumbnail": "", - "image": "" - } - }, - "description": {}, - "type": "keyed", - "content": { - "key": "string", - "value": "string" - }, - "disableKeyEdits": true - } - ] -} \ No newline at end of file diff --git a/modules/info-commands/commands/info.js b/modules/info-commands/commands/info.js index c642b30f..7c405902 100644 --- a/modules/info-commands/commands/info.js +++ b/modules/info-commands/commands/info.js @@ -3,20 +3,43 @@ const { embedType, pufferStringToSize, dateToDiscordTimestamp, - formatDiscordUserName, formatNumber + formatDiscordUserName, + formatNumber, + parseEmbedColor } = require('../../../src/functions/helpers'); -const {MessageEmbed} = require('discord.js'); +const {ChannelType, MessageEmbed} = require('discord.js'); const {AgeFromDate} = require('age-calculator'); const {stringNames} = require('../../invite-tracking/events/guildMemberJoin'); +const {calculateLevelXP, isMaxLevel, displayLevel} = require('../../levels/events/messageCreate'); + +const legacyChannelType = (type) => { + const map = { + [ChannelType.GuildText]: 'GUILD_TEXT', + [ChannelType.GuildVoice]: 'GUILD_VOICE', + [ChannelType.GuildCategory]: 'GUILD_CATEGORY', + [ChannelType.GuildAnnouncement]: 'GUILD_NEWS', + [ChannelType.GuildStageVoice]: 'GUILD_STAGE_VOICE', + [ChannelType.PublicThread]: 'PUBLIC_THREAD', + [ChannelType.PrivateThread]: 'PRIVATE_THREAD', + [ChannelType.AnnouncementThread]: 'NEWS_THREAD', + [ChannelType.GuildForum]: 'GUILD_FORUM', + [ChannelType.GuildMedia]: 'GUILD_MEDIA' + }; + if (typeof type === 'string') return type; + return map[type] || (ChannelType[type] ? ChannelType[type].toString().toUpperCase() : type); +}; // THIS IS PAIN. Rewrite it as soon as possible +module.exports.beforeSubcommand = async function (interaction) { + await interaction.deferReply({ephemeral: true}); +}; module.exports.subcommands = { 'server': async function (interaction) { const moduleStrings = interaction.client.configurations['info-commands']['strings']; const embed = new MessageEmbed() .setTitle(localize('info-commands', 'information-about-server', {s: interaction.guild.name})) - .setColor('GOLD') + .setColor(parseEmbedColor('GOLD')) .setThumbnail(interaction.guild.iconURL()) .setImage(interaction.guild.bannerURL()) .setFooter({text: interaction.client.strings.footer, iconURL: interaction.client.strings.footerImgUrl}); @@ -36,9 +59,9 @@ module.exports.subcommands = { const bans = await interaction.guild.bans.fetch(); embed.addField(moduleStrings.serverinfo.banCount, bans.size.toString(), true); embed.addField(moduleStrings.serverinfo.createdAt, ``, true); - const members = await interaction.guild.members.fetch(); + const members = interaction.guild.members.cache; embed.addField(moduleStrings.serverinfo.members, `\`\`\`| ${localize('info-commands', 'userCount')} | ${localize('info-commands', 'memberCount')} | Online |\n| ${pufferStringToSize(members.size, localize('info-commands', 'userCount').length)} | ${pufferStringToSize(members.filter(m => !m.user.bot).size, localize('info-commands', 'memberCount').length)} | ${pufferStringToSize(members.filter(m => m.presence && (m.presence || {}).status !== 'offline').size, localize('info-commands', 'onlineCount').length)} |\`\`\``); - embed.addField(moduleStrings.serverinfo.channels, `\`\`\`| ${localize('info-commands', 'textChannel')} | ${localize('info-commands', 'voiceChannel')} | ${localize('info-commands', 'categoryChannel')} | ${localize('info-commands', 'otherChannel')} |\n| ${pufferStringToSize(interaction.guild.channels.cache.filter(c => c.type === 'GUILD_TEXT').size.toString(), localize('info-commands', 'textChannel').length)} | ${pufferStringToSize(interaction.guild.channels.cache.filter(c => c.type === 'GUILD_VOICE').size.toString(), localize('info-commands', 'voiceChannel').length)} | ${pufferStringToSize(interaction.guild.channels.cache.filter(c => c.type === 'GUILD_CATEGORY').size.toString(), localize('info-commands', 'categoryChannel').length)} | ${pufferStringToSize(interaction.guild.channels.cache.filter(c => c.type !== 'GUILD_VOICE' && c.type !== 'GUILD_TEXT' && c.type !== 'GUILD_CATEGORY').size.toString(), localize('info-commands', 'otherChannel').length)} |\`\`\``); + embed.addField(moduleStrings.serverinfo.channels, `\`\`\`| ${localize('info-commands', 'textChannel')} | ${localize('info-commands', 'voiceChannel')} | ${localize('info-commands', 'categoryChannel')} | ${localize('info-commands', 'otherChannel')} |\n| ${pufferStringToSize(interaction.guild.channels.cache.filter(c => c.type === ChannelType.GuildText).size.toString(), localize('info-commands', 'textChannel').length)} | ${pufferStringToSize(interaction.guild.channels.cache.filter(c => c.type === ChannelType.GuildVoice).size.toString(), localize('info-commands', 'voiceChannel').length)} | ${pufferStringToSize(interaction.guild.channels.cache.filter(c => c.type === ChannelType.GuildCategory).size.toString(), localize('info-commands', 'categoryChannel').length)} | ${pufferStringToSize(interaction.guild.channels.cache.filter(c => c.type !== ChannelType.GuildVoice && c.type !== ChannelType.GuildText && c.type !== ChannelType.GuildCategory).size.toString(), localize('info-commands', 'otherChannel').length)} |\`\`\``); let featuresstring = ''; interaction.guild.features.forEach(f => { featuresstring = featuresstring + `${f[0].toUpperCase() + f.toLowerCase().substring(1)}, `; @@ -46,35 +69,35 @@ module.exports.subcommands = { if (featuresstring !== '') featuresstring = featuresstring.substring(0, featuresstring.length - 2); else featuresstring = moduleStrings.serverinfo.noFeaturesEnabled; embed.addField(moduleStrings.serverinfo.features, `\`\`\`${featuresstring}\`\`\``); - interaction.reply({embeds: [embed], ephemeral: true}); + interaction.editReply({embeds: [embed]}); }, 'channel': async function (interaction) { const moduleStrings = interaction.client.configurations['info-commands']['strings']; const channel = interaction.options.getChannel('channel') || interaction.channel; const embed = new MessageEmbed() .setTitle(localize('info-commands', 'information-about-channel', {c: channel.name})) - .addField(moduleStrings.channelInfo.type, localize('channelType', channel.type.toString()), true) + .addField(moduleStrings.channelInfo.type, localize('channelType', legacyChannelType(channel.type).toString()), true) .addField(moduleStrings.channelInfo.id, channel.id, true) .addField(moduleStrings.channelInfo.createdAt, ``, true) .addField(moduleStrings.channelInfo.name, channel.name, true) .setFooter({text: interaction.client.strings.footer, iconURL: interaction.client.strings.footerImgUrl}) - .setColor('GREEN'); + .setColor(parseEmbedColor('GREEN')); if (!interaction.client.strings.disableFooterTimestamp) embed.setTimestamp(); if (channel.parent) embed.addField(moduleStrings.channelInfo.parent, channel.parent.name, true); - if (channel.position) embed.addField(moduleStrings.channelInfo.position, channel.position.toString(), true); + if (channel.position) embed.addField(moduleStrings.channelInfo.position, (channel.position + 1).toString(), true); if (channel.topic) embed.setDescription(channel.topic); - if (channel.type.includes('THREAD')) { + if (channel.isThread && channel.isThread()) { if (channel.archiveTimestamp !== channel.createdTimestamp) embed.addField(moduleStrings.channelInfo.threadArchivedAt, ``, true); if (channel.autoArchiveDuration) embed.addField(moduleStrings.channelInfo.threadAutoArchiveDuration, `${channel.autoArchiveDuration}min`, true); if (channel.ownerId) embed.addField(moduleStrings.channelInfo.threadOwner, `<@${channel.ownerId}>`, true); if (channel.messageCount && channel.messageCount < 50) embed.addField(moduleStrings.channelInfo.threadMessages, channel.messageCount.toString(), true); if (channel.memberCount && channel.memberCount < 50) embed.addField(moduleStrings.channelInfo.threadMemberCount, channel.memberCount.toString(), true); } - if (channel.type === 'GUILD_STAGE_VOICE' && channel.stageInstance && !(channel.stageInstance || {}).deleted) { + if (channel.type === ChannelType.GuildStageVoice && channel.stageInstance && !(channel.stageInstance || {}).deleted) { embed.addField(moduleStrings.channelInfo.stageInstanceName, channel.stageInstance.topic, true); embed.addField(moduleStrings.channelInfo.stageInstancePrivacy, localize('stagePrivacy', channel.stageInstance.privacyLevel.toString()), true); } - if (channel.members && channel.members.size !== 0 && (channel.type === 'GUILD_VOICE' || channel.type === 'GUILD_STAGE_VOICE')) { + if (channel.members && channel.members.size !== 0 && (channel.type === ChannelType.GuildVoice || channel.type === ChannelType.GuildStageVoice)) { let memberString = ''; channel.members.forEach(m => { memberString = memberString + `<@${m.user.id}>, `; @@ -82,7 +105,7 @@ module.exports.subcommands = { memberString = memberString.substring(0, memberString.length - 2); embed.addField(moduleStrings.channelInfo.membersInChannel, memberString); } - interaction.reply({embeds: [embed], ephemeral: true}); + interaction.editReply({embeds: [embed]}); }, 'role': async function (interaction) { const moduleStrings = interaction.client.configurations['info-commands']['strings']; @@ -94,7 +117,7 @@ module.exports.subcommands = { .addField(moduleStrings.roleInfo.position, role.position.toString(), true) .addField(moduleStrings.roleInfo.id, role.id, true) .addField(moduleStrings.roleInfo.name, role.name, true) - .setColor(role.color || 'GREEN'); + .setColor(role.color || parseEmbedColor('GREEN')); if (!interaction.client.strings.disableFooterTimestamp) embed.setTimestamp(); if (role.color) embed.addField(moduleStrings.roleInfo.color, role.hexColor, true); if (role.members) { @@ -122,7 +145,7 @@ module.exports.subcommands = { if (role.mentionable) features = features + `• ${localize('info-commands', 'mentionable')}\n`; if (role.managed) features = features + `• ${localize('info-commands', 'managed')}\n`; embed.setDescription(features); - interaction.reply({ephemeral: true, embeds: [embed]}); + interaction.editReply({embeds: [embed]}); }, 'user': async function (interaction) { const moduleStrings = interaction.client.configurations['info-commands']['strings']; @@ -147,8 +170,8 @@ module.exports.subcommands = { const embed = new MessageEmbed() .setTitle(localize('info-commands', 'information-about-user', {u: formatDiscordUserName(member.user)})) - .setColor(member.displayColor || 'GREEN') - .setThumbnail(member.user.avatarURL({dynamic: true})) + .setColor(member.displayColor || parseEmbedColor('GREEN')) + .setThumbnail(member.user.avatarURL({forceStatic: false})) .setFooter({text: interaction.client.strings.footer, iconURL: interaction.client.strings.footerImgUrl}) .addField(moduleStrings.userinfo.tag, formatDiscordUserName(member.user), true) .addField(moduleStrings.userinfo.id, member.user.id, true) @@ -166,13 +189,13 @@ module.exports.subcommands = { let dateString = `${birthday.day}.${birthday.month}${birthday.year ? `.${birthday.year}` : ''}`; if (birthday.year) { const age = new AgeFromDate(new Date(birthday.year, birthday.month - 1, birthday.day)).age; - dateString = `[${dateString}](https://sc-network.net/age?age=${age} "${localize('birthdays', 'age-hover', {a: age})}")`; + dateString = `[${dateString}](https://scnx.xyz/${interaction.client.locale === 'de' ? 'de/' : ''}custom-bot/age-calculator?age=${age} "${localize('birthdays', 'age-hover', {a: age})}")`; } embed.addField(moduleStrings.userinfo.birthday, dateString, true); } if (levelUserData) { - embed.addField(moduleStrings.userinfo.xp, `${formatNumber(levelUserData.xp)}/${formatNumber(levelUserData.level * 750 + ((levelUserData.level - 1) * 500))}`, true); - embed.addField(moduleStrings.userinfo.level, levelUserData.level.toString(), true); + embed.addField(moduleStrings.userinfo.xp, `${formatNumber(isMaxLevel(levelUserData.level, interaction.client) ? calculateLevelXP(interaction.client, interaction.client.configurations['levels']['config'].maximumLevel) : levelUserData.xp)}/${isMaxLevel(levelUserData.level, interaction.client) ? '∞' : formatNumber(calculateLevelXP(interaction.client, levelUserData.level))}`, true); + embed.addField(moduleStrings.userinfo.level, displayLevel(levelUserData.level, interaction.client), true); embed.addField(moduleStrings.userinfo.messages, levelUserData.messages.toString(), true); } if (interaction.client.models['invite-tracking']) { @@ -199,9 +222,8 @@ module.exports.subcommands = { if (permstring !== '') permstring = permstring.substring(0, permstring.length - 2); else permstring = moduleStrings.userinfo.noPermissions; embed.addField(moduleStrings.userinfo.permissions, `\`\`\`${permstring}\`\`\``); - interaction.reply({ + interaction.editReply({ embeds: [embed], - ephemeral: true }); } }; diff --git a/modules/info-commands/module.json b/modules/info-commands/module.json index 1f8020d8..93d917f4 100644 --- a/modules/info-commands/module.json +++ b/modules/info-commands/module.json @@ -1,5 +1,6 @@ { "name": "info-commands", + "fa-icon": "fa-solid fa-circle-info", "author": { "scnxOrgID": "1", "name": "SCDerox (SC Network Team)", @@ -21,4 +22,4 @@ "en": "Adds info-commands with information about specific parts of your server", "de": "Fügt viele Info-Commands zu deinen Server hinzu" } -} \ No newline at end of file +} diff --git a/modules/info-commands/strings.json b/modules/info-commands/strings.json index eea6fb9c..3065a8c4 100644 --- a/modules/info-commands/strings.json +++ b/modules/info-commands/strings.json @@ -19,7 +19,7 @@ "rulesChannel": "Rules-Channel", "dcSystemChannel": "Discord-System-Channel", "verificationLevel": "Verification-Level", - "banCount": "Banns", + "banCount": "Bans", "createdAt": "Created at", "members": "Members", "channels": "Channels", @@ -132,7 +132,7 @@ "name": "Name", "parent": "Category", "topic": "Topic", - "position": "Current position", + "position": "Current position in category", "stageInstanceName": "Stage topic", "stageInstancePrivacy": "Stage Privacy", "threadArchivedAt": "Thread archived at", @@ -149,7 +149,7 @@ "name": "Name", "parent": "Kategorie", "topic": "Kanalbeschreibung", - "position": "Aktuelle Position", + "position": "Aktuelle Position in der Kategorie", "stageInstanceName": "Stage Thema", "stageInstancePrivacy": "Stage Privacy", "threadArchivedAt": "Thread archiviert am", diff --git a/modules/invite-tracking/commands/trace-invites.js b/modules/invite-tracking/commands/trace-invites.js deleted file mode 100644 index 957d68eb..00000000 --- a/modules/invite-tracking/commands/trace-invites.js +++ /dev/null @@ -1,75 +0,0 @@ -const {localize} = require('../../../src/functions/localize'); -const {stringNames} = require('../events/guildMemberJoin'); - -module.exports.run = async function (interaction) { - await interaction.deferReply({ephemeral: true}); - const user = interaction.options.getUser('user', true); - const invitedUsers = await interaction.client.models['invite-tracking']['UserInvite'].findAll({ - where: { - inviter: user.id, - left: false - } - }); - const userInvites = await interaction.client.models['invite-tracking']['UserInvite'].findAll({ - where: { - userID: user.id, - left: false - }, - order: [['createdAt', 'DESC']] - }); - - let content = `**${localize('invite-tracking', 'invited-by')}**\n`; - if (!userInvites[0]) content = content + localize('invite-tracking', 'inviter-not-found'); - else content = content + `${localize('invite-tracking', stringNames[userInvites[0].inviteType])}${userInvites[0].inviter ? ` by <@${userInvites[0].inviter}>` : ''}${userInvites[0].inviteCode ? ` via code [${userInvites[0].inviteCode}](https://discord.gg/${userInvites[0].inviteCode})` : ''}`; - - content = content + `\n\n**${localize('invite-tracking', 'invited-users')}**\n`; - if (invitedUsers.length === 0) content = content + localize('invite-tracking', 'no-users-invited'); - else { - let i = 0; - for (const invite of invitedUsers) { - i++; - if (i > 10) continue; - content = content + `<@${invite.userID}>\n`; - } - if (i > 10) content = content + localize('invite-tracking', 'and-x-more-users', {x: i - 10}) + '\n'; - } - - content = content + `\n**${localize('invite-tracking', 'created-invites')}**\n`; - const guildInvites = await interaction.guild.invites.fetch(); - let y = 0; - for (const invite of guildInvites.filter(i => i.inviter.id === user.id).values()) { - y++; - if (y > 5) continue; - content = content + `[${invite.code}](${invite.url}) (${invite.uses}${invite.maxUses ? `/${invite.maxUses}` : ''} uses) to ${invite.channel.toString()}\n`; - } - if (y > 5) content = content + localize('invite-tracking', 'and-x-more-invites', {x: y - 5}) + '\n'; - if (y === 0) content = content + `${localize('invite-tracking', 'no-invites')}\n`; - - content = content + `\n*${localize('invite-tracking', 'not-showing-left-users')}*`; - await interaction.editReply({content, allowedMentions: {parse: []}, components: [{ - type: 'ACTION_ROW', - components: [ - { - type: 'BUTTON', - label: '🗑️ ' + localize('invite-tracking', 'revoke-user-invite'), - style: 'DANGER', - customId: `uinv-rev-${user.id}` - } - ] - }]}); -}; - -module.exports.config = { - name: 'trace-invites', - defaultMemberPermissions: ['MODERATE_MEMBERS'], - description: localize('invite-tracking', 'trace-command-description'), - - options: [ - { - type: 'USER', - name: 'user', - required: true, - description: localize('invite-tracking', 'argument-user-description') - } - ] -}; \ No newline at end of file diff --git a/modules/invite-tracking/config.json b/modules/invite-tracking/config.json deleted file mode 100644 index a0a03397..00000000 --- a/modules/invite-tracking/config.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "description": {}, - "humanName": {}, - "filename": "config.json", - "commandsWarnings": { - "normal": [ - "/trace-invites" - ] - }, - "content": [ - { - "name": "logchannel-id", - "humanName": { - "en": "Invite-Log-Channel", - "de": "Invite-Log-Kanal" - }, - "default": { - "en": "" - }, - "description": { - "en": "Channel in which information about new members should get logged (optional)", - "de": "Kanal, in welchem Informationen über neue Nutzer geloggt werden sollen (optional)" - }, - "type": "channelID", - "allowNull": true - } - ] -} \ No newline at end of file diff --git a/modules/invite-tracking/events/guildMemberJoin.js b/modules/invite-tracking/events/guildMemberJoin.js deleted file mode 100644 index 436bf804..00000000 --- a/modules/invite-tracking/events/guildMemberJoin.js +++ /dev/null @@ -1,79 +0,0 @@ -const {localize} = require('../../../src/functions/localize'); -const {MessageEmbed} = require('discord.js'); -const {dateToDiscordTimestamp} = require('../../../src/functions/helpers'); - -const stringNames = { - normal: 'normal-invite', - vanity: 'vanity-invite', - permissions: 'missing-permissions', - unknown: 'unknown-invite' -}; -module.exports.stringNames = stringNames; - -module.exports.run = async (client, member, type, invite) => { - if (!client.botReadyAt) return; - if (member.guild.id !== client.guild.id) return; - - const moduleConfig = client.configurations['invite-tracking']['config']; - - const beforeInvites = await client.models['invite-tracking']['UserInvite'].findAll({ - where: { - userID: member.user.id - }, - order: [['createdAt', 'DESC']] - }); - - await client.models['invite-tracking']['UserInvite'].create({ - inviteCode: invite ? invite.code : null, - inviteType: type, - inviter: invite ? invite.inviter.id : null, - userID: member.user.id - }); - - if (moduleConfig['logchannel-id']) { - const c = client.channels.cache.get(moduleConfig['logchannel-id']); - if (!c) return client.logger.error(localize('invite-tracking', 'log-channel-not-found-but-set', {c: moduleConfig['logchannel-id']})); - const components = []; - const embed = new MessageEmbed() - .setTitle('📥 ' + localize('invite-tracking', 'new-member')) - .setFooter({text: client.strings.footer, iconURL: client.strings.footerImgUrl}) - .setColor('GREEN') - .addField(localize('invite-tracking', 'member'), `${member.toString()} (\`${member.user.id}\`)`, true) - .addField(localize('invite-tracking', 'invite-type'), localize('invite-tracking', stringNames[type]), true); - if (client.strings.disableFooterTimestamp) embed.setTimestamp(); - if (beforeInvites.length !== 0) embed.setDescription(localize('invite-tracking', 'joined-for-the-x-time', {u: member.user.username, x: beforeInvites.length, t: dateToDiscordTimestamp(beforeInvites[0].createdAt)})); - if (invite) { - const fetchedInvite = await member.guild.invites.fetch({code: invite.code, force: true}).catch(() => {}); - if (fetchedInvite) invite = fetchedInvite; - let inviteString = localize('invite-tracking', 'invite-code', {c: invite.code, u: invite.url}); - if (invite.channel) inviteString = inviteString + '\n' + localize('invite-tracking', 'invite-channel', {c: invite.channel.toString()}); - if (invite.createdAt) inviteString = inviteString + '\n' + localize('invite-tracking', 'created-at', {t: dateToDiscordTimestamp(invite.createdAt)}); - if (invite.expiresAt) inviteString = inviteString + '\n' + localize('invite-tracking', 'expires-at', {t: dateToDiscordTimestamp(invite.expiresAt)}); - if (invite.inviter) { - const userInvites = await client.models['invite-tracking']['UserInvite'].findAll({ - where: { - inviter: invite.inviter.id - } - }); - inviteString = inviteString + '\n' + localize('invite-tracking', 'inviter', { - u: invite.inviter.toString(), - i: userInvites.length, - a: userInvites.filter(i => !i.left).length - }); - } - if (invite.uses) inviteString = inviteString + '\n' + localize('invite-tracking', 'uses', {u: invite.uses}); - if (invite.maxUses) inviteString = inviteString + '\n' + localize('invite-tracking', 'max-uses', {u: invite.maxUses}); - components.push({ - type: 'ACTION_ROW', - components: [{ - type: 'BUTTON', - label: '🗑️ ' + localize('invite-tracking', 'revoke-invite'), - style: 'DANGER', - customId: `inv-rev-${invite.code}` - }] - }); - embed.addField(localize('invite-tracking', 'invite'), inviteString); - } - c.send({embeds: [embed], components}); - } -}; \ No newline at end of file diff --git a/modules/invite-tracking/events/guildMemberRemove.js b/modules/invite-tracking/events/guildMemberRemove.js deleted file mode 100644 index b041c0fd..00000000 --- a/modules/invite-tracking/events/guildMemberRemove.js +++ /dev/null @@ -1,60 +0,0 @@ -const {localize} = require('../../../src/functions/localize'); -const {MessageEmbed} = require('discord.js'); -const {dateToDiscordTimestamp, formatDiscordUserName} = require('../../../src/functions/helpers'); -const {stringNames} = require('./guildMemberJoin'); - -module.exports.run = async (client, member) => { - if (!client.botReadyAt) return; - if (member.guild.id !== client.guild.id) return; - - await client.models['invite-tracking']['UserInvite'].update({left: true}, { - where: { - userID: member.user.id - } - }); - - const moduleConfig = client.configurations['invite-tracking']['config']; - if (moduleConfig['logchannel-id']) { - const userInvites = await client.models['invite-tracking']['UserInvite'].findAll({ - where: { - userID: member.user.id - }, - order: [['createdAt', 'DESC']] - }); - const invite = userInvites[0]; - if (!invite) return; - const c = client.channels.cache.get(moduleConfig['logchannel-id']); - if (!c) return client.logger.error(localize('invite-tracking', 'log-channel-not-found-but-set', {c: moduleConfig['logchannel-id']})); - const embed = new MessageEmbed() - .setTitle('📤 ' + localize('invite-tracking', 'member-leave')) - .setFooter({text: client.strings.footer, iconURL: client.strings.footerImgUrl}) - .setColor('RED') - .addField(localize('invite-tracking', 'member'), `${formatDiscordUserName(member.user)} (\`${member.user.id}\`)`, true) - .addField(localize('invite-tracking', 'invite-type'), localize('invite-tracking', stringNames[invite.inviteType]), true); - if (!client.strings.disableFooterTimestamp) embed.setTimestamp(); - if (invite.inviteCode) { - let guildInvite = await member.guild.invites.fetch({ code: invite.inviteCode, force: true }).catch(() => {}); - if (!guildInvite) guildInvite = {}; - - let inviteString = localize('invite-tracking', 'invite-code', {c: invite.inviteCode, u: 'https://discord.gg/' + invite.inviteCode}); - - if (guildInvite.channel) inviteString = inviteString + '\n' + localize('invite-tracking', 'invite-channel', {c: guildInvite.channel.toString()}); - if (guildInvite.createdAt) inviteString = inviteString + '\n' + localize('invite-tracking', 'created-at', {t: dateToDiscordTimestamp(guildInvite.createdAt)}); - if (guildInvite.expiresAt) inviteString = inviteString + '\n' + localize('invite-tracking', 'expires-at', {t: dateToDiscordTimestamp(guildInvite.expiresAt)}); - - if (invite.inviter) { - const inviterInvites = await client.models['invite-tracking']['UserInvite'].findAll({ - where: { - inviter: invite.inviter - } - }); - inviteString = inviteString + '\n' + localize('invite-tracking', 'inviter', {u: `<@${invite.inviter}>`, i: inviterInvites.length, a: inviterInvites.filter(i => !i.left).length}); - } - - if (guildInvite.uses) inviteString = inviteString + '\n' + localize('invite-tracking', 'uses', {u: guildInvite.uses}); - if (guildInvite.maxUses) inviteString = inviteString + '\n' + localize('invite-tracking', 'max-uses', {u: guildInvite.maxUses}); - embed.addField(localize('invite-tracking', 'invite'), inviteString); - } - c.send({embeds: [embed]}); - } -}; \ No newline at end of file diff --git a/modules/invite-tracking/events/interactionCreate.js b/modules/invite-tracking/events/interactionCreate.js deleted file mode 100644 index 11640ca2..00000000 --- a/modules/invite-tracking/events/interactionCreate.js +++ /dev/null @@ -1,48 +0,0 @@ -const {localize} = require('../../../src/functions/localize'); -const {formatDiscordUserName} = require('../../../src/functions/helpers'); -exports.run = async (client, interaction) => { - if (!interaction.client.botReadyAt) return; - if (!interaction.isButton()) return; - if (interaction.customId.startsWith('uinv-rev')) { - await interaction.deferReply({ephemeral: true}); - const guildInvites = await interaction.guild.invites.fetch(); - try { - for (const invite of guildInvites.filter(i => i.inviter.id === interaction.customId.replaceAll('uinv-rev-', '')).values()) { - await invite.delete(localize('invite-tracking', 'invite-revoke-audit-log', {u: formatDiscordUserName(interaction.user)})); - } - await interaction.editReply({ - content: localize('invite-tracking', 'revoked-invites-successfully') - }); - } catch (e) { - client.logger.warn(localize('invite-tracking', 'invite-revoked-error', {e})); - await interaction.editReply({ - content: '⚠️ ' + localize('invite-tracking', 'invite-revoked-error', { - e, - c - }) - }); - } - return; - } - if (!interaction.customId.startsWith('inv-rev-')) return; - if (!interaction.member.permissions.has('MANAGE_GUILD')) return interaction.reply({ - ephemeral: true, - content: '⚠️ ' + localize('invite-tracking', 'missing-revoke-permissions') - }); - const code = interaction.customId.replaceAll('inv-rev-', ''); - const invite = await client.guild.invites.fetch(code).catch(() => {}); - if (!invite) return interaction.reply({ - ephemeral: true, - content: '⚠️ ' + localize('invite-tracking', 'invite-not-found') - }); - await interaction.message.edit({embeds: [interaction.message.embeds[0]], components: []}); - invite.delete(localize('invite-tracking', 'invite-revoke-audit-log', {u: formatDiscordUserName(interaction.user)})).then(() => { - interaction.reply({ephemeral: true, content: localize('invite-tracking', 'invite-revoked')}); - }).catch((e) => { - client.logger.warn(localize('invite-tracking', 'invite-revoked-error', {e, c: code})); - interaction.reply({ - ephemeral: true, - content: '⚠️ ' + localize('invite-tracking', 'invite-revoked-error', {e, c: code}) - }); - }); -}; \ No newline at end of file diff --git a/modules/invite-tracking/models/UserInvite.js b/modules/invite-tracking/models/UserInvite.js deleted file mode 100644 index 16a67d55..00000000 --- a/modules/invite-tracking/models/UserInvite.js +++ /dev/null @@ -1,30 +0,0 @@ -const {DataTypes, Model} = require('sequelize'); - -module.exports = class UserInvite extends Model { - static init(sequelize) { - return super.init({ - id: { - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true - }, - left: { - type: DataTypes.BOOLEAN, - defaultValue: false - }, - inviteCode: DataTypes.STRING, - inviteType: DataTypes.STRING, - inviter: DataTypes.STRING, - userID: DataTypes.STRING - }, { - tableName: 'invite-tracking_UserInvite', - timestamps: true, - sequelize - }); - } -}; - -module.exports.config = { - 'name': 'UserInvite', - 'module': 'invite-tracking' -}; \ No newline at end of file diff --git a/modules/invite-tracking/module.json b/modules/invite-tracking/module.json deleted file mode 100644 index 2e89723e..00000000 --- a/modules/invite-tracking/module.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "invite-tracking", - "humanReadableName": { - "en": "Invite-Tracking" - }, - "author": { - "scnxOrgID": "1", - "name": "SCDerox (SC Network Team)", - "link": "https://github.com/SCDerox" - }, - "events-dir": "/events", - "commands-dir": "/commands", - "models-dir": "/models", - "config-example-files": [ - "config.json" - ], - "on-load-event": "onLoad.js", - "tags": [ - "moderation" - ], - "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/invite-tracking", - "description": { - "en": "Track who invited who", - "de": "Track, wer wen eingeladen hat" - } -} \ No newline at end of file diff --git a/modules/invite-tracking/onLoad.js b/modules/invite-tracking/onLoad.js deleted file mode 100644 index 1092406a..00000000 --- a/modules/invite-tracking/onLoad.js +++ /dev/null @@ -1,18 +0,0 @@ -const InvitesTracker = require('@androz2091/discord-invites-tracker'); -const {localize} = require('../../src/functions/localize'); - -module.exports.onLoad = function (client) { - if (!client.inviteHook) { - const tracker = InvitesTracker.init(client, { - fetchGuilds: true, - fetchVanity: true, - fetchAuditLogs: true, - activeGuilds: [client.config.guildID] - }); - client.inviteHook = true; - localize('invite-tracking', 'hook-installed'); - tracker.on('guildMemberAdd', async (member, type, invite) => { - client.emit('guildMemberJoin', member, type, invite); - }); - } -}; \ No newline at end of file diff --git a/modules/levels/commands/leaderboard.js b/modules/levels/commands/leaderboard.js index 38758acf..71c7ba27 100644 --- a/modules/levels/commands/leaderboard.js +++ b/modules/levels/commands/leaderboard.js @@ -2,10 +2,13 @@ const { sendMultipleSiteButtonMessage, truncate, formatNumber, - formatDiscordUserName + formatDiscordUserName, + parseEmbedColor } = require('../../../src/functions/helpers'); const {MessageEmbed} = require('discord.js'); const {localize} = require('../../../src/functions/localize'); +const {displayLevel, isMaxLevel, calculateLevelXP} = require('../events/messageCreate'); +const {client} = require('../../../main'); module.exports.run = async function (interaction) { const moduleStrings = interaction.client.configurations['levels']['strings']; @@ -32,14 +35,13 @@ module.exports.run = async function (interaction) { function addSite(fields) { const embed = new MessageEmbed() .setFooter({text: interaction.client.strings.footer, iconURL: interaction.client.strings.footerImgUrl}) - .setColor('GREEN') + .setColor(parseEmbedColor(moduleStrings.leaderboardEmbed.color || 'GREEN')) .setThumbnail(interaction.guild.iconURL()) .setTitle(moduleStrings.leaderboardEmbed.title) .setDescription(moduleStrings.leaderboardEmbed.description) .addField('\u200b', '\u200b') - .addFields(fields) - .addField('\u200b', '\u200b') - .addField(moduleStrings.leaderboardEmbed.your_level, moduleStrings.leaderboardEmbed.you_are_level_x_with_x_xp.split('%level%').join(thisUser['level']).split('%xp%').join(formatNumber(thisUser['xp']))); + .addFields(fields); + if (thisUser) embed.addField('\u200b', '\u200b').addField(moduleStrings.leaderboardEmbed.your_level, moduleStrings.leaderboardEmbed.you_are_level_x_with_x_xp.split('%level%').join(displayLevel(thisUser['level'], client)).split('%xp%').join(formatNumber(thisUser['xp']))); sites.push(embed); } @@ -66,10 +68,19 @@ module.exports.run = async function (interaction) { const member = interaction.guild.members.cache.get(user.userID); if (!member) continue; userCount++; - if (userCount < 6) userString = userString + `${userCount}. ${moduleConfig['useTags'] ? formatDiscordUserName(member.user) : member.user.toString()}: ${formatNumber(user.xp)}\n`; + if (userCount < 6) userString = userString + localize('levels', 'leaderboard-notation', { + p: userCount, + u: moduleConfig['useTags'] ? formatDiscordUserName(member.user) : member.user.toString(), + l: displayLevel(user.level, client), + xp: formatNumber(isMaxLevel(user.level, client) ? calculateLevelXP(client, client.configurations['levels']['config'].maximumLevel) : user.xp) + }) + '\n'; } if (userCount > 5) userString = userString + localize('levels', 'and-x-other-users', {uc: userCount - 5}); - if (userCount !== 0) currentSiteFields.push({name: localize('levels', 'level', {l: level}), value: userString, inline: true}); + if (userCount !== 0) currentSiteFields.push({ + name: localize('levels', 'level', {l: displayLevel(level, client)}), + value: userString, + inline: true + }); if (i === Object.keys(levels).length || currentSiteFields.length === 6) { addSite(currentSiteFields); currentSiteFields = []; @@ -85,8 +96,8 @@ module.exports.run = async function (interaction) { userString = userString + localize('levels', 'leaderboard-notation', { p: i, u: moduleConfig['useTags'] ? formatDiscordUserName(member.user) : member.user.toString(), - l: user.level, - xp: formatNumber(user.xp) + l: displayLevel(user.level, client), + xp: formatNumber(isMaxLevel(user.level, client) ? calculateLevelXP(client, client.configurations['levels']['config'].maximumLevel) : user.xp) }) + '\n'; if (i === users.filter(u => interaction.guild.members.cache.get(u.userID)).length || i % 20 === 0) { addSite([{ diff --git a/modules/levels/commands/manage-levels.js b/modules/levels/commands/manage-levels.js index b58d69b5..7e2c703c 100644 --- a/modules/levels/commands/manage-levels.js +++ b/modules/levels/commands/manage-levels.js @@ -1,8 +1,13 @@ const {registerNeededEdit} = require('../leaderboardChannel'); const {localize} = require('../../../src/functions/localize'); const {formatDiscordUserName} = require('../../../src/functions/helpers'); +const {calculateLevelXP, displayLevel} = require('../events/messageCreate'); async function runXPAction(interaction, newXP) { + await interaction.deferReply({ + ephemeral: true + }); + const member = interaction.options.getMember('user'); let user = await interaction.client.models['levels']['User'].findOne({ where: { @@ -17,23 +22,15 @@ async function runXPAction(interaction, newXP) { }); } user.xp = newXP(user.xp); - if (user.xp < 0) return interaction.reply({ - ephemeral: true, + if (user.xp < 0) return interaction.editReply({ content: '⚠️ ' + localize('levels', 'negative-xp') }); function runXPCheck() { - const nextLevelXp = user.level * 750 + ((user.level - 1) * 500); + const nextLevelXp = calculateLevelXP(interaction.client, user.level + 1); if (nextLevelXp <= user.xp) { user.level = user.level + 1; - if (interaction.client.configurations.levels.config.reward_roles[user.level.toString()]) { - if (interaction.client.configurations.levels.config.reward_roles[user.level.toString()]) { - for (const role of Object.values(interaction.client.configurations.levels.config.reward_roles)) { - if (member.roles.cache.has(role)) member.roles.remove(role, '[levels] ' + localize('levels', 'granted-rewards-audit-log')).catch(); - } - } - member.roles.add(interaction.client.configurations.levels.config.reward_roles[user.level.toString()]); - } + fixLevelRoles(interaction, member, user.level); runXPCheck(); } } @@ -54,8 +51,7 @@ async function runXPAction(interaction, newXP) { l: user.level, v: user.xp })); - await interaction.reply({ - ephemeral: true, + await interaction.editReply({ content: localize('levels', 'successfully-changed', { l: user.level, u: member.user.toString(), @@ -64,50 +60,55 @@ async function runXPAction(interaction, newXP) { }); } +async function fixLevelRoles(interaction, member, level) { + let highest = null; + for (const key in interaction.client.configurations.levels.config.reward_roles) { + const role = interaction.client.configurations.levels.config.reward_roles[key]; + if (parseInt(key) <= level) { + if (highest && highest < parseInt(key) && interaction.client.configurations.levels.config.onlyTopLevelRole) await member.roles.remove(interaction.client.configurations.levels.config.reward_roles[highest.toString()], '[levels] ' + localize('levels', 'granted-rewards-audit-log')).catch(); + highest = parseInt(key); + await member.roles.add(role, '[levels] ' + localize('levels', 'granted-rewards-audit-log')); + } else if (member.roles.cache.has(role)) await member.roles.remove(role, '[levels] ' + localize('levels', 'granted-rewards-audit-log')).catch(); + } +} + async function runLevelAction(interaction, newLevel) { + await interaction.deferReply({ephemeral: true}); + const member = interaction.options.getMember('user'); const user = await interaction.client.models['levels']['User'].findOne({ where: { userID: member.user.id } }); - if (!user) return interaction.reply({ - ephemeral: true, + if (!user) return interaction.editReply({ content: '⚠️ ' + localize('levels', 'cheat-no-profile') }); user.level = newLevel(user.level); - if (user.level < 1) return interaction.reply({ - ephemeral: true, + if (interaction.client.configurations['levels']['config'].startFromZero) user.level = user.level + 1; + if (user.level < 1) return interaction.editReply({ content: '⚠️ ' + localize('levels', 'negative-level') }); - user.xp = (user.level - 1) * 750 + ((user.level - 2) * 500); - if (interaction.client.configurations.levels.config.reward_roles[user.level.toString()]) { - if (interaction.client.configurations.levels.config.reward_roles[user.level.toString()]) { - for (const role of Object.values(interaction.client.configurations.levels.config.reward_roles)) { - if (member.roles.cache.has(role)) member.roles.remove(role, '[levels] ' + localize('levels', 'granted-rewards-audit-log')).catch(); - } - } - member.roles.add(interaction.client.configurations.levels.config.reward_roles[user.level.toString()]); - } + user.xp = calculateLevelXP(interaction.client, user.level); + await fixLevelRoles(interaction, member, user.level); await user.save(); interaction.client.logger.info(localize('levels', 'manipulated', { u: formatDiscordUserName(interaction.user), m: formatDiscordUserName(member.user), - l: user.level, + l: displayLevel(user.level, interaction.client), v: user.xp })); if (interaction.client.logChannel) await interaction.client.logChannel.send(localize('levels', 'manipulated', { u: formatDiscordUserName(interaction.user), m: formatDiscordUserName(member.user), - l: user.level, + l: displayLevel(user.level, interaction.client), v: user.xp })); - await interaction.reply({ - ephemeral: true, + await interaction.editReply({ content: localize('levels', 'successfully-changed', { - l: user.level, + l: displayLevel(user.level, interaction.client), u: member.user.toString(), x: user.xp }) @@ -142,7 +143,7 @@ module.exports.subcommands = { u: user.userID })); await user.destroy(); - await interaction.editReply(localize('levels', 'removed-xp-successfully')); + await interaction.editReply(localize('levels', 'removed-xp-successfully', {u: user.userID})); } else { const users = await interaction.client.models['levels']['User'].findAll(); for (const user of users) await user.destroy(); diff --git a/modules/levels/commands/profile.js b/modules/levels/commands/profile.js index 066705b7..ba6dc03f 100644 --- a/modules/levels/commands/profile.js +++ b/modules/levels/commands/profile.js @@ -1,7 +1,18 @@ -const {embedType, formatDate, formatNumber} = require('../../../src/functions/helpers'); +const { + embedType, + formatDate, + formatNumber, + parseEmbedColor +} = require('../../../src/functions/helpers'); const {MessageEmbed} = require('discord.js'); const {localize} = require('../../../src/functions/localize'); -const {getMemberRoleFactor} = require('../events/messageCreate'); +const { + getMemberRoleFactor, + calculateLevelXP, + displayLevel, + isMaxLevel +} = require('../events/messageCreate'); +const {client} = require('../../../main'); module.exports.run = async function (interaction) { const moduleStrings = interaction.client.configurations['levels']['strings']; @@ -17,28 +28,34 @@ module.exports.run = async function (interaction) { }); if (!user) return interaction.reply(embedType(moduleStrings['user_not_found'], {}, {ephemeral: true})); - const nextLevelXp = user.level * 750 + ((user.level - 1) * 500); + const nextLevelXp = calculateLevelXP(interaction.client, user.level + 1); const embed = new MessageEmbed() - .setFooter({text: interaction.client.strings.footer, iconURL: interaction.client.strings.footerImgUrl}) - .setColor(moduleStrings.embed.color || 'GREEN') - .setThumbnail(member.user.avatarURL({dynamic: true})) + .setFooter({ + text: interaction.client.strings.footer, + iconURL: interaction.client.strings.footerImgUrl + }) + .setColor(parseEmbedColor(moduleStrings.embed.color || 'GREEN')) + .setThumbnail(member.user.avatarURL({forceStatic: false})) .setTitle(moduleStrings.embed.title.replaceAll('%username%', member.user.username)) .setDescription(moduleStrings.embed.description.replaceAll('%username%', member.user.username)) .addField(moduleStrings.embed.messages, formatNumber(user.messages), true) - .addField(moduleStrings.embed.xp, `${formatNumber(user.xp)}/${formatNumber(nextLevelXp)}`, true) - .addField(moduleStrings.embed.level, user.level.toString(), true); + .addField(moduleStrings.embed.xp, `${formatNumber(isMaxLevel(user.level, interaction.client) ? calculateLevelXP(interaction.client, interaction.client.configurations['levels']['config'].maximumLevel) : user.xp)}/${isMaxLevel(user.level, interaction.client) ? '∞' : formatNumber(nextLevelXp)}`, true) + .addField(moduleStrings.embed.level, displayLevel(user.level, interaction.client), true); - const roleFactor = getMemberRoleFactor(interaction.member); + const roleFactor = getMemberRoleFactor(member); if (roleFactor !== 1) { let roleString = ''; - for (const role of interaction.member.roles.cache.filter(f => moduleConfig['multiplication_roles'][f.id]).values()) { + for (const role of member.roles.cache.filter(f => moduleConfig['multiplication_roles'][f.id]).values()) { roleString = roleString + `\n* <@&${role.id}>: ${moduleConfig['multiplication_roles'][role.id]}x`; } embed.addField(moduleStrings.embed.roleFactor, `${roleString}\n${localize('levels', 'role-factors-total', {f: roleFactor})}`, true); } embed.addField(moduleStrings.embed.joinedAt, formatDate(member.joinedAt), true); - interaction.reply({ephemeral: true, embeds: [embed]}); + interaction.reply({ + ephemeral: true, + embeds: [embed] + }); }; module.exports.config = { diff --git a/modules/levels/configs/config.json b/modules/levels/configs/config.json index fc27c43f..ddd03df4 100644 --- a/modules/levels/configs/config.json +++ b/modules/levels/configs/config.json @@ -17,35 +17,50 @@ { "name": "min-xp", "humanName": { - "en": "XP given at least", - "de": "Mindestens gegebenes XP" + "en": "XP given at least for messages", + "de": "Für Nachrichten mindestens gegebenes XP" }, "default": { "en": 25, "de": 25 }, "description": { - "en": "How much XP the user gets at least", - "de": "So viel XP bekommt ein Benutzer mindestens" + "en": "How much XP the user gets at least for each message", + "de": "So viel XP bekommt ein Benutzer mindestens pro Nachricht" }, "type": "integer" }, { "name": "max-xp", "humanName": { - "en": "XP given at most", - "de": "Maximal gegebenes XP" + "en": "XP given at most for messages", + "de": "Für Nachrichten maximal gegebenes XP" }, "default": { "en": 65, "de": 65 }, "description": { - "en": "How much XP the user gets at most", - "de": "So viel XP bekommt ein Benutzer maximal" + "en": "How much XP the user gets at most for each messages", + "de": "So viel XP bekommt ein Benutzer maximal pro Nachricht" }, "type": "integer" }, + { + "name": "voiceXPPerMinute", + "type": "float", + "default": { + "en": 0.5 + }, + "humanName": { + "en": "XP given per Voice Minute", + "de": "Pro Sprachminute vergebenes XP" + }, + "description": { + "en": "How many XP will be given to users per minute when they are in a voice channel with other members. No XP will be given if they are alone in their channel or are muted or deafened. Numbers will be rounded and XP will be given every 15 minutes or when the user leaves the channel.", + "de": "Wie viel XP Nutzer pro Minute erhalten, wenn sie sich in einem Sprachkanal mit anderen Nutzern befinden. Es wird kein XP vergeben, wenn sie alleine in einem Kanal sind oder stummgeschaltet sind oder den Ton deaktiviert haben. Zahlen werden gerundet und XP wird alle 15 Minuten vergeben, oder wenn der Nutzer den Kanal verlässt." + } + }, { "name": "cooldown", "humanName": { @@ -61,6 +76,105 @@ }, "type": "integer" }, + { + "name": "curveType", + "type": "select", + "content": [ + { + "displayName": { + "en": "Easy Linear", + "de": "Einfacherer Linearfunktion" + }, + "value": "EXPONENTIAL" + }, + { + "displayName": { + "en": "Default Linear", + "de": "Standardmässige Linearfunktion" + }, + "value": "LINEAR" + }, + { + "displayName": { + "en": "Exponentiation (softer start, harder leveling after level 14)", + "de": "Potenzfunktion (leichter start, ab Level 14 härter)" + }, + "value": "EXPONENTIATION" + }, + { + "value": "CUSTOM", + "displayName": { + "en": "Custom formula (dangerous!)", + "de": "Eigene Formel (gefährlich!)" + } + } + ], + "humanName": { + "en": "Type of the leveling curve", + "de": "Art der Levelingkurve" + }, + "default": { + "en": "LINEAR" + }, + "description": { + "en": "Type of the leveling curve. The exponential curve is recommended, as archiving new levels gets harder the higher your level is. Leveling is always the same if you use the linear curve.", + "de": "Art der Levelingkurve. Die exponentielle Kurve wird empfohlen, da mit dieser das Aufsteigen von Leveln schwerer wird je höher das eigene Level ist. Mit der linearen Kurve ist das Aufsteigen zum nächsten Level für alle gleich schwer." + }, + "links": [ + { + "label": { + "en": "Calculate how much XP is needed to level up", + "de": "Berechne, wie viel XP zum Aufsteigen notwendig ist" + }, + "url": "https://scootk.it/level-calculator" + } + ] + }, + { + "name": "customLevelCurve", + "default": { + "en": "" + }, + "allowNull": true, + "humanName": { + "en": "Custom Level Formula (if enabled)", + "de": "Eigene Levelformel (wenn aktiviert)" + }, + "type": "string", + "links": [ + { + "label": { + "en": "Calculate how much XP is needed to level up", + "de": "Berechne, wie viel XP zum Aufsteigen notwendig ist" + }, + "url": "https://scootk.it/level-calculator" + } + ], + "description": { + "en": "Your custom leveling formula. Use the x variable (and no other variables). The result of the formula should be the required XP to reach level x (your variable). Example: \"x*750+((x-1)*500)\" (our default level curve)", + "de": "Deine eigene Levelformel. Nutze nur die x Variabel (und keine andere Variablen). Das Ergebnis deiner Formel sollte die XP-Anzahl sein, die notwendig ist, um Level x zu erreichen (deine Variabel). Beispiel: \"x*750+((x-1)*500)\" (unsere Standartkurve)" + } + }, + { + "name": "levelUpMessagesConditions", + "type": "select", + "content": [ + "all", + "only-role-rewards", + "none" + ], + "humanName": { + "de": "Welche Level-Up-Nachrichten sollen gesendet werden?", + "en": "Which Level-Up-Messages should get sent?" + }, + "default": { + "en": "all" + }, + "description": { + "en": "This settings changes in which cases a level up message should be sent. With the setting \"all\", level up messages will be sent at every level up. With the setting \"only-role-rewards\", level up messages will only be sent if the new level has a role reward. With the \"none\" setting, no level up messages will be sent.", + "de": "Diese Einstellung verändert, welche Art von Level-Up-Nachrichten gesendet werden. Mit der Einstellung \"all\", werden Level-Up-Nachrichten bei jedem Level-Up versendet. Mit der Einstellung \"only-role-rewards\" werden Level-Up-Nachrichten nur gesandt, wenn das neue Level eine Rollenbelohnung hat. Wenn die Einstellung \"none\" gewählt ist, werden keine Level-Up-Nachrichten verschickt." + } + }, { "name": "level_up_channel_id", "humanName": { @@ -102,6 +216,12 @@ "en": "Blacklisted Channels", "de": "Channel ohne XP" }, + "channelTypes": [ + "GUILD_TEXT", + "GUILD_NEWS", + "GUILD_VOICE", + "GUILD_FORUM" + ], "default": { "en": [], "de": [] @@ -140,8 +260,8 @@ "de": {} }, "description": { - "en": "Level, bei denen der Nutzer eine Rolle bekommt. Parameter 1: Level, Parameter 2: Rollen-ID", - "de": "Level at which users should get roles" + "de": "Level, bei denen der Nutzer eine Rolle bekommt. Parameter 1: Level, Parameter 2: Rollen-ID", + "en": "Level at which users should get roles. Parameter 1: Level, Parameter 2: Role-ID" }, "type": "keyed", "content": { @@ -169,6 +289,26 @@ "value": "float" } }, + { + "name": "multiplication_channels", + "humanName": { + "en": "XP Multiplication Channels", + "de": "XP-Multiplikator Kanäle" + }, + "default": { + "en": {}, + "de": {} + }, + "description": { + "en": "Allows you to configure channels that have a higher multiplication factor than normal (default value is 1). Messages sent in these channels will have their XP value multiplied by the multiplier configured here.", + "de": "Erlaubt es dir, den Multiplikationsfaktor von bestimmten Kanälen anzupassen. Standardmäßig haben Rollen einen Wert von 1. Die XP-Werte von Nachrichten, die in hier konfigurierten Kanälen gesendet werden, werden mit den hier eingestellten Multiplikator multipliziert." + }, + "type": "keyed", + "content": { + "key": "channelID", + "value": "float" + } + }, { "name": "onlyTopLevelRole", "humanName": { @@ -209,23 +349,23 @@ "en": false }, "description": { - "en": "Wenn aktiviert wird das Modul die Level-Up-Nachricht zufällig auswählen und nicht die in Nachrichten angegebene verwenden", - "de": "If enabled the module will randomly select a messages from random-levelup-messages and ignore the one set in strings" + "de": "Wenn aktiviert wird das Modul die Level-Up-Nachricht zufällig auswählen und nicht die in Nachrichten angegebene verwenden", + "en": "If enabled the module will randomly select a messages from random-levelup-messages and ignore the one set in strings" }, "type": "boolean" }, { "name": "leaderboard-channel", "humanName": { - "en": "Leaderboard-Channel", - "de": "Ranglisten-Channel" + "en": "Live Leaderboard-Channel", + "de": "Live Ranglisten-Channel" }, "default": { "en": "" }, "description": { - "en": "Wenn gesetzt wird der Bot in diesen Channel eine Nachricht senden, welche die aktuellen Level der Nutzern enthält", - "de": "If set, the bot will send a messages in this channel with the current leaderboard and edit it every five minutes" + "de": "Wenn gesetzt wird der Bot in diesen Channel eine Nachricht senden, welche die aktuellen Level der Nutzern enthält", + "en": "If set, the bot will send a messages in this channel with the current leaderboard and edit it every five minutes" }, "type": "channelID", "content": [ @@ -234,49 +374,95 @@ "allowNull": true }, { - "name": "useTags", + "name": "leaderboard-channel-max-amount", "humanName": { - "en": "Use User's Tags instead of their Mention in the Leaderboard-Channel-Embed", - "de": "Nutze den Tag der Nutzer, anstatt eine Erwähnung im Ranglisten-Channel-Embed" + "en": "Maximum amount of users displayed in live leaderboard Channel", + "de": "Maximale Anzahl von Nutzern im Live Ranglistenkanal" + }, + "default": { + "en": 15 + }, + "maxValue": 25, + "description": { + "de": "Dies ist die Anzahl von Nutzern, die in der Live Rangliste angezeigt werden sollen. /leaderboard zeigt weiterhin die vollständige Rangliste.", + "en": "This is the maximum amount of users displayed in the Live Leaderboard channel. /leaderboard will still show the full leaderboard." + }, + "type": "integer" + }, + { + "name": "maximumLevelEnabled", + "humanName": { + "en": "Enable maximum level?", + "de": "Maximales Level aktivieren?" }, "default": { "en": false }, "description": { - "en": "If enabled, the bot will use the tag of users in the Leaderboard-Channel-Embed instead of their mention.", - "de": "Wenn aktiviert, wird im Ranglisten-Channel-Embed der Tag des Nutzers angezeigt und nicht eine Erwähnung (bei großen Servern empfohlen)" + "en": "If enabled, users can only level until they reach the configured maximum level. After that, they can't level up and can't earn XP. Can be enabled retroactively.", + "de": "Wenn aktiviert können Nutzer nur ein bestimmtes Level erreichen. Sobald sie dieses Level erreicht haben, können sie nicht weiter aufsteigen oder weiter XP verdienen. Kann rückwirkend aktiviert werden." }, "type": "boolean" }, { - "name": "allowCheats", + "dependsOn": "maximumLevelEnabled", + "name": "maximumLevel", "humanName": { - "en": "Cheats" + "en": "Maximum level", + "de": "Maximales Level" + }, + "default": { + "en": 200 + }, + "description": { + "en": "Once a user reaches this level, they neither earn more XP nor level up anymore.", + "de": "Sobald ein Nutzer dieses Level erreicht hat, kann dieser weder mehr XP verdienen noch weiter Level aufsteigen." + }, + "type": "integer" + }, + { + "name": "startFromZero", + "humanName": { + "en": "Start with Level 0?", + "de": "Von Level 0 starten?" }, "default": { "en": false }, "description": { - "en": "If enabled admins can change the XP of other users (not recommended (please leave it of if you want to have a fair levelsystem!!!))", - "de": "Wenn aktiviert können Administratoren die XP von anderen Nutzern editieren (nicht empfohlen, wenn du einen coolen, fairen Server haben willst (wirklich nicht!!!)))" + "en": "If enabled, the initial level of users will be displayed as zero. This doesn't affect leveling, this is a cosmetic setting and can be applied retroactively.", + "de": "Wenn aktiviert werden die Anfangslevel von Nutzern als null angezeigt. Das hat keinen Einfluss auf das Leveling, das ist eine kosmetische Einstellung und kann rückwirkend angewandt werden." + }, + "type": "boolean" + }, + { + "name": "useTags", + "humanName": { + "en": "Use User's Tags instead of their Mention in the Leaderboard-Channel-Embed", + "de": "Nutze den Tag der Nutzer, anstatt eine Erwähnung im Ranglisten-Channel-Embed" + }, + "default": { + "en": false + }, + "description": { + "en": "If enabled, the bot will use the tag of users in the Leaderboard-Channel-Embed instead of their mention.", + "de": "Wenn aktiviert, wird im Ranglisten-Channel-Embed der Tag des Nutzers angezeigt und nicht eine Erwähnung (bei großen Servern empfohlen)" }, "type": "boolean" }, { - "name": "disableSCNetworkProfile", + "name": "allowCheats", "humanName": { - "en": "Disable SC Network Profiles", - "de": "Deaktiviert SC Network Profile" + "en": "Cheats" }, "default": { "en": false }, "description": { - "en": "If enabled admins can change the XP of other users (not recommended (please leave it of if you want to have a fair levelsystem!!!))", + "en": "If enabled admins can change the XP of other users (not recommended (please leave it off if you want to have a fair levelling system!!!))", "de": "Wenn aktiviert können Administratoren die XP von anderen Nutzern editieren (nicht empfohlen, wenn du einen coolen, fairen Server haben willst (wirklich nicht!!!)))" }, - "type": "boolean", - "pro": true + "type": "boolean" } ] } \ No newline at end of file diff --git a/modules/levels/events/botReady.js b/modules/levels/events/botReady.js index dc80fc2f..751b0a39 100644 --- a/modules/levels/events/botReady.js +++ b/modules/levels/events/botReady.js @@ -1,6 +1,20 @@ const {updateLeaderBoard} = require('../leaderboardChannel'); +const {disableModule} = require('../../../src/functions/helpers'); +const {localize} = require('../../../src/functions/localize'); + module.exports.run = async function (client) { + if (client.configurations['levels']['config']['customLevelCurve']) { + const Formula = (await import('fparser')).default; + let customFormula = null; + try { + customFormula = new Formula(client.configurations['levels']['config']['customLevelCurve']); + } catch (e) { + return disableModule('levels', localize('levels', 'invalid-custom-formula')); + } + if (customFormula && (customFormula.getVariables().length !== 1 || customFormula.getVariables()[0] !== 'x')) return disableModule('levels', localize('levels', 'invalid-custom-formula')); + if (customFormula) client.configurations['levels']['config'].customLevelCurveParsed = customFormula; + } if (!client.configurations['levels']['config']['leaderboard-channel']) return; await updateLeaderBoard(client, true); const interval = setInterval(() => { diff --git a/modules/levels/events/interactionCreate.js b/modules/levels/events/interactionCreate.js index f9242494..f68d8696 100644 --- a/modules/levels/events/interactionCreate.js +++ b/modules/levels/events/interactionCreate.js @@ -1,5 +1,6 @@ const {localize} = require('../../../src/functions/localize'); const {embedType, formatNumber} = require('../../../src/functions/helpers'); +const {calculateLevelXP, displayLevel, isMaxLevel} = require('./messageCreate'); module.exports.run = async function (client, interaction) { if (!interaction.client.botReadyAt) return; @@ -14,11 +15,11 @@ module.exports.run = async function (client, interaction) { ephemeral: true, content: localize('levels', 'please-send-a-message') }); - const nextLevelXp = user.level * 750 + ((user.level - 1) * 500); + const nextLevelXp = calculateLevelXP(client, user.level + 1); interaction.reply(embedType(client.configurations['levels']['strings']['leaderboard-button-answer'], { '%name%': interaction.user.username, - '%level%': user.level, - '%userXP%': formatNumber(user.xp), - '%nextLevelXP%': formatNumber(nextLevelXp) + '%level%': displayLevel(user.level, client), + '%userXP%': formatNumber(isMaxLevel(user.level, client) ? calculateLevelXP(client, client.configurations['levels']['config'].maximumLevel - 1) : user.xp), + '%nextLevelXP%': isMaxLevel(user.level, client) ? '∞' : formatNumber(nextLevelXp) }, {ephemeral: true})); }; \ No newline at end of file diff --git a/modules/levels/events/messageCreate.js b/modules/levels/events/messageCreate.js index c9cd2803..8020fdc6 100644 --- a/modules/levels/events/messageCreate.js +++ b/modules/levels/events/messageCreate.js @@ -2,12 +2,52 @@ const { embedType, randomIntFromInterval, randomElementFromArray, - embedTypeV2, formatDiscordUserName + embedTypeV2, formatDiscordUserName, formatNumber } = require('../../../src/functions/helpers'); +const {ChannelType} = require('discord.js'); + +const curves = { + 'EXPONENTIAL': (level) => level * 750 + ((level - 1) * 500), + 'LINEAR': (level) => level * 750, + 'EXPONENTIATION': (level) => 350 * (level - 1) ** 2, + 'CUSTOM': (level) => { + const customFormula = client.configurations['levels']['config'].customLevelCurveParsed; + if (!customFormula) { + console.error(localize('levels', 'no-custom-formula')); + return curves['EXPONENTIAL'](level); + } + return customFormula.evaluate({x: level}); + } +}; + +function calculateLevelXP(client, level) { + return curves[client.configurations['levels']['config'].curveType](level, client); +} + +module.exports.calculateLevelXP = calculateLevelXP; + +function isMaxLevel(level, client) { + if (!client.configurations['levels']['config'].maximumLevelEnabled) return false; + return level - (client.configurations['levels']['config'].startFromZero ? 1 : 0) >= client.configurations['levels']['config'].maximumLevel; +} + +module.exports.isMaxLevel = isMaxLevel; + + +function displayLevel(level, client) { + const displayLevel = level - (client.configurations['levels']['config'].startFromZero ? 1 : 0); + if (isMaxLevel(level, client)) return formatNumber(client.configurations['levels']['config'].maximumLevel); + return formatNumber(displayLevel); +} + +module.exports.displayLevel = displayLevel; + const {registerNeededEdit} = require('../leaderboardChannel'); const {localize} = require('../../../src/functions/localize'); +const {client} = require('../../../main'); + const cooldown = new Set(); -let currentlyLevelingUp = []; +let currentlyLevelingUp = new Set(); function getMemberRoleFactor(member) { let roleFactor = 1; @@ -19,45 +59,44 @@ function getMemberRoleFactor(member) { module.exports.getMemberRoleFactor = getMemberRoleFactor; -module.exports.run = async (client, msg) => { - if (!client.botReadyAt) return; - if (msg.author.bot || msg.system) return; - if (!msg.guild) return; - if (msg.guild.id !== client.guildID) return; - if (cooldown.has(msg.author.id)) return; - +async function grantXPAndLevelUP(client, member, xp, xpType, channel, msg = null) { const moduleConfig = client.configurations['levels']['config']; const moduleStrings = client.configurations['levels']['strings']; - if (msg.content.includes(client.config.prefix)) return; - if (moduleConfig.blacklisted_channels.includes(msg.channel.id) || moduleConfig.blacklisted_channels.includes(msg.channel.parentId)) return; - if (msg.member.roles.cache.filter(r => moduleConfig.blacklistedRoles.includes(r.id)).size !== 0) return; - let xp = randomIntFromInterval(moduleConfig['min-xp'], moduleConfig['max-xp']); let user = await client.models['levels']['User'].findOne({ where: { - userID: msg.author.id + userID: member.user.id } }); if (!user) { user = await client.models['levels']['User'].create({ - userID: msg.author.id, + userID: member.user.id, messages: 0, xp: 0 }); } - user.messages = user.messages + 1; - const nextLevelXp = user.level * 750 + ((user.level - 1) * 500); - xp = xp * getMemberRoleFactor(msg.member); + if (isMaxLevel(user.level, client)) return; + if (xpType === 'message') user.messages = user.messages + 1; + + + const nextLevelXp = calculateLevelXP(client, user.level + 1); + + xp = xp * getMemberRoleFactor(member); + if (moduleConfig['multiplication_channels'][channel.id]) xp = xp * parseFloat(moduleConfig['multiplication_channels'][channel.id]); user.xp = user.xp + xp; + await user.save(); - if (nextLevelXp <= user.xp && !currentlyLevelingUp.includes(msg.author.id)) { - currentlyLevelingUp.push(msg.author.id); - user.level = user.level + 1; - const channel = client.channels.cache.find(c => c.id === moduleConfig.level_up_channel_id); + if (nextLevelXp <= user.xp && !currentlyLevelingUp.has(member.user.id)) { + let i = 1; + while (user.xp >= calculateLevelXP(client, user.level + i)) i++; + currentlyLevelingUp.add(member.user.id); + user.level = user.level + (i - 1); + const levelUpChannel = client.channels.cache.find(c => c.id === moduleConfig.level_up_channel_id && c.type === ChannelType.GuildText); - const specialMessage = client.configurations['levels']['special-levelup-messages'].find(m => m.level === user.level); - const isRewardMessage = !!moduleConfig.reward_roles[user.level.toString()]; + const calculatedLevel = user.level - (client.configurations['levels']['config'].startFromZero ? 1 : 0); + const isRewardMessage = !!moduleConfig.reward_roles[calculatedLevel.toString()]; + const specialMessage = client.configurations['levels']['special-levelup-messages'].find(m => m.level === calculatedLevel); const randomMessages = client.configurations['levels']['random-levelup-messages'].filter(m => m.type === (isRewardMessage ? 'with-reward' : 'normal')); let messageToSend = moduleStrings.level_up_message; @@ -71,22 +110,23 @@ module.exports.run = async (client, msg) => { if (isRewardMessage) { if (moduleConfig.onlyTopLevelRole) { for (const role of Object.values(moduleConfig.reward_roles)) { - if (msg.member.roles.cache.has(role)) await msg.member.roles.remove(role, '[levels] ' + localize('levels', 'granted-rewards-audit-log')).catch(); + if (member.roles.cache.has(role)) await member.roles.remove(role, '[levels] ' + localize('levels', 'granted-rewards-audit-log')).catch(); } } - await msg.member.roles.add(moduleConfig.reward_roles[user.level.toString()], '[levels]' + localize('levels', 'granted-rewards-audit-log')).catch(); + await member.roles.add(moduleConfig.reward_roles[calculatedLevel.toString()], '[levels]' + localize('levels', 'granted-rewards-audit-log')).catch(); } if (specialMessage) messageToSend = specialMessage.message; await sendLevelUpMessage(await embedTypeV2(messageToSend, { - '%mention%': `<@${msg.author.id}>`, - '%avatarURL%': msg.author.avatarURL() || msg.author.defaultAvatarURL, - '%username%': msg.author.username, - '%newLevel%': user.level, - '%role%': isRewardMessage ? `<@&${moduleConfig.reward_roles[user.level.toString()]}>` : localize('levels', 'no-role'), - '%tag%': formatDiscordUserName(msg.author) + '%mention%': `<@${member.user.id}>`, + '%avatarURL%': member.user.avatarURL() || member.user.defaultAvatarURL, + '%username%': member.user.username, + '%newLevel%': displayLevel(user.level, client), + '%role%': isRewardMessage ? `<@&${moduleConfig.reward_roles[calculatedLevel.toString()]}>` : localize('levels', 'no-role'), + '%tag%': formatDiscordUserName(member.user) }, {allowedMentions: {parse: ['users']}})); - currentlyLevelingUp = currentlyLevelingUp.filter(f => f !== msg.author.id); + await user.save(); + currentlyLevelingUp.delete(member.user.id); /** * Sends the level up messages @@ -94,15 +134,37 @@ module.exports.run = async (client, msg) => { * @param {Object} content Content of the message */ async function sendLevelUpMessage(content) { - if (channel) await channel.send(content); - else await msg.reply(content); + if (moduleConfig.levelUpMessagesConditions === 'none' || (moduleConfig.levelUpMessagesConditions === 'only-role-rewards' && !isRewardMessage)) return; + if (levelUpChannel) await levelUpChannel.send(content); + else { + if (msg) await msg.reply(content); + else channel.send(content); + } } } +} + +module.exports.grantXPAndLevelUP = grantXPAndLevelUP; + +module.exports.run = async (client, msg) => { + if (!client.botReadyAt) return; + if (msg.author.bot || msg.system) return; + if (!msg.guild) return; + if (msg.guild.id !== client.guildID) return; + if (cooldown.has(msg.author.id)) return; + + const moduleConfig = client.configurations['levels']['config']; + + if (msg.content.includes(client.config.prefix)) return; + if (moduleConfig.blacklisted_channels.includes(msg.channel.id) || moduleConfig.blacklisted_channels.includes(msg.channel.parentId) || moduleConfig.blacklisted_channels.includes(msg.channel.parent?.parentId)) return; + if (msg.member.roles.cache.filter(r => moduleConfig.blacklistedRoles.includes(r.id)).size !== 0) return; + let xp = randomIntFromInterval(moduleConfig['min-xp'], moduleConfig['max-xp']); + + await grantXPAndLevelUP(client, msg.member, xp, 'message', msg.channel, msg); cooldown.add(msg.author.id); registerNeededEdit(); setTimeout(() => { cooldown.delete(msg.author.id); }, moduleConfig.cooldown); - await user.save(); -}; \ No newline at end of file +}; diff --git a/modules/levels/events/voiceStateUpdate.js b/modules/levels/events/voiceStateUpdate.js new file mode 100644 index 00000000..4d684812 --- /dev/null +++ b/modules/levels/events/voiceStateUpdate.js @@ -0,0 +1,53 @@ +const {ChannelType} = require('discord.js'); +const {grantXPAndLevelUP} = require('./messageCreate'); +const states = new Map(); + +async function startVoiceSession(client, currentState) { + const moduleConfig = client.configurations['levels']['config']; + if (moduleConfig.blacklisted_channels.includes(currentState.channel.id) || moduleConfig.blacklisted_channels.includes(currentState.channel.parentId)) return; + + const int = setInterval(() => { + grantXP(client, currentState?.member).then(() => { + }); + }, 1000 * 60 * 15); + + states.set(currentState.member.id, { + start: new Date(), + channel: currentState.channel, + lastXPTime: new Date(), + end: null, + interval: int + }); +} + +async function endVoiceSession(client, currentState) { + if (!states.has(currentState.member.id)) return; + const oldState = states.get(currentState.member.id); + clearInterval(oldState.interval); + states.delete(currentState.member.id); + await grantXP(client, currentState.member); +} + +async function grantXP(client, member) { + const stateData = states.get(member?.id); + if (!stateData) return; + const diff = new Date().getTime() - stateData.lastXPTime.getTime(); + stateData.lastXPTime = new Date(); + const moduleConfig = client.configurations['levels']['config']; + const timeInMinutes = (diff / (1000 * 60)); + const xp = Math.round(moduleConfig['voiceXPPerMinute'] * timeInMinutes); + await grantXPAndLevelUP(client, member, xp, 'voice', stateData.channel); +} + +module.exports.run = async function (client, oldState, newState) { + if (!client.botReadyAt) return; + if (!newState.guild || newState.member.user.bot) return; + if (newState.guild.id !== client.guildID || client.configurations['levels']['config']['voiceXPPerMinute'] === 0) return; + + if (newState.channel && (client.configurations['levels']['config'].blacklisted_channels.includes(newState.channel.id) || client.configurations['levels']['config'].blacklisted_channels.includes(newState.channel.parentId) || client.configurations['levels']['config'].blacklisted_channels.includes(newState.channel.parent?.parentId))) return; + if (newState.member.roles.cache.filter(r => client.configurations['levels']['config'].blacklistedRoles.includes(r.id)).size !== 0) return; + + if (oldState.channel !== newState.channel || oldState.deaf !== newState.deaf || oldState.mute !== newState.mute) await endVoiceSession(client, newState); + + if (newState.channel && !newState.deaf && !newState.mute && newState.channel.type !== ChannelType.GuildStageVoice) await startVoiceSession(client, newState); +}; \ No newline at end of file diff --git a/modules/levels/leaderboardChannel.js b/modules/levels/leaderboardChannel.js index 8730dff7..7f611806 100644 --- a/modules/levels/leaderboardChannel.js +++ b/modules/levels/leaderboardChannel.js @@ -3,9 +3,15 @@ * @module Levels-Leaderboard * @author Simon Csaba */ -const {MessageEmbed} = require('discord.js'); +const {ChannelType, MessageEmbed} = require('discord.js'); const {localize} = require('../../src/functions/localize'); -const {formatDiscordUserName} = require('../../src/functions/helpers'); +const { + formatDiscordUserName, + formatNumber, + parseEmbedColor +} = require('../../src/functions/helpers'); +const {displayLevel, isMaxLevel, calculateLevelXP} = require('./events/messageCreate'); +const {client} = require('../../main'); let changed = false; /** @@ -20,14 +26,24 @@ module.exports.updateLeaderBoard = async function (client, force = false) { const moduleStrings = client.configurations['levels']['strings']; const channel = await client.channels.fetch(client.configurations['levels']['config']['leaderboard-channel']).catch(() => { }); - if (!channel || channel.type !== 'GUILD_TEXT') return client.logger.error('[levels] ' + localize('levels', 'leaderboard-channel-not-found')); - const messages = (await channel.messages.fetch()).filter(msg => msg.author.id === client.user.id && !msg.system); + if (!channel || channel.type !== ChannelType.GuildText) return client.logger.error('[levels] ' + localize('levels', 'leaderboard-channel-not-found')); + const [messageData] = await client.models['levels']['LiveLeaderboard'].findOrCreate({ + where: { + channelID: channel.id + }, + defaults: { + channelID: channel.id + } + }); + let message = messageData.messageID ? await channel.messages.fetch(messageData.messageID).catch(() => { + }) : null; + const users = await client.models['levels']['User'].findAll({ order: [ ['xp', 'DESC'] ], - limit: 15 + limit: 60 }); let leaderboardString = ''; @@ -35,12 +51,13 @@ module.exports.updateLeaderBoard = async function (client, force = false) { for (const user of users) { const member = channel.guild.members.cache.get(user.userID); if (!member) continue; + if (i >= client.configurations['levels']['config']['leaderboard-channel-max-amount']) continue; i++; leaderboardString = leaderboardString + localize('levels', 'leaderboard-notation', { p: i, u: client.configurations['levels']['config']['useTags'] ? formatDiscordUserName(member.user) : member.user.toString(), - l: user.level, - xp: user.xp + l: displayLevel(user.level, client), + xp: formatNumber(isMaxLevel(user.level, client) ? calculateLevelXP(client, client.configurations['levels']['config'].maximumLevel - 1) : user.xp) }) + '\n'; } if (leaderboardString.length === 0) leaderboardString = localize('levels', 'no-user-on-leaderboard'); @@ -48,7 +65,7 @@ module.exports.updateLeaderBoard = async function (client, force = false) { const embed = new MessageEmbed() .setTitle(moduleStrings.liveLeaderBoardEmbed.title) .setDescription(moduleStrings.liveLeaderBoardEmbed.description) - .setColor(moduleStrings.liveLeaderBoardEmbed.color) + .setColor(parseEmbedColor(moduleStrings.liveLeaderBoardEmbed.color)) .setFooter({text: client.strings.footer, iconURL: client.strings.footerImgUrl}) .setThumbnail(channel.guild.iconURL()) .addField(localize('levels', 'leaderboard'), leaderboardString); @@ -65,8 +82,20 @@ module.exports.updateLeaderBoard = async function (client, force = false) { }] }]; - if (messages.first()) await messages.first().edit({embeds: [embed], components}); - else await channel.send({embeds: [embed], components}); + if (message) { + await message.edit({ + embeds: [embed], + components + }); + if (force) client.logger.info(localize('levels', 'list-location', {l: message.url})); + } else { + message = await channel.send({ + embeds: [embed], + components + }); + messageData.messageID = message.id; + await messageData.save(); + } }; /** diff --git a/modules/levels/models/LiveLeaderboard.js b/modules/levels/models/LiveLeaderboard.js new file mode 100644 index 00000000..69fb1675 --- /dev/null +++ b/modules/levels/models/LiveLeaderboard.js @@ -0,0 +1,25 @@ +const { + DataTypes, + Model +} = require('sequelize'); + +module.exports = class LevelsLiveLeaderboard extends Model { + static init(sequelize) { + return super.init({ + channelID: { + type: DataTypes.STRING, + primaryKey: true + }, + messageID: DataTypes.STRING + }, { + tableName: 'levels_liveleaderboard', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'LiveLeaderboard', + 'module': 'levels' +}; \ No newline at end of file diff --git a/modules/massrole/commands/massrole.js b/modules/massrole/commands/massrole.js index 7aca5215..c546e2ec 100644 --- a/modules/massrole/commands/massrole.js +++ b/modules/massrole/commands/massrole.js @@ -14,6 +14,7 @@ module.exports.subcommands = { if (interaction.replied) return; const moduleStrings = interaction.client.configurations['massrole']['strings']; checkTarget(interaction); + await interaction.guild.members.fetch({time: 600000}); if (target === 'all') { await interaction.deferReply({ephemeral: true}); for (const member of interaction.guild.members.cache.values()) { @@ -72,6 +73,7 @@ module.exports.subcommands = { if (interaction.replied) return; const moduleStrings = interaction.client.configurations['massrole']['strings']; checkTarget(interaction); + await interaction.guild.members.fetch({time: 600000}); if (target === 'all') { await interaction.deferReply({ ephemeral: true }); for (const member of interaction.guild.members.cache.values()) { @@ -134,6 +136,7 @@ module.exports.subcommands = { if (interaction.replied) return; const moduleStrings = interaction.client.configurations['massrole']['strings']; checkTarget(interaction); + await interaction.guild.members.fetch({time: 600000}); if (target === 'all') { await interaction.deferReply({ ephemeral: true }); for (const member of interaction.guild.members.cache.values()) { diff --git a/modules/moderation/commands/moderate.js b/modules/moderation/commands/moderate.js index ea32e192..24d63cd1 100644 --- a/modules/moderation/commands/moderate.js +++ b/modules/moderation/commands/moderate.js @@ -1,7 +1,10 @@ const {localize} = require('../../../src/functions/localize'); const { embedType, dateToDiscordTimestamp, lockChannel, unlockChannel, - sendMultipleSiteButtonMessage, truncate, formatDiscordUserName + sendMultipleSiteButtonMessage, + truncate, + formatDiscordUserName, + parseEmbedColor } = require('../../../src/functions/helpers'); const {moderationAction} = require('../moderationActions'); const durationParser = require('parse-duration'); @@ -104,7 +107,7 @@ module.exports.subcommands = { .setTitle(localize('moderation', 'notes-embed-title', {u: formatDiscordUserName(interaction.options.getUser('user'))})) .setFooter({text: interaction.client.strings.footer, iconURL: interaction.client.strings.footerImgUrl}) .setThumbnail(interaction.options.getUser('user').avatarURL()) - .setColor('GREEN') + .setColor(parseEmbedColor('GREEN')) .setAuthor({name: interaction.client.user.username, iconURL: interaction.client.user.avatarURL()}) .setFields(fields); interaction.editReply({ @@ -229,7 +232,7 @@ module.exports.subcommands = { if (interaction.replied) return; if (!checkRoles(interaction, 3)) return; const parseDuration = interaction.options.getString('duration') ? new Date(new Date().getTime() + durationParser(interaction.options.getString('duration'))) : null; - moderationAction(interaction.client, 'quarantine', interaction.member, interaction.memberToExecuteUpon, interaction.options.getString('reason'), {roles: Array.from(interaction.options.getMember('user').roles.cache.keys())}, parseDuration).then(r => { + moderationAction(interaction.client, 'quarantine', interaction.member, interaction.memberToExecuteUpon, interaction.options.getString('reason'), {roles: Array.from(interaction.options.getMember('user').roles.cache.filter(f => !f.managed).keys())}, parseDuration).then(r => { if (r) { if (parseDuration) interaction.editReply({ content: localize('moderation', 'expiring-action-done', { @@ -410,7 +413,7 @@ module.exports.subcommands = { */ function addSite(fs) { const embed = new MessageEmbed() - .setColor('YELLOW') + .setColor(parseEmbedColor('YELLOW')) .setAuthor({name: interaction.client.user.username, iconURL: interaction.client.user.avatarURL()}) .setTitle(localize('moderation', 'actions-embed-title', { u: formatDiscordUserName(interaction.memberToExecuteUpon.user), diff --git a/modules/moderation/commands/report.js b/modules/moderation/commands/report.js index f0da600a..df2bbf4c 100644 --- a/modules/moderation/commands/report.js +++ b/modules/moderation/commands/report.js @@ -1,5 +1,9 @@ const {localize} = require('../../../src/functions/localize'); -const {embedType, messageLogToStringToPaste} = require('../../../src/functions/helpers'); +const { + embedType, + messageLogToStringToPaste, + parseEmbedColor +} = require('../../../src/functions/helpers'); const {MessageEmbed} = require('discord.js'); module.exports.run = async function (interaction) { @@ -44,7 +48,7 @@ module.exports.run = async function (interaction) { .addField(localize('moderation', 'report-reason'), interaction.options.getString('reason')) .addField(localize('moderation', 'report-user'), interaction.user.toString() + ` \`${interaction.user.id}\``) .addFields(fields) - .setColor('RED') + .setColor(parseEmbedColor('RED')) .setImage(proof ? (proof.proxyURL || proof.url) : null) .setFooter({text: interaction.client.strings.footer, iconURL: interaction.client.strings.footerImgUrl}) .setAuthor({name: interaction.client.user.username, iconURL: interaction.client.user.avatarURL()}) diff --git a/modules/moderation/configs/antiGrief.json b/modules/moderation/configs/antiGrief.json index 242a0472..eb1595f2 100644 --- a/modules/moderation/configs/antiGrief.json +++ b/modules/moderation/configs/antiGrief.json @@ -36,15 +36,15 @@ { "name": "timeframe", "humanName": { - "de": "Zeitfenster", - "en": "Timeframe" + "de": "Zeitfenster (in Stunden)", + "en": "Timeframe (in hours)" }, "default": { "en": 3 }, "description": { "en": "Timeframe in hours in which the limits can not be overstepped", - "de": "Zeitfenster in Stunden, in welchem die Limits nicht übertragen werden dürfen" + "de": "Zeitfenster in Stunden, in welchem die Limits nicht überschritten werden dürfen" }, "type": "integer" }, diff --git a/modules/moderation/configs/antiJoinRaid.json b/modules/moderation/configs/antiJoinRaid.json index 985e368b..e219499e 100644 --- a/modules/moderation/configs/antiJoinRaid.json +++ b/modules/moderation/configs/antiJoinRaid.json @@ -29,8 +29,8 @@ { "name": "timeframe", "humanName": { - "de": "Zeitfenster", - "en": "Timeframe" + "de": "Zeitfenster (in Minuten)", + "en": "Timeframe (in minutes)" }, "default": { "en": 5, diff --git a/modules/moderation/configs/antiSpam.json b/modules/moderation/configs/antiSpam.json index 81a3189d..a1173b89 100644 --- a/modules/moderation/configs/antiSpam.json +++ b/modules/moderation/configs/antiSpam.json @@ -29,16 +29,16 @@ { "name": "timeframe", "humanName": { - "de": "Zeitfenster", - "en": "Timeframe" + "de": "Zeitfenster (in Sekunden)", + "en": "Timeframe (in seconds)" }, "default": { "en": 5, "de": 5 }, "description": { - "en": "Timeframe after which message objects get deleted (and can not longer be used to detect spam)", - "de": "Zeitfenster, in dem Nachrichten gelöscht werden (und nicht länger zur Erkennung von Spam verwendet werden können)" + "en": "Timeframe in seconds after which message objects get deleted (and can not longer be used to detect spam)", + "de": "Zeitfenster in Sekunden, in dem Nachrichten gelöscht werden (und nicht länger zur Erkennung von Spam verwendet werden können)" }, "type": "integer" }, diff --git a/modules/moderation/configs/config.json b/modules/moderation/configs/config.json index 481ce819..6ca5c4c8 100644 --- a/modules/moderation/configs/config.json +++ b/modules/moderation/configs/config.json @@ -167,7 +167,7 @@ "name": "require_reason", "humanName": { "de": "Begründung erzwingen", - "en": "Fore moderators to set a reason" + "en": "Force moderators to set a reason" }, "default": { "en": true, @@ -183,7 +183,7 @@ "name": "require_proof", "humanName": { "de": "Beweis-Bild erzwingen", - "en": "Fore moderators to upload proof" + "en": "Force moderators to upload proof" }, "dependsOn": "require_reason", "default": { @@ -339,6 +339,21 @@ "quarantine" ] }, + { + "name": "defaultMuteDuration", + "humanName": { + "de": "Standardmäßige Mute-Länge", + "en": "Default Mute-Duration" + }, + "type": "string", + "default": { + "en": "14d" + }, + "description": { + "en": "Default mute duration when none was configured. Will also be used for automod features (e.g. when someone posts a blacklisted word). Maximum value of 28 days.", + "de": "Standardmäßige Mute-Länge, wenn keine eingestellt wurde. Wird auch für Automod-Funktionen verwendet (also wenn z.B. jemand ein gesperrtes Wort postet). Höchstlänge von 28 Tagen." + } + }, { "name": "changeNicknames", "humanName": { diff --git a/modules/moderation/events/botReady.js b/modules/moderation/events/botReady.js index 12c018d2..b2ed8ba9 100644 --- a/modules/moderation/events/botReady.js +++ b/modules/moderation/events/botReady.js @@ -3,6 +3,7 @@ const {Op} = require('sequelize'); const {localize} = require('../../../src/functions/localize'); const {embedType} = require('../../../src/functions/helpers'); const {scheduleJob} = require('node-schedule'); +const {ChannelType} = require('discord.js'); const memberCache = {}; const durationParser = require('parse-duration'); @@ -37,7 +38,7 @@ exports.run = async (client) => { if (!verificationConfig.enabled || !verificationConfig['restart-verification-channel']) return; const channel = await client.channels.fetch(verificationConfig['restart-verification-channel']).catch(() => { }); - if (!channel || (channel || {}).type !== 'GUILD_TEXT') return client.logger.error('[moderation] ' + localize('moderation', 'verify-channel-set-but-not-found-or-wrong-type')); + if (!channel || (channel || {}).type !== ChannelType.GuildText) return client.logger.error('[moderation] ' + localize('moderation', 'verify-channel-set-but-not-found-or-wrong-type')); let message = (await channel.messages.fetch()).filter(msg => msg.author.id === client.user.id).last(); if (!message) { message = await channel.send(localize('moderation', 'generating-message')); @@ -68,14 +69,9 @@ exports.run = async (client) => { */ async function updateCache(client) { const moduleConfig = client.configurations['moderation']['config']; - memberCache['quarantine'] = (await (await client.guilds.fetch(client.guildID)).members.fetch()).filter(m => !!m.roles.cache.get(moduleConfig['quarantine-role-id'])); + memberCache['quarantine'] = client.guild.members.cache.filter(m => !!m.roles.cache.get(moduleConfig['quarantine-role-id'])); } -/** - * Removes expired warns - * @param {Client} client - * @return {Promise} - */ async function deleteExpiredWarns(client) { const aD = await client.models['moderation']['ModerationAction'].findAll({ where: { @@ -92,4 +88,4 @@ async function deleteExpiredWarns(client) { } module.exports.updateCache = updateCache; -module.exports.memberCache = memberCache; \ No newline at end of file +module.exports.memberCache = memberCache; diff --git a/modules/moderation/events/guildMemberAdd.js b/modules/moderation/events/guildMemberAdd.js index 75ddb88e..f469d411 100644 --- a/modules/moderation/events/guildMemberAdd.js +++ b/modules/moderation/events/guildMemberAdd.js @@ -2,17 +2,17 @@ const {memberCache} = require('./botReady'); const {moderationAction} = require('../moderationActions'); const {localize} = require('../../../src/functions/localize'); const {embedType} = require('../../../src/functions/helpers'); -const {MessageAttachment} = require('discord.js'); +const {ChannelType, MessageAttachment} = require('discord.js'); const {client} = require('../../../main'); let joinCache = []; -exports.run = async (client, guildMember) => { +module.exports.run = async (client, guildMember) => { if (guildMember.guild.id !== client.config.guildID) return; const moduleConfig = client.configurations['moderation']['config']; // Anti-Punishment-Bypass - if (!!memberCache.quarantine.get(guildMember.user.id)) { + if (memberCache.quarantine && !!memberCache.quarantine.get(guildMember.user.id)) { guildMember.doNotGiveWelcomeRole = true; await guildMember.roles.add(moduleConfig['quarantine-role-id'], `[moderation] ${localize('moderation', 'restored-punishment-audit-log-reason')}`); } @@ -46,7 +46,7 @@ exports.run = async (client, guildMember) => { await member.roles.add(antiJoinRaidConfig.roleID, `[moderation] [${localize('moderation', 'anti-join-raid')}] ${localize('moderation', 'raid-detected')}`); } else { const roles = []; - member.roles.cache.forEach(r => roles.push(r.id)); + member.roles.cache.filter(f => !f.managed).forEach(r => roles.push(r.id)); await moderationAction(client, antiJoinRaidConfig.action, {user: client.user}, member, `[${localize('moderation', 'anti-join-raid')}] ${localize('moderation', 'raid-detected')}`, {roles: roles}); } } @@ -67,7 +67,7 @@ exports.run = async (client, guildMember) => { // JoinGate const joinGateConfig = client.configurations['moderation']['joinGate']; - if (joinGateConfig.enabled) await runJoinGate(); + if (joinGateConfig.enabled && !(guildMember.pending && !['kick', 'ban'].includes(joinGateConfig.action))) await runJoinGate(guildMember); // Verification const verificationConfig = client.configurations['moderation']['verification']; @@ -84,7 +84,7 @@ exports.run = async (client, guildMember) => { async function dmFail() { const channel = await client.channels.fetch(verificationConfig['restart-verification-channel'] || '').catch(() => { }); - if (!channel || (channel || {}).type !== 'GUILD_TEXT') return client.logger.error('[moderation] ' + localize('moderation', 'verify-channel-set-but-not-found-or-wrong-type')); + if (!channel || (channel || {}).type !== ChannelType.GuildText) return client.logger.error('[moderation] ' + localize('moderation', 'verify-channel-set-but-not-found-or-wrong-type')); const m = await channel.send({ content: localize('moderation', 'dms-not-enabled-ping', {p: guildMember.toString()}), @@ -139,42 +139,47 @@ exports.run = async (client, guildMember) => { } } + +}; + +/** + * Runs joingate on this GuildMember + * @returns {Promise} + */ +async function runJoinGate(guildMember) { + const joinGateConfig = client.configurations['moderation']['joinGate']; + if (guildMember.user.bot && joinGateConfig.ignoreBots) return; + if (joinGateConfig.allUsers) return performJoinGateAction(localize('moderation', 'joingate-for-everyone')); + const daysSinceCreation = (new Date().getTime() / 86400000).toFixed(0) - (guildMember.user.createdTimestamp / 86400000).toFixed(0); + if (daysSinceCreation <= joinGateConfig.minAccountAge) return performJoinGateAction(localize('moderation', 'account-age-to-low', { + a: daysSinceCreation, + c: joinGateConfig.minAccountAge + })); + if (!guildMember.user.avatarURL() && joinGateConfig.requireProfilePicture) return performJoinGateAction(localize('moderation', 'no-profile-picture')); + /** - * Runs joingate on this GuildMember - * @returns {Promise} + * Performs the join gate action + * @private + * @param {String} reason Reason for executing the join gate action + * @return {Promise} */ - async function runJoinGate() { - if (guildMember.user.bot && joinGateConfig.ignoreBots) return; - if (joinGateConfig.allUsers) return performJoinGateAction(localize('moderation', 'joingate-for-everyone')); - const daysSinceCreation = (new Date().getTime() / 86400000).toFixed(0) - (guildMember.user.createdTimestamp / 86400000).toFixed(0); - if (daysSinceCreation <= joinGateConfig.minAccountAge) return performJoinGateAction(localize('moderation', 'account-age-to-low', { - a: daysSinceCreation, - c: joinGateConfig.minAccountAge - })); - if (!guildMember.user.avatarURL() && joinGateConfig.requireProfilePicture) return performJoinGateAction(localize('moderation', 'no-profile-picture')); - - /** - * Performs the join gate action - * @private - * @param {String} reason Reason for executing the join gate action - * @return {Promise} - */ - async function performJoinGateAction(reason) { - guildMember.joinGateTriggered = true; - if (joinGateConfig.action === 'give-role') { - if (joinGateConfig.removeOtherRoles) { - guildMember.doNotGiveWelcomeRole = true; - await guildMember.roles.remove(guildMember.roles.cache, `[moderation] [${localize('moderation', 'join-gate')}] ${localize('moderation', 'join-gate-fail', {r: reason})}`); - } - await guildMember.roles.add(joinGateConfig.roleID, `[moderation] [${localize('moderation', 'join-gate')}] ${localize('moderation', 'join-gate-fail', {r: reason})}`); - return; + async function performJoinGateAction(reason) { + guildMember.joinGateTriggered = true; + if (joinGateConfig.action === 'give-role') { + if (joinGateConfig.removeOtherRoles) { + guildMember.doNotGiveWelcomeRole = true; + await guildMember.roles.remove(guildMember.roles.cache, `[moderation] [${localize('moderation', 'join-gate')}] ${localize('moderation', 'join-gate-fail', {r: reason})}`); } - const roles = []; - guildMember.roles.cache.forEach(r => roles.push(r.id)); - await moderationAction(client, joinGateConfig.action, {user: client.user}, guildMember, `[${localize('moderation', 'join-gate')}] ${localize('moderation', 'join-gate-fail', {r: reason})}`, {roles: roles}); + await guildMember.roles.add(joinGateConfig.roleID, `[moderation] [${localize('moderation', 'join-gate')}] ${localize('moderation', 'join-gate-fail', {r: reason})}`); + return; } + const roles = []; + guildMember.roles.cache.forEach(r => roles.push(r.id)); + await moderationAction(client, joinGateConfig.action, {user: client.user}, guildMember, `[${localize('moderation', 'join-gate')}] ${localize('moderation', 'join-gate-fail', {r: reason})}`, {roles: roles}); } -}; +} + +module.exports.runJoinGate = runJoinGate; /** * Sends a user a DM about their verification @@ -190,7 +195,7 @@ async function sendDMPart(verificationConfig, guildMember) { if (!guildMember.client.scnxSetup) return guildMember.client.logger.error('[moderation] Captcha Generation is only available if your bot has an SCNX Integration set up.'); const captcha = await require('../../../src/functions/scnx-integration').generateCaptcha(verificationConfig.captchaLevel); await guildMember.user.send(embedType(verificationConfig['captcha-message'], {}, { - files: [new MessageAttachment(captcha.buffer, 'you-call-it-captcha-we-call-it-ai-training.png')] + files: [new MessageAttachment(captcha.buffer, {name: 'you-call-it-captcha-we-call-it-ai-training.png'})] })); const c = await guildMember.user.createDM(); const col = c.createMessageCollector({time: 120000}); @@ -296,4 +301,4 @@ async function verificationFail(guildMember) { }); } -module.exports.verificationFail = verificationFail; \ No newline at end of file +module.exports.verificationFail = verificationFail; diff --git a/modules/moderation/events/guildMemberUpdate.js b/modules/moderation/events/guildMemberUpdate.js new file mode 100644 index 00000000..78aaa529 --- /dev/null +++ b/modules/moderation/events/guildMemberUpdate.js @@ -0,0 +1,10 @@ +const {runJoinGate} = require('./guildMemberAdd'); +module.exports.run = async function (client, oldGuildMember, newGuildMember) { + if (!client.botReadyAt) return; + const joinGateConfig = client.configurations['moderation']['joinGate']; + const verificationConfig = client.configurations['moderation']['verification']; + + if (oldGuildMember.pending && !newGuildMember.pending && joinGateConfig.enabled && !['kick', 'ban'].includes(joinGateConfig.action)) { + await runJoinGate(newGuildMember); + } +}; \ No newline at end of file diff --git a/modules/moderation/events/messageCreate.js b/modules/moderation/events/messageCreate.js index a4938c64..f916adde 100644 --- a/modules/moderation/events/messageCreate.js +++ b/modules/moderation/events/messageCreate.js @@ -16,7 +16,7 @@ module.exports.run = async (client, msg) => { const antiSpamConfig = client.configurations['moderation']['antiSpam']; if (msg.member.roles.cache.find(r => moduleConfig['moderator-roles_level2'].includes(r.id) || moduleConfig['moderator-roles_level3'].includes(r.id) || moduleConfig['moderator-roles_level4'].includes(r.id))) return; const roles = []; - msg.member.roles.cache.forEach(r => roles.push(r.id)); + msg.member.roles.cache.filter(f => !f.managed).forEach(r => roles.push(r.id)); // Anti-Spam if (antiSpamConfig.enabled) if (!antiSpamConfig.ignoredChannels.includes(msg.channel.id)) { @@ -86,11 +86,12 @@ module.exports.run = async (client, msg) => { */ async function performBadWordAndInviteProtection(msg) { const moduleConfig = msg.client.configurations['moderation']['config']; + const roles = Array.from(msg.member.roles.cache.filter(f => !f.managed).keys()); if (msg.member.roles.cache.find(r => moduleConfig['moderator-roles_level2'].includes(r.id) || moduleConfig['moderator-roles_level3'].includes(r.id) || moduleConfig['moderator-roles_level4'].includes(r.id))) return; if (moduleConfig['action_on_scam_link'] !== 'none') { if (await stopPhishing.checkMessage(msg.content, moduleConfig['action_on_scam_link'] === 'suspicious')) { await msg.delete(); - await moderationAction(msg.client, moduleConfig['action_on_scam_link'], msg.client, msg.member, localize('moderation', 'scam-url-sent', {c: msg.channel.toString()}), {roles: msg.member.roles.cache.keys()}); + await moderationAction(msg.client, moduleConfig['action_on_scam_link'], msg.client, msg.member, localize('moderation', 'scam-url-sent', {c: msg.channel.toString()}), {roles}); return; } } @@ -101,7 +102,7 @@ async function performBadWordAndInviteProtection(msg) { if (containsBlacklistedWord && !msg.channel.nsfw) { if (moduleConfig['action_on_posting_blacklisted_word'] !== 'none') { await msg.delete(); - await moderationAction(msg.client, moduleConfig['action_on_posting_blacklisted_word'], msg.client, msg.member, localize('moderation', 'blacklisted-word', {c: msg.channel.toString()}), {roles: msg.member.roles.cache.keys()}); + await moderationAction(msg.client, moduleConfig['action_on_posting_blacklisted_word'], msg.client, msg.member, localize('moderation', 'blacklisted-word', {c: msg.channel.toString()}), {roles}); } } if (moduleConfig['whitelisted_channels_for_invite_blocking'].includes(msg.channel.id) || moduleConfig['whitelisted_channels_for_invite_blocking'].includes(msg.channel.parentId)) return; @@ -109,7 +110,7 @@ async function performBadWordAndInviteProtection(msg) { if (moduleConfig['action_on_invite'] !== 'none') { if (msg.content.includes('discord.gg/') || msg.content.includes('discordapp.com/invite/')) { await msg.delete(); - await moderationAction(msg.client, moduleConfig['action_on_invite'], msg.client, msg.member, localize('moderation', 'invite-sent', {c: msg.channel.toString()}), {roles: msg.member.roles.cache.keys()}); + await moderationAction(msg.client, moduleConfig['action_on_invite'], msg.client, msg.member, localize('moderation', 'invite-sent', {c: msg.channel.toString()}), {roles}); } } } diff --git a/modules/moderation/moderationActions.js b/modules/moderation/moderationActions.js index a2ca1f9d..c915891f 100644 --- a/modules/moderation/moderationActions.js +++ b/modules/moderation/moderationActions.js @@ -50,7 +50,7 @@ async function moderationAction(client, type, user, victim, reason, additionalDa } switch (type) { case 'mute': - if (!expiringAt) expiringAt = new Date(new Date().getTime() + 1209600000); + if (!expiringAt) expiringAt = new Date(new Date().getTime() + durationParser(moduleConfig.defaultMuteDuration)); await victim.timeout(expiringAt.getTime() - new Date().getTime(), localize('moderation', 'mute-audit-log-reason', { u: formatDiscordUserName(user.user), r: reason @@ -60,7 +60,7 @@ async function moderationAction(client, type, user, victim, reason, additionalDa '%user%': formatDiscordUserName(user.user), '%date%': expiringAt ? formatDate(expiringAt) : null })); - if (moduleConfig['changeNicknameOnQuarantine']) await victim.setNickname(moduleConfig['changeNicknameOnMute'].split('%nickname%').join(victim.nickname ? victim.nickname : victim.user.username), '[moderation] ' + localize('moderation', 'mute-audit-log-reason', { + if (moduleConfig['changeNicknameOnQuarantine']) await victim.setNickname(moduleConfig['changeNicknameOnMute'].split('%nickname%').join(victim.nickname ? victim.nickname : victim.user.displayName), '[moderation] ' + localize('moderation', 'mute-audit-log-reason', { u: formatDiscordUserName(user.user), r: reason })).catch(() => { @@ -75,7 +75,7 @@ async function moderationAction(client, type, user, victim, reason, additionalDa '%reason%': reason, '%user%': formatDiscordUserName(user.user) })); - if (moduleConfig['changeNicknameOnQuarantine']) await victim.setNickname(victim.user.username, '[moderation] ' + localize('moderation', 'unmute-audit-log-reason', { + if (moduleConfig['changeNicknameOnQuarantine']) await victim.setNickname(victim.user.displayName, '[moderation] ' + localize('moderation', 'unmute-audit-log-reason', { u: formatDiscordUserName(user.user), r: reason })); @@ -83,12 +83,12 @@ async function moderationAction(client, type, user, victim, reason, additionalDa case 'quarantine': if (!victim.roles.cache.get(quarantineRole.id)) { if (moduleConfig['remove-all-roles-on-quarantine']) { - await victim.roles.set([quarantineRole], '[moderation] ' + localize('moderation', 'quarantine-audit-log-reason', { + await victim.roles.set([quarantineRole, ...victim.roles.cache.filter(f => f.managed).map(i => i.id)], '[moderation] ' + localize('moderation', 'quarantine-audit-log-reason', { u: formatDiscordUserName(user.user), r: reason })).catch(async e => { client.logger.log(localize('moderation', 'batch-role-remove-failed', {i: victim.id, e})); - for (const role of victim.roles.cache) { // Remove as much roles as possible + for (const role of victim.roles.cache.filter(f => !f.managed)) { // Remove as many roles as possible await victim.roles.remove(role, '[moderation] ' + localize('moderation', 'quarantine-audit-log-reason', { u: formatDiscordUserName(user.user), r: reason @@ -117,7 +117,7 @@ async function moderationAction(client, type, user, victim, reason, additionalDa u: formatDiscordUserName(user.user), r: reason })); - if (moduleConfig['changeNicknameOnQuarantine']) await victim.setNickname(moduleConfig['changeNicknameOnQuarantine'].split('%nickname%').join(victim.nickname ? victim.nickname : victim.user.username), '[moderation] ' + localize('moderation', 'quarantine-audit-log-reason', { + if (moduleConfig['changeNicknameOnQuarantine']) await victim.setNickname(moduleConfig['changeNicknameOnQuarantine'].split('%nickname%').join(victim.nickname ? victim.nickname : victim.user.displayName), '[moderation] ' + localize('moderation', 'quarantine-audit-log-reason', { u: formatDiscordUserName(user.user), r: reason })).catch(() => { @@ -142,11 +142,11 @@ async function moderationAction(client, type, user, victim, reason, additionalDa '%reason%': reason, '%user%': formatDiscordUserName(user.user) })); - if (moduleConfig['changeNicknameOnQuarantine']) await victim.setNickname(victim.user.username).catch(() => { + if (moduleConfig['changeNicknameOnQuarantine']) await victim.setNickname(victim.user.displayName).catch(() => { }); break; case 'kick': - sendMessage(victim, embedType(moduleStrings['kick_message'], { + await sendMessage(victim, embedType(moduleStrings['kick_message'], { '%reason%': reason, '%user%': formatDiscordUserName(user.user) })); @@ -313,8 +313,8 @@ module.exports.moderationAction = moderationAction; * @param {User} user User to send Message to * @param {Object|String} content Content to send to the user */ -function sendMessage(user, content) { - user.send(content).catch(() => { +async function sendMessage(user, content) { + await user.send(content).catch(() => { }); } diff --git a/modules/nicknames/events/botReady.js b/modules/nicknames/events/botReady.js index f1c7cafb..aeb44e66 100644 --- a/modules/nicknames/events/botReady.js +++ b/modules/nicknames/events/botReady.js @@ -4,5 +4,4 @@ module.exports.run = async function (client) { for (const member of client.guild.members.cache.values()) { await renameMember(client, member); } - } \ No newline at end of file diff --git a/modules/nicknames/events/guildMemberUpdate.js b/modules/nicknames/events/guildMemberUpdate.js index 9cc9750a..e01f0b6e 100644 --- a/modules/nicknames/events/guildMemberUpdate.js +++ b/modules/nicknames/events/guildMemberUpdate.js @@ -8,4 +8,4 @@ module.exports.run = async function (client, oldGuildMember, newGuildMember) { await renameMember(client, newGuildMember); -}; +}; \ No newline at end of file diff --git a/modules/nicknames/models/User.js b/modules/nicknames/models/User.js index b2f191af..9e7bf2d5 100644 --- a/modules/nicknames/models/User.js +++ b/modules/nicknames/models/User.js @@ -1,4 +1,4 @@ -const { DataTypes, Model } = require('sequelize'); +const {DataTypes, Model} = require('sequelize'); module.exports = class User extends Model { static init(sequelize) { diff --git a/modules/nicknames/module.json b/modules/nicknames/module.json index 1023e56f..39c91b14 100644 --- a/modules/nicknames/module.json +++ b/modules/nicknames/module.json @@ -13,8 +13,8 @@ "events-dir": "/events", "models-dir": "/models", "config-example-files": [ - "configs/strings.json", - "configs/config.json" + "configs/config.json", + "configs/strings.json" ], "tags": [ "community" diff --git a/modules/nicknames/renameMember.js b/modules/nicknames/renameMember.js index 9e047319..e4ae29bd 100644 --- a/modules/nicknames/renameMember.js +++ b/modules/nicknames/renameMember.js @@ -55,19 +55,22 @@ renameMember = async function (client, guildMember) { } - if (guildMember.displayName === truncate(rolePrefix + memberName, 32-roleSuffix.length).concat(roleSuffix)) return; + if (guildMember.displayName === truncate(rolePrefix + memberName, 32 - roleSuffix.length).concat(roleSuffix)) return; if (guildMember.guild.ownerId === guildMember.id) { - client.logger.error('[nicknames] ' + localize('nicknames', 'owner-cannot-be-renamed', {u: guildMember.user.username})) - return; + client.logger.error('[nicknames] ' + localize('nicknames', 'owner-cannot-be-renamed', {u: guildMember.user.username})); + return; } if (guildMember.guild.ownerId === guildMember.id) { - client.logger.error('[nicknames] ' + localize('nicknames', 'owner-cannot-be-renamed', {u: guildMember.user.username})) - return; + client.logger.error('[nicknames] ' + localize('nicknames', 'owner-cannot-be-renamed', {u: guildMember.user.username})); + return; } try { - await guildMember.setNickname(truncate(rolePrefix + memberName, 32-roleSuffix.length).concat(roleSuffix)); + await guildMember.setNickname(truncate(rolePrefix + memberName, 32 - roleSuffix.length).concat(roleSuffix)); } catch (e) { - client.logger.error('[nicknames] ' + localize('nicknames', 'nickname-error', {u: guildMember.user.username, e: e})) + client.logger.error('[nicknames] ' + localize('nicknames', 'nickname-error', { + u: guildMember.user.username, + e: e + })); } } module.exports.renameMember = renameMember; \ No newline at end of file diff --git a/modules/partner-list/commands/partner.js b/modules/partner-list/commands/partner.js deleted file mode 100644 index 847aeecf..00000000 --- a/modules/partner-list/commands/partner.js +++ /dev/null @@ -1,231 +0,0 @@ -const {embedType, truncate} = require('../../../src/functions/helpers'); -const {generatePartnerList} = require('../partnerlist'); -const {localize} = require('../../../src/functions/localize'); - -module.exports.beforeSubcommand = async function (interaction) { - await interaction.deferReply({ephemeral: true}); -}; - -module.exports.subcommands = { - 'add': async function (interaction) { - const moduleConf = interaction.client.configurations['partner-list']['config']; - if (moduleConf['category-roles'][interaction.options.getString('category')]) { - const owner = await interaction.guild.members.fetch(interaction.options.getUser('owner')); - await owner.roles.add(moduleConf['category-roles'][interaction.options.getString('category')]).catch(() => { - interaction.client.logger.error('[partner-list] ' + localize('partner-list', 'could-not-give-role', {u: owner.user.id})); - }); - } - if (moduleConf.sendNotificationToPartner) { - interaction.options.getUser('owner').send(embedType(moduleConf['newPartnerDM'], { - '%name%': interaction.options.getString('name'), - '%category%': interaction.options.getString('category') - })).catch(() => { - }); - } - await interaction.client.models['partner-list']['Partner'].create({ - invLink: interaction.options.getString('invite-url'), - teamUserID: interaction.user.id, - userID: interaction.options.getUser('owner').id, - name: interaction.options.getString('name'), - category: interaction.options.getString('category') - }); - await generatePartnerList(interaction.client); - }, - 'delete': async function (interaction) { - const partner = await interaction.client.models['partner-list']['Partner'].findOne({ - where: { - id: interaction.options.getString('id') - } - }); - if (!partner) { - interaction.returnEarly = true; - return interaction.editReply({ - content: localize('partner-list', 'partner-not-found') - }); - } - - const moduleConf = interaction.client.configurations['partner-list']['config']; - const member = await interaction.guild.members.fetch(partner.userID).catch(() => { - }); - - if (member && moduleConf['category-roles'][partner.category]) await member.roles.remove(moduleConf['category-roles'][partner.category]).catch(() => { - interaction.client.logger.error('[partner-list] ' + localize('partner-list', 'could-not-remove-role', {u: member.user.id})); - }); - if (member && moduleConf.sendNotificationToPartner) await member.user.send(embedType(moduleConf.byePartnerDM, { - '%name%': partner.name, - '%category%': partner.category - })).catch(() => {}); - - await partner.destroy(); - await generatePartnerList(interaction.client); - }, - 'edit': async function (interaction) { - const partner = await interaction.client.models['partner-list']['Partner'].findOne({ - where: { - id: interaction.options.getString('id') - } - }); - if (!partner) { - interaction.returnEarly = true; - return interaction.editReply({ - content: localize('partner-list', 'partner-not-found') - }); - } - const moduleConf = interaction.client.configurations['partner-list']['config']; - if (interaction.options.getString('name')) partner.name = interaction.options.getString('name'); - if (interaction.options.getString('invite-url')) partner.invLink = interaction.options.getString('invite-url'); - if (interaction.options.getUser('staff')) partner.teamUserID = interaction.options.getUser('staff').id; - if (interaction.options.getUser('owner')) partner.userID = interaction.options.getUser('owner').id; - if (interaction.options.getString('category')) { - const member = await interaction.guild.members.fetch(partner.userID).catch(() => { - }); - if (member && moduleConf['category-roles'][partner.category]) await member.roles.remove(moduleConf['category-roles'][partner.category]).catch(() => { - interaction.client.logger.error('[partner-list] ' + localize('partner-list', 'could-not-remove-role', {u: member.user.id})); - }); - partner.category = interaction.options.getString('category'); - if (member && moduleConf['category-roles'][partner.category]) await member.roles.add(moduleConf['category-roles'][partner.category]).catch(() => { - interaction.client.logger.error('[partner-list] ' + localize('partner-list', 'could-not-give-role', {u: member.user.id})); - }); - } - - await partner.save(); - await generatePartnerList(interaction.client); - } -}; - -module.exports.autoComplete = { - 'edit': { - 'id': autoCompletePartnerID - }, - 'delete': { - 'id': autoCompletePartnerID - } -}; - -/** - * @private - * Run autocomplete on options with partner id - * @param {Interaction} interaction - * @return {Promise} - */ -async function autoCompletePartnerID(interaction) { - const partnerList = await interaction.client.models['partner-list']['Partner'].findAll({ - order: [['createdAt', 'DESC']] - }); - const matches = []; - interaction.value = interaction.value.toLowerCase(); - for (const match of partnerList.filter(p => p.id.toString().includes(interaction.value) || p.name.toLowerCase().includes(interaction.value) || p.category.toLowerCase().includes(interaction.value))) { - if (matches.length !== 25) matches.push({ - value: match.id.toString(), - name: truncate(`${match.category}: ${match.name}`, 100) - }); - } - interaction.respond(matches); -} - -module.exports.run = async function (interaction) { - if (!interaction.returnEarly) await interaction.editReply({content: ':+1: ' + localize('partner-list', 'successful-edit')}); -}; - -module.exports.config = { - name: 'partner', - description: localize('partner-list', 'command-description'), - - defaultMemberPermissions: ['MANAGE_MESSAGES'], - options: function (client) { - const cats = []; - for (const category of client.configurations['partner-list']['config']['categories']) { - cats.push({name: category, value: category}); - } - return [ - { - type: 'SUB_COMMAND', - name: 'add', - description: localize('partner-list', 'padd-description'), - options: [ - { - type: 'STRING', - name: 'name', - required: true, - description: localize('partner-list', 'padd-name-description') - }, - { - type: 'STRING', - name: 'category', - required: true, - description: localize('partner-list', 'padd-category-description'), - choices: cats - }, - { - type: 'USER', - name: 'owner', - required: true, - description: localize('partner-list', 'padd-owner-description') - }, - { - type: 'STRING', - name: 'invite-url', - required: true, - description: localize('partner-list', 'padd-inviteurl-description') - } - ] - }, - { - type: 'SUB_COMMAND', - name: 'edit', - description: localize('partner-list', 'pedit-description'), - options: [ - { - type: 'STRING', - required: true, - name: 'id', - autocomplete: true, - description: localize('partner-list', 'pedit-id-description') - }, - { - type: 'STRING', - name: 'name', - description: localize('partner-list', 'pedit-name-description') - }, - { - type: 'STRING', - name: 'invite-url', - description: localize('partner-list', 'pedit-inviteurl-description') - }, - { - type: 'STRING', - name: 'category', - choices: cats, - description: localize('partner-list', 'pedit-category-description') - }, - { - type: 'USER', - name: 'owner', - description: localize('partner-list', 'pedit-owner-description') - }, - { - - - type: 'USER', - name: 'staff', - description: localize('partner-list', 'pedit-staff-description') - } - ] - }, - { - type: 'SUB_COMMAND', - name: 'delete', - description: localize('partner-list', 'pdelete-description'), - options: [ - { - type: 'STRING', - name: 'id', - autocomplete: true, - description: localize('partner-list', 'pdelete-id-description'), - required: true - } - ] - } - ]; - } -}; \ No newline at end of file diff --git a/modules/partner-list/config.json b/modules/partner-list/config.json deleted file mode 100644 index 1ec295fd..00000000 --- a/modules/partner-list/config.json +++ /dev/null @@ -1,218 +0,0 @@ -{ - "description": {}, - "humanName": { - "en": "Configuration", - "de": "Konfiguration" - }, - "filename": "config.json", - "commandsWarnings": { - "normal": [ - "/partner" - ] - }, - "content": [ - { - "name": "channelID", - "humanName": { - "de": "Kanal", - "en": "Channel" - }, - "default": { - "en": "" - }, - "description": { - "en": "Channel in which the partner-list lives", - "de": "Kanal, in welchem die Partner-Liste sein wird" - }, - "type": "channelID" - }, - { - "name": "embed", - "humanName": { - "en": "Partner-List-Embed" - }, - "default": { - "en": { - "title": "Our partners", - "description": "You can find all of our partners here - If you want to be one of our partners message a staff member!", - "partner-string": "#%id%: [%name%](%invite%) (<@%userID%>)", - "color": "GREEN" - }, - "de": { - "title": "Unsere Partner", - "description": "Hier findest du alles über unsere Partner - Wenn du selbst Partner werden möchtest kontaktiere eins unserer Teammitglieder!", - "partner-string": "#%id%: [%name%](%invite%) (<@%userID%>)", - "color": "GREEN" - } - }, - "description": { - "en": "Configuration of the partnership-embed", - "de": "Konfiguration des Partner-Embeds" - }, - "type": "keyed", - "content": { - "key": "string", - "value": "string" - }, - "disableKeyEdits": true, - "params": [ - { - "name": "invite", - "description": { - "en": "Configured invite to the partner-server (only for \"partner-string\" field)", - "de": "Konfigurierter Invite des Partner-Servers (nur für \"partner-string\" Feld)" - } - }, - { - "name": "name", - "description": { - "en": "Configured name to the partner-server (only for \"partner-string\" field)", - "de": "Konfigurierter Name des Partner-Servers (nur für \"partner-string\" Feld)" - } - }, - { - "name": "userID", - "description": { - "en": "Configured owner-ID to the partner-server (only for \"partner-string\" field)", - "de": "Konfigurierter Owner-ID des Partner-Servers (nur für \"partner-string\" Feld)" - } - }, - { - "name": "teamMemberID", - "description": { - "en": "User who added this partner-server (only for \"partner-string\" field)", - "de": "ID des Nutzers, der den Partner-Server eingetragen hat (nur für \"partner-string\" Feld)" - } - } - ] - }, - { - "name": "categories", - "humanName": { - "en": "Categories", - "de": "Kategorien" - }, - "default": { - "en": [ - "Normal Partners", - "Kooperation", - "Small Partners" - ], - "de": [ - "Normale Partner", - "Kooperation", - "Kleine Partner" - ] - }, - "description": { - "en": "Please specify each category here", - "de": "Bitte liste jede Kategorie hier auf" - }, - "type": "array", - "content": "string" - }, - { - "name": "category-roles", - "humanName": { - "en": "Category-Roles", - "de": "Kategorie-Rollen" - }, - "default": { - "en": {}, - "de": {} - }, - "description": { - "en": "(optional) Role which should be given for a partner in a specific category", - "de": "(optional) Rolle welche Partner in einer bestimmten Kategorie gegeben werden soll" - }, - "type": "keyed", - "content": { - "key": "string", - "value": "roleID" - } - }, - { - "name": "sendNotificationToPartner", - "humanName": { - "en": "Send Partner-Notifications?", - "de": "Partner-Benachrichtigung senden?" - }, - "default": { - "en": false - }, - "description": { - "en": "If enabled, the bot is going to send a DM to the partner when they get added or removed", - "de": "Wenn aktiviert, sendet der Bot eine PN an Partner, wenn sie hinzugefügt oder entfernt werden" - }, - "type": "boolean" - }, - { - "name": "newPartnerDM", - "dependsOn": "sendNotificationToPartner", - "humanName": { - "de": "Partner-Willkommens-PN", - "en": "Partner-Welcome-DM" - }, - "default": { - "en": "Hello, Hello! You are now a partner - congratulations", - "de": "Hi. Du bist jetzt Partner - Herzlichen Glückwunsch" - }, - "description": { - "en": "This message gets send to new partners.", - "de": "Diese Nachricht wird an neue Partner gesendet." - }, - "type": "string", - "allowEmbed": true, - "params": [ - { - "name": "name", - "description": { - "en": "Name of the added partner", - "de": "Name des hinzugefügten Partners" - } - }, - { - "name": "category", - "description": { - "en": "Category of the partner", - "de": "Kategorie des Partners" - } - } - ] - }, - { - "name": "byePartnerDM", - "dependsOn": "sendNotificationToPartner", - "humanName": { - "de": "Partner-Entfernung-PN", - "en": "Partner-Removal-DM" - }, - "default": { - "en": "Sorry, but you are no longer a partner ):", - "de": "Leider bist du nicht länger Partner ):" - }, - "description": { - "en": "This message gets send to the partner when they get removed.", - "de": "Diese Nachricht wird an den Partner gesendet, wenn dieser entfernt wird." - }, - "type": "string", - "allowEmbed": true, - "params": [ - { - "name": "name", - "description": { - "en": "Name of the added partner", - "de": "Name des hinzugefügten Partners" - } - }, - { - "name": "category", - "description": { - "en": "Category of the partner", - "de": "Kategorie des Partners" - } - } - ] - } - ] -} \ No newline at end of file diff --git a/modules/partner-list/events/botReady.js b/modules/partner-list/events/botReady.js deleted file mode 100644 index 73ba5325..00000000 --- a/modules/partner-list/events/botReady.js +++ /dev/null @@ -1,5 +0,0 @@ -const {generatePartnerList} = require('../partnerlist'); - -module.exports.run = async function (client) { - await generatePartnerList(client); -}; \ No newline at end of file diff --git a/modules/partner-list/models/Partner.js b/modules/partner-list/models/Partner.js deleted file mode 100644 index 12877975..00000000 --- a/modules/partner-list/models/Partner.js +++ /dev/null @@ -1,27 +0,0 @@ -const {DataTypes, Model} = require('sequelize'); - -module.exports = class Partner extends Model { - static init(sequelize) { - return super.init({ - id: { - autoIncrement: true, - type: DataTypes.INTEGER, - primaryKey: true - }, - invLink: DataTypes.STRING, - teamUserID: DataTypes.STRING, - userID: DataTypes.STRING, - name: DataTypes.STRING, - category: DataTypes.STRING - }, { - tableName: 'partnerlist_Partner', - timestamps: true, - sequelize - }); - } -}; - -module.exports.config = { - 'name': 'Partner', - 'module': 'partner-list' -}; \ No newline at end of file diff --git a/modules/partner-list/module.json b/modules/partner-list/module.json deleted file mode 100644 index c6c8fb72..00000000 --- a/modules/partner-list/module.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "partner-list", - "author": { - "scnxOrgID": "1", - "name": "SCDerox (SC Network Team)", - "link": "https://github.com/SCDerox" - }, - "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/partnerlist", - "events-dir": "/events", - "commands-dir": "/commands", - "models-dir": "/models", - "config-example-files": [ - "config.json" - ], - "tags": [ - "administration" - ], - "humanReadableName": { - "en": "Partner-List", - "de": "Partner-Liste" - }, - "description": { - "en": "Manage your partnerships with other guilds easily.", - "de": "Erstelle eine Liste mit allen Partnern deines Servers - nach Kategorie sortiert." - } -} \ No newline at end of file diff --git a/modules/partner-list/partnerlist.js b/modules/partner-list/partnerlist.js deleted file mode 100644 index e518340e..00000000 --- a/modules/partner-list/partnerlist.js +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Manages the Partner-List-Embed - * @module Partner-List - * @author Simon Csaba - */ -const {MessageEmbed} = require('discord.js'); -const {localize} = require('../../src/functions/localize'); -const {disableModule, truncate} = require('../../src/functions/helpers'); - -/** - * Generate the partner-list embed - * @param {Client} client - * @returns {Promise} - */ -async function generatePartnerList(client) { - const moduleConf = client.configurations['partner-list']['config']; - const channel = await client.channels.fetch(moduleConf['channelID']).catch(() => { - }); - if (!channel) return disableModule('partner-list', localize('partner-list', 'channel-not-found', {c: moduleConf['channelID']})); - const messages = (await channel.messages.fetch()).filter(msg => msg.author.id === client.user.id); - const partners = await client.models['partner-list']['Partner'].findAll({}); - const sortedByCategory = {}; - partners.forEach(partner => { - if (!sortedByCategory[partner.category]) sortedByCategory[partner.category] = []; - sortedByCategory[partner.category].push(partner); - }); - const embed = new MessageEmbed() - .setTitle(moduleConf['embed']['title']) - .setAuthor({name: client.user.username, iconURL: client.user.avatarURL()}) - .setFooter({text: client.strings.footer, iconURL: client.strings.footerImgUrl}) - .setColor(moduleConf['embed']['color']) - .setDescription(moduleConf['embed']['description']); - moduleConf['categories'].forEach(category => { - if (sortedByCategory[category]) { - let string = ''; - sortedByCategory[category].forEach(partner => { - string = string + moduleConf['embed']['partner-string'].split('%invite%').join(partner.invLink).split('%name%').join(partner.name).split('%userID%').join(partner.userID).split('%id%').join(partner.id).split('%teamMemberID%').join(partner.teamUserID) + '\n'; - }); - embed.addField(category, truncate(string, 1020)); - delete sortedByCategory[category]; - } - }); - for (const category in sortedByCategory) { - let string = ''; - sortedByCategory[category].forEach(partner => { - string = string + moduleConf['embed']['partner-string'].split('%invite%').join(partner.invLink).split('%name%').join(partner.name).split('%userID%').join(partner.userID).split('%id%').join(partner.id).split('%teamMemberID%').join(partner.teamUserID) + '\n'; - }); - embed.addField(category, truncate(string, 1020)); - } - - if (partners.length === 0) embed.addField('ℹ ' + localize('partner-list', 'information'), localize('partner-list', 'no-partners')); - - if (messages.last()) await messages.last().edit({embeds: [embed]}); - else channel.send({embeds: [embed]}); -} - -module.exports.generatePartnerList = generatePartnerList; \ No newline at end of file diff --git a/modules/ping-on-vc-join/config.json b/modules/ping-on-vc-join/config.json index bd871fac..cce3e041 100644 --- a/modules/ping-on-vc-join/config.json +++ b/modules/ping-on-vc-join/config.json @@ -76,6 +76,9 @@ "default": { "en": "" }, + "content": [ + "GUILD_TEXT" + ], "description": { "en": "Channel where the message should be send", "de": "Kanal, in welchen die Nachricht gesendet werden soll" diff --git a/modules/ping-on-vc-join/events/voiceStateUpdate.js b/modules/ping-on-vc-join/events/voiceStateUpdate.js index 56ec57cd..4703c930 100644 --- a/modules/ping-on-vc-join/events/voiceStateUpdate.js +++ b/modules/ping-on-vc-join/events/voiceStateUpdate.js @@ -7,11 +7,10 @@ exports.run = async (client, oldState, newState) => { if (!client.botReadyAt) return; const roleConfig = client.configurations['ping-on-vc-join']['actual-config']; if (roleConfig.assignRoleToUsersInVoiceChannels && roleConfig.voiceRoles.length !== 0) { - console.log(oldState.guildId, newState.guildId); if (oldState.channel && !newState.channel) newState.member.roles.remove(roleConfig.voiceRoles); if (!oldState.channel && newState.channel) newState.member.roles.add(roleConfig.voiceRoles); } - if (!newState.channel) return; + if (!newState.channel || newState.channel.id === oldState?.channel?.id) return; const channel = await client.channels.fetch(newState.channelId); if (channel.guild.id !== client.guild.id) return; diff --git a/modules/ping-on-vc-join/module.json b/modules/ping-on-vc-join/module.json index 9e944642..25206a7b 100644 --- a/modules/ping-on-vc-join/module.json +++ b/modules/ping-on-vc-join/module.json @@ -6,6 +6,7 @@ "link": "https://github.com/SCDerox" }, "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/ping-on-vc-join", + "fa-icon": "fa-solid fa-volume-high", "events-dir": "/events", "config-example-files": [ "config.json", diff --git a/modules/polls/commands/poll.js b/modules/polls/commands/poll.js index 575af41f..bc4c2c49 100644 --- a/modules/polls/commands/poll.js +++ b/modules/polls/commands/poll.js @@ -1,3 +1,4 @@ +const {ChannelType} = require('discord.js'); const {truncate} = require('../../../src/functions/helpers'); const durationParser = require('parse-duration'); const {localize} = require('../../../src/functions/localize'); @@ -5,10 +6,11 @@ const {createPoll, updateMessage} = require('../polls'); module.exports.subcommands = { 'create': async function (interaction) { - if (interaction.options.getChannel('channel', true).type !== 'GUILD_TEXT') interaction.reply({ + if (interaction.options.getChannel('channel', true).type !== ChannelType.GuildText) return interaction.reply({ content: '⚠️ ' + localize('polls', 'not-text-channel'), ephemeral: true }); + await interaction.deferReply({ephemeral: true}); let endAt; if (interaction.options.getString('duration')) endAt = new Date(new Date().getTime() + durationParser(interaction.options.getString('duration'))); const options = []; @@ -21,9 +23,8 @@ module.exports.subcommands = { endAt: endAt, options }, interaction.client); - interaction.reply({ - content: localize('polls', 'created-poll', {c: interaction.options.getChannel('channel').toString()}), - ephemeral: true + await interaction.editReply({ + content: localize('polls', 'created-poll', {c: interaction.options.getChannel('channel').toString()}) }); }, 'end': async function (interaction) { @@ -36,12 +37,12 @@ module.exports.subcommands = { content: '⚠️ ' + localize('polls', 'not-found'), ephemeral: true }); + await interaction.deferReply({ephemeral: true}); poll.expiresAt = new Date(); await poll.save(); await updateMessage(await interaction.guild.channels.cache.get(poll.channelID), poll, interaction.options.getString('msg-id')); - interaction.reply({ - content: localize('polls', 'ended-poll'), - ephemeral: true + await interaction.editReply({ + content: localize('polls', 'ended-poll') }); } }; @@ -93,7 +94,7 @@ module.exports.config = { type: 'CHANNEL', name: 'channel', required: true, - channelTypes: ['GUILD_TEXT'], + channelTypes: [ChannelType.GuildText], description: localize('polls', 'command-poll-create-channel-description') }, { @@ -150,4 +151,4 @@ module.exports.config = { } return options; } -}; \ No newline at end of file +}; diff --git a/modules/polls/configs/strings.json b/modules/polls/configs/strings.json index 4679c9c6..ad3d920e 100644 --- a/modules/polls/configs/strings.json +++ b/modules/polls/configs/strings.json @@ -4,7 +4,7 @@ "de": "Stelle hier die Nachrichten des Modules ein" }, "humanName": { - "en": "Nachrichten", + "en": "Messages", "de": "Nachrichten" }, "filename": "strings.json", diff --git a/modules/polls/module.json b/modules/polls/module.json index b8faf194..9c55497d 100644 --- a/modules/polls/module.json +++ b/modules/polls/module.json @@ -6,7 +6,8 @@ "link": "https://github.com/SCDerox" }, "description": { - "en": "Simple module to create fresh polls on your server!" + "en": "Simple module to create fresh polls on your server! Supports anonymous polls and more.", + "de": "Einfaches Modul, um coole Umfragen auf deinem Server zu erstellen! Unterstützt anonyme Umfragen und mehr." }, "events-dir": "/events", "commands-dir": "/commands", diff --git a/modules/polls/polls.js b/modules/polls/polls.js index cb91da9c..b57c8198 100644 --- a/modules/polls/polls.js +++ b/modules/polls/polls.js @@ -4,7 +4,11 @@ */ const {scheduleJob} = require('node-schedule'); const {MessageEmbed} = require('discord.js'); -const {renderProgressbar, formatDate} = require('../../src/functions/helpers'); +const { + renderProgressbar, + formatDate, + parseEmbedColor +} = require('../../src/functions/helpers'); const {localize} = require('../../src/functions/localize'); /** @@ -55,7 +59,7 @@ async function updateMessage(channel, data, mID = null) { }); const embed = new MessageEmbed() .setTitle(strings.embed.title) - .setColor(strings.embed.color) + .setColor(parseEmbedColor(strings.embed.color)) .setDescription(data.description.replaceAll('[PUBLIC]', '')); let s = ''; let p = ''; @@ -86,7 +90,7 @@ async function updateMessage(channel, data, mID = null) { if (data.expiresAt || data.endAt) { const date = new Date(data.expiresAt || data.endAt); if (date.getTime() <= new Date().getTime()) { - embed.setColor(strings.embed.endedPollColor); + embed.setColor(parseEmbedColor(strings.embed.endedPollColor)); embed.setTitle(strings.embed.endedPollTitle); expired = true; } else { diff --git a/modules/quiz/commands/quiz.js b/modules/quiz/commands/quiz.js index e9c075e1..bc3a32fd 100644 --- a/modules/quiz/commands/quiz.js +++ b/modules/quiz/commands/quiz.js @@ -1,6 +1,10 @@ -const {MessageEmbed} = require('discord.js'); +const {ChannelType, ComponentType, MessageEmbed} = require('discord.js'); const durationParser = require('parse-duration'); -const {formatDate} = require('../../../src/functions/helpers'); +const { + formatDate, + shuffleArray, + parseEmbedColor +} = require('../../../src/functions/helpers'); const {localize} = require('../../../src/functions/localize'); const {createQuiz} = require('../quizUtil'); @@ -38,10 +42,10 @@ async function create(interaction) { } const msg = await interaction.reply({ components: [{ - type: 'ACTION_ROW', + type: ComponentType.ActionRow, components: [{ /* eslint-disable camelcase */ - type: 'SELECT_MENU', + type: ComponentType.StringSelect, custom_id: 'quiz', placeholder: localize('quiz', 'select-correct'), min_values: 1, @@ -54,7 +58,7 @@ async function create(interaction) { }); const collector = msg.createMessageComponentCollector({ filter: i => interaction.user.id === i.user.id, - componentType: 'SELECT_MENU', + componentType: ComponentType.StringSelect, max: 1 }); collector.on('collect', async i => { @@ -117,10 +121,10 @@ module.exports.subcommands = { } else quiz = interaction.client.configurations['quiz']['quizList'][Math.floor(Math.random() * interaction.client.configurations['quiz']['quizList'].length)]; quiz.channel = interaction.channel; - quiz.options = [ + quiz.options = shuffleArray([ ...quiz.wrongOptions.map(o => ({text: o})), ...quiz.correctOptions.map(o => ({text: o, correct: true})) - ]; + ]); quiz.endAt = new Date(new Date().getTime() + durationParser(quiz.duration)); quiz.canChangeVote = false; quiz.private = true; @@ -153,7 +157,7 @@ module.exports.subcommands = { const embed = new MessageEmbed() .setTitle(moduleStrings.embed.leaderboardTitle) - .setColor(moduleStrings.embed.leaderboardColor) + .setColor(parseEmbedColor(moduleStrings.embed.leaderboardColor)) .setFooter({text: interaction.client.strings.footer, iconURL: interaction.client.strings.footerImgUrl}) .setThumbnail(interaction.guild.iconURL()) .addField(moduleStrings.embed.leaderboardSubtitle, leaderboardString); @@ -194,7 +198,7 @@ module.exports.config = { type: 'CHANNEL', name: 'channel', required: true, - channelTypes: ['GUILD_TEXT', 'GUILD_NEWS', 'GUILD_VOICE'], + channelTypes: [ChannelType.GuildText, ChannelType.GuildAnnouncement, ChannelType.GuildVoice], description: localize('quiz', 'cmd-create-channel-description') }, { @@ -236,7 +240,7 @@ module.exports.config = { type: 'CHANNEL', name: 'channel', required: true, - channelTypes: ['GUILD_TEXT', 'GUILD_NEWS', 'GUILD_VOICE'], + channelTypes: [ChannelType.GuildText, ChannelType.GuildAnnouncement, ChannelType.GuildVoice], description: localize('quiz', 'cmd-create-channel-description') }, { diff --git a/modules/quiz/configs/strings.json b/modules/quiz/configs/strings.json index ffd09041..4d5cc913 100644 --- a/modules/quiz/configs/strings.json +++ b/modules/quiz/configs/strings.json @@ -4,7 +4,7 @@ "de": "Stelle hier die Nachrichten des Modules ein" }, "humanName": { - "en": "Nachrichten", + "en": "Messages", "de": "Nachrichten" }, "filename": "strings.json", @@ -56,4 +56,4 @@ "disableKeyEdits": true } ] -} +} \ No newline at end of file diff --git a/modules/quiz/quizUtil.js b/modules/quiz/quizUtil.js index 2e9be56f..e85e7554 100644 --- a/modules/quiz/quizUtil.js +++ b/modules/quiz/quizUtil.js @@ -3,8 +3,12 @@ * @module quiz */ const {scheduleJob} = require('node-schedule'); -const {MessageEmbed} = require('discord.js'); -const {renderProgressbar, formatDate} = require('../../src/functions/helpers'); +const {ChannelType, MessageEmbed} = require('discord.js'); +const { + renderProgressbar, + formatDate, + parseEmbedColor +} = require('../../src/functions/helpers'); const {localize} = require('../../src/functions/localize'); let changed = false; @@ -69,7 +73,7 @@ async function updateMessage(channel, data, mID = null, interaction = null) { }); const embed = new MessageEmbed() .setTitle(strings.embed.title) - .setColor(strings.embed.color) + .setColor(parseEmbedColor(strings.embed.color)) .setDescription(data.description); let allVotes = 0; @@ -120,7 +124,7 @@ async function updateMessage(channel, data, mID = null, interaction = null) { if (data.expiresAt || data.endAt) { const date = new Date(data.expiresAt || data.endAt); if (date.getTime() <= Date.now()) { - embed.setColor(strings.embed.endedQuizColor); + embed.setColor(parseEmbedColor(strings.embed.endedQuizColor)); embed.setTitle(strings.embed.endedQuizTitle); embed.addField('\u200b', localize('quiz', 'correct-highlighted')); } else { @@ -195,7 +199,7 @@ async function updateLeaderboard(client, force = false) { const moduleStrings = client.configurations['quiz']['strings']; const channel = await client.channels.fetch(client.configurations['quiz']['config']['leaderboardChannel']).catch(() => { }); - if (!channel || channel.type !== 'GUILD_TEXT') return client.logger.error('[quiz] ' + localize('quiz', 'leaderboard-channel-not-found')); + if (!channel || channel.type !== ChannelType.GuildText) return client.logger.error('[quiz] ' + localize('quiz', 'leaderboard-channel-not-found')); const messages = (await channel.messages.fetch()).filter(msg => msg.author.id === client.user.id); const users = await client.models['quiz']['QuizUser'].findAll({ @@ -221,7 +225,7 @@ async function updateLeaderboard(client, force = false) { const embed = new MessageEmbed() .setTitle(moduleStrings.embed.leaderboardTitle) - .setColor(moduleStrings.embed.leaderboardColor) + .setColor(parseEmbedColor(moduleStrings.embed.leaderboardColor)) .setFooter({text: client.strings.footer, iconURL: client.strings.footerImgUrl}) .setThumbnail(channel.guild.iconURL()) .addField(moduleStrings.embed.leaderboardSubtitle, leaderboardString); diff --git a/modules/rock-paper-scissors/commands/rock-paper-scissors.js b/modules/rock-paper-scissors/commands/rock-paper-scissors.js index ccfce14d..38dbb36d 100644 --- a/modules/rock-paper-scissors/commands/rock-paper-scissors.js +++ b/modules/rock-paper-scissors/commands/rock-paper-scissors.js @@ -1,5 +1,10 @@ const {localize} = require('../../../src/functions/localize'); -const {MessageEmbed, MessageActionRow, MessageButton} = require('discord.js'); +const { + ActionRowBuilder, + ButtonBuilder, + ComponentType, + MessageEmbed +} = require('discord.js'); const {formatDiscordUserName} = require('../../../src/functions/helpers'); const rpsgames = []; @@ -39,7 +44,10 @@ function findWinner(move1, move2) { } } } - return {win1, win2}; + return { + win1, + win2 + }; } /** @@ -47,23 +55,23 @@ function findWinner(move1, move2) { * @returns {MessageActionRow} */ function rpsrow() { - return new MessageActionRow() + return new ActionRowBuilder() .addComponents( - new MessageButton() + new ButtonBuilder() .setCustomId('rps_scissors') .setLabel(localize('rock-paper-scissors', 'scissors')) .setStyle('PRIMARY') .setEmoji('✂️') ) .addComponents( - new MessageButton() + new ButtonBuilder() .setCustomId('rps_stone') .setLabel(localize('rock-paper-scissors', 'stone')) .setStyle('PRIMARY') .setEmoji('🪨') ) .addComponents( - new MessageButton() + new ButtonBuilder() .setCustomId('rps_paper') .setLabel(localize('rock-paper-scissors', 'paper')) .setStyle('PRIMARY') @@ -76,9 +84,9 @@ function rpsrow() { * @returns {MessageActionRow} */ function playagain() { - return new MessageActionRow() + return new ActionRowBuilder() .addComponents( - new MessageButton() + new ButtonBuilder() .setCustomId('rps_playagain') .setLabel(localize('rock-paper-scissors', 'play-again')) .setStyle('SECONDARY') @@ -94,29 +102,32 @@ function playagain() { * @returns {MessageActionRow} */ function generatePlayer(user1, user2, state1, state2) { - return new MessageActionRow() + const b1 = new ButtonBuilder() + .setCustomId('rps_user1') + .setLabel(formatDiscordUserName(user1)) + .setStyle(statestyle[state1]) + .setDisabled(true); + if (stateemoji[state1]) b1.setEmoji(stateemoji[state1]); + const b2 = new ButtonBuilder() + .setCustomId('rps_user2') + .setLabel(formatDiscordUserName(user2)) + .setStyle(statestyle[state1]) + .setDisabled(true); + if (stateemoji[state1]) b2.setEmoji(stateemoji[state2]); + + return new ActionRowBuilder() .addComponents( - new MessageButton() - .setCustomId('rps_user1') - .setLabel(formatDiscordUserName(user1)) - .setEmoji(stateemoji[state1] || '') - .setStyle(statestyle[state1]) - .setDisabled(true) + b1 ) .addComponents( - new MessageButton() + new ButtonBuilder() .setCustomId('rps_vs') .setStyle('SECONDARY') .setEmoji('⚔️') .setDisabled(true) ) .addComponents( - new MessageButton() - .setCustomId('rps_user2') - .setLabel(formatDiscordUserName(user2)) - .setEmoji(stateemoji[state2] || '') - .setStyle(statestyle[state2]) - .setDisabled(true) + b2 ); } @@ -186,7 +197,7 @@ module.exports.run = async function (interaction) { }); confirmed = await confirmmsg.awaitMessageComponent({ filter: i => i.user.id === user2.id, - componentType: 'BUTTON', + componentType: ComponentType.Button, time: 120000 }).catch(() => { }); @@ -194,13 +205,15 @@ module.exports.run = async function (interaction) { content: localize('rock-paper-scissors', 'invite-expired', { u: interaction.user.toString(), i: '<@' + user2.id + '>' - }), components: [] + }), + components: [] }); if (confirmed.customId === 'deny-invite') return confirmed.update({ content: localize('rock-paper-scissors', 'invite-denied', { u: interaction.user.toString(), i: '<@' + user2.id + '>' - }), components: [] + }), + components: [] }); } @@ -211,7 +224,7 @@ module.exports.run = async function (interaction) { const msg = await (confirmed || interaction)[confirmed ? 'update' : 'reply']({ content: '<@' + interaction.user.id + '>' + (user2.bot ? '' : ' <@' + user2.id + '>'), embeds: [embed], - components: [rpsrow(), generatePlayer(interaction.user, user2, 'none', user2.bot ? 'selected' : 'none')], + components: [rpsrow(), generatePlayer(interaction.user, user2, 'none', user2.bot ? 'selected' : 'none')].map((v) => v.toJSON()), fetchReply: true }); @@ -224,13 +237,16 @@ module.exports.run = async function (interaction) { }; const collector = msg.createMessageComponentCollector({ - componentType: 'BUTTON', + componentType: ComponentType.Button, filter: i => i.user.id === interaction.user.id || i.user.id === user2.id }); collector.on('collect', i => { const game = rpsgames[i.message.id]; - if (i.customId === 'rps_playagain') return i.update({components: resetGame(game), content: mentionUsers(game)}); + if (i.customId === 'rps_playagain') return i.update({ + components: resetGame(game).map(v => v.toJSON()), + content: mentionUsers(game) + }); if (i.user.id === game.user1.id) { game.state1 = 'selected'; @@ -243,7 +259,7 @@ module.exports.run = async function (interaction) { rpsgames[i.message.id] = game; if (!game.selected1 || (!game.selected2 && !user2.bot)) return i.update({ content: mentionUsers(game), - components: [rpsrow(), generatePlayer(game.user1, game.user2, game.state1, game.state2)] + components: [rpsrow(), generatePlayer(game.user1, game.user2, game.state1, game.state2)].map(v => v.toJSON()) }); let resU1 = ''; @@ -289,7 +305,11 @@ module.exports.run = async function (interaction) { if (resU1 === resU2) components = resetGame(game); else components = [generatePlayer(game.user1, game.user2, game.state2, game.state1), playagain()]; } - i.update({content: mentionUsers(game), embeds: [embed], components}); + i.update({ + content: mentionUsers(game), + embeds: [embed], + components: components.map(f => f.toJSON()) + }); }); }; diff --git a/modules/serverinfo/configs/config.json b/modules/serverinfo/configs/config.json deleted file mode 100644 index 7d4b793b..00000000 --- a/modules/serverinfo/configs/config.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "description": { - "en": "Configure the function of the module here", - "de": "Stelle hier die Funktionen des Modules ein" - }, - "humanName": { - "en": "Configuration", - "de": "Konfiguration" - }, - "filename": "config.json", - "content": [ - { - "name": "channelID", - "humanName": { - "en": "Channel" - }, - "default": { - "en": "" - }, - "description": { - "en": "ID of the channel this module should operate in", - "de": "ID des Channels, in welchem die Nachricht gesendet und bearbeitet werden soll" - }, - "type": "channelID" - }, - { - "name": "embed", - "humanName": { - "en": "Embed" - }, - "default": { - "en": { - "title": "Information about this guild", - "description": "You can find some basic information about our guild here", - "color": "GREEN" - }, - "de": { - "title": "Informationen über diesen Server", - "description": "Hier kannst du alle Informationen über unseren Server finden", - "color": "GREEN" - } - }, - "description": { - "en": "You can configure some of the parameters of the embed here", - "de": "Du kannst hier einige Teile des Embeds anpassen" - }, - "type": "keyed", - "content": { - "key": "string", - "value": "string" - }, - "disableKeyEdits": true - } - ] -} \ No newline at end of file diff --git a/modules/serverinfo/configs/fields.json b/modules/serverinfo/configs/fields.json deleted file mode 100644 index 0bd009c2..00000000 --- a/modules/serverinfo/configs/fields.json +++ /dev/null @@ -1,166 +0,0 @@ -{ - "description": { - "en": "Change the Embed-Fields of the serverinfo-embed here", - "de": "Stelle hier die Felder des Serverinfo-Embeds ein" - }, - "humanName": { - "en": "Embed-Fields", - "de": "Embed-Felder" - }, - "filename": "fields.json", - "configElements": true, - "content": [ - { - "name": "name", - "humanName": { - "en": "Feld-Name", - "de": "Feldname" - }, - "default": { - "en": "" - }, - "description": { - "en": "Name of the field", - "de": "Name des Feldes" - }, - "type": "string" - }, - { - "name": "content", - "humanName": { - "en": "Field-Content", - "de": "Feldinhalt" - }, - "default": { - "en": "" - }, - "description": { - "en": "Content of this field", - "de": "Inhalt dieses Feldes" - }, - "type": "string", - "params": [ - { - "name": "memberCount", - "description": { - "en": "Member-Count of this guild", - "de": "Anzahl von Mitgliedern auf deinem Server" - } - }, - { - "name": "botCount", - "description": { - "en": "Bot-Count of this guild", - "de": "Anzahl von Bots auf deinem Server" - } - }, - { - "name": "userCount", - "description": { - "en": "User-Count of this guild", - "de": "Anzahl von Nutzern auf deinem Server" - } - }, - { - "name": "onlineMemberCount", - "description": { - "en": "Count of online members on this guild", - "de": "Anzahl von online Mitgliern auf deinem Server" - } - }, - { - "name": "daysSinceCreation", - "description": { - "en": "Count of days passed since the creation of this guild", - "de": "Anzahl von vergangenen Tagen seit Erstellung deines Servers" - } - }, - { - "name": "guildCreationTimestamp", - "description": { - "en": "Show when the guild was created", - "de": "Datum und Uhrzeit, wenn der Server erstellt wurde" - } - }, - { - "name": "guildBoosts", - "description": { - "en": "Show how often this guild was boosted", - "de": "Zeigt die Anzahl von Boots auf dem Server an" - } - }, - { - "name": "boostLevel", - "description": { - "en": "Shows the current boost-level of this guild", - "de": "Zeigt das aktuelle Boost-Level des Servers an" - } - }, - { - "name": "boosterCount", - "description": { - "en": "Count of boosters on this guild", - "de": "Anzahl von Boostern auf deinem Server" - } - }, - { - "name": "channelCount", - "description": { - "en": "Count of channels on this guild", - "de": "Anzahl von Channeln auf deinem Server" - } - }, - { - "name": "roleCount", - "description": { - "en": "Count of roles on this guild", - "de": "Anzahl von Rollen auf deinem Server" - } - }, - { - "name": "emojiCount", - "description": { - "en": "Count of emojis on this guild", - "de": "Anzahl von Emojis auf deinem Server" - } - }, - { - "name": "newline", - "description": { - "en": "Inserts a new line", - "de": "Fügt eine neue Zeile ein (wie der Name schon sagt)" - } - }, - { - "name": "userWithRoleCount-", - "description": { - "en": "Count of members with a specific role (replace \"\" with an actual role-id)", - "de": "Anzahl von Nutzern mit einer bestimmen Rolle (bitte \"\" mit einer echten Rollen-ID ersetzen)" - } - }, - { - "name": "onlineUserWithRoleCount-", - "description": { - "en": "Count of members with a specific role who are online (replace \"\" with an actual role-id)", - "de": "Anzahl von Nutzern mit einer bestimmen Rolle, die online sind (bitte \"\" mit einer echten Rollen-ID ersetzen)" - } - } - ] - }, - { - "name": "inline", - "humanName": { - "en": "Inline Field?", - "de": "In-Zeilen-Feld?" - }, - "default": { - "en": false - }, - "description": { - "en": "If enabled the field will be inlined", - "de": "Wenn aktiviert wird das Feld bei Discord in einer Zeile mit anderen Feldern angezeigt" - }, - "type": "boolean" - } - ] -} \ No newline at end of file diff --git a/modules/serverinfo/events/botReady.js b/modules/serverinfo/events/botReady.js deleted file mode 100644 index 50c24806..00000000 --- a/modules/serverinfo/events/botReady.js +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Manages the serverinfo-embed - * @module Partner-List - * @author Simon Csaba - */ -const {formatDate, formatDiscordUserName} = require('../../../src/functions/helpers'); -const {MessageEmbed} = require('discord.js'); -const {localize} = require('../../../src/functions/localize'); - -exports.run = async (client) => { - await generateEmbed(client); - const interval = setInterval(() => { - generateEmbed(client); - }, 300000); - client.intervals.push(interval); -}; - -/** - * Generates the serverinfo embed - * @param {Client} client - * @returns {Promise} - */ -async function generateEmbed(client) { - const config = client.configurations['serverinfo']['config']; - const fieldConfig = client.configurations['serverinfo']['fields']; - const channel = await client.channels.fetch(config.channelID).catch(() => { - }); - if (!channel && (channel || {}).type !== 'GUILD_TEXT') return client.logger.error(`[serverinfo] Could not find channel with id ${config.channelID}`); - const messages = (await channel.messages.fetch()).filter(msg => msg.author.id === client.user.id); - const embed = new MessageEmbed() - .setTitle(config.embed.title) - .setDescription(config.embed.description) - .setColor(config.embed.color) - .setFooter({text: client.strings.footer, iconURL: client.strings.footerImgUrl}) - .setThumbnail(channel.guild.iconURL()) - .setAuthor({name: formatDiscordUserName(client.user), iconURL: client.user.avatarURL()}); - if (!client.strings.disableFooterTimestamp) embed.setTimestamp(); - - const guildMembers = await channel.guild.members.fetch({withPresences: true}); - const guildCreationDate = new Date(channel.guild.createdAt); - const guildRoles = await channel.guild.roles.fetch(); - - /** - * Replaces the content with the variables of this module - * @private - * @param {String} content Content to replace variables in - * @returns {String} String with the variables replaced - */ - function replacer(content) { - /** - * Replaces the first member-with-role-count parameters of the input - * @private - */ - function replaceFirst() { - if (content.includes('%userWithRoleCount-')) { - const id = content.split('%userWithRoleCount-')[1].split('%')[0]; - if (content.includes(`%userWithRoleCount-${id}%`)) { - content = content.replaceAll(`%userWithRoleCount-${id}%`, guildMembers.filter(f => f.roles.cache.has(id)).size.toString()); - replaceFirst(); - } - } - if (content.includes('%onlineUserWithRoleCount-')) { - const id = content.split('%onlineUserWithRoleCount-')[1].split('%')[0]; - if (content.includes(`%onlineUserWithRoleCount-${id}%`)) { - content = content.replaceAll(`%onlineUserWithRoleCount-${id}%`, guildMembers.filter(f => f.roles.cache.has(id) && f.presence && (f.presence || {}).status !== 'offline').size.toString()); - replaceFirst(); - } - } - } - - replaceFirst(); - content = content.replaceAll('%memberCount%', guildMembers.size) - .replaceAll('%botCount%', guildMembers.filter(m => m.user.bot).size) - .replaceAll('%userCount%', guildMembers.filter(m => !m.user.bot).size) - .replaceAll('%onlineMemberCount%', guildMembers.filter(m => m.presence && (m.presence || {}).status !== 'offline').size) - .replaceAll('%daysSinceCreation%', ((new Date().getTime() - guildCreationDate.getTime()) / 86400000).toFixed(0)) - .replaceAll('%guildCreationTimestamp%', formatDate(guildCreationDate)) - .replaceAll('%guildBoosts%', channel.guild.premiumSubscriptionCount) - .replaceAll('%boostLevel%', localize('boostTier', channel.guild.premiumTier)) - .replaceAll('%channelCount%', channel.guild.channels.cache.size) - .replaceAll('%roleCount%', guildRoles.size) - .replaceAll('%emojiCount%', channel.guild.emojis.cache.size) - .replaceAll('%newline%', '\n') - .replaceAll('%boosterCount%', guildMembers.filter(m => m.premiumSinceTimestamp).size); - return content; - } - - fieldConfig.forEach(field => { - embed.addField(field.name, replacer(field.content), !!field.inline); - }); - - if (messages.first()) await messages.first().edit({embeds: [embed]}); - else await channel.send({embeds: [embed]}); -} \ No newline at end of file diff --git a/modules/serverinfo/module.json b/modules/serverinfo/module.json deleted file mode 100644 index cc2e23fa..00000000 --- a/modules/serverinfo/module.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "serverinfo", - "author": { - "scnxOrgID": "1", - "name": "SCDerox (SC Network Team)", - "link": "https://github.com/SCDerox" - }, - "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/serverinfo", - "events-dir": "/events", - "config-example-files": [ - "configs/config.json", - "configs/fields.json" - ], - "tags": [ - "community" - ], - "humanReadableName": { - "en": "Server-Information-Channel", - "de": "Serverinformationen" - }, - "description": { - "en": "Simple module to have a channel with a message that shows the users some information about your guild", - "de": "Fortgeschrittenes Modul, um einen Channel zu erstellen, in dem Server-Informationen als Embed angezeigt werden" - } -} \ No newline at end of file diff --git a/modules/starboard/handleStarboard.js b/modules/starboard/handleStarboard.js index 0594c2a1..572d61b2 100644 --- a/modules/starboard/handleStarboard.js +++ b/modules/starboard/handleStarboard.js @@ -84,7 +84,7 @@ module.exports = async (client, msgReaction, user, isReactionRemove = false) => '%userName%': msg.author.username, '%displayName%': msg.member.displayName, '%userTag%': formatDiscordUserName(msg.author), - '%userAvatar%': msg.member.displayAvatarURL({dynamic: true}), + '%userAvatar%': msg.member.displayAvatarURL({forceStatic: false}), '%channelName%': msg.channel.name, '%channelMention%': '<#' + msg.channel.id + '>', '%emoji%': msgReaction.emoji.toString(), @@ -100,4 +100,4 @@ module.exports = async (client, msgReaction, user, isReactionRemove = false) => starMsg: sentMessage.id }); } -}; \ No newline at end of file +}; diff --git a/modules/status-roles/configs/config.json b/modules/status-roles/configs/config.json index 36d2fb0d..92c85945 100644 --- a/modules/status-roles/configs/config.json +++ b/modules/status-roles/configs/config.json @@ -57,6 +57,21 @@ "de": "Entferne alle anderen Rollen von Nutzern mit einem der Wörter im Status" }, "type": "boolean" + }, + { + "name": "ignoreOfflineUsers", + "humanName": { + "en": "Do not remove roles from offline users", + "de": "Rollen von offline Nutzern nicht entfernen" + }, + "type": "boolean", + "default": { + "en": true + }, + "description": { + "en": "When users are offline, they don't have a status, leading to the role being removed. If enabled, the status role won't be removed from offline users, only users that have a different status. Recommended on servers with more than 500 members.", + "de": "Wenn Nutzer offline sind, haben sie keinen Status, was dazu führt, dass die Rolle entfernt wird. Wenn aktiviert, wird die Status-Rolle nicht von offline Nutzern entfernt, nur von Nutzern mit anderem Status. Empfohlen auf Servern mit 500+ Nutzern." + } } ] } \ No newline at end of file diff --git a/modules/status-roles/events/presenceUpdate.js b/modules/status-roles/events/presenceUpdate.js index e8043fd1..2d618452 100644 --- a/modules/status-roles/events/presenceUpdate.js +++ b/modules/status-roles/events/presenceUpdate.js @@ -1,37 +1,27 @@ const {localize} = require('../../../src/functions/localize'); +const {ActivityType} = require('discord.js'); module.exports.run = async function (client, oldPresence, newPresence) { - if (!client.botReadyAt) return; if (newPresence.member.guild.id !== client.guildID) return; const moduleConfig = client.configurations['status-roles']['config']; const roles = moduleConfig.roles; const status = moduleConfig.words; - const member = newPresence.member; - if (newPresence.activities.length > 0) { - if (newPresence.activities[0].state) { - if (status.some(word => newPresence.activities[0].state.toLowerCase().includes(word.toLowerCase()))) { - if (moduleConfig.remove) await member.roles.remove(member.roles.cache.filter(role => !role.managed)); - return member.roles.add(roles, localize('status-role', 'fulfilled')); - } else { - removeRoles(); - } - } else { - removeRoles(); - } + if (status.some(word => newPresence.activities.filter(f => f.type === ActivityType.Custom).some(a => a.state && a.state.toLowerCase().includes(word.toLowerCase())))) { + if (newPresence.member.roles.cache.filter(f => roles.includes(f.id)).size === roles.length) return; + if (moduleConfig.remove) await newPresence.member.roles.remove(newPresence.member.roles.cache.filter(role => !role.managed)); + return newPresence.member.roles.add(roles, localize('status-role', 'fulfilled')); } else { - removeRoles(); + if (newPresence.status === 'offline' && moduleConfig.ignoreOfflineUsers) return; + await removeRoles(); } /** * Removes the roles of a user who no longer fulfills the criteria */ - function removeRoles() { - for (let i = 0; i < roles.length; i++) { - if (member.roles.cache.has(roles[i])) { - member.roles.remove(roles[i], localize('status-role', 'not-fulfilled')); - } - } + async function removeRoles() { + if (newPresence.member.roles.cache.filter(f => roles.includes(f.id)).size === 0) return; + await newPresence.member.roles.remove(roles, localize('status-role', 'not-fulfilled')); } }; \ No newline at end of file diff --git a/modules/suggestions/suggestion.js b/modules/suggestions/suggestion.js index 76a11b84..2124bd58 100644 --- a/modules/suggestions/suggestion.js +++ b/modules/suggestions/suggestion.js @@ -11,7 +11,9 @@ module.exports.generateSuggestionEmbed = generateSuggestionEmbed; async function generateSuggestionEmbed(client, suggestion) { const moduleConfig = client.configurations['suggestions']['config']; const channel = await client.channels.fetch(moduleConfig.suggestionChannel); - const message = await channel.messages.fetch(suggestion.messageID); + const message = await channel.messages.fetch(suggestion.messageID).catch(() => { + }); + if (!message) return; const user = await client.users.fetch(suggestion.suggesterID).catch(() => { }); diff --git a/modules/team-list/config.json b/modules/team-list/config.json index 86c64ce0..02ad5a74 100644 --- a/modules/team-list/config.json +++ b/modules/team-list/config.json @@ -37,6 +37,7 @@ "de": "Jede Rolle, die im Embed gelistet werden soll" }, "type": "array", + "maxLength": 25, "content": "roleID" }, { @@ -125,6 +126,21 @@ "default": { "en": false } + }, + { + "name": "onlineShowHighestRole", + "humanName": { + "en": "Only list the highest role of a user?", + "de": "Nur die höchste Rolle eines Nutzers anzeigen?" + }, + "description": { + "en": "If enabled, a staff member will only be listed under their highest role in the list.", + "de": "Wenn aktiviert, wird ein Teammitglied nur unter seiner höchsten Rolle in der Liste angezeigt." + }, + "type": "boolean", + "default": { + "en": false + } } ] } \ No newline at end of file diff --git a/modules/team-list/events/botReady.js b/modules/team-list/events/botReady.js index be5bbae8..9cdcfef3 100644 --- a/modules/team-list/events/botReady.js +++ b/modules/team-list/events/botReady.js @@ -1,5 +1,9 @@ const isEqual = require('is-equal'); -const {disableModule, truncate} = require('../../../src/functions/helpers'); +const { + disableModule, + truncate, + parseEmbedColor +} = require('../../../src/functions/helpers'); const {localize} = require('../../../src/functions/localize'); const {MessageEmbed} = require('discord.js'); const schedule = require('node-schedule'); @@ -19,7 +23,7 @@ module.exports.run = async function (client) { client.jobs.push(job); }; -let lastSavedEmbed = null; +let lastSavedEmbed = {}; /** * Updates the embed if needed @@ -30,7 +34,7 @@ async function updateEmbedsIfNeeded(client) { const channels = client.configurations['team-list']['config']; for (const channelConfig of channels) { const embed = new MessageEmbed() - .setColor(channelConfig.embed.color) + .setColor(parseEmbedColor(channelConfig.embed.color)) .setTitle(channelConfig.embed.title) .setDescription(channelConfig.embed.description) .setTimestamp() @@ -43,24 +47,28 @@ async function updateEmbedsIfNeeded(client) { }); if (!channel) return disableModule('team-list', localize('team-list', 'channel-not-found', {c: channelConfig['channelID']})); const messages = (await channel.messages.fetch()).filter(msg => msg.author.id === client.user.id); - const guildMembers = await channel.guild.members.fetch(); + const guildMembers = client.guild.members.cache; const roles = (await channel.guild.roles.fetch()).filter(f => channelConfig.roles.includes(f.id)).sort((a, b) => a.position < b.position ? 1 : -1); + const listedUserIDs = []; + let i = 0; for (const role of roles.values()) { let userString = ''; for (const member of guildMembers.filter(m => m.roles.cache.has(role.id)).values()) { + if (listedUserIDs.includes(member.user.id) && channelConfig.onlineShowHighestRole) continue; + listedUserIDs.push(member.user.id); userString = userString + (channelConfig.includeStatus ? `* ${member.user.toString()}: ${statusIcons[(member.presence || {status: 'offline'}).status]} ${localize('team-list', (member.presence || {status: 'offline'}).status)}\n` : `${member.user.toString()}, `); } if (userString === '') userString = localize('team-list', 'no-users-with-role', {r: role.toString()}); else if (!channelConfig.includeStatus) userString = userString.substring(0, userString.length - 2); - + i++; embed.addField(channelConfig['nameOverwrites'][role.id] || role.name, truncate((channelConfig['descriptions'][role.id] ? `${channelConfig['descriptions'][role.id]}\n` : '') + userString, 1024)); } - if (embed.fields.length === 0) embed.addField('⚠️', localize('team-list', 'no-roles-selected')); + if (i === 0) embed.addField('⚠️', localize('team-list', 'no-roles-selected')); - if (isEqual(lastSavedEmbed, embed.toJSON())) return; - lastSavedEmbed = embed.toJSON(); + if (isEqual(lastSavedEmbed[channelConfig['channelID']], embed.toJSON())) continue; + lastSavedEmbed[channelConfig['channelID']] = embed.toJSON(); if (messages.last()) await messages.last().edit({embeds: [embed]}); else channel.send({embeds: [embed]}); diff --git a/modules/team-list/module.json b/modules/team-list/module.json index b1285587..11a6e39f 100644 --- a/modules/team-list/module.json +++ b/modules/team-list/module.json @@ -1,5 +1,6 @@ { "name": "team-list", + "fa-icon": "fa-user-tie", "author": { "scnxOrgID": "1", "name": "SCDerox (SC Network Team)", @@ -18,7 +19,7 @@ "de": "Teammitglieder-Liste" }, "description": { - "en": "List all your staff and explain team-roles in a always up-to-date embed", - "de": "Liste alle deine Teammitglieder und erkläre sie in einem immer up-to-date Embed " + "en": "List all your staff members and explain team roles in always up-to-date embed", + "de": "Liste alle deine Teammitglieder und erkläre sie in einem immer aktuellem Embed" } } \ No newline at end of file diff --git a/modules/temp-channels/config.json b/modules/temp-channels/config.json index 274da8a3..844a52f6 100644 --- a/modules/temp-channels/config.json +++ b/modules/temp-channels/config.json @@ -35,7 +35,7 @@ "de": true }, "description": { - "en": "If enabled the user has the permission to change the name and settings of the voicechanel via both, the Discord-integrated menus and the corresponding /-commands", + "en": "If enabled the user has the permission to change the name and settings of the voice channel via both, the Discord-integrated menus and the corresponding /-commands", "de": "Wenn aktiviert erhält der Ersteller des Channel die Permission \"MANAGE_CHANNEL\" auf diesem Channel, sowie Zugriff auf die entsprechenden Befehle" }, "type": "boolean" @@ -51,7 +51,7 @@ "de": 3 }, "description": { - "en": "Set a timeout here in which the bot should wait before deleting the voicechannel (in secounds)", + "en": "Set a timeout here in which the bot should wait before deleting the voice channel (in seconds)", "de": "Die Anzahl von Sekunden nach einem Channel-Leave, die der Bot warten soll, bevor er einen Channel löscht" }, "type": "integer", @@ -105,6 +105,13 @@ "de": "Nickname des Mitglieds" } }, + { + "name": "number", + "description": { + "en": "The current number of the channel", + "de": "Aktuelle Nummer des Kanals" + } + }, { "name": "tag", "description": { @@ -125,7 +132,7 @@ "de": true }, "description": { - "en": "If enabled the bot will create a new channel for each voicechannel which can be only seen by users in the voicechannel", + "en": "If enabled the bot will create a new channel for each voice channel which can be only seen by users in the voice channel", "de": "Wenn aktiviert wird ein No-Mic-Textchannel für jeden Temp-Channel erstellt, auf welchen nur Nutzer Zugriff haben, die im VC sind" }, "type": "boolean" @@ -171,10 +178,10 @@ }, "default": { "en": "I have created and moved you to your new voice-channel - have fun ^^", - "de": "Tach - ich habe dir nen eigenen Channel erstellt und dich gemovt - Dieser wird nach Inaktivität gelöscht - Have fun^^" + "de": "Tach - ich habe dir nen eigenen Channel erstellt und dich verschoben - Dieser wird nach Inaktivität gelöscht - Have fun^^" }, "description": { - "en": "Set the message that should get send to the user if they join the voicechannel", + "en": "Set the message that should get send to the user if they join the voice channel", "de": "Hier kannst du die Nachricht festlegen, die an den Nutzer geschrieben soll (wenn aktiviert)" }, "type": "string", @@ -215,7 +222,7 @@ "de": true }, "description": { - "en": "If enabled the user has the permission to change the access-mode of the voicechanel", + "en": "If enabled the user has the permission to change the access-mode of the voice chanel", "de": "Wenn aktiviert erhält der Ersteller des Channel die Möglichkeit die Zugriffsberechtigungen für den Kanal festzulegen" }, "type": "boolean" @@ -329,7 +336,7 @@ "name": "edit-error", "humanName": {}, "default": { - "en": "An error occured while editing your channel. One or more of your settings could not be applied. This could be due to missing permissions or an invalid value", + "en": "An error occurred while editing your channel. One or more of your settings could not be applied. This could be due to missing permissions or an invalid value", "de": "Beim Bearbeiten des Kanals ist ein Fehler aufgetreten. Eine oder mehr deiner Einstellungen konnten nicht angewendet werden. Dies kann an fehlenden Rechten oder einem ungültigen Eingabewert liegen" }, "description": { @@ -378,7 +385,7 @@ "de": "Einstellungsnachricht" }, "default": { - "en": "Change the Settings of your temp-channnel here", + "en": "Change the Settings of your temporary channel here", "de": "Ändere die Einstellungen deines Temp-Channels hier" }, "description": { diff --git a/modules/temp-channels/events/botReady.js b/modules/temp-channels/events/botReady.js index 4d063d1b..f49235a2 100644 --- a/modules/temp-channels/events/botReady.js +++ b/modules/temp-channels/events/botReady.js @@ -60,7 +60,7 @@ module.exports.run = async function () { emoji: '📝' }] }]; - const message = embedType(moduleConfig['settingsMessage'], {}, {components}); + const message = embedType(moduleConfig['settingsMessage'], {}, {components: components.map(c => c.toJSON())}); await messages.first().edit(message); } else await sendMessage(settingsChannel); } diff --git a/modules/temp-channels/events/interactionCreate.js b/modules/temp-channels/events/interactionCreate.js index a57abd5c..c4944c56 100644 --- a/modules/temp-channels/events/interactionCreate.js +++ b/modules/temp-channels/events/interactionCreate.js @@ -1,4 +1,4 @@ -const {MessageActionRow, Modal, TextInputComponent} = require('discord.js'); +const {ActionRowBuilder, ModalBuilder, TextInputBuilder, TextInputStyle} = require('discord.js'); const {usersList, channelMode, userAdd, userRemove, channelEdit} = require('../channel-settings'); const {localize} = require('../../../src/functions/localize'); const {Op} = require('sequelize'); @@ -25,15 +25,15 @@ module.exports.run = async function (client, interaction) { }); return; } - const modal = new Modal() + const modal = new ModalBuilder() .setCustomId('tempc-add-modal') .setTitle(localize('temp-channels', 'add-modal-title')); - const userInput = new TextInputComponent() + const userInput = new TextInputBuilder() .setCustomId('add-modal-input') .setLabel(localize('temp-channels', 'add-modal-prompt')) - .setStyle('SHORT') - .setPlaceholder('User#1234'); - const actionRow = new MessageActionRow().addComponents(userInput); + .setStyle(TextInputStyle.Short) + .setPlaceholder(localize('temp-channels', 'edit-modal-username-placeholder')); + const actionRow = new ActionRowBuilder().addComponents(userInput); modal.addComponents(actionRow); await interaction.showModal(modal); } @@ -45,15 +45,15 @@ module.exports.run = async function (client, interaction) { }); return; } - const modal = new Modal() + const modal = new ModalBuilder() .setCustomId('tempc-remove-modal') .setTitle(localize('temp-channels', 'remove-modal-title')); - const userInput = new TextInputComponent() + const userInput = new TextInputBuilder() .setCustomId('remove-modal-input') .setLabel(localize('temp-channels', 'remove-modal-prompt')) - .setStyle('SHORT') - .setPlaceholder('User#1234'); - const actionRow = new MessageActionRow().addComponents(userInput); + .setStyle(TextInputStyle.Short) + .setPlaceholder(localize('temp-channels', 'edit-modal-username-placeholder')); + const actionRow = new ActionRowBuilder().addComponents(userInput); modal.addComponents(actionRow); await interaction.showModal(modal); } @@ -99,46 +99,46 @@ module.exports.run = async function (client, interaction) { return; } const vchann = interaction.guild.channels.cache.get(vc.id); - const modal = new Modal() + const modal = new ModalBuilder() .setCustomId('tempc-edit-modal') .setTitle(localize('temp-channels', 'edit-modal-title')); - const nsfwInput = new TextInputComponent() + const nsfwInput = new TextInputBuilder() .setCustomId('edit-modal-nsfw-input') .setLabel(localize('temp-channels', 'edit-modal-nsfw-prompt')) .setRequired(true) - .setStyle('SHORT') + .setStyle(TextInputStyle.Short) .setPlaceholder(localize('temp-channels', 'edit-modal-nsfw-placeholder')) .setValue(vchann.nsfw.toString()); - const bitrateInput = new TextInputComponent() + const bitrateInput = new TextInputBuilder() .setCustomId('edit-modal-bitrate-input') .setLabel(localize('temp-channels', 'edit-modal-bitrate-prompt')) .setRequired(true) - .setStyle('SHORT') + .setStyle(TextInputStyle.Short) .setPlaceholder(localize('temp-channels', 'edit-modal-bitrate-placeholder')) .setValue(vchann.bitrate.toString()); - const limitInput = new TextInputComponent() + const limitInput = new TextInputBuilder() .setCustomId('edit-modal-limit-input') .setLabel(localize('temp-channels', 'edit-modal-limit-prompt')) .setRequired(true) - .setStyle('SHORT') + .setStyle(TextInputStyle.Short) .setPlaceholder(localize('temp-channels', 'edit-modal-limit-placeholder')) .setValue(vchann.userLimit.toString()); - const nameInput = new TextInputComponent() + const nameInput = new TextInputBuilder() .setCustomId('edit-modal-name-input') .setLabel(localize('temp-channels', 'edit-modal-name-prompt')) .setRequired(true) - .setStyle('SHORT') + .setStyle(TextInputStyle.Short) .setPlaceholder(localize('temp-channels', 'edit-modal-name-placeholder')) .setValue(vchann.name); - const nsfwRow = new MessageActionRow().addComponents(nsfwInput); - const bitrateRow = new MessageActionRow().addComponents(bitrateInput); - const limitRow = new MessageActionRow().addComponents(limitInput); - const nameRow = new MessageActionRow().addComponents(nameInput); + const nsfwRow = new ActionRowBuilder().addComponents(nsfwInput); + const bitrateRow = new ActionRowBuilder().addComponents(bitrateInput); + const limitRow = new ActionRowBuilder().addComponents(limitInput); + const nameRow = new ActionRowBuilder().addComponents(nameInput); modal.addComponents(bitrateRow); modal.addComponents(limitRow); modal.addComponents(nameRow); @@ -188,4 +188,4 @@ module.exports.run = async function (client, interaction) { await channelEdit(interaction, 'modal'); } } -}; \ No newline at end of file +}; diff --git a/modules/temp-channels/events/voiceStateUpdate.js b/modules/temp-channels/events/voiceStateUpdate.js index 47bce223..d802f099 100644 --- a/modules/temp-channels/events/voiceStateUpdate.js +++ b/modules/temp-channels/events/voiceStateUpdate.js @@ -3,6 +3,7 @@ const {Op} = require('sequelize'); const {localize} = require('../../../src/functions/localize'); const {sendMessage} = require('../channel-settings'); const {formatDiscordUserName} = require('../../../src/functions/helpers'); +const {ChannelType} = require('discord.js'); module.exports.run = async function (client, oldState, newState) { if (!client.botReadyAt) return; @@ -56,15 +57,17 @@ module.exports.run = async function (client, oldState, newState) { newState.setChannel(null, '[temp-channels] ' + localize('temp-channels', 'disconnect-audit-log-reason')); alreadyExistingChannel.destroy(); }); - const newChannel = await newState.guild.channels.create(moduleConfig['channelname_format'] + const n = await client.models['temp-channels']['TempChannel'].count({}) + 1; + const newChannel = await newState.guild.channels.create({ + name: moduleConfig['channelname_format'] .split('%username%').join(newState.member.user.username) + .split('%number%').join(n) .split('%nickname%').join(newState.member.nickname || newState.member.user.username) .split('%tag%').join(formatDiscordUserName(newState.member.user)), - { - type: 'GUILD_VOICE', - parent: moduleConfig['category'], - reason: '[temp-channels] ' + localize('temp-channels', 'created-audit-log-reason', {u: formatDiscordUserName(newState.member.user)}) - }); + type: ChannelType.GuildVoice, + parent: moduleConfig['category'], + reason: '[temp-channels] ' + localize('temp-channels', 'created-audit-log-reason', {u: formatDiscordUserName(newState.member.user)}) + }); await newState.setChannel(newChannel.id); if (moduleConfig['allowUserToChangeName']) await newChannel.permissionOverwrites.create(newState.member, {'MANAGE_CHANNELS': true}, { reason: '[temp-channels] ' + localize('temp-channels', 'created-audit-log-reason', {u: formatDiscordUserName(newState.member.user)}) @@ -75,8 +78,9 @@ module.exports.run = async function (client, oldState, newState) { let noMicChannel = null; if (moduleConfig['create_no_mic_channel']) { const everyoneRole = await newChannel.guild.roles.cache.find(role => role.name === '@everyone'); - noMicChannel = await newChannel.guild.channels.create(`${newChannel.name}-no-mic`, { - type: 'GUILD_TEXT', + noMicChannel = await newChannel.guild.channels.create({ + name: `${newChannel.name}-no-mic`, + type: ChannelType.GuildText, parent: moduleConfig['category'], topic: localize('temp-channels', 'no-mic-channel-topic', {u: formatDiscordUserName(newState.member.user)}), reason: '[temp-channels] ' + localize('temp-channels', 'created-audit-log-reason', {u: formatDiscordUserName(newState.member.user)}), diff --git a/modules/temp-channels/module.json b/modules/temp-channels/module.json index 6322f971..05f7e1a3 100644 --- a/modules/temp-channels/module.json +++ b/modules/temp-channels/module.json @@ -20,7 +20,7 @@ "de": "Temporäre Channel" }, "description": { - "en": "Allow users to quickly create voice channels by joining a voicechannel", + "en": "Allow users to quickly create voice channels by joining a voice channel", "de": "Erlaube es Nutzern, ihren eigenen Voice-Channel zu erstellen, indem sie einem VC joinen" } } \ No newline at end of file diff --git a/modules/tic-tak-toe/commands/tic-tac-toe.js b/modules/tic-tak-toe/commands/tic-tac-toe.js index 8c756271..2523de5e 100644 --- a/modules/tic-tak-toe/commands/tic-tac-toe.js +++ b/modules/tic-tak-toe/commands/tic-tac-toe.js @@ -1,11 +1,12 @@ const {localize} = require('../../../src/functions/localize'); +const {ComponentType} = require('discord.js'); const {randomElementFromArray} = require('../../../src/functions/helpers'); module.exports.run = async function (interaction) { const member = interaction.options.getMember('user', true); if (member.user.id === interaction.user.id) return interaction.reply({ ephemeral: true, - content: '⚠️ ' + localize('tic-tac-toe', 'self-invite-not-possible', {r: `<@${((await interaction.guild.members.fetch({withPresences: true})).filter(u => u.presence && u.user.id !== interaction.user.id && !u.user.bot).random() || {user: {id: 'RickAstley'}}).user.id}>`}) + content: '⚠️ ' + localize('tic-tac-toe', 'self-invite-not-possible', {r: `<@${(interaction.guild.members.cache.filter(u => u.presence && u.user.id !== interaction.user.id && !u.user.bot).random() || {user: {id: 'RickAstley'}}).user.id}>`}) }); const rep = await interaction.reply({ content: localize('tic-tac-toe', 'challenge-message', {t: member.toString(), u: interaction.user.toString()}), @@ -38,7 +39,7 @@ module.exports.run = async function (interaction) { let endReason = null; let gameEndReasonType = null; let currentUser = randomElementFromArray([interaction.member, member]); - const a = rep.createMessageComponentCollector({componentType: 'BUTTON'}); + const a = rep.createMessageComponentCollector({componentType: ComponentType.Button}); setTimeout(() => { if (started || a.ended) return; endReason = localize('tic-tac-toe', 'invite-expired', {u: interaction.user.toString(), i: member.toString()}); @@ -235,7 +236,7 @@ module.exports.run = async function (interaction) { module.exports.config = { name: 'tic-tac-toe', description: localize('tic-tac-toe', 'command-description'), - defaultPermission: true, + options: [ { type: 'USER', @@ -244,4 +245,4 @@ module.exports.config = { description: localize('tic-tac-toe', 'user-description') } ] -}; \ No newline at end of file +}; diff --git a/modules/tic-tak-toe/module.json b/modules/tic-tak-toe/module.json index dc47b733..e23b26cc 100644 --- a/modules/tic-tak-toe/module.json +++ b/modules/tic-tak-toe/module.json @@ -1,7 +1,8 @@ { "name": "tic-tak-toe", "humanReadableName": { - "en": "Tick-Tack-Toe" + "en": "Tic Tac Toe", + "de": "Tic-Tac-Toe" }, "author": { "scnxOrgID": "1", diff --git a/modules/tickets/config.json b/modules/tickets/config.json index 1c24b3e9..dd71b3ab 100644 --- a/modules/tickets/config.json +++ b/modules/tickets/config.json @@ -81,8 +81,8 @@ "en": [] }, "description": { - "en": "Nutzer, die in Tickets gepingt werden und diese sehen können", - "de": "Users who get pinged in the tickets and who can see tickets" + "de": "Nutzer, die in Tickets gepingt werden und diese sehen können", + "en": "Users who get pinged in the tickets and who can see tickets" }, "type": "array", "content": "roleID" @@ -90,7 +90,7 @@ { "name": "logChannel", "humanName": { - "en": "Log chanenl", + "en": "Log channel", "de": "Log-Kanal" }, "default": { @@ -105,7 +105,7 @@ { "name": "ticket-create-message", "humanName": { - "en": "Ticket create message", + "en": "Ticket created message", "de": "Ticketerstellungs-Nachricht" }, "default": { @@ -275,7 +275,7 @@ }, "description": { "en": "Button for creating a ticket", - "de": "Button zum erstellen eines Tickets" + "de": "Button zum Erstellen eines Tickets" }, "type": "string", "pro": true diff --git a/modules/tickets/events/botReady.js b/modules/tickets/events/botReady.js index f2796501..a94c4603 100644 --- a/modules/tickets/events/botReady.js +++ b/modules/tickets/events/botReady.js @@ -1,3 +1,4 @@ +const {ChannelType} = require('discord.js'); const {embedType, disableModule, migrate} = require('../../../src/functions/helpers'); const {localize} = require('../../../src/functions/localize'); @@ -11,7 +12,7 @@ module.exports.run = async function (client) { } const channel = await client.channels.fetch(element['ticket-create-channel']).catch(() => { }); - if (!channel || channel.guild.id !== client.config.guildID || channel.type !== 'GUILD_TEXT') return disableModule('tickets', localize('tickets', 'channel-not-found', {c: element['ticket-create-channel']})); + if (!channel || channel.guild.id !== client.config.guildID || channel.type !== ChannelType.GuildText) return disableModule('tickets', localize('tickets', 'channel-not-found', {c: element['ticket-create-channel']})); const components = [{ type: 'ACTION_ROW', components: [{ diff --git a/modules/tickets/events/interactionCreate.js b/modules/tickets/events/interactionCreate.js index f472d765..037cf2b0 100644 --- a/modules/tickets/events/interactionCreate.js +++ b/modules/tickets/events/interactionCreate.js @@ -4,7 +4,8 @@ const { lockChannel, messageLogToStringToPaste, embedType, - formatDiscordUserName + formatDiscordUserName, + parseEmbedColor } = require('../../../src/functions/helpers'); module.exports.run = async function (client, interaction) { @@ -52,7 +53,7 @@ module.exports.run = async function (client, interaction) { await logChannel.send({ embeds: [ new MessageEmbed() - .setColor('DARK_GREEN') + .setColor(parseEmbedColor('DARK_GREEN')) .setTitle(localize('tickets', 'ticket-log-embed-title', {i: ticket.id})) .setFooter({ text: client.strings.footer, @@ -100,11 +101,12 @@ module.exports.run = async function (client, interaction) { { id: rID, type: 'ROLE', - allow: ['SEND_MESSAGES', 'VIEW_CHANNEL', 'READ_MESSAGE_HISTORY' , 'SEND_FILES'] + allow: ['SEND_MESSAGES', 'VIEW_CHANNEL', 'READ_MESSAGE_HISTORY'] } ); }); - const channel = await interaction.guild.channels.create(formatDiscordUserName(interaction.user).split('#').join('-'), { + const channel = await interaction.guild.channels.create({ + name: formatDiscordUserName(interaction.user).split('#').join('-'), parent: element['ticket-create-category'], topic: `Ticket created by ${interaction.user.toString()} by clicking on a message in ${interaction.channel.toString()}`, reason: localize('tickets', 'ticket-created-audit-log', {u: formatDiscordUserName(interaction.user)}), @@ -149,4 +151,4 @@ module.exports.run = async function (client, interaction) { }); } } -}; +}; \ No newline at end of file diff --git a/modules/twitch-notifications/configs/streamers.json b/modules/twitch-notifications/configs/streamers.json index e0c17559..26c2fe04 100644 --- a/modules/twitch-notifications/configs/streamers.json +++ b/modules/twitch-notifications/configs/streamers.json @@ -10,7 +10,9 @@ "elementLimits": { "STARTER": 2, "ACTIVE_GUILD": 5, - "PRO": 15 + "PRO": 15, + "UNLIMITED": 5, + "PROFESSIONAL": 15 }, "filename": "streamers.json", "configElements": true, diff --git a/modules/twitch-notifications/events/botReady.js b/modules/twitch-notifications/events/botReady.js index a0428a56..11bbf958 100644 --- a/modules/twitch-notifications/events/botReady.js +++ b/modules/twitch-notifications/events/botReady.js @@ -25,7 +25,6 @@ function twitchNotifications(client, apiClient) { async function addLiveRole(userID, roleID, liveRole) { if (!liveRole) return; if (!userID || userID === '' || !roleID || roleID === '') return; - await client.guild.members.fetch(); const member = client.guild.members.cache.get(userID); if (!member) { client.logger.error(localize('twitch-notifications', 'user-not-on-twitch', {u: userID})); @@ -102,7 +101,6 @@ function twitchNotifications(client, apiClient) { } else if (stream === null) { if (!streamers[index]['liveRole']) return; if (!streamers[index]['id'] || streamers[index]['id'] === '' || !streamers[index]['role'] || streamers[index]['role'] === '') return; - await client.guild.members.fetch(); const member = client.guild.members.cache.get(streamers[index]['id']); if (!member) { client.logger.error(localize('twitch-notifications', 'user-not-on-twitch', {u: streamers[index]['id']})); diff --git a/modules/uno/commands/uno.js b/modules/uno/commands/uno.js index 7cd7dc4a..64d7c882 100644 --- a/modules/uno/commands/uno.js +++ b/modules/uno/commands/uno.js @@ -1,5 +1,5 @@ const {localize} = require('../../../src/functions/localize'); -const {MessageActionRow, MessageButton} = require('discord.js'); +const {ActionRowBuilder, ButtonBuilder, ComponentType} = require('discord.js'); const cards = [ '0', @@ -14,13 +14,13 @@ const cards = [ const colorEmojis = {'red': '🟥', 'blue': '🟦', 'green': '🟩', 'yellow': '🟨'}; const colors = Object.keys(colorEmojis); -const publicrow = new MessageActionRow() +const publicrow = new ActionRowBuilder() .addComponents( - new MessageButton() + new ButtonBuilder() .setCustomId('uno-deck') .setLabel(localize('uno', 'view-deck')) .setStyle('PRIMARY'), - new MessageButton() + new ButtonBuilder() .setCustomId('uno-uno') .setLabel(localize('uno', 'uno')) .setStyle('PRIMARY') @@ -31,27 +31,27 @@ const publicrow = new MessageActionRow() * @param {Object} player * @param {Object} game * @param {Boolean} neutral - * @return {MessageActionRow} + * @return {ActionRowBuilder} */ function buildDeck(player, game, neutral = false) { - const controlrow = new MessageActionRow(); + const controlrow = new ActionRowBuilder(); if (player.turn && !player.blockRedraw) controlrow.addComponents( - new MessageButton() + new ButtonBuilder() .setCustomId('uno-draw') .setLabel(localize('uno', 'draw')) .setStyle('SECONDARY') ); else controlrow.addComponents( - new MessageButton() + new ButtonBuilder() .setCustomId('uno-update') .setLabel(localize('uno', 'update-button')) .setStyle('SECONDARY') ); - const cardrow1 = new MessageActionRow(); - const cardrow2 = new MessageActionRow(); - const cardrow3 = new MessageActionRow(); - const cardrow4 = new MessageActionRow(); + const cardrow1 = new ActionRowBuilder(); + const cardrow2 = new ActionRowBuilder(); + const cardrow3 = new ActionRowBuilder(); + const cardrow4 = new ActionRowBuilder(); player.cards.slice(0, 20).forEach((c, i) => { let row = cardrow1; @@ -59,7 +59,7 @@ function buildDeck(player, game, neutral = false) { if (i > 9) row = cardrow3; if (i > 14) row = cardrow4; row.addComponents( - new MessageButton() + new ButtonBuilder() .setCustomId('uno-card-' + c.name + '-' + c.color + '-' + i) .setLabel(c.name) .setEmoji(colorEmojis[c.color]) @@ -147,11 +147,17 @@ function perPlayerHandler(i, player, game) { nextPlayer(game, player); game.players[player.n] = player; - i.update({content: localize('uno', 'auto-drawn-skip'), components: buildDeck(player, game)}); + i.update({ + content: localize('uno', 'auto-drawn-skip'), + components: buildDeck(player, game).map(c => c.toJSON()) + }); return game.msg.edit(gameMsg(game)); } } - if (i.customId === 'uno-update') return i.update({content: null, components: buildDeck(player, game)}); + if (i.customId === 'uno-update') return i.update({ + content: null, + components: buildDeck(player, game).map(c => c.toJSON()) + }); if (!player.turn) return i.reply({content: localize('connect-four', 'not-turn'), ephemeral: true}); game.justChoosingColor = false; @@ -178,24 +184,24 @@ function perPlayerHandler(i, player, game) { i.update({ content: localize('uno', 'use-drawn'), components: [ - new MessageActionRow() + new ActionRowBuilder() .addComponents( - new MessageButton() + new ButtonBuilder() .setCustomId('uno-card-' + c.name + '-' + c.color) .setLabel(c.name) .setEmoji(colorEmojis[c.color]) .setStyle('PRIMARY'), - new MessageButton() + new ButtonBuilder() .setCustomId('uno-dont-use-drawn') .setLabel(localize('uno', 'dont-use-drawn')) .setStyle('SECONDARY') ) - ], + ].map(c => c.toJSON()), ephemeral: true }); } else { nextPlayer(game, player); - i.update({components: buildDeck(player, game)}); + i.update({components: buildDeck(player, game).map(c => c.toJSON())}); game.msg.edit(gameMsg(game)); } } else if (i.customId.startsWith('uno-card-')) { @@ -206,7 +212,10 @@ function perPlayerHandler(i, player, game) { color: colors[Math.floor(Math.random() * colors.length)] }); nextPlayer(game, player); - i.update({content: localize('uno', 'missing-uno'), components: buildDeck(player, game)}); + i.update({ + content: localize('uno', 'missing-uno'), + components: buildDeck(player, game).map(c => c.toJSON()) + }); return game.msg.edit(gameMsg(game)); } const name = i.customId.split('-')[2]; @@ -216,13 +225,13 @@ function perPlayerHandler(i, player, game) { color }, player.cards)) return i.update({ content: localize('uno', 'invalid-card', {c: colorEmojis[color] + ' **' + name + '**'}), - components: buildDeck(player, game) + components: buildDeck(player, game).map(c => c.toJSON()) }); const toremove = player.cards.find(c => c.name === name && c.color === color); if (!toremove) return i.update({ content: localize('uno', 'used-card', {c: colorEmojis[color] + ' **' + name + '**'}), - components: buildDeck(player, game) + components: buildDeck(player, game).map(c => c.toJSON()) }); player.cards.splice(player.cards.indexOf(toremove), 1); @@ -245,34 +254,37 @@ function perPlayerHandler(i, player, game) { } return i.update({ content: localize('uno', 'choose-color'), components: [ - new MessageActionRow() + new ActionRowBuilder() .addComponents( - new MessageButton() + new ButtonBuilder() .setCustomId('uno-color-red-' + name) .setEmoji(colorEmojis.red) .setStyle('PRIMARY'), - new MessageButton() + new ButtonBuilder() .setCustomId('uno-color-blue-' + name) .setEmoji(colorEmojis.blue) .setStyle('PRIMARY'), - new MessageButton() + new ButtonBuilder() .setCustomId('uno-color-green-' + name) .setEmoji(colorEmojis.green) .setStyle('PRIMARY'), - new MessageButton() + new ButtonBuilder() .setCustomId('uno-color-yellow-' + name) .setEmoji(colorEmojis.yellow) .setStyle('PRIMARY') ), ...buildDeck(player, game, true).slice(1) - ] + ].map(c => c.toJSON()) }); } else nextPlayer(game, player, 1, name === localize('uno', 'reverse')); if (name === localize('uno', 'draw2')) game.pendingDraws = game.pendingDraws + 2; game.previousCards = [game.previousCards[1], game.previousCards[2], colorEmojis[game.lastCard.color] + ' ' + game.lastCard.name]; game.lastCard = {name, color}; - i.update({content: null, components: buildDeck(player, game)}); + i.update({ + content: null, + components: buildDeck(player, game).map(c => c.toJSON()) + }); game.msg.edit(gameMsg(game)); } else if (i.customId === 'uno-dont-use-drawn' || i.customId.startsWith('uno-color-')) { player.blockRedraw = false; @@ -281,7 +293,10 @@ function perPlayerHandler(i, player, game) { color: i.customId.split('-')[2] }; nextPlayer(game, player); - i.update({content: null, components: buildDeck(player, game)}); + i.update({ + content: null, + components: buildDeck(player, game).map(c => c.toJSON()) + }); game.msg.edit(gameMsg(game)); } game.players[player.n] = player; @@ -306,7 +321,7 @@ function gameMsg(game) { allowedMentions: { users: [game.players.find(p => p.turn).id] }, - components: [publicrow] + components: [publicrow].map(c => c.toJSON()) }; } @@ -381,14 +396,18 @@ module.exports.run = async function (interaction) { color: colors[Math.floor(Math.random() * colors.length)] }); - const m = await p.interaction.followUp({components: buildDeck(p, game), fetchReply: true, ephemeral: true}); - m.createMessageComponentCollector({componentType: 'BUTTON'}).on('collect', i => perPlayerHandler(i, p, game)); + const m = await p.interaction.followUp({ + components: buildDeck(p, game).map(c => c.toJSON()), + fetchReply: true, + ephemeral: true + }); + m.createMessageComponentCollector({componentType: ComponentType.Button}).on('collect', i => perPlayerHandler(i, p, game)); }); } const timeout = setTimeout(startGame, 179000); - const collector = msg.createMessageComponentCollector({componentType: 'BUTTON'}); + const collector = msg.createMessageComponentCollector({componentType: ComponentType.Button}); collector.on('collect', async i => { if (i.customId === 'uno-join') { if (game.players.some(p => p.id === i.user.id)) return i.reply({ @@ -427,8 +446,12 @@ module.exports.run = async function (interaction) { const player = game.players.find(p => p.id === i.user.id); if (!player) return i.reply({content: localize('uno', 'not-in-game'), ephemeral: true}); console.log(player); - const m = await i.reply({components: buildDeck(player, game), fetchReply: true, ephemeral: true}); - m.createMessageComponentCollector({componentType: 'BUTTON'}).on('collect', int => perPlayerHandler(int, player, game)); + const m = await i.reply({ + components: buildDeck(player, game).map(c => c.toJSON()), + fetchReply: true, + ephemeral: true + }); + m.createMessageComponentCollector({componentType: ComponentType.Button}).on('collect', int => perPlayerHandler(int, player, game)); } else if (i.customId === 'uno-uno') { const player = game.players.find(p => p.id === i.user.id); if (!player) return i.reply({content: localize('uno', 'not-in-game'), ephemeral: true}); diff --git a/modules/welcomer/configs/channels.json b/modules/welcomer/configs/channels.json index 1c0bd7f6..8a90cf41 100644 --- a/modules/welcomer/configs/channels.json +++ b/modules/welcomer/configs/channels.json @@ -115,17 +115,10 @@ } }, { - "name": "tag", - "description": { - "en": "Tag of the user", - "de": "Tag des Nutzers" - } - }, - { - "name": "memberProfilePictureUrl", + "name": "memberProfileBannerUrl", "description": { - "en": "URL of the user's avatar", - "de": "URL zum Avatar des Nutzers" + "en": "URL of the banner's avatar", + "de": "URL zum Banner des Nutzers" }, "isImage": true }, @@ -150,13 +143,6 @@ "de": "Anzahl von Nutzern auf dem Server" } }, - { - "name": "mention", - "description": { - "en": "Mention of the user who boosted", - "de": "Erwähnung des Nutzers" - } - }, { "name": "boostCount", "description": { @@ -177,20 +163,6 @@ "en": "Mention of the user who unboosted", "de": "Erwähnung des Nutzers" } - }, - { - "name": "boostCount", - "description": { - "en": "Total count of boosts", - "de": "Gesamte Anzahl an Boosts" - } - }, - { - "name": "guildLevel", - "description": { - "en": "Boost-Level of the guild after the unboost", - "de": "Boost-Level nach dem Boost" - } } ] }, diff --git a/modules/welcomer/configs/config.json b/modules/welcomer/configs/config.json index 2c3d6df0..69e17a70 100644 --- a/modules/welcomer/configs/config.json +++ b/modules/welcomer/configs/config.json @@ -26,6 +26,21 @@ "type": "array", "content": "roleID" }, + { + "name": "assign-roles-immediately", + "humanName": { + "en": "Immediately give roles, instead of waiting for rules acceptance?", + "de": "Rollen sofort geben statt Regelbestätigung abzuwarten?" + }, + "default": { + "en": true + }, + "description": { + "en": "If enabled, roles will be granted immediately when a user joins your server. Otherwise, no roles will be assigned to users before they complete the Discord onboarding.", + "de": "Wenn aktiviert, werden die Rollen sofort vergeben, wenn ein Nutzer deinem Server beitritt. Ansonsten werden Rollen erst zugewiesen, wenn das Discord onboarding abgeschlossen wurde." + }, + "type": "boolean" + }, { "name": "not-send-messages-if-member-is-bot", "humanName": { @@ -45,8 +60,8 @@ { "name": "give-roles-on-boost", "humanName": { - "en": "Zusätzliche Rollen beim Boost geben", - "de": "Give additional roles to boosters" + "de": "Zusätzliche Rollen beim Boost geben", + "en": "Give additional roles to boosters" }, "default": { "en": [], @@ -86,7 +101,7 @@ }, "description": { "en": "If enabled, a DM will be sent to new users. This is often experienced by them as spam and can decrease your new user retention metrics. Please note that not all users will receive this DM, as a huge chunk has DMs disabled.", - "de": "Wenn aktiviert, wird eine PN an neue Nutzer gesendet. Das wird often als Spam empfunden und kann die Anzahl an Nutzern erhöhen, die direkt nach dem Beitritt deinen Server verlassen. Bitte beachte, dass nicht alle Nutzer diese PN erhalten werden, da ein großer Anzahl diese deaktiviert hat." + "de": "Wenn aktiviert, wird eine PN an neue Nutzer gesendet. Das wird often als Spam empfunden und kann die Anzahl an Nutzern erhöhen, die direkt nach dem Beitritt deinen Server verlassen. Bitte beachte, dass nicht alle Nutzer diese PN erhalten werden, da eine großer Anzahl diese deaktiviert hat." } }, { diff --git a/modules/welcomer/events/guildMemberAdd.js b/modules/welcomer/events/guildMemberAdd.js index a695f691..e19641e6 100644 --- a/modules/welcomer/events/guildMemberAdd.js +++ b/modules/welcomer/events/guildMemberAdd.js @@ -14,13 +14,15 @@ module.exports.run = async function (client, guildMember) { const moduleModel = client.models['welcomer']['User']; if (guildMember.user.bot && moduleConfig['not-send-messages-if-member-is-bot']) return; + await guildMember.user.fetch(); const args = { '%mention%': guildMember.toString(), '%servername%': guildMember.guild.name, '%tag%': formatDiscordUserName(guildMember.user), - '%guildUserCount%': (await client.guild.members.fetch()).size, - '%guildMemberCount%': (await client.guild.members.fetch()).filter(m => !m.user.bot).size, + '%guildUserCount%': client.guild.members.cache.size, + '%guildMemberCount%': client.guild.members.cache.filter(m => !m.user.bot).size, '%memberProfilePictureUrl%': guildMember.user.avatarURL() || guildMember.user.defaultAvatarURL, + '%memberProfileBannerUrl%': guildMember.user.bannerURL({size: 1024}), '%createdAt%': formatDate(guildMember.user.createdAt), '%guildLevel%': localize('boostTier', client.guild.premiumTier), '%boostCount%': client.guild.premiumSubscriptionCount, @@ -32,20 +34,12 @@ module.exports.run = async function (client, guildMember) { const moduleChannels = client.configurations['welcomer']['channels']; - if (!guildMember.pending && moduleConfig['give-roles-on-join'].length !== 0) { - setTimeout(async () => { - if (!guildMember.doNotGiveWelcomeRole) { - const m = await guildMember.fetch(true); - m.roles.add(moduleConfig['give-roles-on-join']).then(() => { - }); - } - }, 300); - } + if (!guildMember.pending || moduleConfig['assign-roles-immediately']) assignJoinRoles(guildMember, moduleConfig); for (const channelConfig of moduleChannels.filter(c => c.type === 'join')) { const channel = await guildMember.guild.channels.fetch(channelConfig.channelID).catch(() => { }); - if (!channel) { + if (!channel || !channelConfig.channelID) { client.logger.error(localize('welcomer', 'channel-not-found', {c: channelConfig.channelID})); continue; } @@ -94,4 +88,17 @@ module.exports.run = async function (client, guildMember) { }); } } -}; \ No newline at end of file +}; + +function assignJoinRoles(guildMember, moduleConfig) { + if (moduleConfig['give-roles-on-join'].length === 0) return; + setTimeout(async () => { + if (!guildMember.doNotGiveWelcomeRole) { + const m = await guildMember.fetch(true); + m.roles.add(moduleConfig['give-roles-on-join']).then(() => { + }); + } + }, 500); +} + +module.exports.assignJoinRoles = assignJoinRoles; \ No newline at end of file diff --git a/modules/welcomer/events/guildMemberRemove.js b/modules/welcomer/events/guildMemberRemove.js index 0fb9ab33..6eba7e50 100644 --- a/modules/welcomer/events/guildMemberRemove.js +++ b/modules/welcomer/events/guildMemberRemove.js @@ -19,7 +19,7 @@ module.exports.run = async function (client, guildMember) { for (const channelConfig of moduleChannels.filter(c => c.type === 'leave')) { const channel = await guildMember.guild.channels.fetch(channelConfig.channelID).catch(() => { }); - if (!channel) { + if (!channel || !channelConfig.channelID) { client.logger.error(localize('welcomer', 'channel-not-found', {c: channelConfig.channelID})); continue; } @@ -30,13 +30,15 @@ module.exports.run = async function (client, guildMember) { } if (!message) message = channelConfig.message; + await guildMember.user.fetch(); await channel.send(await embedTypeV2(message || 'Message not found', { '%mention%': guildMember.toString(), '%servername%': guildMember.guild.name, + '%memberProfileBannerUrl%': guildMember.user.bannerURL({size: 1024}), '%tag%': formatDiscordUserName(guildMember.user), - '%guildUserCount%': (await client.guild.members.fetch()).size, - '%guildMemberCount%': (await client.guild.members.fetch()).filter(m => !m.user.bot).size, + '%guildUserCount%': client.guild.members.cache.size, + '%guildMemberCount%': client.guild.members.cache.filter(m => !m.user.bot).size, '%memberProfilePictureUrl%': guildMember.user.avatarURL() || guildMember.user.defaultAvatarURL, '%createdAt%': formatDate(guildMember.user.createdAt), '%guildLevel%': client.guild.premiumTier, diff --git a/modules/welcomer/events/guildMemberUpdate.js b/modules/welcomer/events/guildMemberUpdate.js index d1a1ce67..3643f079 100644 --- a/modules/welcomer/events/guildMemberUpdate.js +++ b/modules/welcomer/events/guildMemberUpdate.js @@ -6,13 +6,13 @@ const { formatDiscordUserName } = require('../../../src/functions/helpers'); const {localize} = require('../../../src/functions/localize'); +const {assignJoinRoles} = require('./guildMemberAdd'); + module.exports.run = async function (client, oldGuildMember, newGuildMember) { const moduleConfig = client.configurations['welcomer']['config']; if (!client.botReadyAt) return; - if (oldGuildMember.pending && !newGuildMember.pending) { - await newGuildMember.roles.add(moduleConfig['give-roles-on-join']); - } + if (oldGuildMember.pending && !newGuildMember.pending && !moduleConfig['assign-roles-immediately']) assignJoinRoles(newGuildMember, moduleConfig); if (newGuildMember.guild.id !== client.guild.id) return; @@ -36,7 +36,7 @@ module.exports.run = async function (client, oldGuildMember, newGuildMember) { for (const channelConfig of moduleChannels.filter(c => c.type === type)) { const channel = await newGuildMember.guild.channels.fetch(channelConfig.channelID).catch(() => { }); - if (!channel) { + if (!channel || !channelConfig.channelID) { client.logger.error(localize('welcomer', 'channel-not-found', {c: channelConfig.channelID})); continue; } @@ -46,13 +46,15 @@ module.exports.run = async function (client, oldGuildMember, newGuildMember) { } if (!message) message = channelConfig.message; + await newGuildMember.user.fetch(); await channel.send(await embedTypeV2(message || 'Message not found', { '%mention%': newGuildMember.toString(), '%servername%': newGuildMember.guild.name, '%tag%': formatDiscordUserName(newGuildMember.user), - '%guildUserCount%': (await client.guild.members.fetch()).size, - '%guildMemberCount%': (await client.guild.members.fetch()).filter(m => !m.user.bot).size, + '%guildUserCount%': client.guild.members.cache.size, + '%guildMemberCount%': client.guild.members.cache.filter(m => !m.user.bot).size, + '%memberProfileBannerUrl%': newGuildMember.user.bannerURL({size: 1024}), '%memberProfilePictureUrl%': newGuildMember.user.avatarURL() || newGuildMember.user.defaultAvatarURL, '%createdAt%': formatDate(newGuildMember.user.createdAt), '%guildLevel%': localize('boostTier', client.guild.premiumTier), diff --git a/modules/welcomer/events/interactionCreate.js b/modules/welcomer/events/interactionCreate.js index e83497f9..3d62e842 100644 --- a/modules/welcomer/events/interactionCreate.js +++ b/modules/welcomer/events/interactionCreate.js @@ -1,5 +1,6 @@ const {localize} = require('../../../src/functions/localize'); const {embedType, formatDiscordUserName} = require('../../../src/functions/helpers'); + module.exports.run = async function (client, interaction) { if (!interaction.isButton()) return; if (!interaction.customId.startsWith('welcome-')) return; @@ -8,7 +9,7 @@ module.exports.run = async function (client, interaction) { ephemeral: true, content: '👋 ' + localize('welcomer', 'welcome-yourself-error') }); - const channelConfig = client.configurations['welcomer']['channels'].find(c => c.channelID === interaction.channel.id); + const channelConfig = client.configurations['welcomer']['channels'].find(c => c.channelID === interaction.channel.id && c.type === 'join'); if (!channelConfig) return interaction.reply({ ephemeral: true, content: '⚠️ ' + localize('welcomer', 'channel-not-found', {c: channelConfig.channelID}) diff --git a/package-lock.json b/package-lock.json index d93ea38c..415f5651 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,13 @@ { "name": "customdcbot", - "version": "3.8.0", + "version": "3.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "customdcbot", - "version": "3.8.0", - "license": "SEE LICENSE IN LICENSE", + "version": "3.1.1", + "license": "LicenseRef-LICENSE", "dependencies": { "@androz2091/discord-invites-tracker": "1.1.1", "@pixelfactory/privatebin": "2.6.1", @@ -16,10 +16,14 @@ "@twurple/auth": "5.3.4", "age-calculator": "1.0.0", "bs58": "5.0.0", + "bufferutil": "4.0.7", "centra": "2.6.0", + "discord-api-types": "0.38.37", "discord-logs": "2.2.1", - "discord.js": "13.17.1", + "discord.js": "14.25.1", "dotenv": "16.3.1", + "erlpack": "github:discord/erlpack", + "fparser": "3.1.0", "fs-extra": "11.1.1", "html-entities": "2.4.0", "is-equal": "1.6.4", @@ -27,19 +31,15 @@ "jsonfile": "6.1.0", "log4js": "6.9.1", "node-schedule": "2.1.1", - "parse-duration": "1.1.0", - "sequelize": "6.33.0", - "sqlite3": "5.1.6", - "stop-discord-phishing": "0.3.3" - }, - "funding": { - "url": "https://github.com/ScootKit/CustomDCBot?sponsor=1" - }, - "optionalDependencies": { - "bufferutil": "4.0.7", - "erlpack": "github:discord/erlpack", + "parse-duration": "1.1.2", + "sequelize": "6.37.7", + "sqlite3": "5.1.7", + "stop-discord-phishing": "0.3.3", "utf-8-validate": "6.0.3", "zlib-sync": "0.1.8" + }, + "devDependencies": { + "eslint": "8.49.0" } }, "node_modules/@ampproject/remapping": { @@ -544,9 +544,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", - "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "optional": true, "peer": true, "engines": { @@ -554,9 +554,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", - "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "optional": true, "peer": true, "engines": { @@ -589,15 +589,14 @@ } }, "node_modules/@babel/helpers": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.15.tgz", - "integrity": "sha512-7pAjK0aSdxOwR+CcYAqgWOGy5dcfvzsTIfFTb2odQqW47MDfv14UaJDY6eng8ylM2EaeKXdxaSWESbkmaQHTmw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "optional": true, "peer": true, "dependencies": { - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" @@ -698,11 +697,14 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", - "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "optional": true, "peer": true, + "dependencies": { + "@babel/types": "^7.28.5" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -2504,125 +2506,45 @@ "peer": true }, "node_modules/@babel/runtime": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.15.tgz", - "integrity": "sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "optional": true, "peer": true, - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "optional": true, "peer": true, "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template/node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "optional": true, "peer": true, "dependencies": { - "@babel/highlight": "^7.22.13", - "chalk": "^2.4.2" + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/template/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "optional": true, - "peer": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/template/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "optional": true, - "peer": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/template/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "optional": true, - "peer": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/template/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "optional": true, - "peer": true - }, - "node_modules/@babel/template/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/template/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "optional": true, - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/template/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "optional": true, - "peer": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/traverse": { "version": "7.24.5", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.5.tgz", @@ -2670,15 +2592,14 @@ } }, "node_modules/@babel/types": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", - "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "optional": true, "peer": true, "dependencies": { - "@babel/helper-string-parser": "^7.24.1", - "@babel/helper-validator-identifier": "^7.24.5", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -2773,33 +2694,184 @@ } }, "node_modules/@discordjs/builders": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-0.16.0.tgz", - "integrity": "sha512-9/NCiZrLivgRub2/kBc0Vm5pMBE5AUdYbdXsLu/yg9ANgvnaJ0bZKTY8yYnLbsEc/LYUP79lEIdC73qEYhWq7A==", - "deprecated": "no longer supported", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.13.1.tgz", + "integrity": "sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w==", "dependencies": { - "@sapphire/shapeshift": "^3.5.1", - "discord-api-types": "^0.36.2", + "@discordjs/formatters": "^0.6.2", + "@discordjs/util": "^1.2.0", + "@sapphire/shapeshift": "^4.0.0", + "discord-api-types": "^0.38.33", "fast-deep-equal": "^3.1.3", - "ts-mixer": "^6.0.1", - "tslib": "^2.4.0" + "ts-mixer": "^6.0.4", + "tslib": "^2.6.3" }, "engines": { - "node": ">=16.9.0" + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" } }, - "node_modules/@discordjs/builders/node_modules/discord-api-types": { - "version": "0.36.3", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.36.3.tgz", - "integrity": "sha512-bz/NDyG0KBo/tY14vSkrwQ/n3HKPf87a0WFW/1M9+tXYK+vp5Z5EksawfCWo2zkAc6o7CClc0eff1Pjrqznlwg==" - }, "node_modules/@discordjs/collection": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.7.0.tgz", - "integrity": "sha512-R5i8Wb8kIcBAFEPLLf7LVBQKBDYUL+ekb23sOgpkpyGT+V4P7V83wTxcsqmX+PbqHt4cEHn053uMWfRqh/Z/nA==", - "deprecated": "no longer supported", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", + "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/formatters": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.2.tgz", + "integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==", + "dependencies": { + "discord-api-types": "^0.38.33" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.0.tgz", + "integrity": "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==", + "dependencies": { + "@discordjs/collection": "^2.1.1", + "@discordjs/util": "^1.1.1", + "@sapphire/async-queue": "^1.5.3", + "@sapphire/snowflake": "^3.5.3", + "@vladfrangu/async_event_emitter": "^2.4.6", + "discord-api-types": "^0.38.16", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.3" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/util": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz", + "integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==", + "dependencies": { + "discord-api-types": "^0.38.33" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz", + "integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==", + "dependencies": { + "@discordjs/collection": "^2.1.0", + "@discordjs/rest": "^2.5.1", + "@discordjs/util": "^1.1.0", + "@sapphire/async-queue": "^1.5.2", + "@types/ws": "^8.5.10", + "@vladfrangu/async_event_emitter": "^2.2.4", + "discord-api-types": "^0.38.1", + "tslib": "^2.6.2", + "ws": "^8.17.0" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz", + "integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==", + "dev": true, "engines": { - "node": ">=16.9.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@expo/bunyan": { @@ -3047,15 +3119,17 @@ } }, "node_modules/@expo/cli/node_modules/form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.4.tgz", + "integrity": "sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==", "optional": true, "peer": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -3128,9 +3202,9 @@ } }, "node_modules/@expo/cli/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "optional": true, "peer": true, "dependencies": { @@ -3441,25 +3515,14 @@ } }, "node_modules/@expo/devcert": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@expo/devcert/-/devcert-1.1.0.tgz", - "integrity": "sha512-ghUVhNJQOCTdQckSGTHctNp/0jzvVoMMkVh+6SHn+TZj8sU15U/npXIDt8NtQp0HedlPaCgkVdMu8Sacne0aEA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@expo/devcert/-/devcert-1.2.1.tgz", + "integrity": "sha512-qC4eaxmKMTmJC2ahwyui6ud8f3W60Ss7pMkpBq40Hu3zyiAaugPXnZ24145U7K36qO9UHdZUVxsCvIpz2RYYCA==", "optional": true, "peer": true, "dependencies": { - "application-config-path": "^0.1.0", - "command-exists": "^1.2.4", - "debug": "^3.1.0", - "eol": "^0.9.1", - "get-port": "^3.2.0", - "glob": "^7.1.2", - "lodash": "^4.17.4", - "mkdirp": "^0.5.1", - "password-prompt": "^1.0.4", - "rimraf": "^2.6.2", - "sudo-prompt": "^8.2.0", - "tmp": "^0.0.33", - "tslib": "^2.4.0" + "@expo/sudo-prompt": "^9.3.1", + "debug": "^3.1.0" } }, "node_modules/@expo/devcert/node_modules/debug": { @@ -3472,19 +3535,6 @@ "ms": "^2.1.1" } }, - "node_modules/@expo/devcert/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "optional": true, - "peer": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, "node_modules/@expo/env": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@expo/env/-/env-0.3.0.tgz", @@ -3833,9 +3883,9 @@ } }, "node_modules/@expo/package-manager/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "optional": true, "peer": true, "dependencies": { @@ -4038,6 +4088,13 @@ "node": ">=12" } }, + "node_modules/@expo/sudo-prompt": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/@expo/sudo-prompt/-/sudo-prompt-9.3.2.tgz", + "integrity": "sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw==", + "optional": true, + "peer": true + }, "node_modules/@expo/vector-icons": { "version": "14.0.1", "resolved": "https://registry.npmjs.org/@expo/vector-icons/-/vector-icons-14.0.1.tgz", @@ -4097,6 +4154,41 @@ "@hapi/hoek": "^9.0.0" } }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true + }, "node_modules/@isaacs/ttlcache": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz", @@ -4332,30 +4424,11 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", - "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", - "dependencies": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" - }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "optional": true, + "devOptional": true, "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -4368,7 +4441,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "optional": true, + "devOptional": true, "engines": { "node": ">= 8" } @@ -4377,7 +4450,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "optional": true, + "devOptional": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -4479,9 +4552,9 @@ } }, "node_modules/@pixelfactory/privatebin/node_modules/base-x": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz", - "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", + "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", "dependencies": { "safe-buffer": "^5.0.1" } @@ -5056,9 +5129,9 @@ } }, "node_modules/@react-native-community/cli-server-api/node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "optional": true, "peer": true, "engines": { @@ -5505,9 +5578,9 @@ } }, "node_modules/@react-native/dev-middleware/node_modules/ws": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz", - "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", "optional": true, "peer": true, "dependencies": { @@ -5591,22 +5664,30 @@ } }, "node_modules/@sapphire/async-queue": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.0.tgz", - "integrity": "sha512-JkLdIsP8fPAdh9ZZjrbHWR/+mZj0wvKS5ICibcLrRI1j84UmLMshx5n9QmL8b95d4onJ2xxiyugTgSAX7AalmA==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", + "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", "engines": { "node": ">=v14.0.0", "npm": ">=7.0.0" } }, "node_modules/@sapphire/shapeshift": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-3.9.2.tgz", - "integrity": "sha512-YRbCXWy969oGIdqR/wha62eX8GNHsvyYi0Rfd4rNW6tSVVa8p0ELiMEuOH/k8rgtvRoM+EMV7Csqz77YdwiDpA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz", + "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" }, + "engines": { + "node": ">=v16" + } + }, + "node_modules/@sapphire/snowflake": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz", + "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==", "engines": { "node": ">=v14.0.0", "npm": ">=7.0.0" @@ -5835,9 +5916,9 @@ "integrity": "sha512-d/MUkJYdOeKycmm75Arql4M5+UuXmf4cHdHKsyw1GcvnNgL6s77UkgSgJ8TE/rI5PYsnwYq5jkcWBLuN/MpQ1A==" }, "node_modules/@types/ws": { - "version": "8.5.5", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz", - "integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", "dependencies": { "@types/node": "*" } @@ -5908,6 +5989,15 @@ "graphql": "^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0" } }, + "node_modules/@vladfrangu/async_event_emitter": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz", + "integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, "node_modules/@xmldom/xmldom": { "version": "0.7.13", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.13.tgz", @@ -5921,7 +6011,8 @@ "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "optional": true }, "node_modules/abort-controller": { "version": "3.0.0", @@ -5954,8 +6045,7 @@ "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", - "optional": true, - "peer": true, + "devOptional": true, "bin": { "acorn": "bin/acorn" }, @@ -5963,6 +6053,15 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/age-calculator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/age-calculator/-/age-calculator-1.0.0.tgz", @@ -5972,6 +6071,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "optional": true, "dependencies": { "debug": "4" }, @@ -6004,10 +6104,26 @@ "node": ">=8" } }, - "node_modules/anser": { - "version": "1.4.10", - "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz", - "integrity": "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==", + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/anser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz", + "integrity": "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==", "optional": true, "peer": true }, @@ -6121,29 +6237,11 @@ "optional": true, "peer": true }, - "node_modules/application-config-path": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/application-config-path/-/application-config-path-0.1.1.tgz", - "integrity": "sha512-zy9cHePtMP0YhwG+CfHm0bgwdnga2X3gZexpdCwEj//dpb+TKajtiC8REEUJUSq6Ab4f9cgNy2l8ObXzCXFkEw==", - "optional": true, - "peer": true - }, "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" - }, - "node_modules/are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=10" - } + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "optional": true }, "node_modules/arg": { "version": "5.0.2", @@ -6156,8 +6254,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "optional": true, - "peer": true + "devOptional": true }, "node_modules/array-buffer-byte-length": { "version": "1.0.1", @@ -6493,7 +6590,8 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "devOptional": true }, "node_modules/base-64": { "version": "0.1.0", @@ -6501,9 +6599,9 @@ "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==" }, "node_modules/base-x": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz", - "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==" + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz", + "integrity": "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==" }, "node_modules/base64-js": { "version": "1.5.1", @@ -6551,7 +6649,6 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "optional": true, "dependencies": { "file-uri-to-path": "1.0.0" } @@ -6590,21 +6687,22 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "devOptional": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "optional": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -6721,7 +6819,6 @@ "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.7.tgz", "integrity": "sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==", "hasInstallScript": true, - "optional": true, "dependencies": { "node-gyp-build": "^4.3.0" }, @@ -6741,6 +6838,16 @@ "resolved": "https://registry.npmjs.org/byte-base64/-/byte-base64-1.1.0.tgz", "integrity": "sha512-56cXelkJrVMdCY9V/3RfDxTh4VfMFCQ5km7B7GkIGfo4bcPL9aACyJLB0Ms3Ezu5rsHmLB2suis96z4fLM03DA==" }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "optional": true, + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/cacache": { "version": "15.3.0", "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", @@ -6818,6 +6925,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/caller-callsite": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", @@ -6854,6 +6973,15 @@ "node": ">=4" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", @@ -6909,9 +7037,9 @@ } }, "node_modules/chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==" }, "node_modules/charenc": { "version": "0.0.2", @@ -7083,6 +7211,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "optional": true, "bin": { "color-support": "bin.js" } @@ -7157,34 +7286,24 @@ } }, "node_modules/compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "optional": true, "peer": true, "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", + "bytes": "3.1.2", + "compressible": "~2.0.18", "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", "vary": "~1.1.2" }, "engines": { "node": ">= 0.8.0" } }, - "node_modules/compression/node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", - "optional": true, - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/compression/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -7202,6 +7321,37 @@ "optional": true, "peer": true }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "optional": true, + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true, + "peer": true + }, "node_modules/compromise": { "version": "13.11.4", "resolved": "https://registry.npmjs.org/compromise/-/compromise-13.11.4.tgz", @@ -7216,7 +7366,8 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "devOptional": true }, "node_modules/connect": { "version": "3.7.0", @@ -7254,7 +7405,8 @@ "node_modules/console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "optional": true }, "node_modules/convert-source-map": { "version": "1.9.0", @@ -7325,9 +7477,9 @@ } }, "node_modules/cosmiconfig/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "optional": true, "peer": true, "dependencies": { @@ -7370,11 +7522,10 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "optional": true, - "peer": true, + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "devOptional": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -7500,16 +7651,34 @@ "node": ">=0.10.0" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "optional": true, - "peer": true, "engines": { "node": ">=4.0.0" } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -7629,7 +7798,8 @@ "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "optional": true }, "node_modules/denodeify": { "version": "1.2.1", @@ -7672,9 +7842,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", - "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "engines": { "node": ">=8" } @@ -7693,9 +7863,12 @@ } }, "node_modules/discord-api-types": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.33.5.tgz", - "integrity": "sha512-dvO5M52v7m7Dy96+XUnzXNsQ/0npsYpU6dL205kAtEDueswoz3aU3bh1UMoK4cQmcGtB1YRyLKqp+DXi05lzFg==" + "version": "0.38.37", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.37.tgz", + "integrity": "sha512-Cv47jzY1jkGkh5sv0bfHYqGgKOWO1peOrGMkDFM4UmaGMOTgOW8QSexhvixa9sVOiz8MnVOBryWYyw/CEVhj7w==", + "workspaces": [ + "scripts/actions/documentation" + ] }, "node_modules/discord-logs": { "version": "2.2.1", @@ -7712,23 +7885,41 @@ "integrity": "sha512-e0zgs7qe1XH/X3KEPnldfkD07LH9O1B9T31U8qoO7lqGSjj3/IrBuvqMeJ1aYejXRK3KOphIUDw6pLIplEW17A==" }, "node_modules/discord.js": { - "version": "13.17.1", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-13.17.1.tgz", - "integrity": "sha512-h13kUf+7ZaP5ZWggzooCxFutvJJvugcAO54oTEIdVr3zQWi0Sf/61S1kETtuY9nVAyYebXR/Ey4C+oWbsgEkew==", - "dependencies": { - "@discordjs/builders": "^0.16.0", - "@discordjs/collection": "^0.7.0", - "@sapphire/async-queue": "^1.5.0", - "@types/node-fetch": "^2.6.3", - "@types/ws": "^8.5.4", - "discord-api-types": "^0.33.5", - "form-data": "^4.0.0", - "node-fetch": "^2.6.7", - "ws": "^8.13.0" + "version": "14.25.1", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.25.1.tgz", + "integrity": "sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g==", + "dependencies": { + "@discordjs/builders": "^1.13.0", + "@discordjs/collection": "1.5.3", + "@discordjs/formatters": "^0.6.2", + "@discordjs/rest": "^2.6.0", + "@discordjs/util": "^1.2.0", + "@discordjs/ws": "^1.2.3", + "@sapphire/snowflake": "3.5.3", + "discord-api-types": "^0.38.33", + "fast-deep-equal": "3.1.3", + "lodash.snakecase": "4.1.1", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.21.3" }, "engines": { - "node": ">=16.6.0", - "npm": ">=7.0.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/dotenv": { @@ -7776,6 +7967,19 @@ "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==" }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -7835,8 +8039,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "optional": true, - "peer": true, "dependencies": { "once": "^1.4.0" } @@ -7873,19 +8075,11 @@ "node": ">=4" } }, - "node_modules/eol": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/eol/-/eol-0.9.1.tgz", - "integrity": "sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg==", - "optional": true, - "peer": true - }, "node_modules/erlpack": { "version": "0.1.3", "resolved": "git+ssh://git@github.com/discord/erlpack.git#cbe76be04c2210fc9cb6ff95910f0937c1011d04", "hasInstallScript": true, "license": "MIT", - "optional": true, "dependencies": { "bindings": "^1.5.0", "nan": "^2.15.0" @@ -7996,12 +8190,9 @@ "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==" }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "engines": { "node": ">= 0.4" } @@ -8034,9 +8225,9 @@ } }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dependencies": { "es-errors": "^1.3.0" }, @@ -8045,13 +8236,14 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dependencies": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -8094,8 +8286,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": ">=10" }, @@ -8103,6 +8294,106 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eslint": { + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz", + "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "8.49.0", + "@humanwhocodes/config-array": "^0.11.11", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -8117,12 +8408,44 @@ "node": ">=4" } }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -8174,9 +8497,9 @@ } }, "node_modules/execa/node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", "optional": true, "peer": true, "dependencies": { @@ -8246,6 +8569,14 @@ "which": "bin/which" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "engines": { + "node": ">=6" + } + }, "node_modules/expo": { "version": "51.0.2", "resolved": "https://registry.npmjs.org/expo/-/expo-51.0.2.tgz", @@ -8431,19 +8762,6 @@ "node": ">=10" } }, - "node_modules/external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -8477,6 +8795,18 @@ "node": ">= 6" } }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, "node_modules/fast-xml-parser": { "version": "4.2.7", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.7.tgz", @@ -8504,7 +8834,7 @@ "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "optional": true, + "devOptional": true, "dependencies": { "reusify": "^1.0.4" } @@ -8581,16 +8911,27 @@ "node": ">=0.8.0" } }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "optional": true + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "optional": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -8691,7 +9032,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "optional": true, + "devOptional": true, "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -8713,10 +9054,24 @@ "micromatch": "^4.0.2" } }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, "node_modules/flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==" + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==" }, "node_modules/flow-enums-runtime": { "version": "0.0.5", @@ -8770,18 +9125,25 @@ } }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { "node": ">= 6" } }, + "node_modules/fparser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fparser/-/fparser-3.1.0.tgz", + "integrity": "sha512-P9hS9RjO7l4JvWHcDUqos0BXAGzJN4WwJBCh7gwja/23TuW7jfpOKZ+jlGoYp4ZUDnbAJ+rDyKLkIJFCLzgZ+w==" + }, "node_modules/freeport-async": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/freeport-async/-/freeport-async-2.0.0.tgz", @@ -8802,6 +9164,11 @@ "node": ">= 0.6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, "node_modules/fs-extra": { "version": "11.1.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", @@ -8829,7 +9196,8 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "devOptional": true }, "node_modules/fsevents": { "version": "2.3.3", @@ -8878,25 +9246,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -8918,15 +9267,20 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -8935,14 +9289,16 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-port": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz", - "integrity": "sha512-x5UJKlgeUiNT8nyo/AcnwLnZuZNcSjSw0kogRB+Whd1fjjFq4B1hySFxSFWWSn4mIBzg3sRNUDFYc4g5gjPoLg==", - "optional": true, - "peer": true, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, "engines": { - "node": ">=4" + "node": ">= 0.4" } }, "node_modules/get-stream": { @@ -8984,10 +9340,16 @@ "node": ">=6" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, "node_modules/glob": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "devOptional": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -9003,6 +9365,33 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/globalthis": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", @@ -9039,11 +9428,11 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -9054,6 +9443,12 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, "node_modules/graphql": { "version": "15.8.0", "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.8.0.tgz", @@ -9130,9 +9525,9 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "engines": { "node": ">= 0.4" }, @@ -9157,7 +9552,8 @@ "node_modules/has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "optional": true }, "node_modules/hasown": { "version": "2.0.2", @@ -9299,6 +9695,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "optional": true, "dependencies": { "agent-base": "6", "debug": "4" @@ -9327,14 +9724,18 @@ } }, "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/ieee754": { @@ -9360,8 +9761,7 @@ "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": ">= 4" } @@ -9382,11 +9782,27 @@ "node": ">=14.0.0" } }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "optional": true, + "devOptional": true, "engines": { "node": ">=0.8.19" } @@ -9418,6 +9834,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "devOptional": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -9431,20 +9848,18 @@ "node_modules/ini": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "optional": true, - "peer": true + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, "node_modules/inquirer": { - "version": "8.2.6", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", - "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", + "version": "8.2.7", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.7.tgz", + "integrity": "sha512-UjOaSel/iddGZJ5xP/Eixh6dY1XghiBw4XK13rCCIJcJfyhhoul/7KhLLUGtebEj6GDYM6Vnx/mVsjx2L/mFIA==", "dependencies": { + "@inquirer/external-editor": "^1.0.0", "ansi-escapes": "^4.2.1", "chalk": "^4.1.1", "cli-cursor": "^3.1.0", "cli-width": "^3.0.0", - "external-editor": "^3.0.3", "figures": "^3.0.0", "lodash": "^4.17.21", "mute-stream": "0.0.8", @@ -9460,6 +9875,43 @@ "node": ">=12.0.0" } }, + "node_modules/inquirer/node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/inquirer/node_modules/@types/node": { + "version": "25.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", + "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "optional": true, + "peer": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/inquirer/node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "optional": true, + "peer": true + }, "node_modules/internal-ip": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", @@ -9503,6 +9955,15 @@ "optional": true, "peer": true }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "optional": true, + "engines": { + "node": ">= 12" + } + }, "node_modules/ip-regex": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", @@ -9734,7 +10195,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "optional": true, + "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -9778,7 +10239,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "optional": true, + "devOptional": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -9892,8 +10353,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -10077,7 +10537,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "optional": true + "devOptional": true }, "node_modules/isobject": { "version": "3.0.1", @@ -10607,11 +11067,10 @@ "optional": true }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "optional": true, - "peer": true, + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "devOptional": true, "dependencies": { "argparse": "^2.0.1" }, @@ -10680,6 +11139,12 @@ "node": ">=4" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, "node_modules/json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -10719,6 +11184,18 @@ "is-buffer": "~1.1.1" } }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -10743,6 +11220,15 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -10781,6 +11267,19 @@ "node": ">=6" } }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lighthouse-logger": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", @@ -11020,7 +11519,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "optional": true, + "devOptional": true, "dependencies": { "p-locate": "^5.0.0" }, @@ -11043,6 +11542,17 @@ "optional": true, "peer": true }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==" + }, "node_modules/lodash.throttle": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", @@ -11252,27 +11762,10 @@ "node": ">=12" } }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } + "node_modules/magic-bytes.js": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.12.1.tgz", + "integrity": "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==" }, "node_modules/make-fetch-happen": { "version": "9.1.0", @@ -11336,6 +11829,14 @@ "optional": true, "peer": true }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/md5": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", @@ -11691,9 +12192,9 @@ } }, "node_modules/metro-inspector-proxy/node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "optional": true, "peer": true, "engines": { @@ -12186,9 +12687,9 @@ } }, "node_modules/metro/node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "optional": true, "peer": true, "engines": { @@ -12208,12 +12709,12 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "optional": true, "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -12260,10 +12761,22 @@ "node": ">=6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "devOptional": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -12275,8 +12788,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "optional": true, - "peer": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -12392,6 +12903,11 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, "node_modules/moment": { "version": "2.29.4", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", @@ -12486,13 +13002,12 @@ "node_modules/nan": { "version": "2.18.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", - "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==", - "optional": true + "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==" }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", @@ -12508,6 +13023,17 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, "node_modules/ncp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", @@ -12572,6 +13098,17 @@ "node": ">=12.0.0" } }, + "node_modules/node-abi": { + "version": "3.85.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz", + "integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", @@ -12580,9 +13117,9 @@ "peer": true }, "node_modules/node-addon-api": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", - "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==" + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==" }, "node_modules/node-dir": { "version": "0.1.17", @@ -12617,9 +13154,9 @@ } }, "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", + "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", "optional": true, "peer": true, "engines": { @@ -12654,7 +13191,6 @@ "version": "4.6.1", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.1.tgz", "integrity": "sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ==", - "optional": true, "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", @@ -12753,6 +13289,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "optional": true, "dependencies": { "abbrev": "1" }, @@ -12819,17 +13356,6 @@ "node": ">=4" } }, - "node_modules/npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "dependencies": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" - } - }, "node_modules/nullthrows": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", @@ -12851,6 +13377,8 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -12932,9 +13460,9 @@ } }, "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "optional": true, "peer": true, "engines": { @@ -12981,6 +13509,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/ora": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", @@ -13017,6 +13562,8 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -13046,7 +13593,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "optional": true, + "devOptional": true, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -13061,7 +13608,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "optional": true, + "devOptional": true, "dependencies": { "p-limit": "^3.0.2" }, @@ -13097,15 +13644,27 @@ "node": ">=6" } }, - "node_modules/pako": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", - "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" - }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/parse-duration": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-1.1.0.tgz", - "integrity": "sha512-z6t9dvSJYaPoQq7quMzdEagSFtpGu+utzHqqxmpVWNNZRIXnvqyCvn9XsTdh7c/w0Bqmdz3RB3YnRaKtpRtEXQ==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-1.1.2.tgz", + "integrity": "sha512-p8EIONG8L0u7f8GFgfVlL4n8rnChTt8O5FSxgxMz2tjc9FMP199wxVKVB6IbKx11uTbKHACSvaLVIKNnoeNR/A==" }, "node_modules/parse-json": { "version": "4.0.0", @@ -13144,22 +13703,11 @@ "node": ">= 0.8" } }, - "node_modules/password-prompt": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/password-prompt/-/password-prompt-1.1.3.tgz", - "integrity": "sha512-HkrjG2aJlvF0t2BMH0e2LB/EHf3Lcq3fNMzy4GYHcQblAvOl+QQji1Lx7WRBMqpVK8p+KR7bCg7oqAMXtdgqyw==", - "optional": true, - "peer": true, - "dependencies": { - "ansi-escapes": "^4.3.2", - "cross-spawn": "^7.0.3" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "optional": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -13168,6 +13716,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -13176,8 +13725,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -13205,9 +13753,9 @@ "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==" }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "optional": true, "peer": true }, @@ -13409,6 +13957,40 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", @@ -13521,8 +14103,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "optional": true, - "peer": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -13532,8 +14112,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": ">=6" } @@ -13578,6 +14157,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "devOptional": true, "funding": [ { "type": "github", @@ -13591,8 +14171,7 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "optional": true + ] }, "node_modules/range-parser": { "version": "1.2.1", @@ -13608,8 +14187,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "optional": true, - "peer": true, "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -13624,8 +14201,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "optional": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -13669,9 +14244,9 @@ } }, "node_modules/react-devtools-core/node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "optional": true, "peer": true, "engines": { @@ -13781,9 +14356,9 @@ "peer": true }, "node_modules/react-native/node_modules/ws": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz", - "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", "optional": true, "peer": true, "dependencies": { @@ -13899,13 +14474,6 @@ "node": ">=4" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", - "optional": true, - "peer": true - }, "node_modules/regenerator-transform": { "version": "0.15.2", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", @@ -14051,6 +14619,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/resolve.exports": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", @@ -14091,7 +14668,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "optional": true, + "devOptional": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -14106,6 +14683,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "devOptional": true, "dependencies": { "glob": "^7.1.3" }, @@ -14128,6 +14706,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "devOptional": true, "funding": [ { "type": "github", @@ -14142,7 +14721,6 @@ "url": "https://feross.org/support" } ], - "optional": true, "dependencies": { "queue-microtask": "^1.2.2" } @@ -14339,9 +14917,9 @@ } }, "node_modules/sequelize": { - "version": "6.33.0", - "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.33.0.tgz", - "integrity": "sha512-GkeCbqgaIcpyZ1EyXrDNIwktbfMldHAGOVXHGM4x8bxGSRAOql5htDWofPvwpfL/FoZ59CaFmfO3Mosv1lDbQw==", + "version": "6.37.7", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.7.tgz", + "integrity": "sha512-mCnh83zuz7kQxxJirtFD7q6Huy6liPanI67BSlbzSYgVNl5eXVdE2CN1FuAeZwG1SNpGsNRCV+bJAVVnykZAFA==", "funding": [ { "type": "opencollective", @@ -14408,25 +14986,129 @@ } }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", "optional": true, "peer": true, "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "optional": true, + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-static/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "optional": true, + "peer": true + }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "optional": true, + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "optional": true, + "peer": true, + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "optional": true, + "peer": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/serve-static/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "optional": true, + "peer": true + }, + "node_modules/serve-static/node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "optional": true, + "peer": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" }, "engines": { "node": ">= 0.8.0" } }, + "node_modules/serve-static/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "optional": true, + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "optional": true }, "node_modules/set-function-length": { "version": "1.2.2", @@ -14489,8 +15171,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -14502,8 +15183,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": ">=8" } @@ -14536,6 +15216,49 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/simple-plist": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/simple-plist/-/simple-plist-1.3.1.tgz", @@ -14644,16 +15367,16 @@ } }, "node_modules/socks": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", - "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", "optional": true, "dependencies": { - "ip": "^2.0.0", + "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" }, "engines": { - "node": ">= 10.13.0", + "node": ">= 10.0.0", "npm": ">= 3.0.0" } }, @@ -14671,12 +15394,6 @@ "node": ">= 10" } }, - "node_modules/socks/node_modules/ip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", - "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==", - "optional": true - }, "node_modules/sorted-array-functions": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz", @@ -14744,13 +15461,14 @@ "peer": true }, "node_modules/sqlite3": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.6.tgz", - "integrity": "sha512-olYkWoKFVNSSSQNvxVUfjiVbz3YtBwTJj+mfV5zpHmqW3sELx2Cf4QCdirMelhM5Zh+KDVaKgQHqCxrqiWHybw==", + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", + "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", "hasInstallScript": true, "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.0", - "node-addon-api": "^4.2.0", + "bindings": "^1.5.0", + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", "tar": "^6.1.11" }, "optionalDependencies": { @@ -15049,6 +15767,18 @@ "node": ">=6" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strnum": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", @@ -15096,13 +15826,6 @@ "node": ">= 6" } }, - "node_modules/sudo-prompt": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-8.2.5.tgz", - "integrity": "sha512-rlBo3HU/1zAJUrkY6jNxDOC9eVYliG6nS4JA8u8KAshITd07tafMc/Br7xQwCSseXwJ2iCcHCE8SNWX3q8Z+kw==", - "optional": true, - "peer": true - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -15157,6 +15880,37 @@ "node": ">=10" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tar/node_modules/minipass": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", @@ -15310,8 +16064,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "optional": true, - "peer": true + "devOptional": true }, "node_modules/thenify": { "version": "3.3.1", @@ -15392,17 +16145,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -15410,16 +16152,6 @@ "optional": true, "peer": true }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "optional": true, - "peer": true, - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -15478,14 +16210,37 @@ "peer": true }, "node_modules/ts-mixer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.3.tgz", - "integrity": "sha512-k43M7uCG1AkTyxgnmI5MPwKoUvS/bRvLvUb7+Pgpdlmok8AoqmUaZxUUw8zKM5B1lqZrt41GjYgnvAi0fppqgQ==" + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", + "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==" }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } }, "node_modules/type-detect": { "version": "4.0.8", @@ -15497,6 +16252,18 @@ "node": ">=4" } }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", @@ -15660,6 +16427,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici": { + "version": "6.21.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", + "integrity": "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==", + "engines": { + "node": ">=18.17" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -15791,6 +16566,15 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/url-join": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.0.tgz", @@ -15813,7 +16597,6 @@ "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.3.tgz", "integrity": "sha512-uIuGf9TWQ/y+0Lp+KGZCMuJWc3N9BHA+l/UmHd/oUHwJJDeysyTRxNQVkbzsIWfGFbRe3OcgML/i0mvVRPOyDA==", "hasInstallScript": true, - "optional": true, "dependencies": { "node-gyp-build": "^4.3.0" }, @@ -15862,9 +16645,9 @@ } }, "node_modules/validator": { - "version": "13.11.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", - "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", + "version": "13.15.26", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz", + "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==", "engines": { "node": ">= 0.10" } @@ -15971,7 +16754,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "optional": true, + "devOptional": true, "dependencies": { "isexe": "^2.0.0" }, @@ -16065,6 +16848,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "optional": true, "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } @@ -16084,6 +16868,15 @@ "optional": true, "peer": true }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -16115,9 +16908,9 @@ } }, "node_modules/ws": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.1.tgz", - "integrity": "sha512-4OOseMUq8AzRBI/7SLMUwO+FEDnguetSk7KMb1sHwvF2w2Wv5Hoj0nlifx8vtGsftE/jWHojPy8sMMzYLJ2G/A==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "engines": { "node": ">=10.0.0" }, @@ -16260,7 +17053,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "optional": true, + "devOptional": true, "engines": { "node": ">=10" }, @@ -16273,7 +17066,6 @@ "resolved": "https://registry.npmjs.org/zlib-sync/-/zlib-sync-0.1.8.tgz", "integrity": "sha512-Xbu4odT5SbLsa1HFz8X/FvMgUbJYWxJYKB2+bqxJ6UOIIPaVGrqHEB3vyXDltSA6tTqBhSGYLgiVpzPQHYi3lA==", "hasInstallScript": true, - "optional": true, "dependencies": { "nan": "^2.17.0" } diff --git a/package.json b/package.json index 195944d8..ed3d2874 100644 --- a/package.json +++ b/package.json @@ -1,23 +1,23 @@ { "name": "customdcbot", - "version": "3.8.0", + "version": "3.1.1", "description": "Create your own discord bot - Fully customizable and with a lot of features", "main": "main.js", "repository": { "type": "git", - "url": "https://github.com/SCNetwork/CustomDCBot.git" + "url": "https://github.com/ScootKit/CustomDCBot.git" }, "scripts": { "start": "node main.js", + "test": "npx eslint ./", "generate-config": "node generate-config.js", "generate-template": "node generate-template.js" }, - "author": "ScootKit Team", + "author": "SC Network Team", "contributors": [ "SCDerox " ], - "funding": "https://github.com/ScootKit/CustomDCBot?sponsor=1", - "license": "SEE LICENSE IN LICENSE", + "license": "LicenseRef-LICENSE", "dependencies": { "@androz2091/discord-invites-tracker": "1.1.1", "@pixelfactory/privatebin": "2.6.1", @@ -26,10 +26,14 @@ "@twurple/auth": "5.3.4", "age-calculator": "1.0.0", "bs58": "5.0.0", + "bufferutil": "4.0.7", "centra": "2.6.0", + "discord-api-types": "0.38.37", "discord-logs": "2.2.1", - "discord.js": "13.17.1", + "discord.js": "14.25.1", "dotenv": "16.3.1", + "erlpack": "github:discord/erlpack", + "fparser": "3.1.0", "fs-extra": "11.1.1", "html-entities": "2.4.0", "is-equal": "1.6.4", @@ -37,15 +41,14 @@ "jsonfile": "6.1.0", "log4js": "6.9.1", "node-schedule": "2.1.1", - "parse-duration": "1.1.0", - "sequelize": "6.33.0", - "sqlite3": "5.1.6", - "stop-discord-phishing": "0.3.3" + "parse-duration": "1.1.2", + "sequelize": "6.37.7", + "sqlite3": "5.1.7", + "stop-discord-phishing": "0.3.3", + "utf-8-validate": "6.0.3", + "zlib-sync": "0.1.8" }, - "optionalDependencies": { - "erlpack": "github:discord/erlpack", - "bufferutil": "4.0.7", - "zlib-sync": "0.1.8", - "utf-8-validate": "6.0.3" + "devDependencies": { + "eslint": "8.49.0" } } \ No newline at end of file diff --git a/src/cli.js b/src/cli.js index fd6a2ecb..73c82b51 100644 --- a/src/cli.js +++ b/src/cli.js @@ -36,7 +36,7 @@ module.exports.commands = [ }).catch(async () => { if (inputElement.client.logChannel) await inputElement.client.logChannel.send('⚠️️ Configuration reloaded failed. Bot shutting down'); console.log('Reload failed. Exiting'); - process.exit(1); + process.exit(0); }); } }, diff --git a/src/commands/help.js b/src/commands/help.js index bee8af8a..4ba6d1e4 100644 --- a/src/commands/help.js +++ b/src/commands/help.js @@ -1,4 +1,10 @@ -const {truncate, formatDate, sendMultipleSiteButtonMessage, formatDiscordUserName} = require('../functions/helpers'); +const { + truncate, + formatDate, + sendMultipleSiteButtonMessage, + formatDiscordUserName, + parseEmbedColor +} = require('../functions/helpers'); const {MessageEmbed} = require('discord.js'); const {localize} = require('../functions/localize'); @@ -15,7 +21,7 @@ module.exports.run = async function (interaction) { const embedFields = []; for (const module in modules) { let content = ''; - if (module !== 'none') content = `*${(interaction.client.modules[module]['config']['description'][interaction.client.locale] || interaction.client.modules[module]['config']['description']['en'])}*\n`; + if (module !== 'none') content = `*${(interaction.client.modules[module]['config']['description'][interaction.client.locale] || interaction.client.modules[module]['config']['description']['en'])}*` + '\n'; for (let d of modules[module]) { content = content + `\n* \`/${d.name}\`: ${d.description}`; d = {...d}; @@ -59,13 +65,6 @@ module.exports.run = async function (interaction) { }); fields.push({ name: localize('help', 'bot-info-titel'), - - /* - *IMPORTANT WARNING: - *Changing or removing the license notice might be a violation of the Business Source License the bot was licensed under. - *Violating the license might lead to deactivation of your bot on Discord and legal action being taken against you. - *Please read the license carefully: https://github.com/ScootKit/CustomDCBot/blob/main/LICENSE - */ value: localize('help', 'bot-info-description', {g: interaction.guild.name}) }); } @@ -107,7 +106,7 @@ module.exports.run = async function (interaction) { */ function addSite(fields, atBeginning = false) { siteCount++; - const embed = new MessageEmbed().setColor('RANDOM') + const embed = new MessageEmbed().setColor(parseEmbedColor('GREEN')) .setDescription(interaction.client.strings.helpembed.description) .setThumbnail(interaction.client.user.avatarURL()) .setAuthor({name: formatDiscordUserName(interaction.user), iconURL: interaction.user.avatarURL()}) @@ -118,21 +117,6 @@ module.exports.run = async function (interaction) { else sites.push(embed); } - if (interaction.client.strings['putBotInfoOnLastSite']) sites[sites.length - 1].setFields(...sites[sites.length - 1].fields, { - name: '\u200b', - value: '\u200b' - }, { - name: localize('help', 'bot-info-titel'), - - /* - *IMPORTANT WARNING: - *Changing or removing the license notice might be a violation of the Business Source License the bot was licensed under. - *Violating the license might lead to deactivation of your bot on Discord and legal action being taken against you. - *Please read the license carefully: https://github.com/ScootKit/CustomDCBot/blob/main/LICENSE - */ - value: localize('help', 'bot-info-description', {g: interaction.guild.name}) - }); - sendMultipleSiteButtonMessage(interaction.channel, sites, [interaction.user.id], interaction); }; diff --git a/src/commands/reload.js b/src/commands/reload.js index cd6598e5..a7a26efb 100644 --- a/src/commands/reload.js +++ b/src/commands/reload.js @@ -14,7 +14,7 @@ module.exports.run = async function (interaction) { if (interaction.client.logChannel) interaction.client.logChannel.send('⚠️️ ' + localize('reload', 'reload-failed')).then(() => { }); await interaction.editReply({content: localize('reload', 'reload-failed-message', {reason})}); - process.exit(1); + process.exit(0); })).then(async (res) => { if (interaction.client.logChannel) interaction.client.logChannel.send('✅ ' + localize('reload', 'reloaded-config', res)).then(() => { }); diff --git a/src/discordjs-fix.js b/src/discordjs-fix.js new file mode 100644 index 00000000..ed5863f7 --- /dev/null +++ b/src/discordjs-fix.js @@ -0,0 +1,152 @@ +const Discord = require('discord.js'); + +const { + ActionRowBuilder, + AttachmentBuilder, + BaseInteraction, + ButtonBuilder, + ButtonStyle, + ComponentType, + EmbedBuilder, + GatewayIntentBits, + Guild, + InteractionResponse, + Message, + ModalBuilder, + MessagePayload, + Partials, + PermissionsBitField, + StringSelectMenuBuilder, + TextInputBuilder, + TextInputStyle +} = Discord; +const permissionNameMap = Object.fromEntries(Object.keys(Discord.PermissionFlagsBits || {}).map(k => [k.toUpperCase(), Discord.PermissionFlagsBits[k]])); + +Discord.MessageEmbed = EmbedBuilder; +Discord.MessageAttachment = AttachmentBuilder; +Discord.MessageActionRow = ActionRowBuilder; +Discord.MessageButton = ButtonBuilder; +Discord.MessageSelectMenu = StringSelectMenuBuilder; +Discord.TextInputComponent = TextInputBuilder; +Discord.Modal = ModalBuilder; +Discord.Permissions = PermissionsBitField; +Discord.Intents = {FLAGS: GatewayIntentBits}; +Discord.Partials = Partials; + +if (EmbedBuilder && !EmbedBuilder.prototype.addField) { + EmbedBuilder.prototype.addField = function (name, value, inline = false) { + return this.addFields({name, value, inline}); + }; +} + +const originalButtonSetStyle = ButtonBuilder.prototype.setStyle; +ButtonBuilder.prototype.setStyle = function (style) { + if (typeof style === 'string') { + const key = style.toUpperCase(); + style = ButtonStyle[key.charAt(0) + key.slice(1).toLowerCase()] || ButtonStyle[key] || style; + } + return originalButtonSetStyle.call(this, style); +}; + +const originalTextInputSetStyle = TextInputBuilder.prototype.setStyle; +TextInputBuilder.prototype.setStyle = function (style) { + if (typeof style === 'string') { + const key = style.toUpperCase(); + style = TextInputStyle[key.charAt(0) + key.slice(1).toLowerCase()] || TextInputStyle[key] || style; + } + return originalTextInputSetStyle.call(this, style); +}; + +if (BaseInteraction && !BaseInteraction.prototype.isSelectMenu) { + BaseInteraction.prototype.isSelectMenu = BaseInteraction.prototype.isStringSelectMenu || function () { + return false; + }; +} + +const normalizeComponentType = (type) => { + if (typeof type !== 'string') return type; + if (type === 'SELECT_MENU') return ComponentType.StringSelect; + if (type === 'STRING_SELECT') return ComponentType.StringSelect; + if (type === 'USER_SELECT') return ComponentType.UserSelect; + if (type === 'ROLE_SELECT') return ComponentType.RoleSelect; + if (type === 'MENTIONABLE_SELECT') return ComponentType.MentionableSelect; + if (type === 'CHANNEL_SELECT') return ComponentType.ChannelSelect; + if (type === 'TEXT_INPUT') return ComponentType.TextInput; + if (type === 'BUTTON') return ComponentType.Button; + if (type === 'ACTION_ROW') return ComponentType.ActionRow; + const pascal = type.charAt(0).toUpperCase() + type.slice(1).toLowerCase(); + return ComponentType[pascal] || ComponentType[type] || type; +}; + +const normalizeStyle = (style) => { + if (typeof style !== 'string') return style; + const up = style.toUpperCase(); + return ButtonStyle[up.charAt(0) + up.slice(1).toLowerCase()] || ButtonStyle[up] || TextInputStyle[up.charAt(0) + up.slice(1).toLowerCase()] || TextInputStyle[up] || style; +}; + +function normalizeComponents(components) { + if (!Array.isArray(components)) return components; + return components.map(comp => { + if (!comp || typeof comp !== 'object') return comp; + const newComp = {...comp}; + if (newComp.type) newComp.type = normalizeComponentType(newComp.type); + if (newComp.style) newComp.style = normalizeStyle(newComp.style); + if (newComp.components) newComp.components = normalizeComponents(newComp.components); + return newComp; + }); +} + +function normalizeMessageOptions(options) { + if (!options || typeof options !== 'object') return options; + const cloned = {...options}; + if (cloned.components) cloned.components = normalizeComponents(cloned.components); + if (cloned.embeds && Array.isArray(cloned.embeds)) { + cloned.embeds = cloned.embeds.map(e => e?.data ? e : (e instanceof EmbedBuilder ? e : new EmbedBuilder(e))); + } + return cloned; +} + +if (MessagePayload && MessagePayload.create) { + const originalMessagePayloadCreate = MessagePayload.create; + MessagePayload.create = function (...args) { + if (args[1]) args[1] = normalizeMessageOptions(args[1]); + return originalMessagePayloadCreate.apply(this, args); + }; +} + +const originalResolve = PermissionsBitField.resolve; +PermissionsBitField.resolve = function (permission, ...args) { + if (typeof permission === 'string') { + const upper = permission.toUpperCase(); + if (permissionNameMap[upper]) permission = permissionNameMap[upper]; + else { + const pascal = permission.toLowerCase().split('_').map(p => p.charAt(0).toUpperCase() + p.slice(1)).join(''); + if (Discord.PermissionFlagsBits && Discord.PermissionFlagsBits[pascal]) permission = Discord.PermissionFlagsBits[pascal]; + } + } + return originalResolve.call(this, permission, ...args); +}; + +function patchCollector(target) { + if (!target || !target.prototype || !target.prototype.createMessageComponentCollector) return; + const original = target.prototype.createMessageComponentCollector; + target.prototype.createMessageComponentCollector = function (options = {}) { + if (options.componentType) options.componentType = normalizeComponentType(options.componentType); + return original.call(this, options); + }; +} + +patchCollector(Message); +patchCollector(InteractionResponse); + +if (Guild && !Object.getOwnPropertyDescriptor(Guild.prototype, 'me')) { + Object.defineProperty(Guild.prototype, 'me', { + get() { + return this.members.me; + } + }); +} + +require.cache[require.resolve('discord.js')].exports = Discord; + +module.exports = Discord; \ No newline at end of file diff --git a/src/events/botReady.js b/src/events/botReady.js index 987d3ca8..32be5b4e 100644 --- a/src/events/botReady.js +++ b/src/events/botReady.js @@ -1,4 +1,6 @@ module.exports.run = async (client) => { + await client.guild.members.fetch({withPresences: true}).catch(() => { + }); if (client.config.disableStatus) client.user.setActivity(null); else await client.user.setActivity(client.config.user_presence); }; \ No newline at end of file diff --git a/src/events/interactionCreate.js b/src/events/interactionCreate.js index 631d7def..49cbe0cb 100644 --- a/src/events/interactionCreate.js +++ b/src/events/interactionCreate.js @@ -9,6 +9,7 @@ module.exports.run = async (client, interaction) => { ephemeral: true }); } + if (!interaction.guild) return; if (client.guild.id !== interaction.guild.id) { if (interaction.isAutocomplete()) return interaction.respond({}); return interaction.reply({ @@ -25,10 +26,13 @@ module.exports.run = async (client, interaction) => { if (client.scnxSetup) return require('./../functions/scnx-integration').customCommandSlashInteraction(interaction); else return interaction.reply({content: '⚠️ ' + localize('command', 'not-found'), ephemeral: true}); } - if (command.module && !client.modules[command.module].enabled) return interaction.reply({ - ephemeral: true, - content: '⚠️ ' + localize('command', 'module-disabled', {m: command.module}) - }); + if (command.module && !client.modules[command.module].enabled) { + if (client.scnxSetup) return require('./../functions/scnx-integration').customCommandSlashInteraction(interaction); + else return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('command', 'module-disabled', {m: command.module}) + }); + } if (command && typeof (command || {}).options === 'function') command.options = await command.options(interaction.client); const group = interaction.options['_group']; const subCommand = interaction.options['_subcommand']; @@ -63,9 +67,10 @@ module.exports.run = async (client, interaction) => { } if (!interaction.isCommand()) return; if (command.restricted === true && !client.config.botOperators.includes(interaction.user.id)) return interaction.reply(embedType(client.strings.not_enough_permissions)); + client.logger.debug(localize('command', 'used', { - tag: formatDiscordUserName(interaction.user), - id: interaction.user.id, + tag: command.forceAnonymous ? '????????????' : formatDiscordUserName(interaction.user), + id: command.forceAnonymous ? 'Hidden Anonymous User' : interaction.user.id, c: command.name + `${group ? ' ' + group : ''}${subCommand ? ' ' + subCommand : ''}` })); @@ -83,7 +88,8 @@ module.exports.run = async (client, interaction) => { else await command.subcommands[subCommand](interaction); if (command.run) await command.run(interaction); } catch (e) { - if (client.captureException) client.captureException(e, { + let traceID = null; + if (client.captureException) traceID = client.captureException(e, { command: command.name, module: command.module, group, @@ -93,16 +99,26 @@ module.exports.run = async (client, interaction) => { interaction.client.logger.error(localize('command', 'execution-failed', { e, c: command.name, + t: traceID || '*Not reportable*', g: group || '', s: subCommand || '' })); if (!interaction.deferred) { interaction.reply({ - content: localize('command', 'execution-failed-message', {e}), + content: localize('command', 'execution-failed-message', { + e, + c: command.name, + t: traceID || '*Not reportable*', + g: group || '', + s: subCommand || '' + }), ephemeral: true }).catch(() => { }); - } else await interaction.editReply(localize('command', 'execution-failed-message')).catch(() => { + } else await interaction.editReply(localize('command', 'execution-failed-message', { + e, + t: traceID || '*Not reportable*' + })).catch(() => { }); } }; diff --git a/src/functions/configuration.js b/src/functions/configuration.js index 0b585c89..178132b8 100644 --- a/src/functions/configuration.js +++ b/src/functions/configuration.js @@ -5,10 +5,23 @@ */ const jsonfile = require('jsonfile'); const fs = require('fs'); -const {logger, client} = require('../../main'); +const {ChannelType} = require('discord.js'); +const { + logger, + client +} = require('../../main'); const {localize} = require('./localize'); const isEqual = require('is-equal'); +const channelTypeMap = { + GUILD_TEXT: ChannelType.GuildText, + GUILD_CATEGORY: ChannelType.GuildCategory, + GUILD_NEWS: ChannelType.GuildAnnouncement, + GUILD_VOICE: ChannelType.GuildVoice, + GUILD_FORUM: ChannelType.GuildForum, + GUILD_STAGE_VOICE: ChannelType.GuildStageVoice +}; + /** * Check every (including module) configuration and load them * @author Simon Csaba @@ -19,7 +32,7 @@ const isEqual = require('is-equal'); async function loadAllConfigs(client) { logger.info(localize('config', 'checking-config')); return new Promise(async (resolve, reject) => { - await fs.readdir(`${__dirname}/../../config-generator`, async (err, files) => { + fs.readdir(`${__dirname}/../../config-generator`, async (err, files) => { for (const f of files) { await checkConfigFile(f).catch((reason) => { logger.error(reason); @@ -94,15 +107,21 @@ async function checkConfigFile(file, moduleName) { const objectData = {}; for (const field of exampleFile.content) { const dependsOnField = field.dependsOn ? exampleFile.content.find(f => f.name === field.dependsOn) : null; + const dependsOnNotField = field.dependsOnNot ? exampleFile.content.find(f => f.name === field.dependsOnNot) : null; if (field.dependsOn && !dependsOnField) return reject(`Depends-On-Field ${field.dependsOn} does not exist.`); + if (field.dependsOnNot && !dependsOnNotField) return reject(`Depends-On-Field ${field.dependsOnNotField} does not exist.`); if (dependsOnField && !(typeof object[dependsOnField.name] === 'undefined' ? (dependsOnField.default[client.locale] || dependsOnField.default['en']) : object[dependsOnField.name])) { objectData[field.name] = configData[field.name] || (field.default[client.locale] || field.default['en']); // Otherwise disabled fields may be overwritten continue; } + if (dependsOnNotField && (typeof object[dependsOnNotField.name] === 'undefined' ? (dependsOnNotField.default[client.locale] || dependsOnNotField.default['en']) : object[dependsOnNotField.name])) { + objectData[field.name] = configData[field.name] || (field.default[client.locale] || field.default['en']); // Otherwise disabled fields may be overwritten + continue; + } try { objectData[field.name] = await checkField(field, object[field.name]); } catch (e) { - return reject(e); + reject(e); } } newConfig.push(objectData); @@ -122,7 +141,8 @@ async function checkConfigFile(file, moduleName) { try { newConfig[field.name] = await checkField(field, configData[field.name]); } catch (e) { - return reject(e); + if (field.name === 'logChannelID' && builtIn && file === 'config') newConfig[field.name] = null; + else return reject(e); } } } @@ -148,7 +168,7 @@ async function checkConfigFile(file, moduleName) { return res(fieldValue); } else if (field.type === 'keyed' && field.disableKeyEdits) for (const key in field.default) if (typeof fieldValue[key] === 'undefined') fieldValue[key] = field.default[key]; if (field.allowNull && field.type !== 'boolean' && !fieldValue) return res(fieldValue); - if (!await checkType(field.type, fieldValue, field.content, field.allowEmbed)) { + if (!await checkType(field, fieldValue)) { if (client.scnxSetup) await require('./scnx-integration').reportIssue(client, { type: 'CONFIGURATION_ISSUE', module: moduleName, @@ -219,33 +239,35 @@ module.exports.loadAllConfigs = loadAllConfigs; /** * Check type of one field - * @param {FieldType} type Type of the field + * @param {ConfigField} field Full field value * @param {String} value Value in the configuration file - * @param {ConfigFormat} contentFormat Format of the content - * @param {Boolean} allowEmbed If embeds are allowed * @returns {Promise} * @private */ -async function checkType(type, value, contentFormat = null, allowEmbed = false) { +async function checkType(field, value) { const {client} = require('../../main'); - switch (type) { + switch (field.type) { case 'integer': if (parseInt(value) === 0) return true; + if (field.maxValue && parseInt(value) > field.maxValue) return false; + if (field.minValue && parseInt(value) < field.minValue) return false; return !!parseInt(value); case 'float': if (parseFloat(value) === 0) return true; + if (field.maxValue && parseFloat(value) > field.maxValue) return false; + if (field.minValue && parseFloat(value) < field.minValue) return false; return !!parseFloat(value); case 'string': case 'emoji': case 'imgURL': case 'timezone': // Timezones can not be checked correctly for their type currently. - if (allowEmbed && typeof value === 'object') return true; + if (field.allowEmbed && typeof value === 'object') return true; return typeof value === 'string'; case 'array': if (!Array.isArray(value)) return false; let errored = false; for (const v of value) { - if (!errored) errored = !(await checkType(contentFormat, v, null, allowEmbed)); + if (!errored) errored = !(await checkType({type: field.content}, v)); } return !errored; case 'userID': @@ -267,7 +289,8 @@ async function checkType(type, value, contentFormat = null, allowEmbed = false) logger.error(localize('config', 'channel-not-on-guild', {id: value})); return false; } - if (!(contentFormat || ['GUILD_TEXT', 'GUILD_CATEGORY', 'GUILD_NEWS', 'GUILD_VOICE', 'GUILD_STAGE_VOICE']).includes(channel.type)) { + const allowedTypes = (field.content || ['GUILD_TEXT', 'GUILD_CATEGORY', 'GUILD_NEWS', 'GUILD_VOICE', 'GUILD_STAGE_VOICE']).map(t => typeof t === 'string' ? (channelTypeMap[t] !== undefined ? channelTypeMap[t] : t) : t); + if (!allowedTypes.includes(channel.type)) { logger.error(localize('config', 'channel-invalid-type', {id: value})); return false; } @@ -291,18 +314,19 @@ async function checkType(type, value, contentFormat = null, allowEmbed = false) let returnValue = true; for (const v in value) { if (returnValue) { - returnValue = await checkType(contentFormat.key, v); - returnValue = await checkType(contentFormat.value, value[v]); + returnValue = await checkType({type: field.content.key}, v); + returnValue = await checkType({type: field.content.value}, value[v]); } } return returnValue; case 'select': - return contentFormat.includes(value); + return typeof field.content[0] !== 'string' ? field.content.find(f => f.value === value) : field.content.includes(value); case 'boolean': return typeof value === 'boolean'; default: - logger.error(`Unknown type: ${type}`); - process.exit(1); + logger.error(`Unknown type: ${field.type}`); + process.exit(0); + ; } } diff --git a/src/functions/helpers.js b/src/functions/helpers.js index f42f2fd4..94e140fa 100644 --- a/src/functions/helpers.js +++ b/src/functions/helpers.js @@ -3,10 +3,10 @@ * @module Helpers */ -const {MessageEmbed, MessageAttachment} = require('discord.js'); +const {ChannelType, ComponentType, MessageEmbed, MessageAttachment, PermissionFlagsBits} = require('discord.js'); const {localize} = require('./localize'); const {PrivatebinClient} = require('@pixelfactory/privatebin'); -const privatebin = new PrivatebinClient('https://paste.scootkit.net'); +const privatebin = new PrivatebinClient('https://paste.scootkit.com'); const isoCrypto = require('isomorphic-webcrypto'); const {encode} = require('bs58'); const crypto = require('crypto'); @@ -59,6 +59,50 @@ function inputReplacer(args, input, returnNull = false) { module.exports.inputReplacer = inputReplacer; +const colors = { + 'YELLOW': 0xF1C40F, + 'GREEN': 0x2ECC71, + 'GOLD': 0xF1C40F, + 'PURPLE': 0x9B59B6, + 'LUMINOUS_VIVID_PINK': 0xE91E63, + 'FUCHSIA': 0xEB459E, + 'ORANGE': 0xE67E22, + 'DARK_AQUA': 0x11806A, + 'DARK_GREEN': 0x1F8B4C, + 'DARK_BLUE': 0x206694, + 'DARK_VIVID_PINK': 0xAD1457, + 'LIGHT_GREY': 0xBCC0C0, + 'GREYPLE': 0x99AAB5, + 'DARK_BUT_NOT_BLACK': 0x2C2F33, + 'NOT_QUITE_BLACK': 0x23272A, + 'DARK_NAVY': 0x2C3E50, + 'DARK_GOLD': 0xC27C0E, + 'DARK_RED': 0x992D22, + 'DARKER_GREY': 0x7F8C8D, + 'DARK_GREY': 0x979C9F, + 'DARK_ORANGE': 0xA84300, + 'DARK_PURPLE': 0x71368A, + 'GREY': 0x95A5A6, + 'NAVY': 0x34495E, + 'BLURPLE': 0x5865F2, + 'BLUE': 0x3498DB, + 'AQUA': 0x1ABC9C, + 'WHITE': 0xFFFFFF, + 'RED': 0xE74C3C +}; + +function parseColor(color) { + if (colors[color]) return colors[color]; + if (typeof color === 'number') return color; + if (typeof color === 'string') { + if (color.startsWith('#')) return parseInt(color.replaceAll('#', ''), 16); + return parseInt(color, 16); + } + return color; +} + +module.exports.parseEmbedColor = parseColor; + /** * Will turn an object or string into embeds * @param {string|array} input Input in the configuration file @@ -99,10 +143,10 @@ function embedType(input, args = {}, optionsToKeep = {}, mergeComponentsRows = [ const embed = new MessageEmbed({ title: inputReplacer(args, embedData.title, true), description: inputReplacer(args, embedData.description, true), - color: embedData.color, + color: parseColor(embedData.color), thumbnail: embedData.thumbnailURL ? {url: inputReplacer(args, embedData.thumbnailURL)} : null, image: embedData.imageURL ? {url: inputReplacer(args, embedData.imageURL)} : null, - timestamp: (embedData.footer?.hideTime || embedData.footer?.disabled) ? null : new Date(), + timestamp: (embedData.footer?.hideTime || embedData.footer?.disabled || client.strings.disableFooterTimestamp) ? null : new Date(), author: embedData.author?.name ? { name: inputReplacer(args, embedData.author.name), iconURL: inputReplacer(args, embedData.author.imageURL, null), @@ -119,6 +163,7 @@ function embedType(input, args = {}, optionsToKeep = {}, mergeComponentsRows = [ optionsToKeep.files.push({attachment: url}); } + if (optionsToKeep.components) optionsToKeep.components = optionsToKeep.components.map(c => (typeof c.toJSON === 'function' ? c.toJSON() : c)); // polyfill for djs migration if (!optionsToKeep.components && client.scnxSetup) optionsToKeep.components = require('./scnx-integration').returnSCNXComponents(input, mergeComponentsRows, args); if (!optionsToKeep.content) optionsToKeep.content = inputReplacer(args, input['content'], true); @@ -135,7 +180,7 @@ function embedTypeSchemaV2(input, args = {}, optionsToKeep = {}, mergeComponents const emb = new MessageEmbed(); if (input['title']) emb.setTitle(inputReplacer(args, input['title'])); if (input['description']) emb.setDescription(inputReplacer(args, input['description'])); - if (input['color']) emb.setColor(input['color']); + if (input['color']) emb.setColor(parseColor(input['color'])); if (input['url']) emb.setURL(input['url']); if ((input['image'] || '').replaceAll(' ', '')) emb.setImage(inputReplacer(args, input['image'])); if ((input['thumbnail'] || '').replaceAll(' ', '')) emb.setThumbnail(inputReplacer(args, input['thumbnail'])); @@ -207,7 +252,7 @@ async function postToSCNetworkPaste(content, opts = { }) { const key = isoCrypto.getRandomValues(new Uint8Array(32)); const res = await privatebin.sendText(content, key, opts); - return `https://paste.scootkit.net${res.url}#${encode(key)}`; + return `https://paste.scootkit.com${res.url}#${encode(key)}`; } module.exports.postToSCNetworkPaste = postToSCNetworkPaste; @@ -306,7 +351,7 @@ async function sendMultipleSiteButtonMessage(channel, sites = [], allowedUserIDs fetchReply: true }); else m = await channel.send({components: [{type: 'ACTION_ROW', components: getButtons(1)}], embeds: [sites[0]]}); - const c = m.createMessageComponentCollector({componentType: 'BUTTON', time: 60000}); + const c = m.createMessageComponentCollector({componentType: ComponentType.Button, time: 60000}); let currentSite = 1; c.on('collect', async (interaction) => { if (!allowedUserIDs.includes(interaction.user.id)) return interaction.reply({ @@ -453,29 +498,37 @@ module.exports.dateToDiscordTimestamp = dateToDiscordTimestamp; async function lockChannel(channel, allowedRoles = [], reason = localize('main', 'channel-lock')) { const dup = await channel.client.models['ChannelLock'].findOne({where: {id: channel.id}}); if (dup) await dup.destroy(); - await channel.client.models['ChannelLock'].create({ - id: channel.id, - lockReason: reason, - permissions: Array.from(channel.permissionOverwrites.cache.values()) - }); - for (const overwrite of channel.permissionOverwrites.cache.filter(e => e.allow.has('SEND_MESSAGES')).values()) { - await overwrite.edit({ - SEND_MESSAGES: false, - SEND_MESSAGES_IN_THREADS: false - }, reason); - } - const everyoneRole = await channel.guild.roles.cache.find(r => r.name === '@everyone'); - if (channel.permissionsFor(everyoneRole).has('VIEW_CHANNEL')) await channel.permissionOverwrites.create(everyoneRole, { - SEND_MESSAGES: false, - SEND_MESSAGES_IN_THREADS: false - }, {reason}); + if (channel.type === ChannelType.PublicThread || channel.type === ChannelType.PrivateThread) { + await channel.setLocked(true, reason); + } else { + await channel.client.models['ChannelLock'].create({ + id: channel.id, + lockReason: reason, + permissions: Array.from(channel.permissionOverwrites.cache.values()) + }); - for (const roleID of allowedRoles) { - await channel.permissionOverwrites.create(roleID, { - SEND_MESSAGES: true + for (const overwrite of channel.permissionOverwrites.cache.filter(e => e.allow.has(PermissionFlagsBits.SendMessages)).values()) { + if (overwrite.type === 'role' && channel.client.guild.members.me.roles.botRole?.id === overwrite.id) continue; + if (overwrite.type === 'member' && channel.client.user.id === overwrite.id) continue; + await overwrite.edit({ + SendMessages: false, + SendMessagesInThreads: false + }, reason); + } + + const everyoneRole = await channel.guild.roles.cache.find(r => r.name === '@everyone'); + if (channel.permissionsFor(everyoneRole).has(PermissionFlagsBits.ViewChannel)) await channel.permissionOverwrites.create(everyoneRole, { + SendMessages: false, + SendMessagesInThreads: false }, {reason}); + + for (const roleID of allowedRoles) { + await channel.permissionOverwrites.create(roleID, { + SendMessages: true + }, {reason}); + } } } @@ -487,8 +540,12 @@ async function lockChannel(channel, allowedRoles = [], reason = localize('main', */ async function unlockChannel(channel, reason = localize('main', 'channel-unlock')) { const item = await channel.client.models['ChannelLock'].findOne({where: {id: channel.id}}); - if (item && (item || {}).permissions) await channel.permissionOverwrites.set(item.permissions, reason); - else channel.client.logger.error(localize('main', 'channel-unlock-data-not-found', {c: channel.id})); + if (channel.type === ChannelType.PublicThread || channel.type === ChannelType.PrivateThread) { + await channel.setLocked(false, reason); + } else { + if (item && (item || {}).permissions) await channel.permissionOverwrites.set(item.permissions, reason); + else channel.client.logger.error(localize('main', 'channel-unlock-data-not-found', {c: channel.id})); + } } module.exports.lockChannel = lockChannel; @@ -549,7 +606,7 @@ module.exports.disableModule = disableModule; */ module.exports.formatNumber = function (number) { if (typeof number === 'string') number = parseInt(number); - return new Intl.NumberFormat(client.locale, {}).format(number); + return new Intl.NumberFormat(client.locale.split('_')[0], {}).format(number); }; /** @@ -559,4 +616,13 @@ module.exports.formatNumber = function (number) { */ module.exports.hashMD5 = function (string) { return crypto.createHash('md5').update(string).digest('hex'); -}; \ No newline at end of file +}; + +module.exports.shuffleArray = function (input) { + const array = [...input]; + for (let i = array.length - 1; i >= 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; +} \ No newline at end of file diff --git a/src/functions/localize.js b/src/functions/localize.js index b241f742..5b335eda 100644 --- a/src/functions/localize.js +++ b/src/functions/localize.js @@ -4,7 +4,7 @@ */ const {client} = require('../../main'); const jsonfile = require('jsonfile'); -const fs = require('fs'); +const fs = require('fs') const locals = {}; loadLocale('en'); @@ -17,7 +17,7 @@ loadLocale('en'); function loadLocale(locale) { if (locals[locale]) return; if (!fs.existsSync(`${__dirname}/../../locales/${locale}.json`)) locale = 'en'; - locals[locale] = jsonfile.readFileSync(`${__dirname}/../../locales/${locale}.json`); + locals[locale] = jsonfile.readFileSync(`${__dirname}/../../locales/${locale}.json`) } /** From bcf08e680c55cf93902e73e367350e47d4cea7d1 Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 9 Jan 2026 17:47:04 +0100 Subject: [PATCH 02/27] fixed missing strings --- locales/en.json | 257 +++++++++++++++++------------------------------- 1 file changed, 92 insertions(+), 165 deletions(-) diff --git a/locales/en.json b/locales/en.json index 4cf4437e..b23bec45 100644 --- a/locales/en.json +++ b/locales/en.json @@ -6,23 +6,24 @@ "login-error": "Bot could not log in. Error: %e", "login-error-token": "Bot could not log in because the provided token is invalid. Please update your token.", "login-error-intents": "Bot could not log in because the intents were not enabled correctly. Please enable \"PRESENCE INTENT\", \"SERVER MEMBERS INTENT\" and \"MESSAGE CONTENT INTENT\" in your Discord-Developer-Dashboard: %url", - "not-invited": "Please invite the bot to your guild before continuing: %inv", + "not-invited": "Please invite the bot to your Discord server before continuing: %inv", "require-code-grant-active": "You might be unable to invite your bot to your server as you have enabled the \"Require public code grant\" option in your Discord Developer Dashboard. Please disable this option: %d", + "interactions-endpoint-active": "You bot will be unable to respond to interactions, because the field \"Interactions Endpoint URL\" has a value in your Discord Developer Dashboard. Please remove any content from this field and restart your bot: %d", "logged-in": "Bot logged in as %tag and is now online.", "logchannel-wrong-type": "There is no Log-Channel set or it has the wrong type (only text-channels are supported).", "config-check-failed": "Configuration-Check failed. You can find more information in your log. The bot exited.", "bot-ready": "The bot initiated successfully and is now listening to commands", - "no-command-permissions": "Could not update guild commands. Please give us permissions to performe this critical action: %inv", + "no-command-permissions": "Could not update server commands. Please give us permissions to performe this critical action: %inv", "perm-sync": "Synced permissions for /%c", "perm-sync-failed": "Failed to synced permissions for /%c: %e", "loading-module": "Loading module %m", - "hidden-module": "Module %m is hidden, meaning that it is not availible. Skipping…", + "hidden-module": "Module %m is hidden, meaning that it is not available. Skipping…", "module-disabled": "Module %m is disabled", "command-loaded": "Loaded command %d/%f", "command-dir": "Loading commands in %d/%f", "global-command-sync": "Synced global application commands", - "guild-command-sync": "Synced guild application commands", - "guild-command-no-sync-required": "Guild application commands are up to date - no syncing required", + "guild-command-sync": "Synced server application commands", + "guild-command-no-sync-required": "Server application commands are up to date - no syncing required", "global-command-no-sync-required": "Global application commands are up to date - no syncing required", "event-loaded": "Loaded events %d/%f", "event-dir": "Loading events in %d/%f", @@ -55,9 +56,9 @@ "moduleconf-regeneration-success": "Module configuration regeneration successfully finished.", "channel-not-found": "Channel with ID \"%id\" could not be found", "user-not-found": "User with ID \"%id\" could not be found", - "channel-not-on-guild": "Channel with ID \"%id\" is not on your guild", + "channel-not-on-guild": "Channel with ID \"%id\" is not on your server", "channel-invalid-type": "Channel with ID \"%id\" has a type that can not be used for this field", - "role-not-found": "Role with ID \"%id\" could not be found on your guild", + "role-not-found": "Role with ID \"%id\" could not be found on your server", "config-reload": "Reloading all configuration..." }, "helpers": { @@ -73,12 +74,13 @@ "not-found": "Command not found", "used": "%tag (%id) used command /%c", "message-used": "%tag (%id) used command %p%c", - "execution-failed": "Execution of command /%c %g %s failed: %e", - "message-execution-failed": "Execution of command %p%c failed: %e", + "execution-failed": "Execution of command /%c %g %s failed (Tracing: %t): %e", + "message-execution-failed": "Execution of command %p%c failed (Tracing: %t): %e", "wrong-guild": "This command is only available on the server **%g**.", "autcomplete-execution-failed": "Execution of auto-complete on command /%c %g %s with option %f failed: %e", - "execution-failed-message": "## \uD83D\uDD34 Command execution failed \uD83D\uDD34\nThis usually happens either due to misconfiguration or due to an error on our site. Please send a screenshot of this message in #support on the [ScootKit Discord](https://scootk.it/dc-en), to resolve this.\n\n### Debugging-Information\n```%e```", - "error-giving-role": "An error occurred when trying to giving to give your your roles ):", + "execution-failed-message": "## \uD83D\uDD34 Command execution failed \uD83D\uDD34\nThis usually happens either due to misconfiguration or due to an error on our site. Please send a screenshot of this message in #support on the [ScootKit Discord](https://scootk.it/dc-en), to resolve this.\n\n### Internal Tracing ID\n`%t`\n### Debugging-Information\n```%e```", + "error-giving-role": "An error occurred when trying to giving to give you your roles ):", + "description-too-long": "The following command description of %c was too long to sync: %s", "module-disabled": "This command is part of the \"%m\" which is disabled. This can either be intended by the server-admins (and slash-commands haven't synced yet) or this could be caused by a configuration error. Please check (or ask the admins) to check the bot's configuration and logs for details." }, "help": { @@ -91,7 +93,7 @@ }, "bot-feedback": { "command-description": "Send feedback about the bot to the bot developer", - "submitted-successfully": "Thanks so much for your feedback! It has been carefully recorded and our team will review it soon. If we have any questions, we may contact you via DM (or if you are on our [Support Server]() we'll open a ticket). Thank you for making [CustomBots]() better for everyone <3\n\nYour feedback is subject to our [Terms of service]() and [Privacy Policy]().", + "submitted-successfully": "Thanks so much for your feedback! It has been carefully recorded and our team will review it soon. If we have any questions, we may contact you via DM (or if you are on our [Support Server]() we'll open a ticket). Thank you for making [CustomBots]() better for everyone <3\n\nYour feedback is subject to our [Terms of service]() and [Privacy Policy]().", "failed-to-submit": "Sorry, but I couldn't send your feedback to our staff. This could be, because you got blocked or because of some server issue we are having. You can always report bugs and submit feedback in our [Feature-Board](https://features.sc-network.net). Thank you.", "feedback-description": "Your feedback. Make sure it's neutral, constructive and helpful" }, @@ -112,7 +114,37 @@ "emoji-too-much-data": "Please **only** enter one emoji and nothing else", "emoji-import": "Imported \"%e\" successfully.", "stealemote-description": "Steals a emote from another server", - "emote-description": "Emote to steal" + "emote-description": "Emote to steal", + "role-command-description": "Assign or remove roles permanently or temporarily", + "role-give-description": "Assign someone a role permanently or temporarily", + "role-user-add-description": "Member that you want to assign the role to", + "role-add-role-description": "Role you want to assign to the member", + "role-add-duration-description": "If you set this parameter, the role will be removed from this user after this duration expires", + "role-user-status-description": "User you want to see temporary roles from", + "role-remove-description": "Remove a role from someone permanently or temporarily", + "role-user-remove-description": "Member that you want to remove the role from", + "role-remove-role-description": "Role you want to remove from the member", + "role-remove-duration-description": "If you set this parameter, the role will be added back to this user after this duration expires", + "role-status-description": "Shows which roles of a user are temporary and when they will be removed", + "role-not-high-enough": "The highest role of the bot is not above %e. The highest role of the bot needs to be above the role you want to remove or assign.", + "unable-to-change-roles": "Changing role %r to %u failed. Error message obtained by Discord:\n```%e```", + "user-not-found": "The user has not been found on your server.", + "duration-wrong": "The value of the duration argument is wrong. Learn more [in our docs]()", + "audit-log-add": "[admin-tools] %u added a role using a command.", + "audit-log-remove": "[admin-tools] %u removed a role using a command.", + "audit-log-add-duration": "[admin-tools] %u added a temporary role using a command that will be removed at %t.", + "audit-log-remove-duration": "[admin-tools] %u removed a temporary role using a command that will be added back at %t.", + "audit-log-temporary-remove": "[admin-tools] This role was added temporarily and has removed since the temporary timeframe expired.", + "audit-log-temporary-add": "[admin-tools] This role has been removed temporarily and has been added back since the temporary timeframe expired.", + "role-add": "%u has been given the role %r.", + "role-remove": "%u has removed the role %r.", + "role-add-duration": "%u has been given the role %r. It will be removed at %t.", + "role-remove-duration": "%r has been removed from %u. It will be given back at %t.", + "user-without-temporary-action": "%u has no roles that are temporary.", + "user-temporary-action-header": "Temporary roles of %u", + "status-remove": "%r will be removed on %t.", + "status-add": "%r will be added back on %t.", + "users-trying-to-manage-higher-role": "Your highest role, %t, is not below %e. To manage a user's role, you the role you are managing needs to be below your highest role." }, "welcomer": { "channel-not-found": "[welcomer] Channel not found: %c", @@ -148,7 +180,7 @@ "sync-command-action-disable-description": "Disable synchronization", "set-command-description": "Sets your birthday", "set-command-day-description": "Day of your birthday", - "set-command-month-description": "Day of your birthday", + "set-command-month-description": "Month of your birthday", "set-command-year-description": "Year of your birthday", "delete-command-description": "Deletes your birthday from this server", "migration-happening": "Database-Schema not up-to-date. Migration database... This could take a while. Do not restart your bot to avoid data loss.", @@ -168,56 +200,16 @@ "11": "November", "12": "December" }, - "giveaways": { - "no-link": "None", - "no-winners": "None", - "not-supported-for-news-channel": "Not supported for news-channels", - "leave-giveaway": "❌ Leave giveaway", - "giveaway-left": "You have successfully left this giveaway 👍", - "required-messages": "Must have %mc new messages (check with `/gmessages`)", - "required-messages-user": "Have at least %mc new messages (%um/%mc messages)", - "roles-required": "Must have one of this roles to enter: %r", - "giveaway-ended-successfully": "Giveaway ended successfully.", - "no-giveaways-found": "No giveaways found", - "gmessages-description": "See your messages for a giveaway", - "jump-to-message-hover": "Jump to message", - "messages": "Nachrichten", - "giveaway-messages": "Giveaway-Messages", - "duration-parsing-failed": "Duration-Parsing failed.", - "channel-type-not-supported": "Channel-Type not supported", - "parameter-parsing-failed": "Parsing of parameters failed", - "started-successfully": "Started giveaway successfully in %c.", - "reroll-done": "Done :+1:", - "select-menu-description": "Will end in #%c on %d", - "no-giveaways-for-reroll": "They are no currently running giveaways. Maybe you are looking for /reroll?", - "select-giveaway-to-end": "Please select the giveaway which you want to end.", - "please-select": "Please select", - "gmanage-description": "Manage giveaways", - "gmanage-start-description": "Start a new giveaway", - "gmanage-channel-description": "Channel to start the giveaway in", - "gmanage-price-description": "Price that can be won", - "gmanage-duration-description": "Duration of the giveaway (e.g: \"2h 40m\" or \"7d 2h 3m\")", - "gmanage-winnercount-description": "Count of winners that should be selected", - "gmanage-requiredmessages-description": "Count of new (!) messages that a user needs to have before entering", - "gmanage-requiredroles-description": "Role that user need to have to enter the giveaway", - "gmanage-sponsor-description": "Sets a different giveaway-starter, useful if you have a sponsor", - "gmanage-sponsorlink-description": "Link to a sponsor if applicable", - "gend-description": "End a giveaway", - "gereroll-description": "Rerolls an ended giveaway", - "gereroll-msgid-description": "Message-ID of the giveaway", - "gereroll-winnercount-description": "How many new winners there should be", - "migration-happening": "Database not up-to-date. Migrating database...", - "migration-done": "Migrated database successfully." - }, "levels": { "leaderboard-channel-not-found": "Leaderboard-Channel not found or wrong type", "leaderboard-notation": "%p. %u: Level %l - %xp XP", + "list-location": "[Level System] The live leaderboard is currently located here: %l. Delete the message and restart the bot, to re-send it.", "leaderboard": "Leaderboard", "no-user-on-leaderboard": "Can't generate a leaderboard, because no one has any XP which is odd, but that's how it is ¯\\_(ツ)_/¯", "and-x-other-users": "and %uc other users", "level": "Level %l", "users": "Users", - "leaderboard-command-description": "Shows the leaderboard of this guild", + "leaderboard-command-description": "Shows the leaderboard of this server", "leaderboard-sortby-description": "How to sort the leaderboard (default: %d)", "profile-command-description": "Shows the profile of you or an an user", "profile-user-description": "User to see the profile from (default: you)", @@ -242,6 +234,8 @@ "edit-xp-user-description": "User to edit", "edit-xp-value-description": "New XP value of the user", "edit-xp-description": "Betrays your community and edits a user's XP", + "no-custom-formula": "No valid custom formula was entered. Using default formula.", + "invalid-custom-formula": "Invalid custom formula was entered. Please either fix the syntax of your custom formula or remove the value of the custom formula field.", "role-factors-total": "Multiplied together, the user receives **%f times more XP** for every message.", "edit-level-description": "Betrays your community and edits a user's levels", "random-messages-enabled-but-non-configured": "You have random messages enabled, but have non configured. Ignoring config.randomMessages configuration.", @@ -250,37 +244,13 @@ "team-list": { "channel-not-found": "Could not find channel with ID %c or the channel has a wrong type (only text-channels supported)", "role-not-found": "Could not find role with ID %r", - "no-users-with-role": "No users on this guild have the %r role yet.", + "no-users-with-role": "No users on this server have the %r role yet.", "no-roles-selected": "No roles listed yet.", "offline": "Offline", "dnd": "Do not disturb", "idle": "Away", "online": "Online" }, - "partner-list": { - "could-not-give-role": "Could not give role to user %u", - "could-not-remove-role": "Could not remove role from user %u", - "partner-not-found": "Partner could not be found. Please check if you are using the right partner-ID. The partner-ID is not identical with the server-id of the partner. The Partner-ID can be found [here](https://gblobscdn.gitbook.com/assets%2F-MNyHzQ4T8hs4m6x1952%2F-MWDvDO9-_JwAGqtD6at%2F-MWDxIcOHB9VcWhjsWt7%2Fscreen_20210320-102628.png?alt=media&token=2f9ac1f7-1a14-445c-b34e-83057789578e) in the partner-embed.", - "successful-edit": "Edited partner-list successfully.", - "channel-not-found": "Could not find channel with ID %c or the channel has a wrong type (only text-channels supported)", - "no-partners": "There are currently no partners. This is odd, but that's how it is ¯\\_(ツ)_/¯\n\nTo add a partner, run `/partner add` as a slash-command.", - "information": "Information", - "command-description": "Manages the partner-list on this server", - "padd-description": "Add a new partner", - "padd-name-description": "Name of the partner", - "padd-category-description": "Please select one of the categories specified in your configuration", - "padd-owner-description": "Owner of the partnered server", - "padd-inviteurl-description": "Invite to the partnered server", - "pedit-description": "Edits an existing partner", - "pedit-id-description": "ID of the partner", - "pedit-name-description": "New name of the partner", - "pedit-inviteurl-description": "New invite to this partner", - "pedit-category-description": "New category of this partner", - "pedit-owner-description": "New owner of the partner server", - "pedit-staff-description": "New designated staff member for this partner server", - "pdelete-description": "Deletes an exiting partner", - "pdelete-id-description": "ID of the partner" - }, "ping-on-vc-join": { "channel-not-found": "Notify channel %c not found", "could-not-send-pn": "Could not send PN to %m" @@ -312,7 +282,7 @@ "vote": "Vote!", "vote-this": "Click on this option to place your vote here", "voted-successfully": "Successfully voted. Thanks for your participation.", - "not-voted-yet": "You have not voted yet, so I cant show you what you voted?", + "not-voted-yet": "You have not voted yet, so I can't show you what you voted.", "you-voted": "You have voted for **%o**.", "remove-vote": "Remove my vote", "removed-vote": "Your vote was removed successfully.", @@ -341,11 +311,6 @@ "audit-log-reason-startup": "Updated channel because of startup", "not-voice-channel-info": "Channel \"%c\" (%id) is a %t and not a voice-channel as recommended" }, - "activities": { - "hook-installed": "Installed hook for generating special activity invites", - "command-description": "Create a in-voice-activity on discord", - "type-description": "Type of the voice activity" - }, "info-commands": { "info-command-description": "Find information about parts of this server", "command-userinfo-description": "Find more information about a user on this server", @@ -391,20 +356,20 @@ }, "stagePrivacy": { "PUBLIC": "Publicly accessible", - "GUILD_ONLY": "Only guild members can join" + "GUILD_ONLY": "Only server members can join" }, "guildVerification": { - "NONE": "None", - "LOW": "Low", - "MEDIUM": "Medium", - "HIGH": "High", - "VERY_HIGH": "Very high" + "0": "None", + "1": "Low", + "2": "Medium", + "3": "High", + "4": "Very high" }, "boostTier": { - "NONE": "None", - "TIER_1": "Level 1", - "TIER_2": "Level 2", - "TIER_3": "Level 3" + "0": "None", + "1": "Level 1", + "2": "Level 2", + "3": "Level 3" }, "temp-channels": { "removed-audit-log-reason": "Removed temp channel, because no one was in it", @@ -449,6 +414,7 @@ "edit-modal-limit-placeholder": "Number between 0 and 99; 0 = unlimited", "edit-modal-name-prompt": "How should your channel be called?", "edit-modal-name-placeholder": "A very creative channel name", + "edit-modal-username-placeholder": "Username of the user", "user-not-found": "User not found" }, "guess-the-number": { @@ -461,6 +427,7 @@ "end-command-description": "Ends the current game", "session-already-running": "There is a session already running in this channel. Please end it with /guess-the-number end", "session-not-running": "There is currently no session running.", + "gamechannel-modus": "You can't use this command in a gamechannel. To end a game, disable the gamechannel modus and try using the end command.", "session-ended-successfully": "Ended session successfully. Locked channel successfully.", "current-session": "Current session", "number": "Number", @@ -491,7 +458,7 @@ "all-users": "All Users", "bots": "Bots", "humans": "Humans", - "not-admin": "⚠ To use this command, you need to be added to the adminRoles option in the SCNX-Dashboard. If you are the owner of this bot please remember to create an override in the guild settings to prevent abuse of this command.", + "not-admin": "⚠ To use this command, you need to be added to the adminRoles option in the SCNX-Dashboard. If you are the owner of this bot please remember to create an override in the server settings to prevent abuse of this command.", "add-reason": "Mass role addition by %u", "remove-reason": "Mass role removal by %u" }, @@ -594,8 +561,8 @@ "quarantine-audit-log-reason": "Got quarantined by %u because of \"%r\"", "kicked-audit-log-reason": "Got kicked by %u because of \"%r\"", "banned-audit-log-reason": "Got banned by %u because of \"%r\"", - "channelmute-audit-log-reason": "Got channel-mutet by %u of \"%r\"", - "unchannelmute-audit-log-reason": "The Channel-Mute got removed by %u of \"%r\"", + "channelmute-audit-log-reason": "Got channel-mutet by %u because of \"%r\"", + "unchannelmute-audit-log-reason": "The Channel-Mute got removed by %u because of \"%r\"", "unbanned-audit-log-reason": "Got unbanned by %u because of \"%r\"", "unquarantine-audit-log-reason": "Got unquarantined by %u because of \"%r\"", "action-expired": "Action expired", @@ -613,7 +580,7 @@ "missing-logchannel": "LogChannel could not be found", "reached-warns": "Reached %w warns", "restored-punishment-audit-log-reason": "Restored punishment", - "anti-join-raid": "ANIT-JOIN-RAID", + "anti-join-raid": "ANTI-JOIN-RAID", "raid-detected": "Raid detected", "joingate-for-everyone": "Join-Gate-Modus: Catch all users", "account-age-to-low": "Account creation age of %a days is to low (required are more then %c)", @@ -678,7 +645,7 @@ "restart-verification-button": "Restart verification process", "member-not-found": "This user could not be found, maybe they already left?", "already-verified": "Seems like you are already verified... Why would you want to repeat this process?", - "restarted-verification": "I have send you another DM about your verification prozess. Please read it carefully and follow the actions described in it. Please not that this action did not re-trigger the manual-verification (if enabled), so spamming this button is useless.", + "restarted-verification": "I have sent you another DM about your verification process. Please read it carefully and follow the actions described in it. Please not that this action did not re-trigger the manual-verification (if enabled), so spamming this button is useless.", "dms-still-disabled": "It seems like your DMs are still disabled. Please enable your DMs to start the verification. This is not optional, you need to do this in order to get access to %g.", "dms-not-enabled-ping": "%p, it seems like you have your DMs disabled. Please enable them and hit the button below this message to verify yourself. You have two minutes to complete this process." }, @@ -708,14 +675,6 @@ "ticket-log-value": "Transcript with %n messages can be found [here](%u).", "closed-by": "👷 Ticket closed by" }, - "custom-commands": { - "not-found": "This custom-command does not longer exist. It might have been deleted or deactivated.", - "parameter-not-set": "This parameter did not get specified", - "true": "True", - "no-roles-default": "⚠\uFE0F You do not have enough permissions to execute this custom command because you are missing the roles required to execute this command.", - "fix-no-reply": "**⚠️ This Custom-Command is not properly set up**\nTo fix this, add the \"Reply to message or interaction\"-Action to this Custom-Command and reload your configuration.", - "false": "False" - }, "reminders": { "command-description": "Set a reminder for yourself", "in-description": "After what time should we remind you? (eg. \"2h 30m\")", @@ -724,15 +683,8 @@ "one-minute-in-future": "Your reminder needs to be at least one minute in the future", "reminder-set": "Reminder set. We'll remind you at %d." }, - "akinator": { - "command-description": "Let akinator guess a character/object/animal", - "type-description": "Select what akinator should guess (default: character)", - "character-name": "Character", - "object-name": "Object", - "animal-name": "Animal" - }, "afk-system": { - "command-description": "Manage your AFK-Status on this guild", + "command-description": "Manage your AFK-Status on this server", "end-command-description": "End your current AFK-Session", "start-command-description": "Start a new AFK-Session", "reason-option-description": "Explain why you started this session", @@ -742,51 +694,10 @@ "afk-nickname-change-audit-log": "Updated user nickname because they started an AFK-Session", "can-not-edit-nickname": "Can not edit nickname of %u: %e" }, - "invite-tracking": { - "hook-installed": "Installed hook to receive more information about invites", - "log-channel-not-found-but-set": "Log-Channel %c not found, but it's set in your configuration.", - "new-member": "New member joined", - "member-leave": "Member left", - "invite-type": "Invite-Type", - "member": "Member", - "invite": "Invite", - "invite-code": "Invite-Code: [%c](%u)", - "invite-channel": "Channel: %c", - "expires-at": "Expires at: %t", - "created-at": "Created at: %t", - "inviter": "Invited by: %u (%a/%i active invites)", - "uses": "Uses: %u", - "createdAt": "Created at: %t", - "max-uses": "Max-Uses: %u", - "normal-invite": "Normal Invite", - "vanity-invite": "Vanity-Invite", - "missing-permissions": "I don't have enough permissions to determine the invite", - "unknown-invite": "Sorry, but I couldn't determine the invite this person used", - "joined-for-the-x-time": "%u joined this server %x times before this, the last one was %t.", - "revoke-invite": "Revoke this invite", - "invite-not-found": "This invite could not be found... Maybe it already got revoked?", - "invite-revoked": "Invite revoked successfully.", - "missing-revoke-permissions": "Sorry, but you can't revoke this invite: Missing `MANAGE_GUILD` permission.", - "invite-revoke-audit-log": "%u revoked this invite", - "invite-revoked-error": "Could not revoke invite %c: %e", - "trace-command-description": "Trace the invites of a user", - "argument-user-description": "User to trace invites from", - "invited-by": "Invited by", - "invited-users": "Invited users", - "inviter-not-found": "Could not determine who invited this user.", - "no-users-invited": "This user hasn't invited any other users.", - "and-x-more-users": "And %x more users", - "and-x-more-invites": "And %x more invites", - "created-invites": "Created invites", - "not-showing-left-users": "Invited users who left are not displayed here.", - "no-invites": "This user has create no invites", - "revoke-user-invite": "Revoke all user's invites", - "revoked-invites-successfully": "All invites from this user got revoked successfully" - }, "tic-tac-toe": { "command-description": "Play tic-tac-toe against someone in the chat", "user-description": "User to play against", - "challenge-message": "%t, %u challenged you to a game of tic-tac-toe! Hit the button below to join the battle! This invitation will expire in about 2 minutes, to don't hesitate to much.", + "challenge-message": "%t, %u challenged you to a game of tic-tac-toe! Hit the button below to join the battle! This invitation will expire in about 2 minutes, so don't hesitate to much.", "accept-invite": "Join game", "deny-invite": "No thanks", "self-invite-not-possible": "Are you really that lonely? Even Simon, a complete introvert with no friends and developer of this bot, can find another user to play tic-tac-toe with... You should be able to do that too, try inviting %r for example, maybe they want to play a round?", @@ -801,7 +712,7 @@ "duel": { "command-description": "Play duel against someone in the chat", "user-description": "User to play against", - "challenge-message": "%t, %u challenged you to a game of duel! Hit the button below to join the battle! This invitation will expire in about 2 minutes, to don't hesitate to much.", + "challenge-message": "%t, %u challenged you to a game of duel! Hit the button below to join the battle! This invitation will expire in about 2 minutes, so don't hesitate to much.", "accept-invite": "Join game", "deny-invite": "No thanks", "self-invite-not-possible": "Are you really that lonely? Even Simon, a complete introvert with no friends and developer of this bot, can find another user to play duel with... You should be able to do that too, try inviting %r for example, maybe they want to play a round?", @@ -836,7 +747,7 @@ "message-drop-earned-money": "The user %u gained %m %c by getting a message drop", "rob-earned-money": "The user %u gained %m %c by robbing from %v", "weekly-earned-money": "The user %u gained %m %c by cashing in their weekly reward", - "daily-earned-money": "The user %u gained %m %c by cashing in their weekly reward", + "daily-earned-money": "The user %u gained %m %c by cashing in their daily reward", "admin-self-abuse": "The admin %a wanted to abuse their permissions by giving them self even more money! This can't and should not be ignored!", "admin-self-abuse-answer": "What a bad admin you are, %u. I'm disappointed with you! I need to report this. If I wish I could ban you!", "added-money": "%i %c has been added to the balance of %u", @@ -926,6 +837,22 @@ "its-a-tie-try-again": "It's a tie! Try again!", "command-description": "Play rock-paper-scissors against the bot or someone in the chat" }, + "rock-paper-scissors": { + "stone": "Stone", + "paper": "Paper", + "scissors": "Scissors", + "won": "won", + "lost": "lost", + "tie": "tie", + "play-again": "Play again", + "challenge-message": "%t, %u challenged you to a game of rock-paper-scissors! Hit the button below to join the game! This invitation will expire in about 2 minutes, so don't hesitate to much.", + "invite-expired": "Sorry, %u, %i didn't accept your request to play rock-paper-scissors in time ):", + "invite-denied": "Sorry, %u, but %i denied your request to play a round of rock-paper-scissors ):", + "rps-title": "Rock Paper Scissors", + "rps-description": "Choose your weapon!", + "its-a-tie-try-again": "It's a tie! Try again!", + "command-description": "Play rock-paper-scissors against the bot or someone in the chat" + }, "connect-four": { "tie": "It's a tie!", "win": "%u has won the game!", @@ -934,7 +861,7 @@ "challenge-message": "%t, %u challenged you to a game of Connect Four! Hit the button below to join the game! This invitation will expire in about 2 minutes, so don't hesitate to much.", "invite-expired": "Sorry, %u, %i didn't accept your request to play Connect Four in time ):", "invite-denied": "Sorry, %u, but %i denied your request to play a round of Connect Four ):", - "command-description": "Play Connect Four against the bot or someone in the chat", + "command-description": "Play Connect Four against someone in the chat", "field-size-description": "The size of the playfield (default: 7)", "challenge-yourself": "You cannot challenge yourself!", "challenge-bot": "You cannot challenge bots!" @@ -954,7 +881,7 @@ "dont-use-drawn": "Dont use", "win": "%u won the game! %turns cards were played.", "win-you": "You've won the game!", - "missing-uno": "⚠️️ You must use the Uno! button before you use your second last card!", + "missing-uno": "⚠️ You must use the Uno! button before you use your second last card!", "choose-color": "Select a color:", "pending-draws": "Use a Draw 2/4 card, otherwise you have to draw %count cards!", "not-ingame": "You're not in this game!", @@ -980,7 +907,7 @@ "vote": "Vote!", "vote-this": "Select this option if you think it's correct.", "voted-successfully": "Selected successfully.", - "not-voted-yet": "You have not selected an option yet, so I cant show you what you selected?", + "not-voted-yet": "You have not selected an option yet, so I can't show you what you selected.", "you-voted": "You've selected **%o** as correct answer.", "change-opinion": "You can change your opinion at any time by selecting another option above the button you just clicked.", "cannot-change-opinion": "You cannot change your selection as the creator of this quiz disabled it.", From cea21e109b4f53b6d77da783eb4c7e7796699f11 Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 9 Jan 2026 17:51:03 +0100 Subject: [PATCH 03/27] fixed broken info-commands --- modules/info-commands/commands/info.js | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/modules/info-commands/commands/info.js b/modules/info-commands/commands/info.js index 7c405902..1c063dd7 100644 --- a/modules/info-commands/commands/info.js +++ b/modules/info-commands/commands/info.js @@ -9,7 +9,6 @@ const { } = require('../../../src/functions/helpers'); const {ChannelType, MessageEmbed} = require('discord.js'); const {AgeFromDate} = require('age-calculator'); -const {stringNames} = require('../../invite-tracking/events/guildMemberJoin'); const {calculateLevelXP, isMaxLevel, displayLevel} = require('../../levels/events/messageCreate'); const legacyChannelType = (type) => { @@ -198,22 +197,6 @@ module.exports.subcommands = { embed.addField(moduleStrings.userinfo.level, displayLevel(levelUserData.level, interaction.client), true); embed.addField(moduleStrings.userinfo.messages, levelUserData.messages.toString(), true); } - if (interaction.client.models['invite-tracking']) { - const invitedUsers = await interaction.client.models['invite-tracking']['UserInvite'].findAll({ - where: { - inviter: member.user.id - } - }); - const userInvites = await interaction.client.models['invite-tracking']['UserInvite'].findAll({ - where: { - userID: member.user.id, - left: false - }, - order: [['createdAt', 'DESC']] - }); - if (userInvites[0]) embed.addField(moduleStrings.userinfo['invited-by'], `${localize('invite-tracking', stringNames[userInvites[0].inviteType])}${userInvites[0].inviter ? ` by <@${userInvites[0].inviter}>` : ''}`, true); - embed.addField(moduleStrings.userinfo.invites, `\`\`\`| ${localize('info-commands', 'total-invites')} | ${localize('info-commands', 'active-invites')} | ${localize('info-commands', 'left-invites')} |\n| ${pufferStringToSize(invitedUsers.length.toString(), localize('info-commands', 'total-invites').length)} | ${pufferStringToSize(invitedUsers.filter(i => !i.left).length.toString(), localize('info-commands', 'active-invites').length)} | ${pufferStringToSize(invitedUsers.filter(i => i.left).length.toString(), localize('info-commands', 'left-invites').length)} |\`\`\``); - } let permstring = ''; member.permissions.toArray().forEach(p => { if (!member.permissions.toArray().includes('ADMINISTRATOR')) permstring = permstring + `${p}, `; From 0095066697f52093292e10254ca93c8cfb17825f Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 9 Jan 2026 17:51:34 +0100 Subject: [PATCH 04/27] fixed duplicated entry --- locales/en.json | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/locales/en.json b/locales/en.json index b23bec45..35cbc727 100644 --- a/locales/en.json +++ b/locales/en.json @@ -837,22 +837,6 @@ "its-a-tie-try-again": "It's a tie! Try again!", "command-description": "Play rock-paper-scissors against the bot or someone in the chat" }, - "rock-paper-scissors": { - "stone": "Stone", - "paper": "Paper", - "scissors": "Scissors", - "won": "won", - "lost": "lost", - "tie": "tie", - "play-again": "Play again", - "challenge-message": "%t, %u challenged you to a game of rock-paper-scissors! Hit the button below to join the game! This invitation will expire in about 2 minutes, so don't hesitate to much.", - "invite-expired": "Sorry, %u, %i didn't accept your request to play rock-paper-scissors in time ):", - "invite-denied": "Sorry, %u, but %i denied your request to play a round of rock-paper-scissors ):", - "rps-title": "Rock Paper Scissors", - "rps-description": "Choose your weapon!", - "its-a-tie-try-again": "It's a tie! Try again!", - "command-description": "Play rock-paper-scissors against the bot or someone in the chat" - }, "connect-four": { "tie": "It's a tie!", "win": "%u has won the game!", From 1746bccfe47e18541a59eccb86f2ceabad9547ad Mon Sep 17 00:00:00 2001 From: Kevin Date: Mon, 26 Jan 2026 17:58:09 +0100 Subject: [PATCH 05/27] Adds ping-protection module. (#168) * Added the base module folders and module.json * Added all folders necessary and the configuration files for each folder * Added a test command * removed the manage file * Added, renamed and deleted some files as necessary and coded the models. * Renamed action.js to moderation.js, coded multiple things, added a new file for correct tracking. * Forgot to update module.json, now updated aswell * Added additional information in ping-protection.js * Disabled allowing reply pings, added the enable moderation and enable advanced configuration in moderation.json and made the choices inside depend on it because I forgot to :/ Added the options to enable/disable pings/modlogs/logs kept after leave and made the choices depend on it + made those choices with numbers select instead of integers for almost 0 user-error issues. * Added support for actually correct parameters and those parameters added into the message editor for the warning message * Added proper support for localization, and coded the events * Completed the full module and fixed some critical bugs that caused the bot to crash * Cleaned up some code notes I used for debugging * Completely finished the module and worked tirelessly for many hours to debug code, has been tested and is currently ongoing extensive testing to ensure absolutely everythig works as supposed to * Debugged absolutely everything, removed like 300 lines of code for polish while remaining the same functions. Removed a few locales that are unused and updated some locales for better understanding. Fully tested extensively. Not verified by GitHub because I code in VSCode. * Added the option to lower mod actions history * Made the deault value of pings to trigger action 10 instead of 5 in basic pings count config * Added the commands warnings for most commands Listed the warnings for all commands except the panel command as the bot already checks for administrator perms. * Almost completely rewrote the module to make sure the modules works as supposed to with SCNX. * Added "automod" abilities - Will now delete the original message by default with the message content and allows to configure both options * (not working correctly) added automod integration and some small changes * Fixed the * Removed the feature that didn't work (reposting), adds a custom message to the automod message block. Also the bot now deletes the rule it created if automod enabled = false * Fixed the bug of the bot still sending the warning and punishing if limit reached with reply pings even when it's allowed in config * Added a funny easter egg * Some QOL improvements, including merging the list commands * Added some new options in the config * Update configuration.json * Fix self-ping condition to allow self-pinging * Ping protection V1, in Discord.JS V14 * Ping Protection V1 * Changed code to the requested changes, and adjusted code logic to actually properly support multiple moderation actions (not tested before, and didn't work during testing) * Made adjustments to code as requested, and added an intent to main.js to make it work properly. * Fixed the missing footer on embeds. Fixed a small typo in the default waning message configuration and added an emoji to the why easter egg. --------- Co-authored-by: Kevinking500 --- locales/en.json | 63 ++ main.js | 2 +- .../commands/ping-protection.js | 226 ++++++ .../configs/configuration.json | 223 ++++++ .../ping-protection/configs/moderation.json | 171 +++++ modules/ping-protection/configs/storage.json | 97 +++ .../events/autoModerationActionExecution.js | 35 + modules/ping-protection/events/botReady.js | 14 + .../ping-protection/events/guildMemberAdd.js | 12 + .../events/guildMemberRemove.js | 18 + .../events/interactionCreate.js | 94 +++ .../ping-protection/events/messageCreate.js | 135 ++++ modules/ping-protection/models/LeaverData.js | 25 + .../ping-protection/models/ModerationLog.js | 39 + modules/ping-protection/models/PingHistory.js | 33 + modules/ping-protection/module.json | 28 + modules/ping-protection/ping-protection.js | 665 ++++++++++++++++++ 17 files changed, 1879 insertions(+), 1 deletion(-) create mode 100644 modules/ping-protection/commands/ping-protection.js create mode 100644 modules/ping-protection/configs/configuration.json create mode 100644 modules/ping-protection/configs/moderation.json create mode 100644 modules/ping-protection/configs/storage.json create mode 100644 modules/ping-protection/events/autoModerationActionExecution.js create mode 100644 modules/ping-protection/events/botReady.js create mode 100644 modules/ping-protection/events/guildMemberAdd.js create mode 100644 modules/ping-protection/events/guildMemberRemove.js create mode 100644 modules/ping-protection/events/interactionCreate.js create mode 100644 modules/ping-protection/events/messageCreate.js create mode 100644 modules/ping-protection/models/LeaverData.js create mode 100644 modules/ping-protection/models/ModerationLog.js create mode 100644 modules/ping-protection/models/PingHistory.js create mode 100644 modules/ping-protection/module.json create mode 100644 modules/ping-protection/ping-protection.js diff --git a/locales/en.json b/locales/en.json index 35cbc727..171abe21 100644 --- a/locales/en.json +++ b/locales/en.json @@ -928,5 +928,68 @@ "nicknames": { "owner-cannot-be-renamed": "The owner of the server (%u) cannot be renamed.", "nickname-error": "An error occurred while trying to change the nickname of %u: %e" + }, + "ping-protection": { + "log-not-a-member": "[Ping Protection] Punishment failed: The pinger is not a member.", + "log-punish-role-error": "[Ping Protection] Punishment failed: I cannot punish %tag because their role is higher than or equal to my highest role.", + "log-mute-error": "[Ping Protection] Punishment failed: I cannot mute %tag: %e", + "log-kick-error": "[Ping Protection] Punishment failed: I cannot kick %tag: %e", + "log-action-log-failed": "[Ping Protection] Punishment logging failed: %e", + "log-manual-delete": "All data for <@%u> (%u) has been deleted successfully.", + "log-manual-delete-logs": "[Ping Protection] All data for user with ID %u has been deleted successfully.", + "log-automod-keyword-limit": "[Ping Protection] Automod keywords exceed 1000 characters limit. Keywords were truncated.", + "punish-log-failed-title": "Punishment failed for user %u", + "punish-log-failed-desc": "An error occured while trying to punish the user %m. Please check the bot's permissions and role hierarchy. See the message below for the error.", + "punish-log-error": "Error: ```%e```", + "punish-role-error": "I cannot punish %tag because their role is higher than or equal to my highest role.", + "reason-basic": "User reached %c pings in the last %w weeks.", + "reason-advanced": "User reached %c pings in the last %d days (Custom timeframe).", + "cmd-desc-module": "Ping protection related commands", + "cmd-desc-group-user": "Every command related to the users", + "cmd-desc-history": "View the ping history of a user", + "cmd-opt-user": "The user to check", + "cmd-desc-actions": "View the moderation action history of a user", + "cmd-desc-panel": "Admin: Open the user management panel", + "cmd-desc-group-list": "Lists protected or whitelisted entities", + "cmd-desc-list-protected": "List all protected users and roles", + "cmd-desc-list-wl": "List all whitelisted roles and channels", + "embed-history-title": "Ping history of %u", + "embed-leaver-warning": "This user left the server at %t. These logs will stay until automatic deletion.", + "no-data-found": "No logs found for this user.", + "embed-actions-title": "Moderation history of %u", + "label-reason": "Reason", + "actions-retention-note": "Note: Moderation actions are retained for 1 - 12 months based on the configuration.", + "no-permission": "You don't have sufficient permissions to use this command.", + "panel-title": "User Panel: %u", + "panel-description": "Manage and view data for %u (%i). View a quick recap of their ping and moderation history, or delete all data stored for this user (Risky).", + "btn-history": "Ping history", + "btn-actions": "Actions history", + "btn-delete": "Delete all data (Risky)", + "list-protected-title": "Protected Users and Roles", + "list-protected-desc": "View all protected users and roles here. When someone pings one of these protected user(s)/role(s), a warning will be sent. Exceptions are whitelisted roles, and when sent in a whitelisted channel.", + "field-prot-users": "Protected Users", + "field-prot-roles": "Protected Roles", + "list-whitelist-title": "Whitelisted Roles and Channels", + "list-whitelist-desc": "View all whitelisted roles and channels here. Whitelisted roles will not get a warning for pinging a protected entity, and pings will be ignored in whitelisted channels.", + "field-wl-roles": "Whitelisted Roles", + "field-wl-channels": "Whitelisted Channels", + "list-none": "None are configured.", + "modal-title": "Confirm data deletion for this user", + "modal-label": "Confirm data deletion by typing this phrase:", + "modal-phrase": "I understand that all data of this user will be deleted and that this action cannot be undone.", + "modal-failed": "The phrase you entered is incorrect. Data deletion cancelled.", + "field-quick-history": "Quick history view (Last %w weeks)", + "field-quick-desc": "Pings history amount: %p\nModeration actions amount: %m", + "history-disabled": "History logging has been disabled by a bot-configurator.\nAre you (one of) the bot-configurators? You can enable history logging in the \"storage\" tab in the 'ping-protection' module ^^", + "leaver-warning-long": "User left at %d. These logs will stay until automatic deletion.", + "leaver-warning-short": "User left at %d.", + "meme-why": "😐 [Why are you the way that you are?]() - You just pinged yourself..", + "meme-played": "🔑 [Congratulations, you played yourself.]()", + "meme-spider": "🕷️ [Is this you?]() - You just pinged yourself.", + "meme-rick": "🎵 [Never gonna give you up, never gonna let you down...]() You just Rick Rolled yourself. Also congrats you unlocked the secret easter egg that only has a 1% chance of appearing!!1!1!!", + "meme-grind": "Why are you even pinging yourself 5 times in a row? Anyways continue some more to possibly get the secret meme\n-# (good luck grinding, only a 1% chance of getting it and during testing I had it once after 83 pings)", + "label-jump": "Jump to Message", + "no-message-link": "This ping was blocked by AutoMod", + "list-entry-text": "%index. **Pinged %target** at %time\n%link" } } \ No newline at end of file diff --git a/main.js b/main.js index a42405bc..e4f7b49f 100644 --- a/main.js +++ b/main.js @@ -12,7 +12,7 @@ const client = new Discord.Client({ partials: [Partials.Message, Partials.GuildMember, Partials.GuildScheduledEvent, Partials.Reaction, Partials.User, Partials.Channel], // Most of these are not needed, but enabling them does not increase CPU / RAM usage and does not introduce problems, as we handle them in the event emitter system allowedMentions: {parse: ['users', 'roles']}, // Disables @everyone mentions because everyone hates them intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildBans, GatewayIntentBits.DirectMessages, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, - GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.GuildPresences, GatewayIntentBits.GuildInvites, GatewayIntentBits.GuildEmojisAndStickers, GatewayIntentBits.GuildMessageReactions, GatewayIntentBits.GuildEmojisAndStickers, GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildWebhooks] + GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.GuildPresences, GatewayIntentBits.GuildInvites, GatewayIntentBits.GuildEmojisAndStickers, GatewayIntentBits.GuildMessageReactions, GatewayIntentBits.GuildEmojisAndStickers, GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildWebhooks, GatewayIntentBits.AutoModerationExecution] }); client.intervals = []; client.jobs = []; diff --git a/modules/ping-protection/commands/ping-protection.js b/modules/ping-protection/commands/ping-protection.js new file mode 100644 index 00000000..71f1eb9c --- /dev/null +++ b/modules/ping-protection/commands/ping-protection.js @@ -0,0 +1,226 @@ +const { + fetchModHistory, + getPingCountInWindow, + generateHistoryResponse, + generateActionsResponse +} = require('../ping-protection'); +const { localize } = require('../../../src/functions/localize'); +const { truncate } = require('../../../src/functions/helpers'); +const { ActionRowBuilder, ButtonBuilder, EmbedBuilder, ButtonStyle, MessageFlags } = require('discord.js'); + +module.exports.run = async function (interaction) { + const group = interaction.options.getSubcommandGroup(false); + const sub = interaction.options.getSubcommand(false); + + if (group) { + return module.exports.subcommands[group][sub](interaction); + } + return module.exports.subcommands[sub](interaction); +}; + +// Handles subcommands +module.exports.subcommands = { + 'user': { + 'history': async function (interaction) { + const user = interaction.options.getUser('user'); + const payload = await generateHistoryResponse(interaction.client, user.id, 1); + await interaction.reply({ ...payload, flags: MessageFlags.Ephemeral }); + }, + 'actions-history': async function (interaction) { + const user = interaction.options.getUser('user'); + const payload = await generateActionsResponse(interaction.client, user.id, 1); + await interaction.reply({ ...payload, flags: MessageFlags.Ephemeral }); + }, + 'panel': async function (interaction) { + const user = interaction.options.getUser('user'); + const pingerId = user.id; + const storageConfig = interaction.client.configurations['ping-protection']['storage']; + const retentionWeeks = (storageConfig && storageConfig.pingHistoryRetention) + ? storageConfig.pingHistoryRetention + : 12; + const timeframeDays = retentionWeeks * 7; + + const pingCount = await getPingCountInWindow(interaction.client, pingerId, timeframeDays); + const modData = await fetchModHistory(interaction.client, pingerId, 1, 1000); + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(`ping-protection_history_${user.id}`) + .setLabel(localize('ping-protection', 'btn-history')) + .setStyle(ButtonStyle.Secondary), + new ButtonBuilder() + .setCustomId(`ping-protection_actions_${user.id}`) + .setLabel(localize('ping-protection', 'btn-actions')) + .setStyle(ButtonStyle.Secondary), + new ButtonBuilder() + .setCustomId(`ping-protection_delete_${user.id}`) + .setLabel(localize('ping-protection', 'btn-delete')) + .setStyle(ButtonStyle.Danger) + ); + + const embed = new EmbedBuilder() + .setTitle(localize('ping-protection', 'panel-title', { u: user.tag })) + .setDescription(localize('ping-protection', 'panel-description', { u: user.toString(), i: user.id })) + .setColor('Blue') + .setThumbnail(user.displayAvatarURL({ dynamic: true })) + .addFields([{ + name: localize('ping-protection', 'field-quick-history', { w: retentionWeeks }), + value: localize('ping-protection', 'field-quick-desc', { p: pingCount, m: modData.total }), + inline: false + }]) + .setFooter({ + text: interaction.client.strings.footer, + iconURL: interaction.client.strings.footerImgUrl + }); + if (!interaction.client.strings.disableFooterTimestamp) embed.setTimestamp(); + + await interaction.reply({ + embeds: [embed.toJSON()], + components: [row.toJSON()], + flags: MessageFlags.Ephemeral + }); + } + }, + 'list': { + 'protected': async function (interaction) { + await listHandler(interaction, 'protected'); + }, + 'whitelisted': async function (interaction) { + await listHandler(interaction, 'whitelisted'); + } + } +}; + +// Handles list subcommands +async function listHandler(interaction, type) { + const config = interaction.client.configurations['ping-protection']['configuration']; + const embed = new EmbedBuilder() + .setColor('Green') + .setFooter({ + text: interaction.client.strings.footer, + iconURL: interaction.client.strings.footerImgUrl + }); + + if (!interaction.client.strings.disableFooterTimestamp) embed.setTimestamp(); + + if (type === 'protected') { + embed.setTitle(localize('ping-protection', 'list-protected-title')); + embed.setDescription(localize('ping-protection', 'list-protected-desc')); + + const usersList = config.protectedUsers.length > 0 + ? config.protectedUsers.map(id => `<@${id}>`).join('\n') + : localize('ping-protection', 'list-none'); + + const rolesList = config.protectedRoles.length > 0 + ? config.protectedRoles.map(id => `<@&${id}>`).join('\n') + : localize('ping-protection', 'list-none'); + + embed.addFields([ + { + name: localize('ping-protection', 'field-prot-users'), + value: truncate(usersList, 1024), + inline: true + }, + { + name: localize('ping-protection', 'field-prot-roles'), + value: truncate(rolesList, 1024), + inline: true + } + ]); + + } else if (type === 'whitelisted') { + embed.setTitle(localize('ping-protection', 'list-whitelist-title')); + embed.setDescription(localize('ping-protection', 'list-whitelist-desc')); + + const rolesList = config.ignoredRoles.length > 0 + ? config.ignoredRoles.map(id => `<@&${id}>`).join('\n') + : localize('ping-protection', 'list-none'); + + const channelsList = config.ignoredChannels.length > 0 + ? config.ignoredChannels.map(id => `<#${id}>`).join('\n') + : localize('ping-protection', 'list-none'); + + embed.addFields([ + { + name: localize('ping-protection', 'field-wl-roles'), + value: truncate(rolesList, 1024), + inline: true }, + { + name: localize('ping-protection', 'field-wl-channels'), + value: truncate(channelsList, 1024), + inline: true } + ]); + } + + await interaction.reply({ + embeds: [embed.toJSON()], + flags: MessageFlags.Ephemeral + }); +} + +module.exports.config = { + name: 'ping-protection', + description: localize('ping-protection', 'cmd-desc-module'), + usage: '/ping-protection', + type: 'slash', + defaultPermission: false, + options: [ + { + type: 'SUB_COMMAND_GROUP', + name: 'user', + description: localize('ping-protection', 'cmd-desc-group-user'), + options: [ + { + type: 'SUB_COMMAND', + name: 'history', + description: localize('ping-protection', 'cmd-desc-history'), + options: [{ + type: 'USER', + name: 'user', + description: localize('ping-protection', 'cmd-opt-user'), + required: true + }] + }, + { + type: 'SUB_COMMAND', + name: 'actions-history', + description: localize('ping-protection', 'cmd-desc-actions'), + options: [{ + type: 'USER', + name: 'user', + description: localize('ping-protection', 'cmd-opt-user'), + required: true + }] + }, + { + type: 'SUB_COMMAND', + name: 'panel', + description: localize('ping-protection', 'cmd-desc-panel'), + options: [{ + type: 'USER', + name: 'user', + description: localize('ping-protection', 'cmd-opt-user'), + required: true + }] + } + ] + }, + { + type: 'SUB_COMMAND_GROUP', + name: 'list', + description: localize('ping-protection', 'cmd-desc-group-list'), + options: [ + { + type: 'SUB_COMMAND', + name: 'protected', + description: localize('ping-protection', 'cmd-desc-list-protected') + }, + { + type: 'SUB_COMMAND', + name: 'whitelisted', + description: localize('ping-protection', 'cmd-desc-list-wl') + } + ] + } + ] +}; \ No newline at end of file diff --git a/modules/ping-protection/configs/configuration.json b/modules/ping-protection/configs/configuration.json new file mode 100644 index 00000000..fb7cf2c7 --- /dev/null +++ b/modules/ping-protection/configs/configuration.json @@ -0,0 +1,223 @@ +{ + "filename": "configuration.json", + "humanName": { + "en": "General Configuration" + }, + "commandsWarnings": { + "normal": [ + "/ping-protection user history", + "/ping-protection user actions-history", + "/ping-protection list roles", + "/ping-protection list users", + "/ping-protection list whitelisted" + ] + }, + "description": { + "en": "Configure protected users/roles, whitelisted roles/members, ignored channels and the notification message." + }, + "content": [ + { + "name": "protectedRoles", + "humanName": { + "en": "Protected Roles" + }, + "description": { + "en": "Specific roles which are protected from pings." + }, + "type": "array", + "content": "roleID", + "default": { + "en": [] + } + }, + { + "name": "protectAllUsersWithProtectedRole", + "humanName": { + "en": "Protect all users with a protected role" + }, + "description": { + "en": "if enabled, all users with at least one protected role will be protected from pings, even if they are not specifically listed as protected users." + }, + "type": "boolean", + "default": { + "en": true + } + }, + { + "name": "protectedUsers", + "humanName": { + "en": "Protected Users" + }, + "description": { + "en": "Specific users who are protected from pings." + }, + "type": "array", + "content": "userID", + "default": { + "en": [] + } + }, + { + "name": "ignoredRoles", + "humanName": { + "en": "Whitelisted Roles" + }, + "description": { + "en": "Roles allowed to ping protected members or roles." + }, + "type": "array", + "content": "roleID", + "default": { + "en": [] + } + }, + { + "name": "ignoredChannels", + "humanName": { + "en": "Whitelisted Channels" + }, + "description": { + "en": "Pings in these channels are ignored." + }, + "type": "array", + "content": "channelID", + "default": { + "en": [] + } + }, + { + "name": "ignoredUsers", + "humanName": { + "en": "Whitelisted Users" + }, + "description": { + "en": "Pings from these users are ignored." + }, + "type": "array", + "content": "userID", + "default": { + "en": [] + } + }, + { + "name": "allowReplyPings", + "humanName": { + "en": "Allow Reply Pings" + }, + "description": { + "en": "If enabled, replying to a protected user (with mention ON) is allowed." + }, + "type": "boolean", + "default": { + "en": false + } + }, + { + "name": "selfPingConfiguration", + "humanName": { + "en": "Self-Ping configuration" + }, + "description": { + "en": "Configure what happens when a protected user pings themselves. Note: Automod overrides this setting meaning this setting will not apply if Automod is enabled." + }, + "type": "select", + "content": [ + "Get punished like normal members", + "Ignored", + "Get fun easter eggs when pinging themselves" + ], + "default": { + "en": "Ignored" + } + }, + { + "name": "enableAutomod", + "humanName": { + "en": "Enable automod" + }, + "description": { + "en": "If enabled, the bot will utilise Discord's native AutoMod to block the message with a ping of a protected user/role." + }, + "type": "boolean", + "default": { + "en": true + } + }, + { + "name": "autoModLogChannel", + "humanName": { + "en": "AutoMod Log Channel" + }, + "description": { + "en": "Channel where AutoMod alerts are sent." + }, + "type": "channelID", + "default": { + "en": [] + }, + "channelTypes": [ + "GUILD_TEXT" + ], + "dependsOn": "enableAutomod" + }, + { + "name": "autoModBlockMessage", + "humanName": { + "en": "AutoMod custom message for message block" }, + "description": { + "en": "Custom text shown to the user when blocked (Max 150 characters)." + }, + "type": "string", + "maxLength": 150, + "default": { + "en": "Protected User Ping: Your message was blocked because you are trying to ping a protected user/role. The content of your message might be sent to a log channel depending on the configuration." + }, + "dependsOn": "enableAutomod" + }, + { + "name": "pingWarningMessage", + "humanName": { + "en": "Warning Message" + }, + "description": { + "en": "The message that gets sent to the user when they ping someone." + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "target-name", + "description": { + "en": "Name of the pinged user/role" + } + }, + { + "name": "target-mention", + "description": { + "en": "Mention of the pinged user/role" + } + }, + { + "name": "target-id", + "description": { + "en": "ID of the pinged user/role" + } + }, + { + "name": "pinger-id", + "description": { + "en": "ID of the user who pinged" + } + } + ], + "default": { + "en": { + "title": "You are not allowed to ping %target-name%!", + "description": "<@%pinger-id%>, You are not allowed to ping %target-mention% due to your role. You can view which roles/members you are not allowed to ping by using the `/ping-protection list protected` command.\n\nIf you were replying, make sure to turn off the mention in the reply.", + "image": "https://scnx-cdn.scootkit.net/1769198862209-rJfCVKzAuo6uQLhPUe9o2P6ArJkDBSVUCEyUQM6bqt5WFKWK.gif", + "color": "#ed4245" + } + } + } + ] +} diff --git a/modules/ping-protection/configs/moderation.json b/modules/ping-protection/configs/moderation.json new file mode 100644 index 00000000..5b08f3f9 --- /dev/null +++ b/modules/ping-protection/configs/moderation.json @@ -0,0 +1,171 @@ +{ + "filename": "moderation.json", + "humanName": { + "en": "Moderation Actions" + }, + "configElementName": { + "en": { + "one": "punishment", + "more": "punishment" + } + }, + "description": { + "en": "Define triggers for punishments." + }, + "configElements": true, + "content": [ + { + "name": "pingsCountBasic", + "humanName": { + "en": "Pings to trigger moderation" + }, + "description": { + "en": "The amount of pings required to trigger a moderation action (Uses 'Ping History Retention' timeframe)." + }, + "type": "integer", + "default": { + "en": 10 + } + }, + { + "name": "useCustomTimeframe", + "humanName": { + "en": "Use a custom timeframe" + }, + "description": { + "en": "If enabled, you can choose your own custom timeframe and the basic configuration will be ignored." + }, + "type": "boolean", + "default": { + "en": false + } + }, + { + "name": "pingsCountAdvanced", + "humanName": { + "en": "Pings to trigger (Custom Timeframe)" + }, + "description": { + "en": "The amount of pings required in the custom timeframe below." + }, + "type": "integer", + "default": { + "en": 5 + }, + "dependsOn": "useCustomTimeframe" + }, + { + "name": "timeframeDays", + "humanName": { + "en": "Timeframe (Days)" + }, + "description": { + "en": "In how many days must these pings occur?" + }, + "type": "integer", + "default": { + "en": 7 + }, + "dependsOn": "useCustomTimeframe" + }, + { + "name": "actionType", + "humanName": { + "en": "Action" + }, + "description": { + "en": "What punishment should be applied?" + }, + "type": "select", + "content": [ + "MUTE", + "KICK" + ], + "default": { + "en": "MUTE" + } + }, + { + "name": "muteDuration", + "humanName": { + "en": "Mute Duration (only if action type is MUTE)" + }, + "description": { + "en": "How long to mute the user? (in minutes)" + }, + "type": "integer", + "default": { + "en": 60 + } + }, + { + "name": "enableActionLogging", + "humanName": { + "en": "Enable action logging" + }, + "description": { + "en": "If enabled, moderation actions will be logged in the channel where a protected user/role got pinged." + }, + "type": "boolean", + "default": { + "en": true + } + }, + { + "name": "actionLogMessage", + "humanName": { + "en": "Action log message" + }, + "description": { + "en": "The message that will be sent when a user is punished for pinging protected users/roles." + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "pinger-mention", + "description": { + "en": "Mention of the user who pinged" + } + }, + { + "name": "pinger-name", + "description": { + "en": "Name of the user who pinged" + } + }, + { + "name": "action", + "description": { + "en": "The action that was taken (muted/kicked)" + } + }, + { + "name": "pings", + "description": { + "en": "Number of pings that triggered the action" + } + }, + { + "name": "timeframe", + "description": { + "en": "The timeframe in days in which the pings occurred" + } + }, + { + "name": "duration", + "description": { + "en": "Duration of the mute in minutes (only for the mute action)" + } + } + ], + "default": { + "en": { + "title": "Moderation action taken against %pinger-name%", + "description": "I have taken action against %pinger-mention% for pinging protected users/roles %pings% times within %timeframe% days.\n **Action:** %action%\n**Duration:** %duration% minutes", + "color": "#ed4245" + } + } + } + ] +} \ No newline at end of file diff --git a/modules/ping-protection/configs/storage.json b/modules/ping-protection/configs/storage.json new file mode 100644 index 00000000..54395ab5 --- /dev/null +++ b/modules/ping-protection/configs/storage.json @@ -0,0 +1,97 @@ +{ + "filename": "storage.json", + "humanName": { + "en": "Data Storage" + }, + "description": { + "en": "Configure how long moderation logs and leaver data are kept." + }, + "content": [ + { + "name": "enablePingHistory", + "humanName": { + "en": "Enable Ping History" + }, + "description": { + "en": "If enabled, the bot will keep a history of pings to enforce moderation actions." + }, + "type": "boolean", + "default": { + "en": true + } + }, + { + "name": "pingHistoryRetention", + "humanName": { + "en": "Ping History Retention" + }, + "description": { + "en": "Decides on how long to keep ping logs. Minimum is 4 weeks (1 month) with a maximum of 24 weeks (6 months). This is the length factor of the 'Basic' punishment timeframe." + }, + "type": "integer", + "default": { + "en": 12 + }, + "minValue": "4", + "maxValue": "24", + "dependsOn": "enablePingHistory" + }, + { + "name": "deleteAllPingHistoryAfterTimeframe", + "humanName": { + "en": "Delete all the pings in history after the timeframe?" + }, + "description": { + "en": "If enabled, the bot will delete ALL the pings history of an user after the timeframe instead of only the ping(s) exceeding the timeframe in the history." + }, + "type": "boolean", + "default": { + "en": false + } + }, + { + "name": "modLogRetention", + "humanName": { + "en": "Moderation Log Retention (Months)" + }, + "description": { + "en": "How long to keep records of punishments (1-12 Months). This is applied when moderation actions are enabled." + }, + "type": "integer", + "default": { + "en": 6 + }, + "minValue": "1", + "maxValue": "12" + }, + { + "name": "enableLeaverDataRetention", + "humanName": { + "en": "Keep user logs after they leave" + }, + "description": { + "en": "If enabled, the bot will keep a history of the user after they leave." + }, + "type": "boolean", + "default": { + "en": true + } + }, + { + "name": "leaverRetention", + "humanName": { + "en": "Leaver Data Retention (Days)" + }, + "description": { + "en": "How long to keep data after a user leaves (1-7 Days)." + }, + "type": "integer", + "default": { + "en": 1 + }, + "minValue": "1", + "maxValue": "7", + "dependsOn": "enableLeaverDataRetention" + } + ] +} \ No newline at end of file diff --git a/modules/ping-protection/events/autoModerationActionExecution.js b/modules/ping-protection/events/autoModerationActionExecution.js new file mode 100644 index 00000000..22f80fae --- /dev/null +++ b/modules/ping-protection/events/autoModerationActionExecution.js @@ -0,0 +1,35 @@ +const { processPing } = require('../ping-protection'); + +// Handles auto mod actions +module.exports.run = async function (client, execution) { + if (execution.ruleTriggerType !== 1) return; + + const config = client.configurations['ping-protection']['configuration']; + if (config.ignoredUsers.includes(execution.userId)) return; + + const matchedKeyword = execution.matchedKeyword || ""; + const rawId = matchedKeyword.replace(/[^0-9]/g, ''); + + let isProtected = config.protectedRoles.includes(rawId) || config.protectedUsers.includes(rawId); + + let originChannel = execution.channel; + if (!originChannel && execution.channelId) { + originChannel = await execution.guild.channels.fetch(execution.channelId).catch(() => null); + } + const memberToPunish = await execution.guild.members.fetch(execution.userId).catch(() => null); + + if (!isProtected && config.protectAllUsersWithProtectedRole) { + try { + const targetMember = await execution.guild.members.fetch(rawId); + if (targetMember && targetMember.roles.cache.some(r => config.protectedRoles.includes(r.id))) { + isProtected = true; + } + } catch (e) {} + } + + if (!isProtected) return; + if (!memberToPunish) return; + + const isRole = config.protectedRoles.includes(rawId); + await processPing(client, execution.userId, rawId, isRole, 'Blocked by AutoMod', originChannel, memberToPunish); +}; \ No newline at end of file diff --git a/modules/ping-protection/events/botReady.js b/modules/ping-protection/events/botReady.js new file mode 100644 index 00000000..6e43412d --- /dev/null +++ b/modules/ping-protection/events/botReady.js @@ -0,0 +1,14 @@ +const { enforceRetention, syncNativeAutoMod } = require('../ping-protection'); +const schedule = require('node-schedule'); + +module.exports.run = async function (client) { + await enforceRetention(client); + await syncNativeAutoMod(client); + + // Daily job + const job = schedule.scheduleJob('0 3 * * *', async () => { + await enforceRetention(client); + await syncNativeAutoMod(client); + }); + client.jobs.push(job); +}; \ No newline at end of file diff --git a/modules/ping-protection/events/guildMemberAdd.js b/modules/ping-protection/events/guildMemberAdd.js new file mode 100644 index 00000000..8420f997 --- /dev/null +++ b/modules/ping-protection/events/guildMemberAdd.js @@ -0,0 +1,12 @@ +/** + * Checks when a member rejoins the server and updates their leaver status + */ + +const { markUserAsRejoined } = require('../ping-protection'); + +module.exports.run = async function (client, member) { + if (!client.botReadyAt) return; + if (member.guild.id !== client.guildID) return; + + await markUserAsRejoined(client, member.id); +}; \ No newline at end of file diff --git a/modules/ping-protection/events/guildMemberRemove.js b/modules/ping-protection/events/guildMemberRemove.js new file mode 100644 index 00000000..58fa7704 --- /dev/null +++ b/modules/ping-protection/events/guildMemberRemove.js @@ -0,0 +1,18 @@ +/** + * Checks when a member leaves the server and handles data retention and/or deletion + */ + +const { markUserAsLeft, deleteAllUserData } = require('../ping-protection'); + +module.exports.run = async function (client, member) { + if (!client.botReadyAt) return; + if (member.guild.id !== client.guildID) return; + + const storageConfig = client.configurations['ping-protection']['storage']; + + if (storageConfig && storageConfig.enableLeaverDataRetention) { + await markUserAsLeft(client, member.id); + } else { + await deleteAllUserData(client, member.id); + } +}; \ No newline at end of file diff --git a/modules/ping-protection/events/interactionCreate.js b/modules/ping-protection/events/interactionCreate.js new file mode 100644 index 00000000..05fd99bb --- /dev/null +++ b/modules/ping-protection/events/interactionCreate.js @@ -0,0 +1,94 @@ +const { ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder, MessageFlags } = require('discord.js'); +const { deleteAllUserData, generateHistoryResponse, generateActionsResponse } = require('../ping-protection'); +const { localize } = require('../../../src/functions/localize'); + +// Interaction handler +module.exports.run = async function (client, interaction) { + if (!client.botReadyAt) return; + + if (interaction.isButton() && interaction.customId.startsWith('ping-protection_')) { + + // Ping history pagination + if (interaction.customId.startsWith('ping-protection_hist-page_')) { + const parts = interaction.customId.split('_'); + const userId = parts[2]; + const targetPage = parseInt(parts[3]); + + const replyOptions = await generateHistoryResponse(client, userId, targetPage); + await interaction.update(replyOptions); + return; + } + + if (interaction.customId.startsWith('ping-protection_mod-page_')) { + const parts = interaction.customId.split('_'); + const userId = parts[2]; + const targetPage = parseInt(parts[3]); + + const replyOptions = await generateActionsResponse(client, userId, targetPage); + await interaction.update(replyOptions); + return; + } + + // Panel buttons + const [prefix, action, userId] = interaction.customId.split('_'); + + const isAdmin = interaction.member.permissions.has('Administrator') || + (client.config.admins || []).includes(interaction.user.id); + + if (['history', 'actions', 'delete'].includes(action)) { + if (!isAdmin) return interaction.reply({ + content: localize('ping-protection', 'no-permission'), + flags: MessageFlags.Ephemeral }); + } + + if (action === 'history') { + const replyOptions = await generateHistoryResponse(client, userId, 1); + await interaction.reply({ + ...replyOptions, + flags: MessageFlags.Ephemeral + }); + } + + else if (action === 'actions') { + const replyOptions = await generateActionsResponse(client, userId, 1); + await interaction.reply({ + ...replyOptions, + flags: MessageFlags.Ephemeral + }); + } + else if (action === 'delete') { + const modal = new ModalBuilder() + .setCustomId(`ping-protection_confirm-delete_${userId}`) + .setTitle(localize('ping-protection', 'modal-title')); + + const input = new TextInputBuilder() + .setCustomId('confirmation_text') + .setLabel(localize('ping-protection', 'modal-label')) + .setStyle(TextInputStyle.Paragraph) + .setPlaceholder(localize('ping-protection', 'modal-phrase')) + .setRequired(true); + + const row = new ActionRowBuilder().addComponents(input); + modal.addComponents(row); + + await interaction.showModal(modal); + } + } + + if (interaction.isModalSubmit() && interaction.customId.startsWith('ping-protection_confirm-delete_')) { + const userId = interaction.customId.split('_')[2]; + const userInput = interaction.fields.getTextInputValue('confirmation_text'); + const requiredPhrase = localize('ping-protection', 'modal-phrase', { locale: interaction.locale }); + + if (userInput === requiredPhrase) { + await deleteAllUserData(client, userId); + await interaction.reply({ + content: `✅ ${localize('ping-protection', 'log-manual-delete', {u: userId})}`, + flags: MessageFlags.Ephemeral }); + } else { + await interaction.reply({ + content: `❌ ${localize('ping-protection', 'modal-failed')}`, + flags: MessageFlags.Ephemeral }); + } + } +}; \ No newline at end of file diff --git a/modules/ping-protection/events/messageCreate.js b/modules/ping-protection/events/messageCreate.js new file mode 100644 index 00000000..e551fb04 --- /dev/null +++ b/modules/ping-protection/events/messageCreate.js @@ -0,0 +1,135 @@ +const { + processPing, + sendPingWarning +} = require('../ping-protection'); +const { localize } = require('../../../src/functions/localize'); +const { randomElementFromArray } = require('../../../src/functions/helpers'); + +// Tracks the last meme for duplicates + counts for grind message +const lastMemeMap = new Map(); +const selfPingCountMap = new Map(); + +// Handles messages +module.exports.run = async function (client, message) { + if (!client.botReadyAt) return; + if (!message.guild) return; + if (message.guild.id !== client.guildID) return; + + const config = client.configurations['ping-protection']['configuration']; + + if (message.author.bot) return; + + if (config.ignoredChannels.includes(message.channel.id)) return; + if (config.ignoredUsers.includes(message.author.id)) return; + if (message.member.roles.cache.some(role => config.ignoredRoles.includes(role.id))) return; + + // Check for protected pings + const pingedProtectedRole = message.mentions.roles.some(role => config.protectedRoles.includes(role.id)); + const protectedMentions = new Set(); + const mentionedUsers = message.mentions.users; + + if (mentionedUsers.size > 0) { + mentionedUsers.forEach(user => { + if (config.protectedUsers.includes(user.id)) { + protectedMentions.add(user.id); + } + else if (config.protectAllUsersWithProtectedRole) { + const member = message.mentions.members.get(user.id); + if (member && member.roles.cache.some(r => config.protectedRoles.includes(r.id))) { + protectedMentions.add(user.id); + } + } + }); + } + + // Handles reply pings + if (config.allowReplyPings && message.mentions.repliedUser) { + const repliedId = message.mentions.repliedUser.id; + + if (protectedMentions.has(repliedId)) { + const manualMentionRegex = new RegExp(`<@!?${repliedId}>`); + const isManualPing = manualMentionRegex.test(message.content); + + if (!isManualPing) { + protectedMentions.delete(repliedId); + } + } + } + + // Determines if any protected entities were pinged + const pingedProtectedUser = protectedMentions.size > 0; + + if (!pingedProtectedRole && !pingedProtectedUser) return; + + let target = null; + if (pingedProtectedUser) { + const firstId = protectedMentions.values().next().value; + target = message.mentions.users.get(firstId); + } else if (pingedProtectedRole) { + target = message.mentions.roles.find(r => config.protectedRoles.includes(r.id)); + } + + if (!target) return; + + // Funny easter egg when they ping themselves + if (target.id === message.author.id && config.selfPingConfiguration === "Ignored") return; + if (target.id === message.author.id && config.selfPingConfiguration === "Get fun easter eggs when pinging themselves") { + const secretChance = 0.01; // Secret for a reason.. (1% chance) + const standardMemes = [ + localize('ping-protection', 'meme-why'), + localize('ping-protection', 'meme-played'), + localize('ping-protection', 'meme-spider') + ]; + const secretMeme = localize('ping-protection', 'meme-rick'); + const currentCount = (selfPingCountMap.get(message.author.id) || 0) + 1; + selfPingCountMap.set(message.author.id, currentCount); + + setTimeout(() => { + selfPingCountMap.delete(message.author.id); + }, 300000); + + const roll = Math.random(); + let content = ''; + + if (roll < secretChance) { + content = secretMeme; + lastMemeMap.set(message.author.id, -1); + selfPingCountMap.delete(message.author.id); + } else if (currentCount === 5) { + content = localize('ping-protection', 'meme-grind'); + } else { + const lastIndex = lastMemeMap.get(message.author.id); + + let possibleMemes = standardMemes.map((_, index) => index); + if (lastIndex !== undefined && lastIndex !== -1 && standardMemes.length > 1) { + possibleMemes = possibleMemes.filter(i => i !== lastIndex); + } + + const randomIndex = randomElementFromArray(possibleMemes); + content = standardMemes[randomIndex]; + lastMemeMap.set(message.author.id, randomIndex); + } + await message.reply({ content: content }).catch(() => {}); + return; + } + + await sendPingWarning(client, message, target, config); + + const isRole = !target.username; + let memberToPunish = message.member; + if (!memberToPunish) { + try { + memberToPunish = await message.guild.members.fetch(message.author.id); + } catch (e) {return;} + } + + await processPing( + client, + message.author.id, + target.id, + isRole, + message.url, + message.channel, + memberToPunish + ); +}; \ No newline at end of file diff --git a/modules/ping-protection/models/LeaverData.js b/modules/ping-protection/models/LeaverData.js new file mode 100644 index 00000000..1727dcff --- /dev/null +++ b/modules/ping-protection/models/LeaverData.js @@ -0,0 +1,25 @@ +const { DataTypes, Model } = require('sequelize'); + +module.exports = class PingProtectionLeaverData extends Model { + static init(sequelize) { + return super.init({ + userId: { + type: DataTypes.STRING, + primaryKey: true + }, + leftAt: { + type: DataTypes.DATE, + defaultValue: DataTypes.NOW + } + }, { + tableName: 'ping_protection_leaver_data', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + name: 'LeaverData', + module: 'ping-protection' +}; \ No newline at end of file diff --git a/modules/ping-protection/models/ModerationLog.js b/modules/ping-protection/models/ModerationLog.js new file mode 100644 index 00000000..c90099f8 --- /dev/null +++ b/modules/ping-protection/models/ModerationLog.js @@ -0,0 +1,39 @@ +const { DataTypes, Model } = require('sequelize'); + +module.exports = class PingProtectionModerationLog extends Model { + static init(sequelize) { + return super.init({ + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false + }, + victimID: { + type: DataTypes.STRING, + allowNull: false + }, + type: { + type: DataTypes.STRING, + allowNull: false + }, + reason: { + type: DataTypes.STRING, + allowNull: true + }, + actionDuration: { + type: DataTypes.INTEGER, + allowNull: true + }, + }, { + tableName: 'ping_protection_mod_log', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'ModerationLog', + 'module': 'ping-protection' +}; \ No newline at end of file diff --git a/modules/ping-protection/models/PingHistory.js b/modules/ping-protection/models/PingHistory.js new file mode 100644 index 00000000..268418a8 --- /dev/null +++ b/modules/ping-protection/models/PingHistory.js @@ -0,0 +1,33 @@ +const { DataTypes, Model } = require('sequelize'); + +module.exports = class PingProtectionPingHistory extends Model { + static init(sequelize) { + return super.init({ + userId: { + type: DataTypes.STRING, + allowNull: false + }, + messageUrl: { + type: DataTypes.STRING, + allowNull: true + }, + targetId: { + type: DataTypes.STRING, + allowNull: true + }, + isRole: { + type: DataTypes.BOOLEAN, + defaultValue: false + } + }, { + tableName: 'ping_protection_history', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + name: 'PingHistory', + module: 'ping-protection' +}; \ No newline at end of file diff --git a/modules/ping-protection/module.json b/modules/ping-protection/module.json new file mode 100644 index 00000000..b945a1c7 --- /dev/null +++ b/modules/ping-protection/module.json @@ -0,0 +1,28 @@ +{ + "name": "ping-protection", + "author": { + "scnxOrgID": "148", + "name": "Kevin", + "link": "https://github.com/Kevinking500" + }, + "openSourceURL": "https://github.com/Kevinking500/CustomDCBot/tree/main/modules/ping-protection", + "commands-dir": "/commands", + "events-dir": "/events", + "models-dir": "/models", + "config-example-files": [ + "configs/configuration.json", + "configs/moderation.json", + "configs/storage.json" + ], + "tags": [ + "moderation" + ], + "humanReadableName": { + "en": "Ping-Protection", + "de": "Ping-Schutz" + }, + "description": { + "en": "Powerful and highly customizable ping-protection module to protect members/roles from unwanted mentions with moderation capabilities.", + "de": "Leistungsstarkes und hochgradig anpassbares Ping-Schutz-Modul zum Schutz von Mitgliedern/Rollen vor unerwünschten Erwähnungen mit Moderationsfunktionen." + } +} \ No newline at end of file diff --git a/modules/ping-protection/ping-protection.js b/modules/ping-protection/ping-protection.js new file mode 100644 index 00000000..34b3579c --- /dev/null +++ b/modules/ping-protection/ping-protection.js @@ -0,0 +1,665 @@ +/** + * Logic for the Ping Protection module + * @module ping-protection + * @author itskevinnn + */ +const { Op } = require('sequelize'); +const { ActionRowBuilder, ButtonBuilder, EmbedBuilder, ButtonStyle } = require('discord.js'); +const { embedType, embedTypeV2, formatDate } = require('../../src/functions/helpers'); +const { localize } = require('../../src/functions/localize'); + +const recentPings = new Set(); + +// Data handling +async function addPing(client, userId, messageUrl, targetId, isRole) { + const config = client.configurations['ping-protection']['configuration']; + const duplicateWindow = config.enableAutomod ? 5000 : 2000; + const debounceKey = `${userId}_${targetId}`; + + if (recentPings.has(debounceKey)) return; + recentPings.add(debounceKey); + setTimeout(() => { + recentPings.delete(debounceKey); + }, duplicateWindow); + + const recentDuplicate = await client.models['ping-protection']['PingHistory'].findOne({ + where: { + userId: userId, + targetId: targetId, + createdAt: { [Op.gt]: new Date(Date.now() - duplicateWindow) } + } + }); + + if (recentDuplicate) return; + await client.models['ping-protection']['PingHistory'].create({ + userId: userId, + messageUrl: messageUrl || 'Blocked by AutoMod', + targetId: targetId, + isRole: isRole + }); +} +// Gets ping count in timeframe +async function getPingCountInWindow(client, userId, days) { + const cutoffDate = new Date(); + cutoffDate.setDate(cutoffDate.getDate() - days); + + return await client.models['ping-protection']['PingHistory'].count({ + where: { + userId: userId, + createdAt: { [Op.gt]: cutoffDate } + } + }); +} +// Fetches ping history +async function fetchPingHistory(client, userId, page = 1, limit = 8) { + const offset = (page - 1) * limit; + const { count, rows } = await client.models['ping-protection']['PingHistory'].findAndCountAll({ + where: { userId: userId }, + order: [['createdAt', 'DESC']], + limit: limit, + offset: offset + }); + return { total: count, history: rows }; +} +// Fetches moderation history +async function fetchModHistory(client, userId, page = 1, limit = 8) { + if (!client.models['ping-protection'] || !client.models['ping-protection']['ModerationLog']) return { total: 0, history: [] }; + try { + const offset = (page - 1) * limit; + const { count, rows } = await client.models['ping-protection']['ModerationLog'].findAndCountAll({ + where: { victimID: userId }, + order: [['createdAt', 'DESC']], + limit: limit, + offset: offset + }); + return { total: count, history: rows }; + } catch (e) { + return { total: 0, history: [] }; + } +} +// Gets leaver status +async function getLeaverStatus(client, userId) { + return await client.models['ping-protection']['LeaverData'].findByPk(userId); +} + +// Makes sure the channel ID from config is valid for Discord +function getSafeChannelId(configValue) { + if (!configValue) return null; + let rawId = null; + if (Array.isArray(configValue) && configValue.length > 0) rawId = configValue[0]; + else if (typeof configValue === 'string') rawId = configValue; + + if (rawId && (typeof rawId === 'string' || typeof rawId === 'number')) { + const finalId = rawId.toString(); + if (finalId.length > 5) return finalId; + } + return null; +} +// Sends ping warning message +async function sendPingWarning(client, message, target, moduleConfig) { + const warningMsg = moduleConfig.pingWarningMessage; + if (!warningMsg) return; + + let warnMsg = { ...warningMsg }; + const placeholders = { + '%target-name%': target.name || target.tag || target.username || 'Unknown', + '%target-mention%': target.toString(), + '%target-id%': target.id, + '%pinger-id%': message.author.id + }; + + try { + let messageOptions = await embedTypeV2(warnMsg, placeholders); + return message.reply(messageOptions).catch(async () => { + return message.channel.send(messageOptions).catch(() => {}); + }); + } catch (error) { + client.logger.warn(`[Ping Protection] ${error.message}`); + } +} + +// Syncs the native AutoMod rule based on configuration +async function syncNativeAutoMod(client) { + const config = client.configurations['ping-protection']['configuration']; + + try { + const guild = await client.guilds.fetch(client.guildID); + const rules = await guild.autoModerationRules.fetch(); + const existingRule = rules.find(r => r.name === 'Ping Protection System'); + + // Logic to disable/delete the rule + if (!config || !config.enableAutomod) { + if (existingRule) { + await existingRule.delete().catch(() => {}); + } + return; + } + + const keywords = []; + if (config.protectedRoles) { + config.protectedRoles.forEach(roleId => { + keywords.push(`<@&${roleId}>`); + }); + } + + const protectedIdsSet = new Set(config.protectedUsers || []); + if (config.protectAllUsersWithProtectedRole && config.protectedRoles && config.protectedRoles.length > 0) { + guild.members.cache.forEach(member => { + if (member.roles.cache.some(r => config.protectedRoles.includes(r.id))) { + protectedIdsSet.add(member.id); + } + }); + } + + protectedIdsSet.forEach(id => { + keywords.push(`<@${id}>`); + keywords.push(`<@!${id}>`); + }); + + if (keywords.length === 0) { + if (existingRule) { + await existingRule.delete().catch(() => {}); + } + return; + } + + if (keywords.length > 1000) { + client.logger.warn(localize('ping-protection', 'log-automod-keyword-limit')); + keywords.splice(1000); + } + + // AutoMod rule data + const actions = []; + const blockMetadata = {}; + if (config.autoModBlockMessage) { + blockMetadata.customMessage = config.autoModBlockMessage; + } + actions.push({ type: 1, metadata: blockMetadata }); + + const alertChannelId = getSafeChannelId(config.autoModLogChannel); + if (alertChannelId) { + actions.push({ + type: 2, + metadata: { channel: alertChannelId } + }); + } + + const ruleData = { + name: 'Ping Protection System', + eventType: 1, + triggerType: 1, + triggerMetadata: { + keywordFilter: keywords + }, + actions: actions, + enabled: true, + exemptRoles: config.ignoredRoles || [], + exemptChannels: config.ignoredChannels || [] + }; + + if (existingRule) { + await guild.autoModerationRules.edit(existingRule.id, ruleData); + } else { + await guild.autoModerationRules.create(ruleData); + } + } catch (error) { + client.logger.error(`[ping-protection] AutoMod Sync/Cleanup Failed: ${error.message}`); + } +} + +// Makes the history embed +async function generateHistoryResponse(client, userId, page = 1) { + const storageConfig = client.configurations['ping-protection']['storage']; + const limit = 8; + const isEnabled = !!storageConfig.enablePingHistory; + + let total = 0, history = [], totalPages = 1; + + if (isEnabled) { + const data = await fetchPingHistory(client, userId, page, limit); + total = data.total; + history = data.history; + totalPages = Math.ceil(total / limit) || 1; + } + + const user = await client.users.fetch(userId).catch(() => ({ + username: 'Unknown User', + displayAvatarURL: () => null + })); + + const leaverData = await getLeaverStatus(client, userId); + let description = ""; + + if (leaverData) { + const dateStr = formatDate(leaverData.leftAt); + const warningKey = history.length > 0 + ? 'leaver-warning-long' + : 'leaver-warning-short'; + description += `⚠️ ${localize('ping-protection', warningKey, { d: dateStr })}\n\n`; + } + + if (!isEnabled) { + description += localize('ping-protection', 'history-disabled'); + } else if (history.length === 0) { + description += localize('ping-protection', 'no-data-found'); + } else { + const lines = history.map((entry, index) => { + const timeString = formatDate(entry.createdAt); + + let targetString = "Detected"; + if (entry.targetId) { + targetString = entry.isRole ? `<@&${entry.targetId}>` : `<@${entry.targetId}>`; + } + + const hasValidLink = entry.messageUrl && entry.messageUrl !== 'Blocked by AutoMod'; + const linkText = hasValidLink + ? `[${localize('ping-protection', 'label-jump')}](${entry.messageUrl})` + : localize('ping-protection', 'no-message-link'); + + return localize('ping-protection', 'list-entry-text', { + index: (page - 1) * limit + index + 1, + target: targetString, + time: timeString, + link: linkText + }); + }); + description += lines.join('\n\n'); + } + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(`ping-protection_hist-page_${userId}_${page - 1}`) + .setLabel(localize('helpers', 'back')) + .setStyle(ButtonStyle.Primary) + .setDisabled(page <= 1), + new ButtonBuilder() + .setCustomId('ping_protection_page_count') + .setLabel(`${page}/${totalPages}`) + .setStyle(ButtonStyle.Secondary) + .setDisabled(true), + new ButtonBuilder() + .setCustomId(`ping-protection_hist-page_${userId}_${page + 1}`) + .setLabel(localize('helpers', 'next')) + .setStyle(ButtonStyle.Primary) + .setDisabled(page >= totalPages || !isEnabled) + ); + + const embed = new EmbedBuilder() + .setTitle(localize('ping-protection', 'embed-history-title', { + u: user.username + })) + .setThumbnail(user.displayAvatarURL({ + dynamic: true + })) + .setDescription(description) + .setColor('Orange') + .setFooter({ + text: client.strings.footer, + iconURL: client.strings.footerImgUrl + }); + + if (!client.strings.disableFooterTimestamp) embed.setTimestamp(); + return { + embeds: [embed.toJSON()], + components: [row.toJSON()] + }; +} + +// Makes the moderation actions history embed +async function generateActionsResponse(client, userId, page = 1) { + const moderationConfig = client.configurations['ping-protection']['moderation']; + const limit = 8; + const isEnabled = moderationConfig && Array.isArray(moderationConfig) && moderationConfig.length > 0; + + let total = 0, history = [], totalPages = 1; + + const data = await fetchModHistory(client, userId, page, limit); + total = data.total; + history = data.history; + totalPages = Math.ceil(total / limit) || 1; + + const user = await client.users.fetch(userId).catch(() => ({ + username: 'Unknown User', + displayAvatarURL: () => null + })); + + let description = ""; + + if (history.length === 0) { + description += localize('ping-protection', 'no-data-found'); + } else { + const lines = history.map((entry, index) => { + const duration = entry.actionDuration ? ` (${entry.actionDuration}m)` : ''; + const reasonText = entry.reason || localize('ping-protection', 'no-reason') || 'No reason'; + return `${(page - 1) * limit + index + 1}. **${entry.type}${duration}** - ${formatDate(entry.createdAt)}\n${localize('ping-protection', 'label-reason')}: ${reasonText}`; + }); + description += lines.join('\n\n') + `\n\n*${localize('ping-protection', 'actions-retention-note')}*`; + } + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(`ping-protection_mod-page_${userId}_${page - 1}`) + .setLabel(localize('helpers', 'back')) + .setStyle(ButtonStyle.Primary) + .setDisabled(page <= 1), + new ButtonBuilder() + .setCustomId('ping_protection_page_count') + .setLabel(`${page}/${totalPages}`) + .setStyle(ButtonStyle.Secondary) + .setDisabled(true), + new ButtonBuilder() + .setCustomId(`ping-protection_mod-page_${userId}_${page + 1}`) + .setLabel(localize('helpers', 'next')) + .setStyle(ButtonStyle.Primary) + .setDisabled(page >= totalPages || (!isEnabled && history.length === 0)) + ); + + const embed = new EmbedBuilder() + .setTitle(localize('ping-protection', 'embed-actions-title', { + u: user.username + })) + .setThumbnail(user.displayAvatarURL({ + dynamic: true + })) + .setDescription(description) + .setColor(isEnabled + ? 'Red' + : 'Grey' + ) + .setFooter({ + text: client.strings.footer, + iconURL: client.strings.footerImgUrl + }); + + if (!client.strings.disableFooterTimestamp) embed.setTimestamp(); + return { + embeds: [embed.toJSON()], + components: [row.toJSON()] + }; +} + +// Handles data deletion +async function deleteAllUserData(client, userId) { + await client.models['ping-protection']['PingHistory'].destroy({ + where: { userId: userId } + }); + await client.models['ping-protection']['ModerationLog'].destroy({ + where: { victimID: userId } + }); + await client.models['ping-protection']['LeaverData'].destroy({ + where: { userId: userId } + }); + client.logger.info(localize('ping-protection', 'log-manual-delete-logs', { + u: userId + })); +} + +async function markUserAsLeft(client, userId) { + await client.models['ping-protection']['LeaverData'].upsert({ + userId: userId, + leftAt: new Date() + }); +} + +async function markUserAsRejoined(client, userId) { + await client.models['ping-protection']['LeaverData'].destroy({ + where: { userId: userId } + }); +} + +// Enforces data retention +async function enforceRetention(client) { + const storageConfig = client.configurations['ping-protection']['storage']; + if (!storageConfig) return; + + if (storageConfig.enablePingHistory) { + const historyCutoff = new Date(); + const retentionWeeks = storageConfig.pingHistoryRetention || 12; + historyCutoff.setDate(historyCutoff.getDate() - (retentionWeeks * 7)); + + if (storageConfig.DeleteAllPingHistoryAfterTimeframe) { + const usersWithExpiredData = await client.models['ping-protection']['PingHistory'].findAll({ + where: { + createdAt: { [Op.lt]: historyCutoff } + }, + attributes: ['userId'], + group: ['userId'] + }); + + const userIdsToWipe = usersWithExpiredData.map(entry => entry.userId); + if (userIdsToWipe.length > 0) { + await client.models['ping-protection']['PingHistory'].destroy({ + where: { userId: userIdsToWipe } + }); + } + } + else { + await client.models['ping-protection']['PingHistory'].destroy({ + where: { createdAt: { [Op.lt]: historyCutoff } } + }); + } + } + if (storageConfig.modLogRetention) { + const modCutoff = new Date(); + modCutoff.setMonth(modCutoff.getMonth() - (storageConfig.modLogRetention || 6)); + await client.models['ping-protection']['ModerationLog'].destroy({ + where: { + createdAt: { [Op.lt]: modCutoff } + } + }); + } + if (storageConfig.enableLeaverDataRetention) { + const leaverCutoff = new Date(); + leaverCutoff.setDate(leaverCutoff.getDate() - (storageConfig.leaverRetention || 1)); + const leaversToDelete = await client.models['ping-protection']['LeaverData'].findAll({ + where: { + leftAt: { [Op.lt]: leaverCutoff } + } + }); + for (const leaver of leaversToDelete) { + await deleteAllUserData(client, leaver.userId); + await leaver.destroy(); + } + } +} + +// Executes moderation action +async function executeAction(client, member, rule, reason, storageConfig, originChannel = null, stats = {}) { + const actionType = rule.actionType; + + // Sends action log if enabled + const sendActionLog = async () => { + if (!rule.enableActionLogging || !originChannel) return; + + const logMsgConfig = rule.actionLogMessage; + if (!logMsgConfig) return; + let safeMsg = { ...logMsgConfig }; + + const placeholders = { + '%pinger-mention%': member.toString(), + '%pinger-name%': member.user.tag, + '%action%': rule.actionType, + '%duration%': rule.muteDuration || 'N/A', + '%pings%': stats.pingCount || 'N/A', + '%timeframe%': stats.timeframeDays || 'N/A' + }; + + try { + let messageOptions = await embedTypeV2(safeMsg, placeholders); + await originChannel.send(messageOptions).catch(() => {}); + } catch (error) { + client.logger.warn(localize('ping-protection', 'log-action-log-failed', { + e: error.message + })); + } + }; + + // Sends error message if action fails + const sendErrorLog = async (error) => { + if (!originChannel) return; + + const errorEmbed = new EmbedBuilder() + .setTitle(localize('ping-protection', 'punish-log-failed-title', { + u: member.user.tag + })) + .setDescription( + localize('ping-protection', 'punish-log-failed-desc', { + m: member.toString() + }) + + `\n${localize('ping-protection', 'punish-log-error', { + e: error.message + })}` + ) + .setColor("#ed4245") + .setFooter({ + text: client.strings.footer, + iconURL: client.strings.footerImgUrl + }); + if (!client.strings.disableFooterTimestamp) errorEmbed.setTimestamp(); + + await originChannel.send({ embeds: [errorEmbed.toJSON()] }).catch(() => {}); + }; + + if (!member) { + client.logger.debug(localize('ping-protection', 'log-not-a-member')); + return false; + } + + const botMember = await member.guild.members.fetch(client.user.id); + if (botMember.roles.highest.position <= member.roles.highest.position) { + await sendErrorLog({ + message: localize('ping-protection', 'punish-role-error', { + tag: member.user.tag + }) + }); + client.logger.warn(localize('ping-protection', 'log-punish-role-error', { + tag: member.user.tag + })); + return false; + } + + const logDb = async (type, duration = null) => { + try { + await client.models['ping-protection']['ModerationLog'].create({ + victimID: member.id, type, actionDuration: duration, reason + }); + } catch (dbError) {} + }; + + if (actionType === 'MUTE') { + const durationMs = rule.muteDuration * 60000; + await logDb('MUTE', rule.muteDuration); + try { + await member.timeout(durationMs, reason); + await sendActionLog(); + return true; + } catch (error) { + await sendErrorLog(error); + client.logger.warn(localize('ping-protection', 'log-mute-error', { + tag: member.user.tag, + e: error.message + })); + return false; + } + + } + else if (actionType === 'KICK') { + await logDb('KICK'); + try { + await member.kick(reason); + await sendActionLog(); + return true; + } catch (error) { + await sendErrorLog(error); + client.logger.warn(localize('ping-protection', 'log-kick-error', { + tag: member.user.tag, + e: error.message + })); + return false; + } + } + return false; +} + +// Processes a ping event +async function processPing(client, userId, targetId, isRole, messageUrl, originChannel, memberToPunish) { + const config = client.configurations['ping-protection']['configuration']; + const storageConfig = client.configurations['ping-protection']['storage']; + const moderationRules = client.configurations['ping-protection']['moderation']; + + if (storageConfig?.enablePingHistory) { + try { + await addPing(client, userId, messageUrl, targetId, isRole); + } catch (e) {} + } + + if (!moderationRules || !Array.isArray(moderationRules) || moderationRules.length === 0) return; + + for (let i = moderationRules.length - 1; i >= 0; i--) { + const rule = moderationRules[i]; + + const retentionWeeks = storageConfig?.pingHistoryRetention || 12; + const timeframeDays = rule.useCustomTimeframe + ? (rule.timeframeDays || 7) + : (retentionWeeks * 7); + + const pingCount = await getPingCountInWindow(client, userId, timeframeDays); + const requiredCount = rule.useCustomTimeframe + ? rule.pingsCountAdvanced + : rule.pingsCountBasic; + + if (pingCount >= requiredCount) { + const oneMinuteAgo = new Date(Date.now() - 60000); + try { + const recentLog = await client.models['ping-protection']['ModerationLog'].findOne({ + where: { + victimID: userId, + createdAt: { [Op.gt]: oneMinuteAgo } + } + }); + if (recentLog) break; + } catch (e) {} + + const generatedReason = rule.useCustomTimeframe + ? localize('ping-protection', 'reason-advanced', { + c: pingCount, + d: timeframeDays }) + : localize('ping-protection', 'reason-basic', { + c: pingCount, + w: retentionWeeks }); + + if (memberToPunish) { + const success = await executeAction( + client, + memberToPunish, + rule, + generatedReason, + storageConfig, + originChannel, + { pingCount, timeframeDays } + ); + + if (success) break; + } + } + } +} + +module.exports = { + addPing, + getPingCountInWindow, + sendPingWarning, + syncNativeAutoMod, + processPing, + fetchPingHistory, + fetchModHistory, + executeAction, + deleteAllUserData, + getLeaverStatus, + markUserAsLeft, + markUserAsRejoined, + enforceRetention, + generateHistoryResponse, + generateActionsResponse, + getSafeChannelId +}; \ No newline at end of file From de23ed25678aac906645b7be8352321bd5ab15bf Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 18 Feb 2026 01:13:37 +0100 Subject: [PATCH 06/27] Ping Protection V1.1 (#171) * Added the base module folders and module.json * Added all folders necessary and the configuration files for each folder * Added a test command * removed the manage file * Added, renamed and deleted some files as necessary and coded the models. * Renamed action.js to moderation.js, coded multiple things, added a new file for correct tracking. * Forgot to update module.json, now updated aswell * Added additional information in ping-protection.js * Disabled allowing reply pings, added the enable moderation and enable advanced configuration in moderation.json and made the choices inside depend on it because I forgot to :/ Added the options to enable/disable pings/modlogs/logs kept after leave and made the choices depend on it + made those choices with numbers select instead of integers for almost 0 user-error issues. * Added support for actually correct parameters and those parameters added into the message editor for the warning message * Added proper support for localization, and coded the events * Completed the full module and fixed some critical bugs that caused the bot to crash * Cleaned up some code notes I used for debugging * Completely finished the module and worked tirelessly for many hours to debug code, has been tested and is currently ongoing extensive testing to ensure absolutely everythig works as supposed to * Debugged absolutely everything, removed like 300 lines of code for polish while remaining the same functions. Removed a few locales that are unused and updated some locales for better understanding. Fully tested extensively. Not verified by GitHub because I code in VSCode. * Added the option to lower mod actions history * Made the deault value of pings to trigger action 10 instead of 5 in basic pings count config * Added the commands warnings for most commands Listed the warnings for all commands except the panel command as the bot already checks for administrator perms. * Almost completely rewrote the module to make sure the modules works as supposed to with SCNX. * Added "automod" abilities - Will now delete the original message by default with the message content and allows to configure both options * (not working correctly) added automod integration and some small changes * Fixed the * Removed the feature that didn't work (reposting), adds a custom message to the automod message block. Also the bot now deletes the rule it created if automod enabled = false * Fixed the bug of the bot still sending the warning and punishing if limit reached with reply pings even when it's allowed in config * Added a funny easter egg * Some QOL improvements, including merging the list commands * Added some new options in the config * Update configuration.json * Fix self-ping condition to allow self-pinging * Ping protection V1, in Discord.JS V14 * Ping Protection V1 * Changed code to the requested changes, and adjusted code logic to actually properly support multiple moderation actions (not tested before, and didn't work during testing) * Made adjustments to code as requested, and added an intent to main.js to make it work properly. * Fixed the missing footer on embeds. Fixed a small typo in the default waning message configuration and added an emoji to the why easter egg. * Ping Protection V1.1 * Quickly updated locales for better explanation about exceptions for whitelisted * Ping Protection V1.1 remastered * Another remastered version * Added categories in 2 configs * Used proper emoji's that SCNX supports * Used the new updated ones I suggested that were added (and hopefully also work) * Updated some small changes from Copilot --------- Co-authored-by: Kevinking500 --- locales/en.json | 124 +++++++++--------- .../commands/ping-protection.js | 15 ++- .../configs/configuration.json | 66 ++++++++-- .../ping-protection/configs/moderation.json | 20 +-- modules/ping-protection/configs/storage.json | 39 +++++- .../events/interactionCreate.js | 2 +- modules/ping-protection/ping-protection.js | 18 ++- 7 files changed, 182 insertions(+), 102 deletions(-) diff --git a/locales/en.json b/locales/en.json index 171abe21..ffdbde03 100644 --- a/locales/en.json +++ b/locales/en.json @@ -930,66 +930,66 @@ "nickname-error": "An error occurred while trying to change the nickname of %u: %e" }, "ping-protection": { - "log-not-a-member": "[Ping Protection] Punishment failed: The pinger is not a member.", - "log-punish-role-error": "[Ping Protection] Punishment failed: I cannot punish %tag because their role is higher than or equal to my highest role.", - "log-mute-error": "[Ping Protection] Punishment failed: I cannot mute %tag: %e", - "log-kick-error": "[Ping Protection] Punishment failed: I cannot kick %tag: %e", - "log-action-log-failed": "[Ping Protection] Punishment logging failed: %e", - "log-manual-delete": "All data for <@%u> (%u) has been deleted successfully.", - "log-manual-delete-logs": "[Ping Protection] All data for user with ID %u has been deleted successfully.", - "log-automod-keyword-limit": "[Ping Protection] Automod keywords exceed 1000 characters limit. Keywords were truncated.", - "punish-log-failed-title": "Punishment failed for user %u", - "punish-log-failed-desc": "An error occured while trying to punish the user %m. Please check the bot's permissions and role hierarchy. See the message below for the error.", - "punish-log-error": "Error: ```%e```", - "punish-role-error": "I cannot punish %tag because their role is higher than or equal to my highest role.", - "reason-basic": "User reached %c pings in the last %w weeks.", - "reason-advanced": "User reached %c pings in the last %d days (Custom timeframe).", - "cmd-desc-module": "Ping protection related commands", - "cmd-desc-group-user": "Every command related to the users", - "cmd-desc-history": "View the ping history of a user", - "cmd-opt-user": "The user to check", - "cmd-desc-actions": "View the moderation action history of a user", - "cmd-desc-panel": "Admin: Open the user management panel", - "cmd-desc-group-list": "Lists protected or whitelisted entities", - "cmd-desc-list-protected": "List all protected users and roles", - "cmd-desc-list-wl": "List all whitelisted roles and channels", - "embed-history-title": "Ping history of %u", - "embed-leaver-warning": "This user left the server at %t. These logs will stay until automatic deletion.", - "no-data-found": "No logs found for this user.", - "embed-actions-title": "Moderation history of %u", - "label-reason": "Reason", - "actions-retention-note": "Note: Moderation actions are retained for 1 - 12 months based on the configuration.", - "no-permission": "You don't have sufficient permissions to use this command.", - "panel-title": "User Panel: %u", - "panel-description": "Manage and view data for %u (%i). View a quick recap of their ping and moderation history, or delete all data stored for this user (Risky).", - "btn-history": "Ping history", - "btn-actions": "Actions history", - "btn-delete": "Delete all data (Risky)", - "list-protected-title": "Protected Users and Roles", - "list-protected-desc": "View all protected users and roles here. When someone pings one of these protected user(s)/role(s), a warning will be sent. Exceptions are whitelisted roles, and when sent in a whitelisted channel.", - "field-prot-users": "Protected Users", - "field-prot-roles": "Protected Roles", - "list-whitelist-title": "Whitelisted Roles and Channels", - "list-whitelist-desc": "View all whitelisted roles and channels here. Whitelisted roles will not get a warning for pinging a protected entity, and pings will be ignored in whitelisted channels.", - "field-wl-roles": "Whitelisted Roles", - "field-wl-channels": "Whitelisted Channels", - "list-none": "None are configured.", - "modal-title": "Confirm data deletion for this user", - "modal-label": "Confirm data deletion by typing this phrase:", - "modal-phrase": "I understand that all data of this user will be deleted and that this action cannot be undone.", - "modal-failed": "The phrase you entered is incorrect. Data deletion cancelled.", - "field-quick-history": "Quick history view (Last %w weeks)", - "field-quick-desc": "Pings history amount: %p\nModeration actions amount: %m", - "history-disabled": "History logging has been disabled by a bot-configurator.\nAre you (one of) the bot-configurators? You can enable history logging in the \"storage\" tab in the 'ping-protection' module ^^", - "leaver-warning-long": "User left at %d. These logs will stay until automatic deletion.", - "leaver-warning-short": "User left at %d.", - "meme-why": "😐 [Why are you the way that you are?]() - You just pinged yourself..", - "meme-played": "🔑 [Congratulations, you played yourself.]()", - "meme-spider": "🕷️ [Is this you?]() - You just pinged yourself.", - "meme-rick": "🎵 [Never gonna give you up, never gonna let you down...]() You just Rick Rolled yourself. Also congrats you unlocked the secret easter egg that only has a 1% chance of appearing!!1!1!!", - "meme-grind": "Why are you even pinging yourself 5 times in a row? Anyways continue some more to possibly get the secret meme\n-# (good luck grinding, only a 1% chance of getting it and during testing I had it once after 83 pings)", - "label-jump": "Jump to Message", - "no-message-link": "This ping was blocked by AutoMod", - "list-entry-text": "%index. **Pinged %target** at %time\n%link" + "log-not-a-member": "[Ping Protection] Punishment failed: The pinger is not a member.", + "log-punish-role-error": "[Ping Protection] Punishment failed: I cannot punish %tag because their role is higher than or equal to my highest role.", + "log-mute-error": "[Ping Protection] Punishment failed: I cannot mute %tag: %e", + "log-kick-error": "[Ping Protection] Punishment failed: I cannot kick %tag: %e", + "log-action-log-failed": "[Ping Protection] Punishment logging failed: %e", + "log-data-deletion": "[Ping Protection] All data for the user with ID %u has been deleted successfully.", + "log-automod-keyword-limit": "[Ping Protection] Automod keywords exceed 1000 characters limit. Keywords were truncated.", + "punish-log-failed-title": "Punishment failed for user %u", + "punish-log-failed-desc": "An error occured while trying to punish the user %m. Please check the bot's permissions and role hierarchy. See the message below for the error.", + "punish-log-error": "Error: ```%e```", + "punish-role-error": "I cannot punish %tag because their role is higher than or equal to my highest role.", + "reason-basic": "User reached %c pings in the last %w weeks.", + "reason-advanced": "User reached %c pings in the last %d days (Custom timeframe).", + "cmd-desc-module": "Ping protection related commands", + "cmd-desc-group-user": "Every command related to the users", + "cmd-desc-history": "View the ping history of a user", + "cmd-opt-user": "The user to check", + "cmd-desc-actions": "View the moderation action history of a user", + "cmd-desc-panel": "Admin: Open the user management panel", + "cmd-desc-group-list": "Lists protected or whitelisted entities", + "cmd-desc-list-protected": "List of all the protected users and roles", + "cmd-desc-list-wl": "List of all the whitelisted roles, channels and users", + "embed-history-title": "Ping history of %u", + "no-data-found": "No logs found for this user.", + "embed-actions-title": "Moderation history of %u", + "label-reason": "Reason", + "actions-retention-note": "Note: Moderation actions are retained for 1 - 12 months based on the configuration.", + "no-permission": "You don't have sufficient permissions to use this command.", + "panel-title": "User Panel: %u", + "panel-description": "Manage and view data for %u (%i). View a quick recap of their ping and moderation history, or delete all data stored for this user (Risky).", + "btn-history": "Ping history", + "btn-actions": "Actions history", + "btn-delete": "Delete all data (Risky)", + "list-protected-title": "Protected Users and Roles", + "list-protected-desc": "View all protected users and roles here. When someone pings one of these protected user(s)/role(s), a warning will be sent. Exceptions are when pinged by someone with a whitelisted role/as a whitelisted user or when it's sent in a whitelisted channel.", + "field-protected-users": "Protected Users", + "field-protected-roles": "Protected Roles", + "list-whitelist-title": "Whitelisted Roles, Users and Channels", + "list-whitelist-desc": "View all whitelisted roles, users and channels here. Whitelisted roles and users will not get a warning for pinging a protected entity, and pings from them or in whitelisted channels will be ignored.", + "field-wl-roles": "Whitelisted Roles", + "field-wl-channels": "Whitelisted Channels", + "field-wl-users": "Whitelisted Users", + "list-none": "None are configured.", + "modal-title": "Confirm data deletion for this user", + "modal-label": "Confirm data deletion by typing this phrase:", + "modal-phrase": "I understand that all data of this user will be deleted and that this action cannot be undone.", + "modal-failed": "The phrase you entered is incorrect. Data deletion cancelled.", + "modal-success-data-deletion": "All data for the user <@%u> (%u) has been deleted successfully", + "field-quick-history": "Quick history view (Last %w weeks)", + "field-quick-desc": "Pings history amount: %p\nModeration actions amount: %m", + "history-disabled": "History logging has been disabled by a bot-configurator.\nAre you (one of) the bot-configurators? You can enable history logging in the \"Data Storage\" tab in the 'ping-protection' module ^^", + "leaver-warning-long": "This user left the server at %d. These logs will stay until automatic deletion.", + "leaver-warning-short": "This user left the server at %d.", + "meme-why": "😐 [Why are you the way that you are?]() - You just pinged yourself..", + "meme-played": "🔑 [Congratulations, you played yourself.]()", + "meme-spider": "🕷️ [Is this you?]() - You just pinged yourself.", + "meme-rick": "🎵 [Never gonna give you up, never gonna let you down...]() You just Rick Rolled yourself. Also congrats you unlocked the secret easter egg that only has a 1% chance of appearing!!1!1!!", + "meme-grind": "Why are you even pinging yourself 5 times in a row? Anyways continue some more to possibly get the secret meme\n-# (good luck grinding, only a 1% chance of getting it and during testing I had it once after 83 pings)", + "label-jump": "Jump to Message", + "no-message-link": "This ping was blocked by AutoMod", + "list-entry-text": "%index. **Pinged %target** at %time\n%link" } -} \ No newline at end of file +} diff --git a/modules/ping-protection/commands/ping-protection.js b/modules/ping-protection/commands/ping-protection.js index 71f1eb9c..d8ac43c7 100644 --- a/modules/ping-protection/commands/ping-protection.js +++ b/modules/ping-protection/commands/ping-protection.js @@ -117,12 +117,12 @@ async function listHandler(interaction, type) { embed.addFields([ { - name: localize('ping-protection', 'field-prot-users'), + name: localize('ping-protection', 'field-protected-users'), value: truncate(usersList, 1024), inline: true }, { - name: localize('ping-protection', 'field-prot-roles'), + name: localize('ping-protection', 'field-protected-roles'), value: truncate(rolesList, 1024), inline: true } @@ -140,6 +140,10 @@ async function listHandler(interaction, type) { ? config.ignoredChannels.map(id => `<#${id}>`).join('\n') : localize('ping-protection', 'list-none'); + const usersList = config.ignoredUsers.length > 0 + ? config.ignoredUsers.map(id => `<@${id}>`).join('\n') + : localize('ping-protection', 'list-none'); + embed.addFields([ { name: localize('ping-protection', 'field-wl-roles'), @@ -148,7 +152,12 @@ async function listHandler(interaction, type) { { name: localize('ping-protection', 'field-wl-channels'), value: truncate(channelsList, 1024), - inline: true } + inline: true }, + { + name: localize('ping-protection', 'field-wl-users'), + value: truncate(usersList, 1024), + inline: true + } ]); } diff --git a/modules/ping-protection/configs/configuration.json b/modules/ping-protection/configs/configuration.json index fb7cf2c7..acd5b7d0 100644 --- a/modules/ping-protection/configs/configuration.json +++ b/modules/ping-protection/configs/configuration.json @@ -5,19 +5,57 @@ }, "commandsWarnings": { "normal": [ - "/ping-protection user history", - "/ping-protection user actions-history", - "/ping-protection list roles", - "/ping-protection list users", - "/ping-protection list whitelisted" + "/ping-protection user history", + "/ping-protection user actions-history", + "/ping-protection list roles", + "/ping-protection list users", + "/ping-protection list whitelisted" ] }, "description": { "en": "Configure protected users/roles, whitelisted roles/members, ignored channels and the notification message." }, + "categories": [ + { + "id": "protection", + "icon": "fa-solid fa-shield", + "displayName": { + "en": "Protected" + } + }, + { + "id": "whitelisted", + "icon": "fa-solid fa-badge-check", + "displayName": { + "en": "Whitelists" + } + }, + { + "id": "rules", + "icon": "fas fa-gears", + "displayName": { + "en": "Ping rules" + } + }, + { + "id": "automod", + "icon": "far fa-robot", + "displayName": { + "en": "AutoMod settings" + } + }, + { + "id": "messages", + "icon": "fa-duotone fa-regular fa-triangle-exclamation", + "displayName": { + "en": "Warning message" + } + } + ], "content": [ { "name": "protectedRoles", + "category": "protection", "humanName": { "en": "Protected Roles" }, @@ -32,6 +70,7 @@ }, { "name": "protectAllUsersWithProtectedRole", + "category": "protection", "humanName": { "en": "Protect all users with a protected role" }, @@ -45,6 +84,7 @@ }, { "name": "protectedUsers", + "category": "protection", "humanName": { "en": "Protected Users" }, @@ -59,6 +99,7 @@ }, { "name": "ignoredRoles", + "category": "whitelisted", "humanName": { "en": "Whitelisted Roles" }, @@ -73,6 +114,7 @@ }, { "name": "ignoredChannels", + "category": "whitelisted", "humanName": { "en": "Whitelisted Channels" }, @@ -87,6 +129,7 @@ }, { "name": "ignoredUsers", + "category": "whitelisted", "humanName": { "en": "Whitelisted Users" }, @@ -101,6 +144,7 @@ }, { "name": "allowReplyPings", + "category": "rules", "humanName": { "en": "Allow Reply Pings" }, @@ -114,6 +158,7 @@ }, { "name": "selfPingConfiguration", + "category": "rules", "humanName": { "en": "Self-Ping configuration" }, @@ -132,6 +177,7 @@ }, { "name": "enableAutomod", + "category": "automod", "humanName": { "en": "Enable automod" }, @@ -145,6 +191,7 @@ }, { "name": "autoModLogChannel", + "category": "automod", "humanName": { "en": "AutoMod Log Channel" }, @@ -162,20 +209,23 @@ }, { "name": "autoModBlockMessage", + "category": "automod", "humanName": { - "en": "AutoMod custom message for message block" }, + "en": "AutoMod custom message for message block" + }, "description": { "en": "Custom text shown to the user when blocked (Max 150 characters)." }, "type": "string", "maxLength": 150, "default": { - "en": "Protected User Ping: Your message was blocked because you are trying to ping a protected user/role. The content of your message might be sent to a log channel depending on the configuration." + "en": "Your message was blocked because you are trying to ping a protected user/role. The message content might be logged depending on the configuration." }, "dependsOn": "enableAutomod" }, { "name": "pingWarningMessage", + "category": "messages", "humanName": { "en": "Warning Message" }, @@ -220,4 +270,4 @@ } } ] -} +} \ No newline at end of file diff --git a/modules/ping-protection/configs/moderation.json b/modules/ping-protection/configs/moderation.json index 5b08f3f9..1c15ed63 100644 --- a/modules/ping-protection/configs/moderation.json +++ b/modules/ping-protection/configs/moderation.json @@ -15,12 +15,12 @@ "configElements": true, "content": [ { - "name": "pingsCountBasic", + "name": "pingsCount", "humanName": { "en": "Pings to trigger moderation" }, "description": { - "en": "The amount of pings required to trigger a moderation action (Uses 'Ping History Retention' timeframe)." + "en": "The amount of pings required to trigger a moderation action." }, "type": "integer", "default": { @@ -33,27 +33,13 @@ "en": "Use a custom timeframe" }, "description": { - "en": "If enabled, you can choose your own custom timeframe and the basic configuration will be ignored." + "en": "If enabled, you can choose your own custom timeframe of days in which the pings must occur to trigger the moderation action." }, "type": "boolean", "default": { "en": false } }, - { - "name": "pingsCountAdvanced", - "humanName": { - "en": "Pings to trigger (Custom Timeframe)" - }, - "description": { - "en": "The amount of pings required in the custom timeframe below." - }, - "type": "integer", - "default": { - "en": 5 - }, - "dependsOn": "useCustomTimeframe" - }, { "name": "timeframeDays", "humanName": { diff --git a/modules/ping-protection/configs/storage.json b/modules/ping-protection/configs/storage.json index 54395ab5..995a1ca1 100644 --- a/modules/ping-protection/configs/storage.json +++ b/modules/ping-protection/configs/storage.json @@ -6,9 +6,33 @@ "description": { "en": "Configure how long moderation logs and leaver data are kept." }, + "categories": [ + { + "id": "pings", + "icon": "fa-regular fa-clock-rotate-left", + "displayName": { + "en": "Ping History" + } + }, + { + "id": "moderation", + "icon": "fas fa-hammer", + "displayName": { + "en": "Moderation Logs" + } + }, + { + "id": "leavers", + "icon": "fas fa-right-from-bracket", + "displayName": { + "en": "Leaver Data" + } + } + ], "content": [ { "name": "enablePingHistory", + "category": "pings", "humanName": { "en": "Enable Ping History" }, @@ -22,22 +46,24 @@ }, { "name": "pingHistoryRetention", + "category": "pings", "humanName": { "en": "Ping History Retention" }, "description": { - "en": "Decides on how long to keep ping logs. Minimum is 4 weeks (1 month) with a maximum of 24 weeks (6 months). This is the length factor of the 'Basic' punishment timeframe." + "en": "Decides on how long to keep ping logs. Minimum is 4 weeks (1 month) with a maximum of 96 weeks (2 years). This is the length factor of the 'Basic' punishment timeframe." }, "type": "integer", "default": { "en": 12 }, "minValue": "4", - "maxValue": "24", + "maxValue": "96", "dependsOn": "enablePingHistory" }, { "name": "deleteAllPingHistoryAfterTimeframe", + "category": "pings", "humanName": { "en": "Delete all the pings in history after the timeframe?" }, @@ -51,21 +77,23 @@ }, { "name": "modLogRetention", + "category": "moderation", "humanName": { "en": "Moderation Log Retention (Months)" }, "description": { - "en": "How long to keep records of punishments (1-12 Months). This is applied when moderation actions are enabled." + "en": "How long to keep records of punishments (1 - 24 Months). This is applied when moderation actions are enabled." }, "type": "integer", "default": { - "en": 6 + "en": 12 }, "minValue": "1", - "maxValue": "12" + "maxValue": "24" }, { "name": "enableLeaverDataRetention", + "category": "leavers", "humanName": { "en": "Keep user logs after they leave" }, @@ -79,6 +107,7 @@ }, { "name": "leaverRetention", + "category": "leavers", "humanName": { "en": "Leaver Data Retention (Days)" }, diff --git a/modules/ping-protection/events/interactionCreate.js b/modules/ping-protection/events/interactionCreate.js index 05fd99bb..042de12a 100644 --- a/modules/ping-protection/events/interactionCreate.js +++ b/modules/ping-protection/events/interactionCreate.js @@ -83,7 +83,7 @@ module.exports.run = async function (client, interaction) { if (userInput === requiredPhrase) { await deleteAllUserData(client, userId); await interaction.reply({ - content: `✅ ${localize('ping-protection', 'log-manual-delete', {u: userId})}`, + content: `✅ ${localize('ping-protection', 'modal-success-data-deletion', {u: userId})}`, flags: MessageFlags.Ephemeral }); } else { await interaction.reply({ diff --git a/modules/ping-protection/ping-protection.js b/modules/ping-protection/ping-protection.js index 34b3579c..012143dd 100644 --- a/modules/ping-protection/ping-protection.js +++ b/modules/ping-protection/ping-protection.js @@ -389,7 +389,7 @@ async function deleteAllUserData(client, userId) { await client.models['ping-protection']['LeaverData'].destroy({ where: { userId: userId } }); - client.logger.info(localize('ping-protection', 'log-manual-delete-logs', { + client.logger.info(localize('ping-protection', 'log-data-deletion', { u: userId })); } @@ -441,7 +441,7 @@ async function enforceRetention(client) { } if (storageConfig.modLogRetention) { const modCutoff = new Date(); - modCutoff.setMonth(modCutoff.getMonth() - (storageConfig.modLogRetention || 6)); + modCutoff.setMonth(modCutoff.getMonth() - (storageConfig.modLogRetention || 12)); await client.models['ping-protection']['ModerationLog'].destroy({ where: { createdAt: { [Op.lt]: modCutoff } @@ -597,16 +597,22 @@ async function processPing(client, userId, targetId, isRole, messageUrl, originC for (let i = moderationRules.length - 1; i >= 0; i--) { const rule = moderationRules[i]; - + const retentionWeeks = storageConfig?.pingHistoryRetention || 12; const timeframeDays = rule.useCustomTimeframe ? (rule.timeframeDays || 7) : (retentionWeeks * 7); const pingCount = await getPingCountInWindow(client, userId, timeframeDays); - const requiredCount = rule.useCustomTimeframe - ? rule.pingsCountAdvanced - : rule.pingsCountBasic; + const requiredCount = + rule.pingsCount ?? + rule.pingsCountAdvanced ?? + rule.pingsCountBasic; + + // Skip this rule if no valid threshold is configured + if (typeof requiredCount !== 'number' || !Number.isFinite(requiredCount)) { + continue; + } if (pingCount >= requiredCount) { const oneMinuteAgo = new Date(Date.now() - 60000); From acf8bf7979c637bbec18fd116e1fc1941623908d Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 27 Feb 2026 13:33:47 +0100 Subject: [PATCH 07/27] bumped changes --- locales/en.json | 66 +- main.js | 92 +- modules/birthday/birthday.js | 203 ----- modules/birthday/commands/birthday.js | 128 --- modules/birthday/config.json | 241 ------ modules/birthday/events/botReady.js | 24 - modules/birthday/events/guildMemberRemove.js | 5 - modules/birthday/models/User.js | 32 - modules/birthday/module.json | 26 - .../guess-the-number/events/messageCreate.js | 5 +- modules/guess-the-number/guessTheNumber.js | 3 +- modules/moderation/commands/moderate.js | 783 ++++++++++-------- modules/moderation/commands/report.js | 35 +- modules/moderation/configs/antiGrief.json | 36 +- modules/moderation/configs/antiJoinRaid.json | 36 +- modules/moderation/configs/antiSpam.json | 59 +- modules/moderation/configs/config.json | 125 ++- modules/moderation/configs/joinGate.json | 42 +- modules/moderation/configs/lockdown.json | 221 +++++ modules/moderation/configs/strings.json | 75 +- modules/moderation/configs/verification.json | 65 +- modules/moderation/events/botReady.js | 3 + modules/moderation/events/guildMemberAdd.js | 18 +- modules/moderation/events/messageCreate.js | 5 + modules/moderation/lockdown.js | 424 ++++++++++ modules/moderation/models/LockdownState.js | 47 ++ modules/moderation/moderationActions.js | 46 +- modules/temp-channels/channel-settings.js | 29 +- modules/temp-channels/events/botReady.js | 86 +- .../temp-channels/events/voiceStateUpdate.js | 26 +- modules/temp-channels/locales.json | 29 - src/commands/help.js | 361 ++++++-- src/discordjs-fix.js | 1 + src/events/interactionCreate.js | 7 + src/functions/configuration.js | 10 +- src/functions/helpers.js | 445 +++++++++- 36 files changed, 2408 insertions(+), 1431 deletions(-) delete mode 100644 modules/birthday/birthday.js delete mode 100644 modules/birthday/commands/birthday.js delete mode 100644 modules/birthday/config.json delete mode 100644 modules/birthday/events/botReady.js delete mode 100644 modules/birthday/events/guildMemberRemove.js delete mode 100644 modules/birthday/models/User.js delete mode 100644 modules/birthday/module.json create mode 100644 modules/moderation/configs/lockdown.json create mode 100644 modules/moderation/lockdown.js create mode 100644 modules/moderation/models/LockdownState.js delete mode 100644 modules/temp-channels/locales.json diff --git a/locales/en.json b/locales/en.json index ffdbde03..5cc2e895 100644 --- a/locales/en.json +++ b/locales/en.json @@ -78,10 +78,11 @@ "message-execution-failed": "Execution of command %p%c failed (Tracing: %t): %e", "wrong-guild": "This command is only available on the server **%g**.", "autcomplete-execution-failed": "Execution of auto-complete on command /%c %g %s with option %f failed: %e", - "execution-failed-message": "## \uD83D\uDD34 Command execution failed \uD83D\uDD34\nThis usually happens either due to misconfiguration or due to an error on our site. Please send a screenshot of this message in #support on the [ScootKit Discord](https://scootk.it/dc-en), to resolve this.\n\n### Internal Tracing ID\n`%t`\n### Debugging-Information\n```%e```", - "error-giving-role": "An error occurred when trying to giving to give you your roles ):", + "execution-failed-message": "## 🔴 Command execution failed 🔴\nThis usually happens either due to misconfiguration or due to an error on our site. Please send a screenshot of this message in #support on the [ScootKit Discord](https://scootk.it/dc-en), to resolve this.\n\n### Internal Tracing ID\n`%t`\n### Debugging-Information\n```%e```", + "error-giving-role": "An error occurred when trying to give you your roles ):\nPlease ask the server administrators to confirm that the highest role of the bot is above the role that the bot is supposed to assign.", "description-too-long": "The following command description of %c was too long to sync: %s", - "module-disabled": "This command is part of the \"%m\" which is disabled. This can either be intended by the server-admins (and slash-commands haven't synced yet) or this could be caused by a configuration error. Please check (or ask the admins) to check the bot's configuration and logs for details." + "module-disabled": "This command is part of the \"%m\" which is disabled. This can either be intended by the server-admins (and slash-commands haven't synced yet) or this could be caused by a configuration error. Please check (or ask the admins) to check the bot's configuration and logs for details.", + "command-disabled": "This command is currently disabled by the server configuration. If you believe this is an error, please contact a server administrator." }, "help": { "bot-info-titel": "ℹ️ Bot-Info", @@ -89,7 +90,12 @@ "stats-title": "📊 Stats", "stats-content": "Active modules: %am\nRegistered commands: %rc\nBot-Version: %v\nRunning on server %si\n[Server-Plan](https://scnx.xyz/plan): %pl\nLast restart: %lr\nLast reload: %lR", "command-description": "Show every commands", - "slash-commands-title": "Slash-Commands" + "slash-commands-title": "Slash-Commands", + "select-module-placeholder": "Select a module to view its commands", + "select-module-hint": "👇 Use the dropdown below to browse commands by module.", + "back-to-overview": "Back to overview", + "modules-overview": "📋 Modules & Commands", + "built-in-description": "Core commands built into the bot" }, "bot-feedback": { "command-description": "Send feedback about the bot to the bot developer", @@ -417,36 +423,6 @@ "edit-modal-username-placeholder": "Username of the user", "user-not-found": "User not found" }, - "guess-the-number": { - "command-description": "Manage your guess-the-number-games", - "status-command-description": "Shows the current status of a guess-the-number-game in this channel", - "create-command-description": "Create a new guess-the-number-game in this channel", - "create-min-description": "Minimal value users can guess", - "create-max-description": "Maximal value users can guess", - "create-number-description": "Number users should guess to win", - "end-command-description": "Ends the current game", - "session-already-running": "There is a session already running in this channel. Please end it with /guess-the-number end", - "session-not-running": "There is currently no session running.", - "gamechannel-modus": "You can't use this command in a gamechannel. To end a game, disable the gamechannel modus and try using the end command.", - "session-ended-successfully": "Ended session successfully. Locked channel successfully.", - "current-session": "Current session", - "number": "Number", - "min-val": "Min-Value", - "max-val": "Max-Value", - "owner": "Owner", - "guess-count": "Count of guesses", - "min-max-discrepancy": "`min` can't be bigger or equal to `max`", - "max-discrepancy": "`number` can't be bigger than `max`.", - "min-discrepancy": "`number` can't be smaller than `min`.", - "emoji-guide-button": "What does the reaction under my guess mean?", - "guide-wrong-guess": "Your guess was wrong (but your entry was valid)", - "guide-win": "You guessed correctly - you win :tada:", - "guide-admin-guess": "Your guess was invalid, because you are an admin - admins can't participate because they can see the correct number", - "guide-invalid-guess": "Your guess was invalid (e.g. below the minimal / over the maximal number, not a number, …)", - "created-successfully": "Created game successfully. Users can now start guessing in this channel. The winning number is **%n**. You can always check the status by running `/guess-the-number-status`. Note that you as an admin can not guess.", - "game-ended": "Game ended", - "game-started": "Game started" - }, "massrole": { "command-description": "Manage roles for all members", "add-subcommand-description": "Add a role to all members", @@ -542,6 +518,28 @@ "moderate-unchannel-mute-description": "Removes a channel-mute from this channel", "moderate-lock-command-description": "Lock the current channel", "moderate-unlock-command-description": "Unlock the current channel", + "moderate-lockdown-command-description": "Activate or lift server-wide lockdown", + "moderate-lockdown-enable-description": "True to activate lockdown, false to lift it", + "lockdown-not-enabled": "The lockdown system is not enabled. Enable it in the lockdown configuration.", + "lockdown-already-active": "A lockdown is already active.", + "lockdown-not-active": "No lockdown is currently active.", + "lockdown-activated": "Server Lockdown Activated", + "lockdown-lifted": "Server Lockdown Lifted", + "lockdown-activated-reply": "Lockdown activated. %c channels have been locked.", + "lockdown-lifted-reply": "Lockdown lifted. %c channels have been restored.", + "lockdown-log-description": "**Reason:** %r\n**Triggered by:** %u\n**Type:** %t\n**Affected channels:** %c", + "lockdown-lift-log-description": "**Reason:** %r\n**Lifted by:** %u\n**Restored channels:** %c", + "lockdown-automatic": "Automatic", + "lockdown-manual": "Manual", + "lockdown-system": "System", + "lockdown-auto-lift-reason": "Auto-lift timer expired", + "lockdown-restored": "Lockdown state restored from database after restart", + "lockdown-joinraid-trigger": "Join raid detected", + "lockdown-spam-trigger": "Excessive spam detected", + "lockdown-joingate-trigger": "Excessive join-gate violations detected", + "lockdown-restore-failed": "Failed to restore permissions for channel %c: %e", + "lockdown-users-kicked": "Users Kicked", + "lockdown-users-kicked-description": "%k non-moderator users were disconnected from voice channels.", "moderate-user-description": "User on who the action should get performed", "moderate-userid-description": "ID of a user", "moderate-days-description": "Number of days of messages to delete", diff --git a/main.js b/main.js index e4f7b49f..6191a4a7 100644 --- a/main.js +++ b/main.js @@ -23,33 +23,6 @@ const jsonfile = require('jsonfile'); const centra = require('centra'); const readline = require('readline'); -const optionTypeMap = { - SUB_COMMAND: ApplicationCommandOptionType.Subcommand, - SUB_COMMAND_GROUP: ApplicationCommandOptionType.SubcommandGroup, - STRING: ApplicationCommandOptionType.String, - INTEGER: ApplicationCommandOptionType.Integer, - BOOLEAN: ApplicationCommandOptionType.Boolean, - USER: ApplicationCommandOptionType.User, - CHANNEL: ApplicationCommandOptionType.Channel, - ROLE: ApplicationCommandOptionType.Role, - MENTIONABLE: ApplicationCommandOptionType.Mentionable, - NUMBER: ApplicationCommandOptionType.Number, - ATTACHMENT: ApplicationCommandOptionType.Attachment -}; -const channelTypeMap = { - GUILD_TEXT: ChannelType.GuildText, - GUILD_VOICE: ChannelType.GuildVoice, - GUILD_NEWS: ChannelType.GuildAnnouncement, - GUILD_STAGE_VOICE: ChannelType.GuildStageVoice, - GUILD_CATEGORY: ChannelType.GuildCategory -}; -const permissionMap = { - ADMINISTRATOR: PermissionFlagsBits.Administrator, - MANAGE_EMOJIS_AND_STICKERS: PermissionFlagsBits.ManageGuildExpressions, - MODERATE_MEMBERS: PermissionFlagsBits.ModerateMembers, - MANAGE_MESSAGES: PermissionFlagsBits.ManageMessages -}; - // Parsing parameters let config; let confDir = `${__dirname}/config`; @@ -169,22 +142,24 @@ async function startUp() { } logger.info(localize('main', 'sync-db')); if (scnxSetup) await require('./src/functions/scnx-integration').beforeInit(client); - await client.login(config.token).catch(async (e) => { - if (e.code === 'TOKEN_INVALID') { - if (scnxSetup) await require('./src/functions/scnx-integration').reportIssue(client, { - type: 'CORE_FAILURE', - errorDescription: 'invalid_token' - }); - logger.fatal(localize('main', 'login-error-token')); - } else if (e.code === 'DISALLOWED_INTENTS') { - if (scnxSetup) await require('./src/functions/scnx-integration').reportIssue(client, { - type: 'CORE_FAILURE', - errorDescription: 'disallowed_intents' - }); - logger.fatal(localize('main', 'login-error-intents', {url: `https://discord.com/developers/applications/`})); - } else logger.fatal(localize('main', 'login-error', {e})); - process.exit(); - }); + if (!client.isReady()) { + await client.login(config.token).catch(async (e) => { + if (e.code === 'TOKEN_INVALID') { + if (scnxSetup) await require('./src/functions/scnx-integration').reportIssue(client, { + type: 'CORE_FAILURE', + errorDescription: 'invalid_token' + }); + logger.fatal(localize('main', 'login-error-token')); + } else if (e.code === 'DISALLOWED_INTENTS') { + if (scnxSetup) await require('./src/functions/scnx-integration').reportIssue(client, { + type: 'CORE_FAILURE', + errorDescription: 'disallowed_intents' + }); + logger.fatal(localize('main', 'login-error-intents', {url: `https://discord.com/developers/applications/`})); + } else logger.fatal(localize('main', 'login-error', {e})); + process.exit(); + }); + } const app = JSON.parse((await centra(`https://discord.com/api/applications/@me`, 'GET').header('Authorization', `Bot ${client.token}`).send()).body.toString()); if (app.bot_require_code_grant) { if (scnxSetup) await require('./src/functions/scnx-integration').reportIssue(client, { @@ -300,7 +275,9 @@ rl.on('line', (input) => { async function syncCommandsIfNeeded() { const enabledCommands = commands.filter(c => { if (!c.module) return true; - return client.modules[c.module].enabled; + if (!client.modules[c.module].enabled) return false; + if (typeof c.disabled === 'function' && c.disabled(client)) return false; + return true; }); /** @@ -322,6 +299,32 @@ async function syncCommandsIfNeeded() { const oldGuildCommands = await (await client.guilds.fetch(config.guildID)).commands.fetch().catch(handleSyncFailure); const oldGlobalCommands = await client.application.commands.fetch().catch(handleSyncFailure); + const optionTypeMap = { + SUB_COMMAND: ApplicationCommandOptionType.Subcommand, + SUB_COMMAND_GROUP: ApplicationCommandOptionType.SubcommandGroup, + STRING: ApplicationCommandOptionType.String, + INTEGER: ApplicationCommandOptionType.Integer, + BOOLEAN: ApplicationCommandOptionType.Boolean, + USER: ApplicationCommandOptionType.User, + CHANNEL: ApplicationCommandOptionType.Channel, + ROLE: ApplicationCommandOptionType.Role, + MENTIONABLE: ApplicationCommandOptionType.Mentionable, + NUMBER: ApplicationCommandOptionType.Number, + ATTACHMENT: ApplicationCommandOptionType.Attachment + }; + const channelTypeMap = { + GUILD_TEXT: ChannelType.GuildText, + GUILD_VOICE: ChannelType.GuildVoice, + GUILD_NEWS: ChannelType.GuildAnnouncement, + GUILD_STAGE_VOICE: ChannelType.GuildStageVoice, + GUILD_CATEGORY: ChannelType.GuildCategory + }; + const permissionMap = { + ADMINISTRATOR: PermissionFlagsBits.Administrator, + MANAGE_EMOJIS_AND_STICKERS: PermissionFlagsBits.ManageGuildExpressions, + MODERATE_MEMBERS: PermissionFlagsBits.ModerateMembers, + MANAGE_MESSAGES: PermissionFlagsBits.ManageMessages + }; function normalizePermission(permission) { if (typeof permission === 'string') { @@ -615,6 +618,7 @@ async function loadCommandsInDir(dir, moduleName = null) { restricted: props.config.restricted, defaultMemberPermissions: props.config.defaultMemberPermissions || null, options: props.config.options || [], + disabled: props.config.disabled || null, subcommands: props.subcommands, beforeSubcommand: props.beforeSubcommand, run: props.run, diff --git a/modules/birthday/birthday.js b/modules/birthday/birthday.js deleted file mode 100644 index 3cf18822..00000000 --- a/modules/birthday/birthday.js +++ /dev/null @@ -1,203 +0,0 @@ -/** - * Manages the birthday-embed - * @module Birthdays - * @author Simon Csaba - */ -const { - embedType, - disableModule, - truncate, - embedTypeV2, - formatDiscordUserName, - parseEmbedColor -} = require('../../src/functions/helpers'); -const {MessageEmbed} = require('discord.js'); -const {AgeFromDate} = require('age-calculator'); -const {localize} = require('../../src/functions/localize'); - -/** - * Generate the BirthdayEmbed in the configured channel - * @param {Client} client Client - * @param {boolean} notifyUsers If enabled the bot will notify users who have birthday today - * @returns {Promise} - */ -generateBirthdayEmbed = async function (client, notifyUsers = false) { - const moduleConf = client.configurations['birthday']['config']; - - const channel = await client.channels.fetch(moduleConf['channelID']).catch(() => { - }); - if (!channel) return disableModule('birthdays', localize('birthdays', 'channel-not-found', {c: moduleConf.channelID})); - if (!moduleConf.enableBirthdayEmbed) { - if (notifyUsers) await notifyBirthdayUsers(); - return; - } - const messages = (await channel.messages.fetch()).filter(msg => msg.author.id === client.user.id); - - if (notifyUsers && !moduleConf.notificationChannelOverwriteID) { - for (const m of messages.filter(msg => msg.id !== messages.last().id).values()) { - if (m.deletable) await m.delete(); // Removing old messages - } - } - - const embeds = [ - new MessageEmbed() - .setTitle(moduleConf['birthdayEmbed']['title']) - .setDescription(moduleConf['birthdayEmbed']['description']) - .setColor(parseEmbedColor(moduleConf['birthdayEmbed']['color'])) - .setAuthor({name: client.user.username, iconURL: client.user.avatarURL()}) - .setFooter({text: client.strings.footer, iconURL: client.strings.footerImgUrl}) - .addFields([ - { - name: localize('months', '1'), - value: await getUserStringForMonth(client, channel, 1), - inline: true - }, - { - name: localize('months', '2'), - value: await getUserStringForMonth(client, channel, 2), - inline: true - }, - { - name: localize('months', '3'), - value: await getUserStringForMonth(client, channel, 3), - inline: true - }, - { - name: localize('months', '4'), - value: await getUserStringForMonth(client, channel, 4), - inline: true - }, - { - name: localize('months', '5'), - value: await getUserStringForMonth(client, channel, 5), - inline: true - }, - { - name: localize('months', '6'), - value: await getUserStringForMonth(client, channel, 6), - inline: true - }, - { - name: localize('months', '7'), - value: await getUserStringForMonth(client, channel, 7), - inline: true - }, - { - name: localize('months', '8'), - value: await getUserStringForMonth(client, channel, 8), - inline: true - }, - { - name: localize('months', '9'), - value: await getUserStringForMonth(client, channel, 9), - inline: true - }, - { - name: localize('months', '10'), - value: await getUserStringForMonth(client, channel, 10), - inline: true - }, - { - name: localize('months', '11'), - value: await getUserStringForMonth(client, channel, 11), - inline: true - }, - { - name: localize('months', '12'), - value: await getUserStringForMonth(client, channel, 12), - inline: true - }]) - ]; - - if ((moduleConf['birthdayEmbed']['thumbnail'] || '').replaceAll(' ', '')) embeds[0].setThumbnail(moduleConf['birthdayEmbed']['thumbnail']); - if ((moduleConf['birthdayEmbed']['image'] || '').replaceAll(' ', '')) embeds[0].setImage(moduleConf['birthdayEmbed']['image']); - if (!client.strings.disableFooterTimestamp) embeds[0].setTimestamp(); - - if (messages.last()) await messages.last().edit({embeds}); - else channel.send({embeds}); - - if (notifyUsers) await notifyBirthdayUsers(); - - /** - * Notifies users who have birthday - * @returns {Promise} - */ - async function notifyBirthdayUsers() { - const birthdayUsers = await client.models['birthday']['User'].findAll({ - where: { - month: new Date().getMonth() + 1, - day: new Date().getDate() - } - }); - if (!birthdayUsers) return; - - if (moduleConf['birthday_role']) { - const guildMembers = client.guild.members.cache; - for (const member of guildMembers.values()) { - if (!member) return; - if (member.roles.cache.has(moduleConf['birthday_role'])) { - await member.roles.remove(moduleConf['birthday_role']); - } - } - } - - const birthdayMessageChannel = moduleConf.notificationChannelOverwriteID ? await client.guild.channels.fetch(moduleConf.notificationChannelOverwriteID) : channel; - - for (const user of birthdayUsers) { - const member = channel.guild.members.cache.get(user.id); - if (!member) return; - if (user.year) { - birthdayMessageChannel.send(await embedTypeV2(moduleConf['birthday_message_with_age'], { - '%age%': new Date().getFullYear() - user.year, - '%tag%': formatDiscordUserName(member.user), - '%username%': member.user.username, - '%avatarURL%': member.user.avatarURL() || member.user.defaultAvatarURL, - '%mention%': `<@${user.id}>` - })); - } else { - birthdayMessageChannel.send(await embedTypeV2(moduleConf['birthday_message'], { - '%tag%': formatDiscordUserName(member.user), - '%avatarURL%': member.user.avatarURL() || member.user.defaultAvatarURL, - '%mention%': `<@${user.id}>` - })); - } - if (moduleConf['birthday_role']) await member.roles.add(moduleConf['birthday_role']); - } - } -}; - -module.exports.generateBirthdayEmbed = generateBirthdayEmbed; - -/** - * Get UserString for a month - * @private - * @param {Client} client Client - * @param {Channel} channel Channel to send embed in - * @param {Number} month Month to render results from - * @returns {Promise} - */ -async function getUserStringForMonth(client, channel, month) { - const monthData = await client.models['birthday']['User'].findAll({ - where: { - month: month - } - }); - monthData.sort((a, b) => { - return a.day - b.day; - }); - let string = ''; - for (const user of monthData) { - let dateString = `${user.day}.${month}${user.year ? `.${user.year}` : ''}`; - if (user.year && !client.configurations['birthday']['config'].disableSync) { - const age = new AgeFromDate(new Date(user.year, user.month - 1, user.day)).age; - if (age < 13 || age > 125) { - await user.destroy(); - continue; - } - dateString = `[${dateString}](https://scnx.xyz/${client.locale === 'de' ? 'de/' : ''}custom-bot/age-calculator?age=${age} "${localize('birthdays', 'age-hover', {a: age})}")`; - } - if (channel.guild.members.cache.get(user.id)) string = string + `${dateString}: ${client.configurations['birthday']['config'].useTags ? formatDiscordUserName(channel.guild.members.cache.get(user.id).user) : channel.guild.members.cache.get(user.id).user.toString()}\n`; - } - if (string.length === 0) string = localize('birthdays', 'no-bd-this-month'); - return truncate(string, 1024); -} \ No newline at end of file diff --git a/modules/birthday/commands/birthday.js b/modules/birthday/commands/birthday.js deleted file mode 100644 index 9bceb1d0..00000000 --- a/modules/birthday/commands/birthday.js +++ /dev/null @@ -1,128 +0,0 @@ -const {generateBirthdayEmbed} = require('../birthday'); -const {AgeFromDateString, AgeFromDate} = require('age-calculator'); -const {embedType} = require('../../../src/functions/helpers'); -const {localize} = require('../../../src/functions/localize'); - -module.exports.beforeSubcommand = async function (interaction) { - interaction.birthday = await interaction.client.models['birthday']['User'].findOne({ - where: { - id: interaction.user.id - } - }); -}; - -module.exports.subcommands = { - 'status': async function (interaction) { - if (!interaction.birthday) return interaction.reply({ - ephemeral: true, - content: '⚠️️ ' + localize('birthdays', 'no-birthday-set') - }); - const date = new Date(interaction.birthday.year, interaction.birthday.month - 1, interaction.birthday.day); - interaction.reply({ - ephemeral: true, - content: localize('birthdays', 'birthday-status', { - dd: interaction.birthday.day, - mm: interaction.birthday.month, - yyyy: (interaction.birthday.year ? `.${interaction.birthday.year}` : ''), - age: interaction.birthday.year ? ', ' + (localize('birthdays', 'your-age', {age: new AgeFromDate(date).age})) : '' - }) - }); - - }, - 'delete': async function (interaction) { - if (!interaction.birthday) return interaction.reply({ - ephemeral: true, - content: '⚠️ ' + localize('birthdays', 'no-birthday-set') - }); - await interaction.birthday.destroy(); - interaction.birthday = null; - interaction.reply({ - ephemeral: true, - content: '🗑️ ' + localize('birthdays', 'deleted-successfully') - }); - interaction.regenerateEmbed = true; - }, - 'set': async function (interaction) { - const day = interaction.options.getInteger('day', true); - const month = interaction.options.getInteger('month', true); - const year = interaction.options.getInteger('year'); - - if ((day > 31 || day < 1) || (month > 12 || month < 1)) return interaction.reply({ - ephemeral: true, - content: '⚠️ ' + localize('birthdays', 'invalid-date') - }); - - if (year) { - const age = new AgeFromDate(new Date(year, month - 1, day)).age; - if (age < 13) return interaction.reply({ - ephemeral: true, - content: '⚠️ ' + localize('birthdays', 'against-tos', {waitTime: 13 - age}) - }); - if (age > 125) return interaction.reply({ - ephemeral: true, - content: '⚠️ ' + localize('birthdays', 'too-old') - }); - } - - if (!interaction.birthday) { - interaction.birthday = await interaction.client.models.birthday['User'].create({ - id: interaction.user.id - }); - } - - interaction.birthday.day = day; - interaction.birthday.month = month; - interaction.birthday.year = year; - interaction.birthday.sync = false; - interaction.regenerateEmbed = true; - - await interaction.reply(embedType(interaction.client.configurations['birthday']['config']['successfully_changed'], {}, {ephemeral: true})); - } -}; - -module.exports.run = async function (interaction) { - if (interaction.birthday) await interaction.birthday.save(); - if (interaction.regenerateEmbed) await generateBirthdayEmbed(interaction.client); -}; - -module.exports.config = { - name: 'birthday', - description: localize('birthdays', 'command-description'), - - options: [{ - type: 'SUB_COMMAND', - name: 'status', - description: localize('birthdays', 'status-command-description') - }, - { - type: 'SUB_COMMAND', - name: 'set', - description: localize('birthdays', 'set-command-description'), - options: [ - { - type: 'INTEGER', - required: true, - name: 'day', - description: localize('birthdays', 'set-command-day-description') - }, - { - type: 'INTEGER', - required: true, - name: 'month', - description: localize('birthdays', 'set-command-month-description') - }, - { - type: 'INTEGER', - required: false, - name: 'year', - description: localize('birthdays', 'set-command-year-description') - } - ] - }, - { - type: 'SUB_COMMAND', - name: 'delete', - description: localize('birthdays', 'delete-command-description') - } - ] -}; \ No newline at end of file diff --git a/modules/birthday/config.json b/modules/birthday/config.json deleted file mode 100644 index c1227e11..00000000 --- a/modules/birthday/config.json +++ /dev/null @@ -1,241 +0,0 @@ -{ - "description": {}, - "humanName": { - "en": "Configuration", - "de": "Konfiguration" - }, - "filename": "config.json", - "content": [ - { - "name": "channelID", - "humanName": { - "en": "Birthday-Channel", - "de": "Geburtstag-Kanal" - }, - "default": { - "en": "" - }, - "description": { - "en": "Channel to run send the Birthday-Embed (and notifications, if not overwritten) in", - "de": "Kanal, in welchem das Geburtstags-Embed (und Benachrichtigung, falls nicht überschrieben) versendet werden soll" - }, - "type": "channelID" - }, - { - "name": "notificationChannelOverwriteID", - "allowNull": true, - "humanName": { - "en": "(optional) Notification-Channel", - "de": "(optional) Benachrichtigung-Kanal" - }, - "type": "channelID", - "description": { - "de": "Kanal, in welchen Nutzern zu ihrem Geburtstag gratuliert werden soll. Wenn dieses Feld leer ist, wird der Geburtstags-Kanal verwendet. In diesem Kanal werden die Geburtstags-Nachrichten vom Vortag, im Gegensatz zum Geburtstags-Kanal, nicht jeden Tag automatisch geleert.", - "en": "Channel in which \"Happy birthday\"-messages should get send. If this field is empty, the message will get send in the Birthday-Channel. Old birthday notifications won't get removed automatically from this channel, in contrast to the Birthday-Channel." - }, - "default": { - "en": "" - } - }, - { - "name": "enableBirthdayEmbed", - "humanName": { - "en": "Birthday-Embed enabled", - "de": "Birthday-Embed aktiviert" - }, - "default": { - "en": true - }, - "description": { - "en": "If enabled, a messages (which will update itself) will be sent in the Birthday-Channel, which contains all Birthdays", - "de": "Wenn aktiviert, wird in den Geburtstag-Channel einen Nachricht gesendet (aktualisiert sich automatisch), welche alle Geburtstage enthält" - }, - "type": "boolean" - }, - { - "name": "birthday_message", - "allowGeneratedImage": true, - "humanName": { - "en": "Birthday Message", - "de": "Geburtstags-Nachricht" - }, - "default": { - "en": "Happy birthday, %mention%!" - }, - "description": { - "en": "Message that gets send if the user has not set a birthday", - "de": "Diese Nachricht wird verschickt, wenn der Nutzer kein Geburtsjahr angegeben hat und Geburtstag hat" - }, - "type": "string", - "allowEmbed": true, - "params": [ - { - "name": "tag", - "description": { - "en": "Tag of the user", - "de": "Tag des Nutzers" - } - }, - { - "name": "avatarURL", - "isImage": true, - "description": { - "en": "Avatar of the user", - "de": "Profilbild des Nutzers" - } - }, - { - "name": "username", - "description": { - "en": "Username of the user", - "de": "Nutzername des Nutzers" - } - }, - { - "name": "mention", - "description": { - "en": "Mention of the user", - "de": "Erwähnung des Nutzers" - } - } - ] - }, - { - "name": "birthday_message_with_age", - "allowGeneratedImage": true, - "humanName": { - "en": "Birthday message with age", - "de": "Geburtstags-Nachricht mit Alter" - }, - "default": { - "en": "Happy birthday, %mention%! You are now %age% years old!", - "de": "Alles Gute zum %age%ten Geburtstag, %mention%!" - }, - "description": { - "en": "Message that gets send if the user has not set a birthday", - "de": "Diese Nachricht wird verschickt, wenn der Nutzer kein Geburtsjahr angegeben hat und Geburtstag hat" - }, - "type": "string", - "allowEmbed": true, - "params": [ - { - "name": "tag", - "description": { - "en": "Tag of the user", - "de": "Tag des Nutzers" - } - }, - { - "name": "avatarURL", - "isImage": true, - "description": { - "en": "Avatar of the user", - "de": "Profilbild des Nutzers" - } - }, - { - "name": "username", - "description": { - "en": "Username of the user", - "de": "Nutzername des Nutzers" - } - }, - { - "name": "mention", - "description": { - "en": "Mention of the user", - "de": "Erwähnung des Nutzers" - } - }, - { - "name": "age", - "description": { - "en": "New age of user", - "de": "Neues Alter des Nutzers" - } - } - ] - }, - { - "name": "birthday_role", - "humanName": { - "en": "Birthday-Role", - "de": "Geburtstags-Rolle" - }, - "default": { - "en": "" - }, - "description": { - "en": "Role that is given to users when they have birthday (Leave out to disable)", - "de": "Diese Rolle wird an Leute vergeben, die Geburtstag haben und wieder entfernt, wenn ihr Geburtstag vorbei ist (Leer lassen, um zu deaktivieren) [Tipp: Stelle diese Rolle so ein, dass sie ganz oben angezeigt wird, denn Geburtstage sind etwas besonderes ^^]" - }, - "type": "roleID", - "allowNull": true - }, - { - "name": "successfully_changed", - "humanName": { - "en": "\"Successfully changed\"-Message", - "de": "\"Erfolgreich geändert\"-Nachricht" - }, - "default": { - "en": "Successfully changed record!", - "de": "Die Änderungen wurden gespeichert!" - }, - "description": { - "en": "Message that gets send when the bot changes an item", - "de": "Diese Nachricht wird verschickt, wenn eine Änderung übernommen wurde." - }, - "type": "string", - "allowEmbed": true - }, - { - "name": "birthdayEmbed", - "humanName": { - "en": "Birthday-Embed", - "de": "Geburtstags-Embed" - }, - "default": { - "en": { - "title": "Birthdays", - "color": "GREEN", - "thumbnail": " ", - "image": " ", - "description": "Here you can find every birthday - add yours with /birthday set [Year]" - }, - "de": { - "title": "Geburtstage", - "color": "GREEN", - "thumbnail": " ", - "image": " ", - "description": "Hier siehst du die Geburtstage unserer Mitglieder - du kannst deinen Geburtstag mit `/birthday set [Year]` hinzufügen." - } - }, - "description": { - "en": "Change settings of the birthday-embed here", - "de": "Passe hier das Geburtstage-Embed an (Du kannst einige Optionen gerne leer lassen)" - }, - "type": "keyed", - "content": { - "key": "string", - "value": "string" - }, - "disableKeyEdits": true - }, - { - "name": "useTags", - "humanName": { - "en": "Use User's Tags instead of their Mention", - "de": "Nutze den Tag der Nutzer, anstatt eine Erwähnung" - }, - "default": { - "en": false - }, - "description": { - "en": "If enabled, the bot will use the tag of users in the birthday embed instead of their mention.", - "de": "Wenn aktiviert, wird im Geburtags-Embed der Tag des Nutzers angezeigt und nicht eine Erwähnung (bei großen Servern empfohlen)" - }, - "type": "boolean" - } - ] -} \ No newline at end of file diff --git a/modules/birthday/events/botReady.js b/modules/birthday/events/botReady.js deleted file mode 100644 index 12ac4178..00000000 --- a/modules/birthday/events/botReady.js +++ /dev/null @@ -1,24 +0,0 @@ -const {generateBirthdayEmbed} = require('../birthday'); -const schedule = require('node-schedule'); -const {localize} = require('../../../src/functions/localize'); - -module.exports.run = async function (client) { - // Migration - const dbVersion = await client.models['DatabaseSchemeVersion'].findOne({where: {model: 'birthday_User'}}); - if (!dbVersion) { - client.logger.info('[birthdays] ' + localize('birthdays', 'migration-happening')); - const data = await client.models['birthday']['User'].findAll({attributes: ['id', 'month', 'day', 'year', 'sync']}); - await client.models['birthday']['User'].sync({force: true}); - for (const user of data) { - await client.models['birthday']['User'].create(user); - } - client.logger.info('[giveaways] ' + localize('birthdays', 'migration-done')); - await client.models['DatabaseSchemeVersion'].create({model: 'birthday_User', version: 'V1'}); - } - - await generateBirthdayEmbed(client); - const job = schedule.scheduleJob('1 0 * * *', async () => { // Every day at 00:01 https://crontab.guru/#0_0_*_*_* - await generateBirthdayEmbed(client, true); - }); - client.jobs.push(job); -}; \ No newline at end of file diff --git a/modules/birthday/events/guildMemberRemove.js b/modules/birthday/events/guildMemberRemove.js deleted file mode 100644 index f351205b..00000000 --- a/modules/birthday/events/guildMemberRemove.js +++ /dev/null @@ -1,5 +0,0 @@ -const {generateBirthdayEmbed} = require('../birthday'); - -module.exports.run = async function (client) { - await generateBirthdayEmbed(client); -}; \ No newline at end of file diff --git a/modules/birthday/models/User.js b/modules/birthday/models/User.js deleted file mode 100644 index 8540246a..00000000 --- a/modules/birthday/models/User.js +++ /dev/null @@ -1,32 +0,0 @@ -const {DataTypes, Model} = require('sequelize'); - -module.exports = class BirthdayUser extends Model { - static init(sequelize) { - return super.init({ - id: { - type: DataTypes.STRING, - primaryKey: true - }, - month: DataTypes.INTEGER, - day: DataTypes.INTEGER, - year: DataTypes.INTEGER, - verified: { - type: DataTypes.BOOLEAN, - defaultValue: false - }, - sync: { - type: DataTypes.BOOLEAN, - defaultValue: false - } - }, { - tableName: 'birthday_usersV2', - timestamps: true, - sequelize - }); - } -}; - -module.exports.config = { - 'name': 'User', - 'module': 'birthday' -}; \ No newline at end of file diff --git a/modules/birthday/module.json b/modules/birthday/module.json deleted file mode 100644 index 848fa034..00000000 --- a/modules/birthday/module.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "birthday", - "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/birthday", - "author": { - "scnxOrgID": "1", - "name": "SCDerox (SC Network Team)", - "link": "https://github.com/SCDerox" - }, - "commands-dir": "/commands", - "models-dir": "/models", - "events-dir": "/events", - "config-example-files": [ - "config.json" - ], - "tags": [ - "community" - ], - "humanReadableName": { - "en": "Birthday-Calendar", - "de": "Geburtstags-Kalender" - }, - "description": { - "en": "Let users set their birthday and congratulate them when they have birthday", - "de": "Lasse deine Nutzer ihre Geburtstage eintragen und gratuliere automatisch, wenn sie Geburtstag haben!" - } -} \ No newline at end of file diff --git a/modules/guess-the-number/events/messageCreate.js b/modules/guess-the-number/events/messageCreate.js index 582aaabe..a7b7f4f7 100644 --- a/modules/guess-the-number/events/messageCreate.js +++ b/modules/guess-the-number/events/messageCreate.js @@ -34,7 +34,8 @@ module.exports.run = async (client, msg) => { await msg.react('✅'); game.ended = true; await game.save(); - await lockChannel(msg.channel, client.configurations['guess-the-number']['config'].adminRoles, '[guess-the-number] ' + localize('guess-the-number', 'game-ended')); + const isGamechannel = client.configurations['guess-the-number']['channel'].enabled && client.configurations['guess-the-number']['channel'].channel === msg.channel.id; + if (!isGamechannel) await lockChannel(msg.channel, client.configurations['guess-the-number']['config'].adminRoles, '[guess-the-number] ' + localize('guess-the-number', 'game-ended')); await msg.reply(embedType(client.configurations['guess-the-number']['config']['endMessage'], { '%min%': game.min, '%max%': game.max, @@ -42,5 +43,5 @@ module.exports.run = async (client, msg) => { '%guessCount%': game.guessCount, '%number%': game.number })); - if (client.configurations['guess-the-number']['channel'].enabled && client.configurations['guess-the-number']['channel'].channel === msg.channel.id) await startGame(msg.channel, randomIntFromInterval(client.configurations['guess-the-number']['channel'].minInt, client.configurations['guess-the-number']['channel'].maxInt), client.configurations['guess-the-number']['channel'].minInt, client.configurations['guess-the-number']['channel'].maxInt); + if (isGamechannel) await startGame(msg.channel, randomIntFromInterval(client.configurations['guess-the-number']['channel'].minInt, client.configurations['guess-the-number']['channel'].maxInt), client.configurations['guess-the-number']['channel'].minInt, client.configurations['guess-the-number']['channel'].maxInt); }; \ No newline at end of file diff --git a/modules/guess-the-number/guessTheNumber.js b/modules/guess-the-number/guessTheNumber.js index 9ffd8e03..748a2a53 100644 --- a/modules/guess-the-number/guessTheNumber.js +++ b/modules/guess-the-number/guessTheNumber.js @@ -34,5 +34,6 @@ module.exports.startGame = async function (channel, number, min, max, ownerID = })); await m.pin(); - await unlockChannel(channel, '[guess-the-number] ' + localize('guess-the-number', 'game-started')); + const channelLock = await channel.client.models['ChannelLock'].findOne({where: {id: channel.id}}); + if (channelLock) await unlockChannel(channel, '[guess-the-number] ' + localize('guess-the-number', 'game-started')); }; \ No newline at end of file diff --git a/modules/moderation/commands/moderate.js b/modules/moderation/commands/moderate.js index 24d63cd1..c2ff96c5 100644 --- a/modules/moderation/commands/moderate.js +++ b/modules/moderation/commands/moderate.js @@ -4,9 +4,11 @@ const { sendMultipleSiteButtonMessage, truncate, formatDiscordUserName, - parseEmbedColor + parseEmbedColor, + safeSetFooter } = require('../../../src/functions/helpers'); const {moderationAction} = require('../moderationActions'); +const {activateLockdown, liftLockdown, isLockdownActive} = require('../lockdown'); const durationParser = require('parse-duration'); const {MessageEmbed} = require('discord.js'); const {Op} = require('sequelize'); @@ -16,7 +18,7 @@ module.exports.beforeSubcommand = async function (interaction) { if (interaction.options.getUser('user')) { interaction.memberToExecuteUpon = interaction.options.getMember('user'); if (!interaction.memberToExecuteUpon) { - if (interaction.options['_subcommand'] !== 'ban') return interaction.reply({ + if (!['ban', 'actions'].includes(interaction.options['_subcommand'])) return interaction.reply({ ephemeral: true, content: '⚠️ ' + localize('moderation', 'user-not-on-server') }); @@ -105,11 +107,11 @@ module.exports.subcommands = { }); const embed = new MessageEmbed() .setTitle(localize('moderation', 'notes-embed-title', {u: formatDiscordUserName(interaction.options.getUser('user'))})) - .setFooter({text: interaction.client.strings.footer, iconURL: interaction.client.strings.footerImgUrl}) .setThumbnail(interaction.options.getUser('user').avatarURL()) .setColor(parseEmbedColor('GREEN')) .setAuthor({name: interaction.client.user.username, iconURL: interaction.client.user.avatarURL()}) .setFields(fields); + safeSetFooter(embed, interaction.client); interaction.editReply({ embeds: [embed] }); @@ -348,6 +350,31 @@ module.exports.subcommands = { interaction.editReply({content: '⚠️ ' + r}); }); }, + 'lockdown': async function (interaction) { + if (interaction.replied) return; + if (!checkRoles(interaction, 4)) return; + const lockdownConfig = interaction.client.configurations['moderation']['lockdown']; + if (!lockdownConfig || !lockdownConfig.enabled) return interaction.editReply({ + content: '⚠️ ' + localize('moderation', 'lockdown-not-enabled') + }); + const enable = interaction.options.getBoolean('enable'); + if (enable) { + if (await isLockdownActive(interaction.client)) return interaction.editReply({ + content: '⚠️ ' + localize('moderation', 'lockdown-already-active') + }); + const reason = interaction.options.getString('reason') || localize('moderation', 'no-reason'); + const result = await activateLockdown(interaction.client, reason, formatDiscordUserName(interaction.user), false); + if (!result) return interaction.editReply({content: '⚠️ ' + localize('moderation', 'lockdown-already-active')}); + interaction.editReply({content: '🔒 ' + localize('moderation', 'lockdown-activated-reply', {c: result.affectedChannels.toString()})}); + } else { + if (!await isLockdownActive(interaction.client)) return interaction.editReply({ + content: '⚠️ ' + localize('moderation', 'lockdown-not-active') + }); + const result = await liftLockdown(interaction.client, interaction.options.getString('reason') || localize('moderation', 'no-reason'), formatDiscordUserName(interaction.user)); + if (!result) return interaction.editReply({content: '⚠️ ' + localize('moderation', 'lockdown-not-active')}); + interaction.editReply({content: '🔓 ' + localize('moderation', 'lockdown-lifted-reply', {c: result.restoredChannels.toString()})}); + } + }, 'lock': async function (interaction) { if (interaction.replied) return; if (!checkRoles(interaction, 2)) return; @@ -421,8 +448,8 @@ module.exports.subcommands = { })) .setDescription(localize('moderation', 'actions-embed-description', {u: formatDiscordUserName(interaction.memberToExecuteUpon.user)})) .setThumbnail(interaction.memberToExecuteUpon.user.avatarURL()) - .setFooter({text: interaction.client.strings.footer, iconURL: interaction.client.strings.footerImgUrl}) .addFields(fs); + safeSetFooter(embed, interaction.client); sites.push(embed); } @@ -516,7 +543,7 @@ function checkRoles(interaction, minLevel) { else interaction.reply(data); return false; } - if (!interaction.memberToExecuteUpon) return true; + if (!interaction.memberToExecuteUpon || interaction.memberToExecuteUpon.notFound) return true; if (interaction.memberToExecuteUpon.roles.cache.find(r => allowedRoles.includes(r.id))) { const data = embedType(interaction.client.configurations['moderation']['strings']['this_is_a_mod'], { '%required_level%': minLevel @@ -533,408 +560,430 @@ module.exports.config = { description: localize('moderation', 'moderate-command-description'), defaultMemberPermissions: ['MODERATE_MEMBERS'], - options: [ - { - type: 'SUB_COMMAND_GROUP', - name: 'notes', - description: localize('moderation', 'moderate-notes-command-description'), - options: [ - { - type: 'SUB_COMMAND', - name: 'view', - description: localize('moderation', 'moderate-notes-command-view'), - options: [ + options: function (client) { + const opts = [ + { + type: 'SUB_COMMAND_GROUP', + name: 'notes', + description: localize('moderation', 'moderate-notes-command-description'), + options: [ + { + type: 'SUB_COMMAND', + name: 'view', + description: localize('moderation', 'moderate-notes-command-view'), + options: [ + { + type: 'USER', + name: 'user', + required: true, + description: localize('moderation', 'moderate-user-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'create', + description: localize('moderation', 'moderate-notes-command-create'), + options: [ + { + type: 'USER', + name: 'user', + required: true, + description: localize('moderation', 'moderate-user-description') + }, + { + type: 'STRING', + name: 'notes', + required: true, + description: localize('moderation', 'moderate-notes-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'edit', + description: localize('moderation', 'moderate-notes-command-edit'), + options: [ + { + type: 'USER', + name: 'user', + required: true, + description: localize('moderation', 'moderate-user-description') + }, + { + type: 'INTEGER', + name: 'note-id', + required: true, + description: localize('moderation', 'moderate-note-id-description') + }, + { + type: 'STRING', + name: 'notes', + required: true, + description: localize('moderation', 'moderate-notes-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'delete', + description: localize('moderation', 'moderate-notes-command-delete'), + options: [ + { + type: 'USER', + name: 'user', + required: true, + description: localize('moderation', 'moderate-user-description') + }, + { + type: 'INTEGER', + name: 'note-id', + required: true, + description: localize('moderation', 'moderate-note-id-description') + } + ] + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'ban', + description: localize('moderation', 'moderate-ban-command-description'), + options: function (client) { + return [{ + type: 'USER', + name: 'user', + required: true, + description: localize('moderation', 'moderate-user-description') + }, { - type: 'USER', - name: 'user', - required: true, - description: localize('moderation', 'moderate-user-description') - } - ] - }, - { - type: 'SUB_COMMAND', - name: 'create', - description: localize('moderation', 'moderate-notes-command-create'), - options: [ + type: 'STRING', + name: 'reason', + required: client.configurations['moderation']['config']['require_reason'], + description: localize('moderation', 'moderate-reason-description') + }, { - type: 'USER', - name: 'user', - required: true, - description: localize('moderation', 'moderate-user-description') + type: 'ATTACHMENT', + name: 'proof', + required: (client.configurations['moderation']['config']['require_proof'] && client.configurations['moderation']['config']['require_reason']), + description: localize('moderation', 'moderate-proof-description') }, { type: 'STRING', - name: 'notes', - required: true, - description: localize('moderation', 'moderate-notes-description') - } - ] - }, - { - type: 'SUB_COMMAND', - name: 'edit', - description: localize('moderation', 'moderate-notes-command-edit'), - options: [ - { - type: 'USER', - name: 'user', - required: true, - description: localize('moderation', 'moderate-user-description') + name: 'duration', + required: false, + description: localize('moderation', 'moderate-duration-description') }, { type: 'INTEGER', - name: 'note-id', - required: true, - description: localize('moderation', 'moderate-note-id-description') - }, - { - type: 'STRING', - name: 'notes', - required: true, - description: localize('moderation', 'moderate-notes-description') + name: 'days', + required: false, + description: localize('moderation', 'moderate-days-description') } - ] - }, - { - type: 'SUB_COMMAND', - name: 'delete', - description: localize('moderation', 'moderate-notes-command-delete'), - options: [ + ]; + } + }, + { + type: 'SUB_COMMAND', + name: 'quarantine', + description: localize('moderation', 'moderate-quarantine-command-description'), + options: function (client) { + return [{ + type: 'USER', + name: 'user', + required: true, + description: localize('moderation', 'moderate-user-description') + }, { - type: 'USER', - name: 'user', - required: true, - description: localize('moderation', 'moderate-user-description') + type: 'STRING', + name: 'reason', + required: client.configurations['moderation']['config']['require_reason'], + description: localize('moderation', 'moderate-reason-description') }, { - type: 'INTEGER', - name: 'note-id', - required: true, - description: localize('moderation', 'moderate-note-id-description') + type: 'STRING', + name: 'duration', + required: false, + description: localize('moderation', 'moderate-duration-description') } - ] + ]; } - ] - }, - { - type: 'SUB_COMMAND', - name: 'ban', - description: localize('moderation', 'moderate-ban-command-description'), - options: function (client) { - return [{ - type: 'USER', - name: 'user', - required: true, - description: localize('moderation', 'moderate-user-description') - }, - { + }, + { + type: 'SUB_COMMAND', + name: 'unban', + description: localize('moderation', 'moderate-unban-command-description'), + options: function (client) { + return [{ type: 'STRING', - name: 'reason', - required: client.configurations['moderation']['config']['require_reason'], - description: localize('moderation', 'moderate-reason-description') + name: 'id', + required: true, + autocomplete: true, + description: localize('moderation', 'moderate-userid-description') }, - { - type: 'ATTACHMENT', - name: 'proof', - required: (client.configurations['moderation']['config']['require_proof'] && client.configurations['moderation']['config']['require_reason']), - description: localize('moderation', 'moderate-proof-description') + { + type: 'STRING', + name: 'reason', + required: client.configurations['moderation']['config']['require_reason'], + description: localize('moderation', 'moderate-reason-description') + } + ]; + } + }, + { + type: 'SUB_COMMAND', + name: 'unquarantine', + description: localize('moderation', 'moderate-unquarantine-command-description'), + options: function (client) { + return [{ + type: 'USER', + name: 'user', + required: true, + description: localize('moderation', 'moderate-user-description') }, - { - type: 'STRING', - name: 'duration', - required: false, - description: localize('moderation', 'moderate-duration-description') + { + type: 'STRING', + name: 'reason', + required: client.configurations['moderation']['config']['require_reason'], + description: localize('moderation', 'moderate-reason-description') + } + ]; + } + }, + { + type: 'SUB_COMMAND', + name: 'clear', + description: localize('moderation', 'moderate-clear-command-description'), + options: [{ + type: 'INTEGER', + name: 'amount', + required: false, + description: localize('moderation', 'moderate-clear-amount-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'kick', + description: localize('moderation', 'moderate-kick-command-description'), + options: function (client) { + return [{ + type: 'USER', + name: 'user', + required: true, + description: localize('moderation', 'moderate-user-description') }, - { - type: 'INTEGER', - name: 'days', - required: false, - description: localize('moderation', 'moderate-days-description') - } - ]; - } - }, - { - type: 'SUB_COMMAND', - name: 'quarantine', - description: localize('moderation', 'moderate-quarantine-command-description'), - options: function (client) { - return [{ - type: 'USER', - name: 'user', - required: true, - description: localize('moderation', 'moderate-user-description') - }, - { - type: 'STRING', - name: 'reason', - required: client.configurations['moderation']['config']['require_reason'], - description: localize('moderation', 'moderate-reason-description') + { + type: 'STRING', + name: 'reason', + required: client.configurations['moderation']['config']['require_reason'], + description: localize('moderation', 'moderate-reason-description') + }, + { + type: 'ATTACHMENT', + name: 'proof', + required: (client.configurations['moderation']['config']['require_proof'] && client.configurations['moderation']['config']['require_reason']), + description: localize('moderation', 'moderate-proof-description') + } + ]; + } + }, + { + type: 'SUB_COMMAND', + name: 'mute', + description: localize('moderation', 'moderate-mute-command-description'), + options: function (client) { + return [{ + type: 'USER', + name: 'user', + required: true, + description: localize('moderation', 'moderate-user-description') }, - { - type: 'STRING', - name: 'duration', - required: false, - description: localize('moderation', 'moderate-duration-description') - } - ]; - } - }, - { - type: 'SUB_COMMAND', - name: 'unban', - description: localize('moderation', 'moderate-unban-command-description'), - options: function (client) { - return [{ - type: 'STRING', - name: 'id', - required: true, - autocomplete: true, - description: localize('moderation', 'moderate-userid-description') - }, - { - type: 'STRING', - name: 'reason', - required: client.configurations['moderation']['config']['require_reason'], - description: localize('moderation', 'moderate-reason-description') - } - ]; - } - }, - { - type: 'SUB_COMMAND', - name: 'unquarantine', - description: localize('moderation', 'moderate-unquarantine-command-description'), - options: function (client) { - return [{ - type: 'USER', - name: 'user', - required: true, - description: localize('moderation', 'moderate-user-description') - }, - { - type: 'STRING', - name: 'reason', - required: client.configurations['moderation']['config']['require_reason'], - description: localize('moderation', 'moderate-reason-description') - } - ]; - } - }, - { - type: 'SUB_COMMAND', - name: 'clear', - description: localize('moderation', 'moderate-clear-command-description'), - options: [{ - type: 'INTEGER', - name: 'amount', - required: false, - description: localize('moderation', 'moderate-clear-amount-description') - } - ] - }, - { - type: 'SUB_COMMAND', - name: 'kick', - description: localize('moderation', 'moderate-kick-command-description'), - options: function (client) { - return [{ - type: 'USER', - name: 'user', - required: true, - description: localize('moderation', 'moderate-user-description') - }, - { - type: 'STRING', - name: 'reason', - required: client.configurations['moderation']['config']['require_reason'], - description: localize('moderation', 'moderate-reason-description') + { + type: 'STRING', + name: 'duration', + required: true, + description: localize('moderation', 'moderate-duration-description') + }, + { + type: 'STRING', + name: 'reason', + required: client.configurations['moderation']['config']['require_reason'], + description: localize('moderation', 'moderate-reason-description') + }, + { + type: 'ATTACHMENT', + name: 'proof', + required: (client.configurations['moderation']['config']['require_proof'] && client.configurations['moderation']['config']['require_reason']), + description: localize('moderation', 'moderate-proof-description') + } + ]; + } + }, + { + type: 'SUB_COMMAND', + name: 'unmute', + description: localize('moderation', 'moderate-unmute-command-description'), + options: function (client) { + return [{ + type: 'USER', + name: 'user', + required: true, + description: localize('moderation', 'moderate-user-description') }, - { - type: 'ATTACHMENT', - name: 'proof', - required: (client.configurations['moderation']['config']['require_proof'] && client.configurations['moderation']['config']['require_reason']), - description: localize('moderation', 'moderate-proof-description') - } - ]; - } - }, - { - type: 'SUB_COMMAND', - name: 'mute', - description: localize('moderation', 'moderate-mute-command-description'), - options: function (client) { - return [{ - type: 'USER', - name: 'user', - required: true, - description: localize('moderation', 'moderate-user-description') - }, - { - type: 'STRING', - name: 'duration', + { + type: 'STRING', + name: 'reason', + required: client.configurations['moderation']['config']['require_reason'], + description: localize('moderation', 'moderate-reason-description') + } + ]; + } + }, + { + type: 'SUB_COMMAND', + name: 'warn', + description: localize('moderation', 'moderate-warn-command-description'), + options: function (client) { + return [{ + type: 'USER', + name: 'user', required: true, - description: localize('moderation', 'moderate-duration-description') + description: localize('moderation', 'moderate-user-description') }, - { - type: 'STRING', - name: 'reason', - required: client.configurations['moderation']['config']['require_reason'], - description: localize('moderation', 'moderate-reason-description') + { + type: 'STRING', + name: 'reason', + required: client.configurations['moderation']['config']['require_reason'], + description: localize('moderation', 'moderate-reason-description') + }, + { + type: 'ATTACHMENT', + name: 'proof', + required: (client.configurations['moderation']['config']['require_proof'] && client.configurations['moderation']['config']['require_reason']), + description: localize('moderation', 'moderate-proof-description') + } + ]; + } + }, + { + type: 'SUB_COMMAND', + name: 'channel-mute', + description: localize('moderation', 'moderate-channel-mute-description'), + options: function (client) { + return [{ + type: 'USER', + name: 'user', + required: true, + description: localize('moderation', 'moderate-user-description') }, - { - type: 'ATTACHMENT', - name: 'proof', - required: (client.configurations['moderation']['config']['require_proof'] && client.configurations['moderation']['config']['require_reason']), - description: localize('moderation', 'moderate-proof-description') - } - ]; - } - }, - { - type: 'SUB_COMMAND', - name: 'unmute', - description: localize('moderation', 'moderate-unmute-command-description'), - options: function (client) { - return [{ - type: 'USER', - name: 'user', - required: true, - description: localize('moderation', 'moderate-user-description') - }, - { - type: 'STRING', - name: 'reason', - required: client.configurations['moderation']['config']['require_reason'], - description: localize('moderation', 'moderate-reason-description') - } - ]; - } - }, - { - type: 'SUB_COMMAND', - name: 'warn', - description: localize('moderation', 'moderate-warn-command-description'), - options: function (client) { - return [{ - type: 'USER', - name: 'user', - required: true, - description: localize('moderation', 'moderate-user-description') - }, - { - type: 'STRING', - name: 'reason', - required: client.configurations['moderation']['config']['require_reason'], - description: localize('moderation', 'moderate-reason-description') + { + type: 'STRING', + name: 'reason', + required: client.configurations['moderation']['config']['require_reason'], + description: localize('moderation', 'moderate-reason-description') + }, + { + type: 'ATTACHMENT', + name: 'proof', + required: (client.configurations['moderation']['config']['require_proof'] && client.configurations['moderation']['config']['require_reason']), + description: localize('moderation', 'moderate-proof-description') + } + ]; + } + }, + { + type: 'SUB_COMMAND', + name: 'remove-channel-mute', + description: localize('moderation', 'moderate-unchannel-mute-description'), + options: function (client) { + return [{ + type: 'USER', + name: 'user', + required: true, + description: localize('moderation', 'moderate-user-description') }, - { - type: 'ATTACHMENT', - name: 'proof', - required: (client.configurations['moderation']['config']['require_proof'] && client.configurations['moderation']['config']['require_reason']), - description: localize('moderation', 'moderate-proof-description') - } - ]; - } - }, - { - type: 'SUB_COMMAND', - name: 'channel-mute', - description: localize('moderation', 'moderate-channel-mute-description'), - options: function (client) { - return [{ + { + type: 'STRING', + name: 'reason', + required: client.configurations['moderation']['config']['require_reason'], + description: localize('moderation', 'moderate-reason-description') + } + ]; + } + }, + { + type: 'SUB_COMMAND', + name: 'actions', + description: localize('moderation', 'moderate-actions-command-description'), + options: [{ type: 'USER', name: 'user', required: true, description: localize('moderation', 'moderate-user-description') - }, - { + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'revoke-warn', + description: localize('moderation', 'moderate-unwarn-command-description'), + options: function (client) { + return [{ type: 'STRING', - name: 'reason', - required: client.configurations['moderation']['config']['require_reason'], - description: localize('moderation', 'moderate-reason-description') - }, - { - type: 'ATTACHMENT', - name: 'proof', - required: (client.configurations['moderation']['config']['require_proof'] && client.configurations['moderation']['config']['require_reason']), - description: localize('moderation', 'moderate-proof-description') - } - ]; - } - }, - { - type: 'SUB_COMMAND', - name: 'remove-channel-mute', - description: localize('moderation', 'moderate-unchannel-mute-description'), - options: function (client) { - return [{ - type: 'USER', - name: 'user', - required: true, - description: localize('moderation', 'moderate-user-description') - }, - { + name: 'warn-id', + required: true, + autocomplete: true, + description: localize('moderation', 'moderate-warnid-description') + }, { type: 'STRING', name: 'reason', required: client.configurations['moderation']['config']['require_reason'], description: localize('moderation', 'moderate-reason-description') } - ]; - } - }, - { - type: 'SUB_COMMAND', - name: 'actions', - description: localize('moderation', 'moderate-actions-command-description'), - options: [{ - type: 'USER', - name: 'user', - required: true, - description: localize('moderation', 'moderate-user-description') + ]; + } + }, + { + type: 'SUB_COMMAND', + name: 'lock', + description: localize('moderation', 'moderate-lock-command-description'), + options: function (client) { + return [ + { + type: 'STRING', + name: 'reason', + required: client.configurations['moderation']['config']['require_reason'], + description: localize('moderation', 'moderate-reason-description') + } + ]; + } + }, + { + type: 'SUB_COMMAND', + name: 'unlock', + description: localize('moderation', 'moderate-unlock-command-description') } - ] - }, - { - type: 'SUB_COMMAND', - name: 'revoke-warn', - description: localize('moderation', 'moderate-unwarn-command-description'), - options: function (client) { - return [{ - type: 'STRING', - name: 'warn-id', + ]; + const lockdownConfig = client.configurations['moderation']['lockdown']; + if (lockdownConfig && lockdownConfig.enabled) { + opts.push({ + type: 'SUB_COMMAND', + name: 'lockdown', + description: localize('moderation', 'moderate-lockdown-command-description'), + options: [{ + type: 'BOOLEAN', + name: 'enable', required: true, - autocomplete: true, - description: localize('moderation', 'moderate-warnid-description') + description: localize('moderation', 'moderate-lockdown-enable-description') }, { type: 'STRING', name: 'reason', required: client.configurations['moderation']['config']['require_reason'], description: localize('moderation', 'moderate-reason-description') - } - ]; - } - }, - { - type: 'SUB_COMMAND', - name: 'lock', - description: localize('moderation', 'moderate-lock-command-description'), - options: function (client) { - return [ - { - type: 'STRING', - name: 'reason', - required: client.configurations['moderation']['config']['require_reason'], - description: localize('moderation', 'moderate-reason-description') - } - ]; - } - }, - { - type: 'SUB_COMMAND', - name: 'unlock', - description: localize('moderation', 'moderate-unlock-command-description') + }] + }); } - ] + return opts; + } }; \ No newline at end of file diff --git a/modules/moderation/commands/report.js b/modules/moderation/commands/report.js index df2bbf4c..1134b883 100644 --- a/modules/moderation/commands/report.js +++ b/modules/moderation/commands/report.js @@ -2,7 +2,8 @@ const {localize} = require('../../../src/functions/localize'); const { embedType, messageLogToStringToPaste, - parseEmbedColor + parseEmbedColor, + safeSetFooter } = require('../../../src/functions/helpers'); const {MessageEmbed} = require('discord.js'); @@ -37,23 +38,23 @@ module.exports.run = async function (interaction) { value: `[${localize('moderation', 'file')}](${proof.proxyURL || proof.url})`, inline: true }); + const reportEmbed = new MessageEmbed() + .setTitle(localize('moderation', 'report-embed-title')) + .setDescription(localize('moderation', 'report-embed-description')) + .addField(localize('moderation', 'reported-user'), interaction.options.getUser('user').toString() + ` \`${interaction.options.getUser('user').id}\``, true) + .addField(localize('moderation', 'message-log'), localize('moderation', 'message-log-description', {u: logUrl}), true) + .addField(localize('moderation', 'channel'), interaction.channel.toString(), true) + .addField(localize('moderation', 'report-reason'), interaction.options.getString('reason')) + .addField(localize('moderation', 'report-user'), interaction.user.toString() + ` \`${interaction.user.id}\``) + .addFields(fields) + .setColor(parseEmbedColor('RED')) + .setImage(proof ? (proof.proxyURL || proof.url) : null) + .setAuthor({name: interaction.client.user.username, iconURL: interaction.client.user.avatarURL()}) + .setTimestamp(); + safeSetFooter(reportEmbed, interaction.client); + logChannel.send({ - embeds: [ - new MessageEmbed() - .setTitle(localize('moderation', 'report-embed-title')) - .setDescription(localize('moderation', 'report-embed-description')) - .addField(localize('moderation', 'reported-user'), interaction.options.getUser('user').toString() + ` \`${interaction.options.getUser('user').id}\``, true) - .addField(localize('moderation', 'message-log'), localize('moderation', 'message-log-description', {u: logUrl}), true) - .addField(localize('moderation', 'channel'), interaction.channel.toString(), true) - .addField(localize('moderation', 'report-reason'), interaction.options.getString('reason')) - .addField(localize('moderation', 'report-user'), interaction.user.toString() + ` \`${interaction.user.id}\``) - .addFields(fields) - .setColor(parseEmbedColor('RED')) - .setImage(proof ? (proof.proxyURL || proof.url) : null) - .setFooter({text: interaction.client.strings.footer, iconURL: interaction.client.strings.footerImgUrl}) - .setAuthor({name: interaction.client.user.username, iconURL: interaction.client.user.avatarURL()}) - .setTimestamp() - ], + embeds: [reportEmbed], content: pingContent }); interaction.editReply(embedType(interaction.client.configurations['moderation']['strings']['submitted-report-message'], { diff --git a/modules/moderation/configs/antiGrief.json b/modules/moderation/configs/antiGrief.json index eb1595f2..ea9e16eb 100644 --- a/modules/moderation/configs/antiGrief.json +++ b/modules/moderation/configs/antiGrief.json @@ -31,7 +31,8 @@ "de": "Aktiviert oder deaktiviert das Anti-Join-Grief-System" }, "type": "boolean", - "elementToggle": true + "elementToggle": true, + "category": "settings" }, { "name": "timeframe", @@ -46,7 +47,8 @@ "en": "Timeframe in hours in which the limits can not be overstepped", "de": "Zeitfenster in Stunden, in welchem die Limits nicht überschritten werden dürfen" }, - "type": "integer" + "type": "integer", + "category": "settings" }, { "name": "max_warn", @@ -61,7 +63,8 @@ "en": "Maximal amount of warns a moderator can give in the timeframe until they get quarantined", "de": "Maximale Anzahl von Verwarnungen, die ein Moderator in dem Zeitfenster vergeben kann, bis sie in Quarantäne gesteckt werden" }, - "type": "integer" + "type": "integer", + "category": "actions" }, { "name": "max_mute", @@ -76,7 +79,8 @@ "en": "Maximal amount of mutes a moderator can give in the timeframe until they get quarantined", "de": "Maximale Anzahl von Mutes, die ein Moderator in dem Zeitfenster vergeben kann, bis sie in Quarantäne gesteckt werden" }, - "type": "integer" + "type": "integer", + "category": "actions" }, { "name": "max_kick", @@ -91,7 +95,8 @@ "en": "Maximal amount of kicks a moderator can give in the timeframe until they get quarantined", "de": "Maximale Anzahl von Kicks, die ein Moderator in dem Zeitfenster vergeben kann, bis sie in Quarantäne gesteckt werden" }, - "type": "integer" + "type": "integer", + "category": "actions" }, { "name": "max_ban", @@ -106,7 +111,26 @@ "en": "Maximal amount of bans a moderator can give in the timeframe until they get quarantined", "de": "Maximale Anzahl von Bans, die ein Moderator in dem Zeitfenster vergeben kann, bis sie in Quarantäne gesteckt werden" }, - "type": "integer" + "type": "integer", + "category": "actions" + } + ], + "categories": [ + { + "id": "settings", + "icon": "fas fa-gears", + "displayName": { + "en": "Detection Settings", + "de": "Erkennungseinstellungen" + } + }, + { + "id": "actions", + "icon": "fas fa-hammer", + "displayName": { + "en": "Actions", + "de": "Aktionen" + } } ] } \ No newline at end of file diff --git a/modules/moderation/configs/antiJoinRaid.json b/modules/moderation/configs/antiJoinRaid.json index e219499e..aa7c6852 100644 --- a/modules/moderation/configs/antiJoinRaid.json +++ b/modules/moderation/configs/antiJoinRaid.json @@ -24,7 +24,8 @@ "de": "Aktiviert oder deaktiviert das Anti-Join-Raid-System" }, "type": "boolean", - "elementToggle": true + "elementToggle": true, + "category": "settings" }, { "name": "timeframe", @@ -40,7 +41,8 @@ "en": "Timeframe in which join actions should be recorded (in minutes)", "de": "Zeitfenster, in welchem Serverbeitritte gezählt werden sollen (in Minuten)" }, - "type": "integer" + "type": "integer", + "category": "settings" }, { "name": "maxJoinsInTimeframe", @@ -56,7 +58,8 @@ "en": "Count of joins that are allowed to happen in the selected timeframe", "de": "Anzahl an Serverbeitritten, die im ausgewählten Zeitfenster zugelassen werden" }, - "type": "integer" + "type": "integer", + "category": "settings" }, { "name": "action", @@ -79,7 +82,8 @@ "quarantine", "ban", "give-role" - ] + ], + "category": "actions" }, { "name": "roleID", @@ -94,7 +98,8 @@ "en": "Only if action = give-role. Role that gets given to users who trigger the antiJoinRaid-System", "de": "Nur verfügbar, wenn Aktion = give-role. Rolle, die Nutzern gegeben wird, die das Anti-Join-Raid-System auslösen" }, - "type": "roleID" + "type": "roleID", + "category": "actions" }, { "name": "removeOtherRoles", @@ -110,7 +115,26 @@ "en": "Only if action = give-role. If enabled other roles that have been give to the user get removed after a short interval (and the giving of the role from \"roleID\" will be delayed)", "de": "Nur verfügbar, wenn Aktion = give-role. Wenn aktiviert, werden andere Rollen die der Nutzer hat nach einem kurzen Zeitraum entfernt (und das Vergeben der Rolle von \"Rolle\" wird verzögert)" }, - "type": "boolean" + "type": "boolean", + "category": "actions" + } + ], + "categories": [ + { + "id": "settings", + "icon": "fas fa-gears", + "displayName": { + "en": "Detection Settings", + "de": "Erkennungseinstellungen" + } + }, + { + "id": "actions", + "icon": "fas fa-hammer", + "displayName": { + "en": "Actions", + "de": "Aktionen" + } } ] } \ No newline at end of file diff --git a/modules/moderation/configs/antiSpam.json b/modules/moderation/configs/antiSpam.json index a1173b89..3542a71e 100644 --- a/modules/moderation/configs/antiSpam.json +++ b/modules/moderation/configs/antiSpam.json @@ -24,7 +24,8 @@ "de": "Aktiviert oder deaktiviert das Anti-Spam-System" }, "type": "boolean", - "elementToggle": true + "elementToggle": true, + "category": "settings" }, { "name": "timeframe", @@ -40,7 +41,8 @@ "en": "Timeframe in seconds after which message objects get deleted (and can not longer be used to detect spam)", "de": "Zeitfenster in Sekunden, in dem Nachrichten gelöscht werden (und nicht länger zur Erkennung von Spam verwendet werden können)" }, - "type": "integer" + "type": "integer", + "category": "settings" }, { "name": "maxMessagesInTimeframe", @@ -56,7 +58,8 @@ "en": "Count of messages that are allowed to be sent in the selected timeframe", "de": "Anzahl an Nachrichten, die im ausgewählten Zeitfenster erlaubt sind" }, - "type": "integer" + "type": "integer", + "category": "settings" }, { "name": "maxDuplicatedMessagesInTimeframe", @@ -72,7 +75,8 @@ "en": "Count of identical messages that are allowed to be sent in the selected timeframe", "de": "Anzahl an gleichen Nachrichten, die im ausgewählten Zeitfenster erlaubt sind" }, - "type": "integer" + "type": "integer", + "category": "settings" }, { "name": "maxPingsInTimeframe", @@ -88,7 +92,8 @@ "en": "Count of pings (also counts replies) that are allowed to be sent in the selected timeframe", "de": "Anzahl an Erwähnungen (zählt auch Antworten), die im ausgewählten Zeitfenster erlaubt sind" }, - "type": "integer" + "type": "integer", + "category": "settings" }, { "name": "maxMassPings", @@ -104,7 +109,8 @@ "en": "Count of mass pings (= @everyone, @here and roles) that are allowed to be sent in the selected timeframe", "de": "Anzahl an Massenerwähnungen (= @everyone, @here und Rollen), die im ausgewählten Zeitfenster erlaubt sind" }, - "type": "integer" + "type": "integer", + "category": "settings" }, { "name": "action", @@ -127,7 +133,8 @@ "kick", "quarantine", "ban" - ] + ], + "category": "actions" }, { "name": "sendChatMessage", @@ -143,7 +150,8 @@ "en": "If enabled the bot will send a chat message if it has to take action agains a bot", "de": "Wenn aktiviert, wird der Bot eine Nachricht in den Chat senden, wenn er eine Aktion gegen einen Bot ausführen musste" }, - "type": "boolean" + "type": "boolean", + "category": "actions" }, { "name": "message", @@ -177,7 +185,8 @@ "de": "Grund der Aktion" } } - ] + ], + "category": "actions" }, { "name": "ignoredChannels", @@ -194,7 +203,8 @@ "de": "Du kannst hier Kanäle einstellen, die ignoriert werden sollen" }, "type": "array", - "content": "channelID" + "content": "channelID", + "category": "exemptions" }, { "name": "ignoredRoles", @@ -211,7 +221,34 @@ "de": "Du kannst hier Rollen einstellen, die ignoriert werden sollen" }, "type": "array", - "content": "roleID" + "content": "roleID", + "category": "exemptions" + } + ], + "categories": [ + { + "id": "settings", + "icon": "fas fa-gears", + "displayName": { + "en": "Detection Settings", + "de": "Erkennungseinstellungen" + } + }, + { + "id": "actions", + "icon": "fas fa-hammer", + "displayName": { + "en": "Actions", + "de": "Aktionen" + } + }, + { + "id": "exemptions", + "icon": "fa-solid fa-shield", + "displayName": { + "en": "Exemptions", + "de": "Ausnahmen" + } } ] } \ No newline at end of file diff --git a/modules/moderation/configs/config.json b/modules/moderation/configs/config.json index 6ca5c4c8..ef76531f 100644 --- a/modules/moderation/configs/config.json +++ b/modules/moderation/configs/config.json @@ -33,7 +33,8 @@ "en": "Moderative actions will get logged in this channel", "de": "Moderative Aktionen werden in diesem Kanal geloggt" }, - "type": "channelID" + "type": "channelID", + "category": "general" }, { "name": "quarantine-role-id", @@ -48,7 +49,8 @@ "en": "When a user gets quarantined, all of their roles will get removed and this quarantine-role wil get assigned", "de": "Wenn ein Nutzer in Quarantäne gesteckt wird, werden alle Rollen von diesem entfernt und nur diese hinzugefügt" }, - "type": "roleID" + "type": "roleID", + "category": "roles" }, { "name": "report-channel-id", @@ -64,7 +66,8 @@ "de": "Kanal, in welchem Nutzer-Reports should get send. (optional, default: Log-Kanal)" }, "type": "channelID", - "allowNull": true + "allowNull": true, + "category": "reports" }, { "name": "remove-all-roles-on-quarantine", @@ -80,7 +83,8 @@ "en": "If enabled all roles from a user get removed if they get quarantined (they get saved an can be restored with /unquarantine)", "de": "Wenn diese Option aktiviert ist, werden alle Rollen eines Nutzers entfernt, wenn er in Quarantäne gesetzt wird (sie werden gespeichert und mit /unquarantine wiederhergestellt)" }, - "type": "boolean" + "type": "boolean", + "category": "roles" }, { "name": "moderator-roles_level1", @@ -96,7 +100,8 @@ "de": "Rollen, die folgende Aktionen ausführen können: Warn" }, "type": "array", - "content": "roleID" + "content": "roleID", + "category": "roles" }, { "name": "moderator-roles_level2", @@ -112,7 +117,8 @@ "de": "Rollen, die folgende Aktionen ausführen können: Warn, Mute, Unmute, Channelmute, Channel-Mute entfernen" }, "type": "array", - "content": "roleID" + "content": "roleID", + "category": "roles" }, { "name": "moderator-roles_level3", @@ -128,7 +134,8 @@ "de": "Rollen, die folgende Aktionen ausführen können: Warn, Mute, Unmute, Kick, Clear" }, "type": "array", - "content": "roleID" + "content": "roleID", + "category": "roles" }, { "name": "moderator-roles_level4", @@ -144,7 +151,8 @@ "de": "Rollen, die folgende Aktionen ausführen können: Warn, Mute, Unmute, Kick, Clear, Ban, Unban" }, "type": "array", - "content": "roleID" + "content": "roleID", + "category": "roles" }, { "name": "roles-to-ping-on-report", @@ -161,7 +169,8 @@ "de": "Rollen, die im log-Kanal gepingt werden sollen, wenn ein Nutzer jemanden Reportet" }, "type": "array", - "content": "roleID" + "content": "roleID", + "category": "reports" }, { "name": "require_reason", @@ -177,7 +186,8 @@ "en": "Should moderators be required to set a reason?", "de": "Sollen Moderatoren verpflichtet werden, eine Begründung anzugeben?" }, - "type": "boolean" + "type": "boolean", + "category": "reports" }, { "name": "require_proof", @@ -194,7 +204,8 @@ "en": "Should moderators be required to upload proof for their actions?", "de": "Sollen Moderatoren verpflichtet werden, einen Beweis hochzuladen?" }, - "type": "boolean" + "type": "boolean", + "category": "reports" }, { "name": "action_on_invite", @@ -218,7 +229,8 @@ "kick", "quarantine", "ban" - ] + ], + "category": "automod" }, { "name": "action_on_scam_link", @@ -242,7 +254,8 @@ "kick", "quarantine", "ban" - ] + ], + "category": "automod" }, { "name": "scam_link_level", @@ -262,7 +275,8 @@ "content": [ "confirmed", "suspicious" - ] + ], + "category": "automod" }, { "name": "whitelisted_channels_for_invite_blocking", @@ -279,7 +293,8 @@ "de": "Kanäle oder Kategorien, in welchen die Invitesperre deaktiviert ist" }, "type": "array", - "content": "channelID" + "content": "channelID", + "category": "automod" }, { "name": "whitelisted_roles_for_invite_blocking", @@ -296,7 +311,8 @@ "de": "Rollen, welche die Invitesperre umgehen dürfen" }, "type": "array", - "content": "roleID" + "content": "roleID", + "category": "automod" }, { "name": "blacklisted_words", @@ -313,7 +329,8 @@ "de": "Wörter, die blockiert sind" }, "type": "array", - "content": "string" + "content": "string", + "category": "automod" }, { "name": "action_on_posting_blacklisted_word", @@ -337,7 +354,8 @@ "kick", "ban", "quarantine" - ] + ], + "category": "automod" }, { "name": "defaultMuteDuration", @@ -352,7 +370,8 @@ "description": { "en": "Default mute duration when none was configured. Will also be used for automod features (e.g. when someone posts a blacklisted word). Maximum value of 28 days.", "de": "Standardmäßige Mute-Länge, wenn keine eingestellt wurde. Wird auch für Automod-Funktionen verwendet (also wenn z.B. jemand ein gesperrtes Wort postet). Höchstlänge von 28 Tagen." - } + }, + "category": "actions" }, { "name": "changeNicknames", @@ -368,7 +387,8 @@ "en": "If enabled, the user will get renamed when they get muted or quarantined", "de": "Wenn aktiviert, wird der Nutzer umbenannt, wenn er gemutet oder in Quarantäne gesteckt wird" }, - "type": "boolean" + "type": "boolean", + "category": "nicknames" }, { "name": "changeNicknameOnMute", @@ -393,7 +413,8 @@ "en": "Original nickname of the user" } } - ] + ], + "category": "nicknames" }, { "name": "changeNicknameOnQuarantine", @@ -417,7 +438,8 @@ "en": "Original nickname of the user" } } - ] + ], + "category": "nicknames" }, { "name": "automod", @@ -437,7 +459,8 @@ "content": { "key": "integer", "value": "string" - } + }, + "category": "automod" }, { "name": "warnsExpire", @@ -452,7 +475,8 @@ "en": "If enabled, warns will be deleted automatically after a certain period of time. Warns expired this way will completely disappear and can not be viewed again after they expired.", "de": "Wenn aktiviert, werden Warns automatisch nach einer bestimmten Zeitspanne gelöscht. Auf diese Weiße abgelaufene Warns werden komplett verschwinden und können nie erneut gesehen werden." }, - "type": "boolean" + "type": "boolean", + "category": "actions" }, { "name": "warnExpiration", @@ -468,7 +492,58 @@ "en": "Warns will be automatically deleted after this value after it's creation. Please note that this action will delete existing warns if they expired. Enter an english value, such as \"1y\" (= 1 year), \"3 Months\" (= 3 Months) oder \"2w\" (= 2 Weeks).", "de": "Warnungen werden automatisch gelöscht, wenn sie diese Zeitspanne nach Erstellung erreicht haben. Trage einen englischen Wert, wie \"1y\" (= 1 Jahr), \"3 Months\" (= 3 Monate) oder \"2w\" (= 2 Woche) ein." }, - "type": "string" + "type": "string", + "category": "actions" + } + ], + "categories": [ + { + "id": "general", + "icon": "fas fa-gears", + "displayName": { + "en": "General Settings", + "de": "Allgemeine Einstellungen" + } + }, + { + "id": "roles", + "icon": "fa-solid fa-users", + "displayName": { + "en": "Roles & Permissions", + "de": "Rollen & Berechtigungen" + } + }, + { + "id": "reports", + "icon": "fa-solid fa-flag", + "displayName": { + "en": "Reports", + "de": "Meldungen" + } + }, + { + "id": "automod", + "icon": "far fa-robot", + "displayName": { + "en": "Auto-Moderation", + "de": "Auto-Moderation" + } + }, + { + "id": "actions", + "icon": "fas fa-hammer", + "displayName": { + "en": "Actions & Punishments", + "de": "Aktionen & Bestrafungen" + } + }, + { + "id": "nicknames", + "icon": "fa-solid fa-user-pen", + "displayName": { + "en": "Nickname Management", + "de": "Nicknamen-Verwaltung" + } } ] } \ No newline at end of file diff --git a/modules/moderation/configs/joinGate.json b/modules/moderation/configs/joinGate.json index dcacd24a..95194423 100644 --- a/modules/moderation/configs/joinGate.json +++ b/modules/moderation/configs/joinGate.json @@ -24,7 +24,8 @@ "de": "Aktiviere oder deaktiviere das Join-Gate" }, "type": "boolean", - "elementToggle": true + "elementToggle": true, + "category": "general" }, { "name": "allUsers", @@ -39,7 +40,8 @@ "en": "If enabled all users action against all new users will be taken", "de": "Wenn aktiviert, werden Aktionen gegen alle neuen Nutzer ausgefüht" }, - "type": "boolean" + "type": "boolean", + "category": "general" }, { "name": "action", @@ -62,7 +64,8 @@ "quarantine", "ban", "give-role" - ] + ], + "category": "roles" }, { "name": "roleID", @@ -77,7 +80,8 @@ "en": "Only if action = give-role. Role that gets given to users who fail the join gate", "de": "Nur verfügbar, wenn Aktion = give-role. Rolle, die Nutzern gegeben wird, die das Join-Gate nicht bestehen" }, - "type": "roleID" + "type": "roleID", + "category": "roles" }, { "name": "removeOtherRoles", @@ -93,7 +97,8 @@ "en": "Only if action = give-role. If enabled other roles that have been give to the user get removed after a short interval (and the giving of the role from \"roleID\" will be delayed)", "de": "Nur verfügbar, wenn Aktion = give-role. Wenn aktiviert, werden andere Rollen die der Nutzer hat nach einem kurzen Zeitraum entfernt (und das Vergeben der Rolle von \"Rolle\" wird verzögert)" }, - "type": "boolean" + "type": "boolean", + "category": "roles" }, { "name": "minAccountAge", @@ -109,7 +114,8 @@ "en": "Age of the account of a new user that is required to be set to pass the join gate (in days)", "de": "Alter des Accounts eines neuen Nutzers, der beitritt, welches benötigt wird um das Join-Gate zu bestehen (in Tagen)" }, - "type": "integer" + "type": "integer", + "category": "general" }, { "name": "requireProfilePicture", @@ -125,7 +131,8 @@ "en": "If enabled users are required to have a profile picture set to pass the join gate", "de": "Wenn aktiviert, brauchen Nutzer ein Profilbild um das Join-Gate zu bestehen" }, - "type": "boolean" + "type": "boolean", + "category": "general" }, { "name": "ignoreBots", @@ -141,7 +148,26 @@ "en": "If enabled bots are allowed to pass the join gate without any restrictions", "de": "Wenn aktiviert, bestehen Bots das Join-Gate ohne Beschränkungen" }, - "type": "boolean" + "type": "boolean", + "category": "general" + } + ], + "categories": [ + { + "id": "general", + "icon": "fas fa-door-open", + "displayName": { + "en": "General Settings", + "de": "Allgemeine Einstellungen" + } + }, + { + "id": "roles", + "icon": "fa-solid fa-users", + "displayName": { + "en": "Roles", + "de": "Rollen" + } } ] } \ No newline at end of file diff --git a/modules/moderation/configs/lockdown.json b/modules/moderation/configs/lockdown.json new file mode 100644 index 00000000..51b15db9 --- /dev/null +++ b/modules/moderation/configs/lockdown.json @@ -0,0 +1,221 @@ +{ + "description": { + "en": "Configure the server-wide lockdown system. This is separate from per-channel lock/unlock commands.", + "de": "Konfiguriere das serverweite Lockdown-System. Dies ist getrennt von den kanalweisen Sperr-/Entsperr-Befehlen." + }, + "humanName": { + "en": "Lockdown Configuration", + "de": "Lockdown-Konfiguration" + }, + "filename": "lockdown.json", + "content": [ + { + "name": "enabled", + "humanName": { + "en": "Enable lockdown system?", + "de": "Lockdown-System aktivieren?" + }, + "default": { + "en": false + }, + "description": { + "en": "Enables the /moderate lockdown command and automatic lockdown triggers", + "de": "Aktiviert den /moderate lockdown Befehl und automatische Lockdown-Auslöser" + }, + "type": "boolean", + "elementToggle": true, + "category": "general" + }, + { + "name": "logChannel", + "type": "channelID", + "dependsOn": "enabled", + "humanName": { + "en": "Lockdown log channel", + "de": "Lockdown-Log-Kanal" + }, + "default": { + "en": "" + }, + "description": { + "en": "Channel where detailed lockdown log entries are posted. Falls back to the moderation log channel if not set.", + "de": "Kanal, in dem detaillierte Lockdown-Logeinträge gepostet werden. Fällt auf den Moderations-Logkanal zurück, wenn nicht gesetzt." + }, + "category": "general" + }, + { + "name": "sendMessageInAffectedChannels", + "type": "boolean", + "dependsOn": "enabled", + "humanName": { + "en": "Send message in affected channels?", + "de": "Nachricht in betroffenen Kanälen senden?" + }, + "default": { + "en": true + }, + "description": { + "en": "If enabled, the lockdown/lift message will be sent in every affected channel", + "de": "Wenn aktiviert, wird die Lockdown-/Aufhebungsnachricht in jedem betroffenen Kanal gesendet" + }, + "category": "messages" + }, + { + "name": "lockdownMessage", + "type": "string", + "allowEmbed": true, + "dependsOn": "sendMessageInAffectedChannels", + "humanName": { + "en": "Lockdown activation message", + "de": "Lockdown-Aktivierungsnachricht" + }, + "description": { + "en": "Message sent in affected channels when lockdown is activated", + "de": "Nachricht, die in betroffenen Kanälen gesendet wird, wenn der Lockdown aktiviert wird" + }, + "default": { + "en": "🔒 **Server Lockdown** - This server is currently in lockdown mode. Reason: %reason%", + "de": "🔒 **Server-Lockdown** - Dieser Server befindet sich im Lockdown-Modus. Grund: %reason%" + }, + "params": [ + { + "name": "reason", + "description": { + "en": "Reason for the lockdown", + "de": "Grund für den Lockdown" + } + }, + { + "name": "user", + "description": { + "en": "User who activated the lockdown (or 'System' for automatic)", + "de": "Nutzer, der den Lockdown aktiviert hat (oder 'System' bei automatisch)" + } + } + ], + "category": "messages" + }, + { + "name": "liftMessage", + "type": "string", + "allowEmbed": true, + "dependsOn": "sendMessageInAffectedChannels", + "humanName": { + "en": "Lockdown lifted message", + "de": "Lockdown-Aufhebungsnachricht" + }, + "description": { + "en": "Message sent in affected channels when lockdown is lifted", + "de": "Nachricht, die in betroffenen Kanälen gesendet wird, wenn der Lockdown aufgehoben wird" + }, + "default": { + "en": "🔓 **Lockdown Lifted** - The server lockdown has been lifted. You can chat again.", + "de": "🔓 **Lockdown aufgehoben** - Der Server-Lockdown wurde aufgehoben. Ihr könnt wieder schreiben." + }, + "params": [ + { + "name": "user", + "description": { + "en": "User who lifted the lockdown", + "de": "Nutzer, der den Lockdown aufgehoben hat" + } + } + ], + "category": "messages" + }, + { + "name": "autoLiftAfter", + "type": "integer", + "dependsOn": "enabled", + "humanName": { + "en": "Auto-lift lockdown after (minutes, 0 = manual only)", + "de": "Lockdown automatisch aufheben nach (Minuten, 0 = nur manuell)" + }, + "default": { + "en": 0 + }, + "description": { + "en": "Automatically lift the lockdown after this many minutes. Set to 0 to require manual lifting.", + "de": "Den Lockdown nach dieser Anzahl Minuten automatisch aufheben. Auf 0 setzen für nur manuelle Aufhebung." + }, + "category": "automation" + }, + { + "name": "autoTriggerOnJoinRaid", + "type": "boolean", + "dependsOn": "enabled", + "humanName": { + "en": "Auto-lockdown on join raid?", + "de": "Automatischer Lockdown bei Join-Raid?" + }, + "default": { + "en": false + }, + "description": { + "en": "Automatically activate lockdown when the anti-join-raid system is triggered", + "de": "Lockdown automatisch aktivieren, wenn das Anti-Join-Raid-System ausgelöst wird" + }, + "category": "automation" + }, + { + "name": "autoTriggerOnJoinGate", + "type": "boolean", + "dependsOn": "enabled", + "humanName": { + "en": "Auto-lockdown on join-gate violations?", + "de": "Automatischer Lockdown bei Join-Gate-Verletzungen?" + }, + "default": { + "en": false + }, + "description": { + "en": "Automatically activate lockdown when the join-gate system is triggered. Thresholds are configured in the Join-Gate configuration.", + "de": "Lockdown automatisch aktivieren, wenn das Join-Gate-System ausgelöst wird. Schwellwerte werden in der Join-Gate-Konfiguration konfiguriert." + }, + "category": "automation" + }, + { + "name": "autoTriggerOnSpam", + "type": "boolean", + "dependsOn": "enabled", + "humanName": { + "en": "Auto-lockdown on spam detection?", + "de": "Automatischer Lockdown bei Spam-Erkennung?" + }, + "default": { + "en": false + }, + "description": { + "en": "Automatically activate lockdown when the anti-spam system is triggered. Thresholds are configured in the Anti-Spam configuration.", + "de": "Lockdown automatisch aktivieren, wenn das Anti-Spam-System ausgelöst wird. Schwellwerte werden in der Anti-Spam-Konfiguration konfiguriert." + }, + "category": "automation" + } + ], + "categories": [ + { + "id": "general", + "icon": "fas fa-gears", + "displayName": { + "en": "General Settings", + "de": "Allgemeine Einstellungen" + } + }, + { + "id": "messages", + "icon": "fas fa-comment-dots", + "displayName": { + "en": "Messages", + "de": "Nachrichten" + } + }, + { + "id": "automation", + "icon": "far fa-robot", + "displayName": { + "en": "Automation", + "de": "Automatisierung" + } + } + ] +} \ No newline at end of file diff --git a/modules/moderation/configs/strings.json b/modules/moderation/configs/strings.json index 23e79c29..392255a3 100644 --- a/modules/moderation/configs/strings.json +++ b/modules/moderation/configs/strings.json @@ -28,7 +28,8 @@ "en": "Required mod-level to do this." } } - ] + ], + "category": "actions" }, { "name": "user_not_found", @@ -41,7 +42,8 @@ "en": "Message that gets send if the user provided an invalid userid" }, "type": "string", - "allowEmbed": true + "allowEmbed": true, + "category": "actions" }, { "name": "missing_reason", @@ -54,7 +56,8 @@ "en": "Message that gets send if the user does not provide a reason and 'require reason' is activated" }, "type": "string", - "allowEmbed": true + "allowEmbed": true, + "category": "errors" }, { "name": "this_is_a_mod", @@ -67,7 +70,8 @@ "en": "Message that gets send if the user tries to mute another moderator" }, "type": "string", - "allowEmbed": true + "allowEmbed": true, + "category": "actions" }, { "name": "submitted-report-message", @@ -94,7 +98,8 @@ "en": "URL to the message log" } } - ] + ], + "category": "actions" }, { "name": "mute_message", @@ -121,7 +126,8 @@ "en": "Reason of the mute" } } - ] + ], + "category": "actions" }, { "name": "channel_mute", @@ -153,7 +159,8 @@ "en": "Channel from which the user got muted" } } - ] + ], + "category": "actions" }, { "name": "remove-channel_mute", @@ -185,7 +192,8 @@ "en": "Channel from which the user got unmuted" } } - ] + ], + "category": "actions" }, { "name": "tmpmute_message", @@ -218,7 +226,8 @@ "en": "Timestamp when this action expires" } } - ] + ], + "category": "actions" }, { "name": "quarantine_message", @@ -245,7 +254,8 @@ "en": "Reason of the mute" } } - ] + ], + "category": "actions" }, { "name": "tmpquarantine_message", @@ -278,7 +288,8 @@ "en": "Date when the quarantine is going to be removed automatically" } } - ] + ], + "category": "actions" }, { "name": "unquarantine_message", @@ -305,7 +316,8 @@ "en": "Reason of the mute" } } - ] + ], + "category": "actions" }, { "name": "unmute_message", @@ -332,7 +344,8 @@ "en": "Reason of the unmute" } } - ] + ], + "category": "actions" }, { "name": "kick_message", @@ -359,7 +372,8 @@ "en": "Reason of the kick" } } - ] + ], + "category": "actions" }, { "name": "ban_message", @@ -386,7 +400,8 @@ "en": "Reason of the ban" } } - ] + ], + "category": "actions" }, { "name": "tmpban_message", @@ -419,7 +434,8 @@ "en": "Date on which the ban expires" } } - ] + ], + "category": "actions" }, { "name": "warn_message", @@ -446,7 +462,8 @@ "en": "Reason of the warn" } } - ] + ], + "category": "actions" }, { "name": "lock_channel_message", @@ -473,7 +490,8 @@ "en": "Reason of the lock" } } - ] + ], + "category": "actions" }, { "name": "unlock_channel_message", @@ -494,7 +512,26 @@ "en": "Tag of the moderator" } } - ] + ], + "category": "actions" + } + ], + "categories": [ + { + "id": "actions", + "icon": "fas fa-hammer", + "displayName": { + "en": "Action Messages", + "de": "Aktionsnachrichten" + } + }, + { + "id": "errors", + "icon": "fa-duotone fa-regular fa-triangle-exclamation", + "displayName": { + "en": "Error Messages", + "de": "Fehlermeldungen" + } } ] } \ No newline at end of file diff --git a/modules/moderation/configs/verification.json b/modules/moderation/configs/verification.json index 48bf0cf3..bd97f94f 100644 --- a/modules/moderation/configs/verification.json +++ b/modules/moderation/configs/verification.json @@ -23,7 +23,8 @@ "de": "Wenn aktiviert, wird Verifikation auf deinem Server aktiviert" }, "type": "boolean", - "elementToggle": true + "elementToggle": true, + "category": "general" }, { "name": "verification-needed-role", @@ -39,7 +40,8 @@ "de": "Rolle, die Nutzer erhalten, bevor sie sich verifiziert haben" }, "type": "roleID", - "allowNull": true + "allowNull": true, + "category": "roles" }, { "name": "verification-passed-role", @@ -55,7 +57,8 @@ "de": "Rolle, die Nutzern gegeben werden soll, wenn sie sich erfolgreich verifiziert haben" }, "type": "roleID", - "allowNull": true + "allowNull": true, + "category": "roles" }, { "name": "verification-log", @@ -69,7 +72,8 @@ "de": "Kanal, in welchem alle Verifikation-Aktionen dokumentiert werden sollen" }, "type": "channelID", - "allowNull": true + "allowNull": true, + "category": "general" }, { "name": "type", @@ -89,7 +93,8 @@ "content": [ "manual", "captcha" - ] + ], + "category": "general" }, { "name": "captchaLevel", @@ -110,7 +115,8 @@ "easy", "medium", "hard" - ] + ], + "category": "messages" }, { "name": "actionOnFail", @@ -132,7 +138,8 @@ "quarantine", "ban", "mute" - ] + ], + "category": "general" }, { "name": "restart-verification-channel", @@ -148,7 +155,8 @@ "de": "(optional) Kanal in welchem Nutzer ganz einfach den Verifikationsprozess neustarten können (zum Beispiel, wenn der Nutzer PNs deaktiviert hat) und benachrichtigt werden, wenn wir sie nicht erreichen konnten" }, "type": "channelID", - "allowNull": true + "allowNull": true, + "category": "general" }, { "name": "captcha-message", @@ -165,7 +173,8 @@ "de": "Diese Nachricht wird an den Nutzer gesendet, der ein Captcha durchführen muss" }, "type": "string", - "allowEmbed": true + "allowEmbed": true, + "category": "messages" }, { "name": "manual-verification-message", @@ -182,7 +191,8 @@ "de": "Diese Nachricht wird an Nutzer geschickt, die manuell verifiziert werden müssen" }, "type": "string", - "allowEmbed": true + "allowEmbed": true, + "category": "messages" }, { "name": "captcha-failed-message", @@ -199,7 +209,8 @@ "de": "Diese Nachricht wird an Nutzer gesendet, bei denen die Verifikation fehlgeschlagen ist" }, "type": "string", - "allowEmbed": true + "allowEmbed": true, + "category": "messages" }, { "name": "captcha-succeeded-message", @@ -216,7 +227,8 @@ "de": "Diese Nachricht wird gesendet, wenn ein Nutzer die Verifikation erfolgreich abgeschlossen hat" }, "type": "string", - "allowEmbed": true + "allowEmbed": true, + "category": "messages" }, { "name": "verify-channel-first-message", @@ -233,7 +245,34 @@ "de": "Das ist die Informations-Nachricht im Verfikationskanal." }, "type": "string", - "allowEmbed": true + "allowEmbed": true, + "category": "messages" + } + ], + "categories": [ + { + "id": "general", + "icon": "fa-solid fa-badge-check", + "displayName": { + "en": "General Settings", + "de": "Allgemeine Einstellungen" + } + }, + { + "id": "messages", + "icon": "fas fa-comment-dots", + "displayName": { + "en": "Messages", + "de": "Nachrichten" + } + }, + { + "id": "roles", + "icon": "fa-solid fa-users", + "displayName": { + "en": "Roles", + "de": "Rollen" + } } ] } \ No newline at end of file diff --git a/modules/moderation/events/botReady.js b/modules/moderation/events/botReady.js index b2ed8ba9..cff01d6c 100644 --- a/modules/moderation/events/botReady.js +++ b/modules/moderation/events/botReady.js @@ -4,6 +4,7 @@ const {localize} = require('../../../src/functions/localize'); const {embedType} = require('../../../src/functions/helpers'); const {scheduleJob} = require('node-schedule'); const {ChannelType} = require('discord.js'); +const {restoreLockdownState} = require('../lockdown'); const memberCache = {}; const durationParser = require('parse-duration'); @@ -34,6 +35,8 @@ exports.run = async (client) => { }); } + await restoreLockdownState(client); + const verificationConfig = client.configurations['moderation']['verification']; if (!verificationConfig.enabled || !verificationConfig['restart-verification-channel']) return; const channel = await client.channels.fetch(verificationConfig['restart-verification-channel']).catch(() => { diff --git a/modules/moderation/events/guildMemberAdd.js b/modules/moderation/events/guildMemberAdd.js index f469d411..44172322 100644 --- a/modules/moderation/events/guildMemberAdd.js +++ b/modules/moderation/events/guildMemberAdd.js @@ -1,5 +1,6 @@ const {memberCache} = require('./botReady'); const {moderationAction} = require('../moderationActions'); +const {activateLockdown, isLockdownActive} = require('../lockdown'); const {localize} = require('../../../src/functions/localize'); const {embedType} = require('../../../src/functions/helpers'); const {ChannelType, MessageAttachment} = require('discord.js'); @@ -62,6 +63,10 @@ module.exports.run = async (client, guildMember) => { const roles = []; guildMember.roles.cache.forEach(r => roles.push(r.id)); await moderationAction(client, antiJoinRaidConfig.action, {user: client.user}, guildMember, `[${localize('moderation', 'anti-join-raid')}] ${localize('moderation', 'raid-detected')}`, {roles: roles}); + const lockdownConfig = client.configurations['moderation']['lockdown']; + if (lockdownConfig && lockdownConfig.enabled && lockdownConfig.autoTriggerOnJoinRaid && !await isLockdownActive(client)) { + await activateLockdown(client, localize('moderation', 'lockdown-joinraid-trigger'), localize('moderation', 'lockdown-system'), true); + } } } @@ -171,11 +176,16 @@ async function runJoinGate(guildMember) { await guildMember.roles.remove(guildMember.roles.cache, `[moderation] [${localize('moderation', 'join-gate')}] ${localize('moderation', 'join-gate-fail', {r: reason})}`); } await guildMember.roles.add(joinGateConfig.roleID, `[moderation] [${localize('moderation', 'join-gate')}] ${localize('moderation', 'join-gate-fail', {r: reason})}`); - return; + } else { + const roles = []; + guildMember.roles.cache.forEach(r => roles.push(r.id)); + await moderationAction(client, joinGateConfig.action, {user: client.user}, guildMember, `[${localize('moderation', 'join-gate')}] ${localize('moderation', 'join-gate-fail', {r: reason})}`, {roles: roles}); + } + + const lockdownConfig = client.configurations['moderation']['lockdown']; + if (lockdownConfig && lockdownConfig.enabled && lockdownConfig.autoTriggerOnJoinGate && !await isLockdownActive(client)) { + await activateLockdown(client, localize('moderation', 'lockdown-joingate-trigger'), localize('moderation', 'lockdown-system'), true); } - const roles = []; - guildMember.roles.cache.forEach(r => roles.push(r.id)); - await moderationAction(client, joinGateConfig.action, {user: client.user}, guildMember, `[${localize('moderation', 'join-gate')}] ${localize('moderation', 'join-gate-fail', {r: reason})}`, {roles: roles}); } } diff --git a/modules/moderation/events/messageCreate.js b/modules/moderation/events/messageCreate.js index f916adde..1af6bd09 100644 --- a/modules/moderation/events/messageCreate.js +++ b/modules/moderation/events/messageCreate.js @@ -1,4 +1,5 @@ const {moderationAction} = require('../moderationActions'); +const {activateLockdown, isLockdownActive} = require('../lockdown'); const {embedType} = require('../../../src/functions/helpers'); const {localize} = require('../../../src/functions/localize'); const stopPhishing = require('stop-discord-phishing'); @@ -72,6 +73,10 @@ module.exports.run = async (client, msg) => { '%reason%': reason, '%userid%': msg.author.id })); + const lockdownConfig = client.configurations['moderation']['lockdown']; + if (lockdownConfig && lockdownConfig.enabled && lockdownConfig.autoTriggerOnSpam && !await isLockdownActive(client)) { + await activateLockdown(client, localize('moderation', 'lockdown-spam-trigger'), localize('moderation', 'lockdown-system'), true); + } } } diff --git a/modules/moderation/lockdown.js b/modules/moderation/lockdown.js new file mode 100644 index 00000000..3f468172 --- /dev/null +++ b/modules/moderation/lockdown.js @@ -0,0 +1,424 @@ +const {ChannelType, PermissionFlagsBits} = require('discord.js'); +const {MessageEmbed} = require('discord.js'); +const {embedType, parseEmbedColor, safeSetFooter} = require('../../src/functions/helpers'); +const {localize} = require('../../src/functions/localize'); + +let autoLiftTimeout = null; +let lockdownInProgress = false; + +/** + * Check if a lockdown is currently active + * @param {Client} client Discord client + * @returns {Promise} + */ +async function isLockdownActive(client) { + const state = await client.models['moderation']['LockdownState'].findOne({where: {active: true}}); + return !!state; +} + +/** + * Restore lockdown state after bot restart + * @param {Client} client Discord client + * @returns {Promise} + */ +async function restoreLockdownState(client) { + const state = await client.models['moderation']['LockdownState'].findOne({where: {active: true}}); + if (!state) return; + + const lockdownConfig = client.configurations['moderation']['lockdown']; + if (!lockdownConfig || !lockdownConfig.enabled) return; + + client.logger.info(localize('moderation', 'lockdown-restored')); + + if (lockdownConfig.autoLiftAfter > 0 && state.startedAt) { + const elapsed = (Date.now() - new Date(state.startedAt).getTime()) / 60000; + const remaining = lockdownConfig.autoLiftAfter - elapsed; + if (remaining <= 0) { + await liftLockdown(client, localize('moderation', 'lockdown-auto-lift-reason'), localize('moderation', 'lockdown-system')); + } else { + autoLiftTimeout = setTimeout(async () => { + await liftLockdown(client, localize('moderation', 'lockdown-auto-lift-reason'), localize('moderation', 'lockdown-system')); + }, remaining * 60000); + } + } +} + +/** + * Activate server-wide lockdown + * @param {Client} client Discord client + * @param {string} reason Reason for the lockdown + * @param {string} triggeredBy Display name of who/what triggered the lockdown + * @param {boolean} isAutomatic Whether this was triggered automatically + * @returns {Promise} Summary of affected channels and roles + */ +async function activateLockdown(client, reason, triggeredBy, isAutomatic = false) { + if (lockdownInProgress) return null; + if (await isLockdownActive(client)) return null; + lockdownInProgress = true; + + try { + const lockdownConfig = client.configurations['moderation']['lockdown']; + const guild = client.guild; + const moduleConfig = client.configurations['moderation']['config']; + + const affectedChannels = []; + const permissionBackup = []; + + const botHighestRole = guild.members.me.roles.highest; + + const moderatorRoles = new Set([ + ...(moduleConfig['moderator-roles_level4'] || []) + ]); + + // PHASE 1: Collect all permission overwrites BEFORE making any changes + const channelsToLockdown = []; + for (const [, channel] of guild.channels.cache) { + if (channel.type === ChannelType.GuildCategory) continue; + if (!channel.permissionsFor(guild.members.me).has(PermissionFlagsBits.ManageChannels)) continue; + if (!channel.permissionsFor(guild.members.me).has(PermissionFlagsBits.ViewChannel)) continue; + if (!channel.permissionOverwrites || !channel.permissionOverwrites.cache) continue; + + const overwrites = Array.from(channel.permissionOverwrites.cache.values()).map(o => ({ + id: o.id, + type: o.type, + allow: o.allow.bitfield.toString(), + deny: o.deny.bitfield.toString() + })); + permissionBackup.push({channelID: channel.id, overwrites}); + channelsToLockdown.push(channel); + } + + // PHASE 2: Save backup to database BEFORE applying any changes + // This ensures we can restore even if something fails during lockdown + const lockdownState = await client.models['moderation']['LockdownState'].create({ + active: true, + reason, + triggeredBy, + isAutomatic, + permissionBackup, + startedAt: new Date() + }); + + client.logger.info(`[moderation] [lockdown] Backup saved to database with ${permissionBackup.length} channels`); + + // PHASE 3: Now apply the lockdown changes + // If any error occurs here, the backup is already saved and can be restored + let successfullyLockedCount = 0; + for (const channel of channelsToLockdown) { + try { + const everyoneRole = guild.roles.everyone; + const isVoiceChannel = channel.type === ChannelType.GuildVoice; + const isStageChannel = channel.type === ChannelType.GuildStageVoice; + + // Lock text channels + if (!isVoiceChannel && !isStageChannel) { + if (channel.permissionOverwrites) { + await channel.permissionOverwrites.edit(everyoneRole, { + SendMessages: false, + SendMessagesInThreads: false, + AddReactions: false, + CreatePublicThreads: false, + CreatePrivateThreads: false + }, {reason: `[moderation] [lockdown] ${reason}`}).catch(() => {}); + } + + for (const [, role] of guild.roles.cache) { + if (role.id === everyoneRole.id) continue; + if (role.managed) continue; + if (role.position >= botHighestRole.position) continue; + if (moderatorRoles.has(role.id)) continue; + + if (!channel.permissionOverwrites || !channel.permissionOverwrites.cache) continue; + + const overwrite = channel.permissionOverwrites.cache.get(role.id); + if (overwrite && overwrite.allow.has(PermissionFlagsBits.SendMessages)) { + await channel.permissionOverwrites.edit(role, { + SendMessages: false, + SendMessagesInThreads: false, + CreatePublicThreads: false, + CreatePrivateThreads: false + }, {reason: `[moderation] [lockdown] ${reason}`}).catch(() => {}); + } + } + + for (const modRoleId of moderatorRoles) { + if (!channel.permissionOverwrites) continue; + await channel.permissionOverwrites.edit(modRoleId, { + SendMessages: true, + SendMessagesInThreads: true, + CreatePublicThreads: true, + CreatePrivateThreads: true + }, {reason: `[moderation] [lockdown] Moderator override`}).catch(() => {}); + } + } + + // Lock voice channels (including voice text channels) + if (isVoiceChannel) { + if (channel.permissionOverwrites) { + await channel.permissionOverwrites.edit(everyoneRole, { + Connect: false, + Speak: false, + SendMessages: false, + SendMessagesInThreads: false, + CreatePublicThreads: false, + CreatePrivateThreads: false + }, {reason: `[moderation] [lockdown] ${reason}`}).catch(() => {}); + } + + for (const [, role] of guild.roles.cache) { + if (role.id === everyoneRole.id) continue; + if (role.managed) continue; + if (role.position >= botHighestRole.position) continue; + if (moderatorRoles.has(role.id)) continue; + + if (!channel.permissionOverwrites || !channel.permissionOverwrites.cache) continue; + + const overwrite = channel.permissionOverwrites.cache.get(role.id); + if (overwrite && (overwrite.allow.has(PermissionFlagsBits.Connect) || overwrite.allow.has(PermissionFlagsBits.Speak) || overwrite.allow.has(PermissionFlagsBits.SendMessages))) { + await channel.permissionOverwrites.edit(role, { + Connect: false, + Speak: false, + SendMessages: false, + SendMessagesInThreads: false, + CreatePublicThreads: false, + CreatePrivateThreads: false + }, {reason: `[moderation] [lockdown] ${reason}`}).catch(() => {}); + } + } + + for (const modRoleId of moderatorRoles) { + if (!channel.permissionOverwrites) continue; + await channel.permissionOverwrites.edit(modRoleId, { + Connect: true, + Speak: true, + SendMessages: true, + SendMessagesInThreads: true, + CreatePublicThreads: true, + CreatePrivateThreads: true + }, {reason: `[moderation] [lockdown] Moderator override`}).catch(() => {}); + } + } + + // Lock stage channels + if (isStageChannel) { + if (channel.permissionOverwrites) { + await channel.permissionOverwrites.edit(everyoneRole, { + Connect: false, + RequestToSpeak: false, + SendMessages: false, + SendMessagesInThreads: false, + CreatePublicThreads: false, + CreatePrivateThreads: false + }, {reason: `[moderation] [lockdown] ${reason}`}).catch(() => {}); + } + + for (const [, role] of guild.roles.cache) { + if (role.id === everyoneRole.id) continue; + if (role.managed) continue; + if (role.position >= botHighestRole.position) continue; + if (moderatorRoles.has(role.id)) continue; + + // Safety check before accessing cache + if (!channel.permissionOverwrites || !channel.permissionOverwrites.cache) continue; + + const overwrite = channel.permissionOverwrites.cache.get(role.id); + if (overwrite && (overwrite.allow.has(PermissionFlagsBits.Connect) || overwrite.allow.has(PermissionFlagsBits.RequestToSpeak) || overwrite.allow.has(PermissionFlagsBits.SendMessages))) { + await channel.permissionOverwrites.edit(role, { + Connect: false, + RequestToSpeak: false, + SendMessages: false, + SendMessagesInThreads: false, + CreatePublicThreads: false, + CreatePrivateThreads: false + }, {reason: `[moderation] [lockdown] ${reason}`}).catch(() => {}); + } + } + + for (const modRoleId of moderatorRoles) { + if (!channel.permissionOverwrites) continue; + await channel.permissionOverwrites.edit(modRoleId, { + Connect: true, + RequestToSpeak: true, + SendMessages: true, + SendMessagesInThreads: true, + CreatePublicThreads: true, + CreatePrivateThreads: true + }, {reason: `[moderation] [lockdown] Moderator override`}).catch(() => {}); + } + } + + affectedChannels.push(channel.id); + successfullyLockedCount++; + + if (lockdownConfig.sendMessageInAffectedChannels && typeof channel.send === 'function') { + const msgPayload = embedType(lockdownConfig.lockdownMessage, { + '%reason%': reason, + '%user%': triggeredBy + }); + await channel.send(msgPayload).catch(() => {}); + } + } catch (error) { + client.logger.error(`[moderation] [lockdown] Failed to lock channel ${channel.id}: ${error.message}`); + } + } + + client.logger.info(`[moderation] [lockdown] Successfully locked ${successfullyLockedCount}/${channelsToLockdown.length} channels`); + + let kickedUsersCount = 0; + let totalVoiceUsers = 0; + for (const [, channel] of guild.channels.cache) { + if (channel.type !== ChannelType.GuildVoice && channel.type !== ChannelType.GuildStageVoice) continue; + if (!channel.members) continue; + + for (const [, member] of channel.members) { + totalVoiceUsers++; + const isModerator = member.roles.cache.some(role => moderatorRoles.has(role.id)); + if (isModerator) continue; + + try { + await member.voice.disconnect(`[moderation] [lockdown] ${reason}`); + kickedUsersCount++; + } catch (error) { + client.logger.warn(`[moderation] [lockdown] Failed to kick user ${member.id} from voice: ${error.message}`); + } + } + } + + if (totalVoiceUsers > 0) { + client.logger.info(`[moderation] [lockdown] Kicked ${kickedUsersCount}/${totalVoiceUsers} non-moderator users from voice channels`); + } + + const logChannel = await getLogChannel(client, lockdownConfig); + if (logChannel) { + const lockdownEmbed = new MessageEmbed() + .setColor(parseEmbedColor('RED')) + .setTitle('🔒 ' + localize('moderation', 'lockdown-activated')) + .setDescription(localize('moderation', 'lockdown-log-description', { + r: reason, + u: triggeredBy, + t: isAutomatic ? localize('moderation', 'lockdown-automatic') : localize('moderation', 'lockdown-manual'), + c: affectedChannels.length.toString() + })) + .setTimestamp(); + + if (kickedUsersCount > 0) { + lockdownEmbed.addField( + '👢 ' + localize('moderation', 'lockdown-users-kicked', {}, 'Users Kicked'), + localize('moderation', 'lockdown-users-kicked-description', {k: kickedUsersCount.toString()}, `${kickedUsersCount} non-moderator users were disconnected from voice channels.`) + ); + } + + safeSetFooter(lockdownEmbed, client); + await logChannel.send({ + embeds: [lockdownEmbed] + }).catch(() => {}); + } + + if (lockdownConfig.autoLiftAfter > 0) { + autoLiftTimeout = setTimeout(async () => { + await liftLockdown(client, localize('moderation', 'lockdown-auto-lift-reason'), localize('moderation', 'lockdown-system')); + }, lockdownConfig.autoLiftAfter * 60000); + } + + return {affectedChannels: affectedChannels.length}; + } finally { + lockdownInProgress = false; + } +} + +/** + * Lift server-wide lockdown + * @param {Client} client Discord client + * @param {string} reason Reason for lifting + * @param {string} liftedBy Display name of who lifted the lockdown + * @returns {Promise} Summary of restored channels + */ +async function liftLockdown(client, reason, liftedBy) { + if (lockdownInProgress) return null; + const state = await client.models['moderation']['LockdownState'].findOne({where: {active: true}}); + if (!state) return null; + lockdownInProgress = true; + + try { + const lockdownConfig = client.configurations['moderation']['lockdown']; + const guild = client.guild; + + if (autoLiftTimeout) { + clearTimeout(autoLiftTimeout); + autoLiftTimeout = null; + } + + let restoredCount = 0; + for (const backup of (state.permissionBackup || [])) { + const channel = guild.channels.cache.get(backup.channelID); + if (!channel) continue; + if (!channel.permissionOverwrites) continue; + + try { + await channel.permissionOverwrites.set(backup.overwrites.map(o => ({ + id: o.id, + type: o.type, + allow: BigInt(o.allow), + deny: BigInt(o.deny) + })), `[moderation] [lockdown-lift] ${reason}`); + restoredCount++; + + if (lockdownConfig.sendMessageInAffectedChannels && typeof channel.send === 'function') { + await channel.send(embedType(lockdownConfig.liftMessage, { + '%user%': liftedBy + })).catch(() => {}); + } + } catch (e) { + client.logger.warn(localize('moderation', 'lockdown-restore-failed', {c: backup.channelID, e: e.toString()})); + } + } + + const logChannel = await getLogChannel(client, lockdownConfig); + if (logChannel) { + const liftEmbed = new MessageEmbed() + .setColor(parseEmbedColor('GREEN')) + .setTitle('🔓 ' + localize('moderation', 'lockdown-lifted')) + .setDescription(localize('moderation', 'lockdown-lift-log-description', { + r: reason, + u: liftedBy, + c: restoredCount.toString() + })) + .setTimestamp(); + safeSetFooter(liftEmbed, client); + await logChannel.send({ + embeds: [liftEmbed] + }).catch(() => {}); + } + + state.active = false; + await state.save(); + + return {restoredChannels: restoredCount}; + } finally { + lockdownInProgress = false; + } +} + +/** + * Get the log channel for lockdown events + * @private + * @param {Client} client Discord client + * @param {Object} lockdownConfig Lockdown configuration + * @returns {Promise} + */ +async function getLogChannel(client, lockdownConfig) { + if (lockdownConfig.logChannel) { + const ch = await client.channels.fetch(lockdownConfig.logChannel).catch(() => {}); + if (ch) return ch; + } + const moduleConfig = client.configurations['moderation']['config']; + if (moduleConfig['logchannel-id']) { + return client.channels.fetch(moduleConfig['logchannel-id']).catch(() => null); + } + return client.logChannel || null; +} + +module.exports.activateLockdown = activateLockdown; +module.exports.liftLockdown = liftLockdown; +module.exports.isLockdownActive = isLockdownActive; +module.exports.restoreLockdownState = restoreLockdownState; \ No newline at end of file diff --git a/modules/moderation/models/LockdownState.js b/modules/moderation/models/LockdownState.js new file mode 100644 index 00000000..d6a104fe --- /dev/null +++ b/modules/moderation/models/LockdownState.js @@ -0,0 +1,47 @@ +const {DataTypes, Model} = require('sequelize'); + +module.exports = class LockdownState extends Model { + static init(sequelize) { + return super.init({ + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + active: { + type: DataTypes.BOOLEAN, + defaultValue: true + }, + reason: { + type: DataTypes.STRING, + allowNull: true + }, + triggeredBy: { + type: DataTypes.STRING, + allowNull: true + }, + isAutomatic: { + type: DataTypes.BOOLEAN, + defaultValue: false + }, + permissionBackup: { + type: DataTypes.JSON, + allowNull: true, + defaultValue: [] + }, + startedAt: { + type: DataTypes.DATE, + allowNull: true + } + }, { + tableName: 'moderation_lockdown_state', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'LockdownState', + 'module': 'moderation' +}; diff --git a/modules/moderation/moderationActions.js b/modules/moderation/moderationActions.js index c915891f..eadc7154 100644 --- a/modules/moderation/moderationActions.js +++ b/modules/moderation/moderationActions.js @@ -1,5 +1,5 @@ const {scheduleJob} = require('node-schedule'); -const {embedType, formatDate, dateToDiscordTimestamp, formatDiscordUserName} = require('../../src/functions/helpers'); +const {embedType, formatDate, dateToDiscordTimestamp, formatDiscordUserName, safeSetFooter} = require('../../src/functions/helpers'); const {MessageEmbed} = require('discord.js'); const {localize} = require('../../src/functions/localize'); const durationParser = require('parse-duration'); @@ -60,7 +60,7 @@ async function moderationAction(client, type, user, victim, reason, additionalDa '%user%': formatDiscordUserName(user.user), '%date%': expiringAt ? formatDate(expiringAt) : null })); - if (moduleConfig['changeNicknameOnQuarantine']) await victim.setNickname(moduleConfig['changeNicknameOnMute'].split('%nickname%').join(victim.nickname ? victim.nickname : victim.user.displayName), '[moderation] ' + localize('moderation', 'mute-audit-log-reason', { + if (moduleConfig['changeNicknames'] && moduleConfig['changeNicknameOnMute']) await victim.setNickname(moduleConfig['changeNicknameOnMute'].split('%nickname%').join(victim.nickname ? victim.nickname : victim.user.displayName), '[moderation] ' + localize('moderation', 'mute-audit-log-reason', { u: formatDiscordUserName(user.user), r: reason })).catch(() => { @@ -75,12 +75,21 @@ async function moderationAction(client, type, user, victim, reason, additionalDa '%reason%': reason, '%user%': formatDiscordUserName(user.user) })); - if (moduleConfig['changeNicknameOnQuarantine']) await victim.setNickname(victim.user.displayName, '[moderation] ' + localize('moderation', 'unmute-audit-log-reason', { + if (moduleConfig['changeNicknames'] && moduleConfig['changeNicknameOnMute']) await victim.setNickname(victim.user.displayName, '[moderation] ' + localize('moderation', 'unmute-audit-log-reason', { u: formatDiscordUserName(user.user), r: reason })); break; case 'quarantine': + if (victim.roles.cache.get(quarantineRole.id)) { + const previousQuarantineAction = await client.models['moderation']['ModerationAction'].findOne({ + where: {victimID: victim.id, type: 'quarantine'}, + order: [['createdAt', 'DESC']] + }); + if (previousQuarantineAction && previousQuarantineAction.additionalData && previousQuarantineAction.additionalData.roles) { + additionalData.roles = previousQuarantineAction.additionalData.roles; + } + } if (!victim.roles.cache.get(quarantineRole.id)) { if (moduleConfig['remove-all-roles-on-quarantine']) { await victim.roles.set([quarantineRole, ...victim.roles.cache.filter(f => f.managed).map(i => i.id)], '[moderation] ' + localize('moderation', 'quarantine-audit-log-reason', { @@ -117,7 +126,7 @@ async function moderationAction(client, type, user, victim, reason, additionalDa u: formatDiscordUserName(user.user), r: reason })); - if (moduleConfig['changeNicknameOnQuarantine']) await victim.setNickname(moduleConfig['changeNicknameOnQuarantine'].split('%nickname%').join(victim.nickname ? victim.nickname : victim.user.displayName), '[moderation] ' + localize('moderation', 'quarantine-audit-log-reason', { + if (moduleConfig['changeNicknames'] && moduleConfig['changeNicknameOnQuarantine']) await victim.setNickname(moduleConfig['changeNicknameOnQuarantine'].split('%nickname%').join(victim.nickname ? victim.nickname : victim.user.displayName), '[moderation] ' + localize('moderation', 'quarantine-audit-log-reason', { u: formatDiscordUserName(user.user), r: reason })).catch(() => { @@ -142,7 +151,7 @@ async function moderationAction(client, type, user, victim, reason, additionalDa '%reason%': reason, '%user%': formatDiscordUserName(user.user) })); - if (moduleConfig['changeNicknameOnQuarantine']) await victim.setNickname(victim.user.displayName).catch(() => { + if (moduleConfig['changeNicknames'] && moduleConfig['changeNicknameOnQuarantine']) await victim.setNickname(victim.user.displayName).catch(() => { }); break; case 'kick': @@ -286,17 +295,24 @@ async function moderationAction(client, type, user, victim, reason, additionalDa value: additionalData.channel.toString(), inline: true }); - await channel.send({ - // eslint-disable-next-line - embeds: [new MessageEmbed().setColor(expiringAt ? 0xf1c40f : (type.includes('un') ? 0x2ecc71 : 0xe74c3c)).setFooter({ - text: client.strings.footer, - iconURL: client.strings.footerImgUrl - }).setTimestamp().setImage(proof ? (proof.proxyURL || proof.url) : null).setAuthor({ + const modEmbed = new MessageEmbed() + .setColor(expiringAt ? 0xf1c40f : (type.includes('un') ? 0x2ecc71 : 0xe74c3c)) + .setTimestamp() + .setImage(proof ? (proof.proxyURL || proof.url) : null) + .setAuthor({ name: formatDiscordUserName(client.user), - iconURL: client - .user.avatarURL() - }).setTitle(`${localize('moderation', 'case')} #${modAction.actionID}`).setThumbnail(client.user.avatarURL()).addField(localize('moderation', 'victim'), `${formatDiscordUserName(victim.user)}\n\`${victim.user.id}\``, true) - .addField('User', `${formatDiscordUserName(user.user)}\n\`${user.user.id}\``, true).addField(localize('moderation', 'action'), expiringAt ? `tmp-${type}` : type, true).addFields(fields).addField(localize('moderation', 'reason'), reason)] + iconURL: client.user.avatarURL() + }) + .setTitle(`${localize('moderation', 'case')} #${modAction.actionID}`) + .setThumbnail(client.user.avatarURL()) + .addField(localize('moderation', 'victim'), `${formatDiscordUserName(victim.user)}\n\`${victim.user.id}\``, true) + .addField('User', `${formatDiscordUserName(user.user)}\n\`${user.user.id}\``, true) + .addField(localize('moderation', 'action'), expiringAt ? `tmp-${type}` : type, true) + .addFields(fields) + .addField(localize('moderation', 'reason'), reason); + safeSetFooter(modEmbed, client); + await channel.send({ + embeds: [modEmbed] }); } const {updateCache} = require('./events/botReady'); diff --git a/modules/temp-channels/channel-settings.js b/modules/temp-channels/channel-settings.js index daee26be..75d8c86b 100644 --- a/modules/temp-channels/channel-settings.js +++ b/modules/temp-channels/channel-settings.js @@ -31,14 +31,19 @@ module.exports.channelMode = async function (interaction, callerInfo) { } if (publicTemp) { - await vchann.lockPermissions; + await vchann.lockPermissions(); await vchann.permissionOverwrites.delete(vchann.guild.roles.everyone); await interaction.editReply(embedType(moduleConfig['modeSwitched'], {'%mode%': 'public'}, {ephemeral: true})); } else if (!publicTemp) { - await vchann.lockPermissions; - await vchann.permissionOverwrites.create(vchann.guild.roles.everyone, {'CONNECT': false}); + await vchann.lockPermissions(); + const guildRoles = await interaction.guild.roles.fetch(); + for (const [, role] of guildRoles) { + await vchann.permissionOverwrites.create(role, {'CONNECT': false}); + } + await vchann.permissionOverwrites.create(interaction.guild.members.me, {'CONNECT': true}); + await vchann.permissionOverwrites.create(interaction.member, {'CONNECT': true}); if (allowedUsers.at(0) !== '') { for (const user of allowedUsers) { await vchann.permissionOverwrites.create(interaction.guild.members.cache.get(user), {'CONNECT': true}); @@ -313,6 +318,20 @@ module.exports.sendMessage = async function (channel) { emoji: '📝' }] }]; - const message = embedType(moduleConfig['settingsMessage'], {}, {components}); - channel.send(message); + const messagePayload = embedType(moduleConfig['settingsMessage'], {}, {components}); + + const [messageData] = await client.models['temp-channels']['SettingsMessage'].findOrCreate({ + where: {channelID: channel.id}, + defaults: {channelID: channel.id} + }); + + let message = messageData.messageID ? await channel.messages.fetch(messageData.messageID).catch(() => { + }) : null; + if (message) { + await message.edit(messagePayload); + } else { + message = await channel.send(messagePayload); + messageData.messageID = message.id; + await messageData.save(); + } }; \ No newline at end of file diff --git a/modules/temp-channels/events/botReady.js b/modules/temp-channels/events/botReady.js index f49235a2..50c303b5 100644 --- a/modules/temp-channels/events/botReady.js +++ b/modules/temp-channels/events/botReady.js @@ -1,4 +1,4 @@ -const {migrate, embedType} = require('../../../src/functions/helpers'); +const {migrate} = require('../../../src/functions/helpers'); const {client} = require('../../../main'); const {sendMessage} = require('../channel-settings'); const {localize} = require('../../../src/functions/localize'); @@ -6,62 +6,34 @@ module.exports.run = async function () { const settingsChannel = client.channels.cache.get(client.configurations['temp-channels']['config']['settingsChannel']); await migrate('temp-channels', 'TempChannelV1', 'TempChannel'); + // Cleanup orphaned temp channels on startup + const tempChannels = await client.models['temp-channels']['TempChannel'].findAll(); + let cleanedCount = 0; + for (const tempChannel of tempChannels) { + try { + const dcChannel = await client.channels.fetch(tempChannel.id).catch(() => null); + + if (!dcChannel) { + await tempChannel.destroy(); + cleanedCount++; + continue; + } + + if (dcChannel.members.size === 0) { + await dcChannel.delete(`[temp-channels] ${localize('temp-channels', 'removed-audit-log-reason')}`).catch(() => {}); + await tempChannel.destroy(); + cleanedCount++; + } + } catch (error) { + client.logger.warn(`[temp-channels] Failed to cleanup channel ${tempChannel.id}: ${error.message}`); + } + } + + if (cleanedCount > 0) { + client.logger.info(`[temp-channels] Cleaned up ${cleanedCount} empty or orphaned temp channel(s) on startup`); + } + if (settingsChannel) { - const messages = (await settingsChannel.messages.fetch()).filter(msg => msg.author.id === client.user.id); - if (messages.first()) { - const moduleConfig = client.configurations['temp-channels']['config']; - const components = [{ - type: 'ACTION_ROW', - components: [ - { - type: 'BUTTON', - label: localize('temp-channels', 'add-user'), - style: 'SUCCESS', - customId: 'tempc-add', - emoji: '➕' - }, - { - type: 'BUTTON', - label: localize('temp-channels', 'remove-user'), - style: 'DANGER', - customId: 'tempc-remove', - emoji: '➖' - }, - { - type: 'BUTTON', - label: localize('temp-channels', 'list-users'), - style: 'PRIMARY', - customId: 'tempc-list', - emoji: '📃' - }] - }, - { - type: 'ACTION_ROW', - components: [ - { - type: 'BUTTON', - label: localize('temp-channels', 'public-channel'), - style: 'SUCCESS', - customId: 'tempc-public', - emoji: '🔓' - }, - { - type: 'BUTTON', - label: localize('temp-channels', 'private-channel'), - style: 'DANGER', - customId: 'tempc-private', - emoji: '🔒' - }, - { - type: 'BUTTON', - label: localize('temp-channels', 'edit-channel'), - style: 'SECONDARY', - customId: 'tempc-edit', - emoji: '📝' - }] - }]; - const message = embedType(moduleConfig['settingsMessage'], {}, {components: components.map(c => c.toJSON())}); - await messages.first().edit(message); - } else await sendMessage(settingsChannel); + await sendMessage(settingsChannel); } }; \ No newline at end of file diff --git a/modules/temp-channels/events/voiceStateUpdate.js b/modules/temp-channels/events/voiceStateUpdate.js index d802f099..ea4925b5 100644 --- a/modules/temp-channels/events/voiceStateUpdate.js +++ b/modules/temp-channels/events/voiceStateUpdate.js @@ -17,12 +17,26 @@ module.exports.run = async function (client, oldState, newState) { }); if (oldChannel) { setTimeout(async () => { - const dcOldChannel = await client.channels.fetch(oldChannel.id).catch(() => { - }); - if (dcOldChannel && dcOldChannel.members.size === 0) { - await dcOldChannel.delete(`[temp-channels] ${localize('temp-channels', 'removed-audit-log-reason')}`).catch(() => { - }); - await oldChannel.destroy(); + try { + const dcOldChannel = await client.channels.fetch(oldChannel.id).catch(() => null); + if (dcOldChannel && dcOldChannel.members.size === 0) { + if (oldChannel.noMicChannel) { + const noMicChannel = await client.channels.fetch(oldChannel.noMicChannel).catch(() => null); + if (noMicChannel) { + await noMicChannel.delete(`[temp-channels] ${localize('temp-channels', 'removed-audit-log-reason')}`).catch((e) => { + client.logger.warn(`[temp-channels] Failed to delete no-mic channel ${oldChannel.noMicChannel}: ${e.message}`); + }); + } + } + await dcOldChannel.delete(`[temp-channels] ${localize('temp-channels', 'removed-audit-log-reason')}`).catch((e) => { + client.logger.warn(`[temp-channels] Failed to delete temp channel ${oldChannel.id}: ${e.message}`); + }); + await oldChannel.destroy(); + } else if (!dcOldChannel) { + await oldChannel.destroy(); + } + } catch (error) { + client.logger.warn(`[temp-channels] Error during channel cleanup: ${error.message}`); } }, moduleConfig['timeout'] * 1000); } diff --git a/modules/temp-channels/locales.json b/modules/temp-channels/locales.json deleted file mode 100644 index 3b105afc..00000000 --- a/modules/temp-channels/locales.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "en": { - "temp-channels": { - "removed-audit-log-reason": "Removed temp channel, because no one was in it", - "permission-update-audit-log-reason": "Updated permissions, to make sure only people in the VC can see the no-mic-channel", - "created-audit-log-reason": "Created Temp-Channel for %u", - "move-audit-log-reason": "Moved user to their voice channel", - "no-mic-channel-topic": "Welcome to %u's no-mic-channel. You will see this channel as long as you are connected to this temp-channel.", - "disconnect-audit-log-reason": "The old channel of the user could not be found - disconnecting them - hopefully they join again", - "command-description": "Manage your temp-channel", - "mode-subcommand-description": "Change the mode of your channel", - "public-option-description": "local public-option-description", - "add-subcommand-description": "Add users, that will be able to join your channel, while it is private", - "remove-subcommand-description": "Remove users from you channel", - "add-user-option-description": "The user to be added", - "remove-user-option-description": "The user to be removed", - "list-subcommand-description": "List the users with access to your channel", - "edit-subcommand-description": "Edit various settings of yout channel", - "user-limit-option-description": "Change the user-limit of your channel", - "bitrate-option-description": "Change the bitrate of your channel (min. 8000)", - "name-option-description": "Change the name of your channel", - "nsfw-option-description": "Change, whether your channel is age-restricted or not", - "no-added-user": "There are no users to be displayed here", - "nothing-changed": "Your channel already had these settings.", - "no-disconnect": "Couldn't disconnect the user from your channel. This could be due to missing permissions, or the user not being in your voice-channel", - "edit-error": "An error occurred while editing your channel. one or more of your settings couldn't be applied. This could be due to missing permissions or an invalid value." - } - } -} \ No newline at end of file diff --git a/src/commands/help.js b/src/commands/help.js index 4ba6d1e4..36864556 100644 --- a/src/commands/help.js +++ b/src/commands/help.js @@ -1,123 +1,308 @@ const { truncate, formatDate, - sendMultipleSiteButtonMessage, - formatDiscordUserName, parseEmbedColor } = require('../functions/helpers'); -const {MessageEmbed} = require('discord.js'); +const { + ContainerBuilder, + SectionBuilder, + TextDisplayBuilder, + SeparatorBuilder, + SeparatorSpacingSize, + ThumbnailBuilder, + ActionRowBuilder, + StringSelectMenuBuilder, + ButtonBuilder, + ButtonStyle, + MessageFlags +} = require('discord.js'); const {localize} = require('../functions/localize'); +const SELECT_MENU_MAX = 25; + module.exports.run = async function (interaction) { const modules = {}; for (const command of interaction.client.commands) { if (command.module && !interaction.client.modules[command.module].enabled) continue; + if (typeof command.disabled === 'function' && command.disabled(interaction.client)) continue; if (!modules[command.module || 'none']) modules[command.module || 'none'] = []; modules[command.module || 'none'].push(command); } - const sites = []; - let siteCount = 0; - - const embedFields = []; - for (const module in modules) { - let content = ''; - if (module !== 'none') content = `*${(interaction.client.modules[module]['config']['description'][interaction.client.locale] || interaction.client.modules[module]['config']['description']['en'])}*` + '\n'; - for (let d of modules[module]) { - content = content + `\n* \`/${d.name}\`: ${d.description}`; + + const moduleKeys = Object.keys(modules); + const allSelectOptions = []; + for (const mod of moduleKeys) { + const label = mod === 'none' + ? interaction.client.strings.helpembed.build_in + : (interaction.client.modules[mod]['config']['humanReadableName'][interaction.client.locale] || + interaction.client.modules[mod]['config']['humanReadableName']['en'] || mod); + allSelectOptions.push({ + label: truncate(label, 100), + value: mod, + description: mod !== 'none' + ? truncate(interaction.client.modules[mod]['config']['description'][interaction.client.locale] || + interaction.client.modules[mod]['config']['description']['en'] || '', 100) + : localize('help', 'built-in-description'), + emoji: mod === 'none' ? '⚙️' : '📦' + }); + } + + const selectPages = []; + for (let i = 0; i < allSelectOptions.length; i = i + SELECT_MENU_MAX) { + selectPages.push(allSelectOptions.slice(i, i + SELECT_MENU_MAX)); + } + let currentSelectPage = 0; + + /** + * Build the overview using Components V2 + * @private + * @param {number} page Current select menu page index + * @returns {Array} Array of V2 component objects + */ + function buildOverviewComponents(page) { + const headerContainer = new ContainerBuilder() + .setAccentColor(parseEmbedColor('GREEN')); + + const headerSection = new SectionBuilder() + .addTextDisplayComponents( + new TextDisplayBuilder().setContent(`# ${interaction.client.strings.helpembed.title.replaceAll('%site%', '')}\n${interaction.client.strings.helpembed.description}`) + ) + .setThumbnailAccessory( + new ThumbnailBuilder().setURL(interaction.client.user.displayAvatarURL()) + ); + headerContainer.addSectionComponents(headerSection); + headerContainer.addSeparatorComponents(new SeparatorBuilder().setDivider(true).setSpacing(SeparatorSpacingSize.Small)); + headerContainer.addTextDisplayComponents(new TextDisplayBuilder().setContent(`### ${localize('help', 'modules-overview')}`)); + + let moduleList = ''; + for (const mod of moduleKeys) { + const label = mod === 'none' + ? interaction.client.strings.helpembed.build_in + : (interaction.client.modules[mod]['config']['humanReadableName'][interaction.client.locale] || + interaction.client.modules[mod]['config']['humanReadableName']['en'] || mod); + const cmdNames = modules[mod].map(c => `\`/${c.name}\``).join(', '); + moduleList = moduleList + `${mod === 'none' ? '⚙️' : '📦'} **${label}**: ${truncate(cmdNames, 200)}\n`; + } + headerContainer.addTextDisplayComponents(new TextDisplayBuilder().setContent(truncate(moduleList, 4000))); + headerContainer.addSeparatorComponents(new SeparatorBuilder().setDivider(true).setSpacing(SeparatorSpacingSize.Small)); + headerContainer.addTextDisplayComponents(new TextDisplayBuilder().setContent(`-# ${localize('help', 'select-module-hint')}`)); + + const placeholder = selectPages.length > 1 + ? localize('help', 'select-module-placeholder') + ` (${page + 1}/${selectPages.length})` + : localize('help', 'select-module-placeholder'); + + const selectRow = new ActionRowBuilder().addComponents( + new StringSelectMenuBuilder() + .setCustomId('help-module-select') + .setPlaceholder(truncate(placeholder, 150)) + .addOptions(selectPages[page]) + ); + headerContainer.addActionRowComponents(selectRow); + + if (selectPages.length > 1) { + const navRow = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId('help-page-prev') + .setLabel('◀') + .setStyle(ButtonStyle.Secondary) + .setDisabled(page === 0), + new ButtonBuilder() + .setCustomId('help-page-next') + .setLabel('▶') + .setStyle(ButtonStyle.Secondary) + .setDisabled(page >= selectPages.length - 1) + ); + headerContainer.addActionRowComponents(navRow); + } + + const result = [headerContainer]; + + if (!interaction.client.strings['putBotInfoOnLastSite'] || !interaction.client.strings['disableHelpEmbedStats']) { + const infoContainer = new ContainerBuilder() + .setAccentColor(parseEmbedColor('BLUE')); + + if (!interaction.client.strings['putBotInfoOnLastSite']) { + infoContainer.addTextDisplayComponents(new TextDisplayBuilder().setContent( + `### ${localize('help', 'bot-info-titel')}\n${localize('help', 'bot-info-description', {g: interaction.guild.name})}` + )); + } + if (!interaction.client.strings['disableHelpEmbedStats']) { + if (!interaction.client.strings['putBotInfoOnLastSite']) { + infoContainer.addSeparatorComponents(new SeparatorBuilder().setDivider(true).setSpacing(SeparatorSpacingSize.Small)); + } + infoContainer.addTextDisplayComponents(new TextDisplayBuilder().setContent( + `### ${localize('help', 'stats-title')}\n${localize('help', 'stats-content', { + am: Object.keys(interaction.client.modules).length, + rc: interaction.client.commands.length, + v: interaction.client.scnxSetup ? interaction.client.scnxData.bot.version : null, + si: interaction.client.scnxSetup ? interaction.client.scnxData.bot.instanceID : null, + pl: interaction.client.scnxSetup ? localize('scnx', 'plan-' + interaction.client.scnxData.plan) : null, + lr: formatDate(interaction.client.readyAt), + lR: formatDate(interaction.client.botReadyAt) + })}` + )); + } + result.push(infoContainer); + } + + return result; + } + + /** + * Build a module detail view using Components V2 + * @private + * @param {string} mod Module key + * @returns {Promise} Array of V2 component objects + */ + async function buildModuleComponents(mod) { + const label = mod === 'none' + ? interaction.client.strings.helpembed.build_in + : (interaction.client.modules[mod]['config']['humanReadableName'][interaction.client.locale] || + interaction.client.modules[mod]['config']['humanReadableName']['en'] || mod); + const description = mod !== 'none' + ? (interaction.client.modules[mod]['config']['description'][interaction.client.locale] || + interaction.client.modules[mod]['config']['description']['en'] || '') + : ''; + + const container = new ContainerBuilder() + .setAccentColor(parseEmbedColor('GREEN')); + + const headerSection = new SectionBuilder() + .addTextDisplayComponents( + new TextDisplayBuilder().setContent(`# ${mod === 'none' ? '⚙️' : '📦'} ${label}${description ? '\n*' + description + '*' : ''}`) + ) + .setThumbnailAccessory( + new ThumbnailBuilder().setURL(interaction.client.user.displayAvatarURL()) + ); + container.addSectionComponents(headerSection); + container.addSeparatorComponents(new SeparatorBuilder().setDivider(true).setSpacing(SeparatorSpacingSize.Small)); + + for (let d of modules[mod]) { + let content = `### \`/${d.name}\`\n${d.description}`; d = {...d}; if (typeof d.options === 'function') d.options = await d.options(interaction.client); if ((d.options || []).filter(o => o.type === 'SUB_COMMAND' || o.type === 'SUB_COMMANDS_GROUP').length !== 0) { for (const c of d.options) { - addSubCommand(c); - } - } - - /** - * Add a bullet-point for a subcommand - * @private - * @param {Object} command Command to add - * @param {String} bulletPointStyle Style of bullet-points to use - * @param {String} tab Tabs to use to make the message look good - */ - function addSubCommand(command, tab = ' ') { - content = content + `\n${tab}* \`${command.name}\`: ${command.description}`; - if (command.type === 'SUB_COMMAND_GROUP' && (command.options || []).filter(o => o.type === 'SUB_COMMAND').length !== 0) { - for (const c of command.options) { - addSubCommand(c, ' '); - } + content = content + formatSubCommand(c, '\n'); } } + container.addTextDisplayComponents(new TextDisplayBuilder().setContent(truncate(content, 4000))); } - embedFields.push({ - name: `**${module === 'none' ? interaction.client.strings.helpembed.build_in : (interaction.client.modules[module]['config']['humanReadableName'][interaction.client.locale] || interaction.client.modules[module]['config']['humanReadableName']['en'] || module)}**`, - value: truncate(content, 1024) - }); - } - embedFields.filter(f => f.name === '**' + interaction.client.strings.helpembed.build_in + '**').forEach(f => { - const fields = [ - f - ]; - if (!interaction.client.strings['putBotInfoOnLastSite']) { - fields.push({ - name: '\u200b', - value: '\u200b' - }); - fields.push({ - name: localize('help', 'bot-info-titel'), - value: localize('help', 'bot-info-description', {g: interaction.guild.name}) - }); - } - if (!interaction.client.strings['disableHelpEmbedStats']) fields.push({ - name: localize('help', 'stats-title'), - value: localize('help', 'stats-content', { - am: Object.keys(interaction.client.modules).length, - rc: interaction.client.commands.length, - v: interaction.client.scnxSetup ? interaction.client.scnxData.bot.version : null, - si: interaction.client.scnxSetup ? interaction.client.scnxData.bot.instanceID : null, - pl: interaction.client.scnxSetup ? localize('scnx', 'plan-' + interaction.client.scnxData.plan) : null, - lr: formatDate(interaction.client.readyAt), - lR: formatDate(interaction.client.botReadyAt) - }) - }); - addSite( - fields, - true); - }); + container.addSeparatorComponents(new SeparatorBuilder().setDivider(true).setSpacing(SeparatorSpacingSize.Small)); + + const pageForMod = selectPages.findIndex(p => p.some(o => o.value === mod)); + const selectPage = pageForMod !== -1 ? pageForMod : 0; + const placeholder = selectPages.length > 1 + ? localize('help', 'select-module-placeholder') + ` (${selectPage + 1}/${selectPages.length})` + : localize('help', 'select-module-placeholder'); - let fieldCount = 0; - let fieldCache = []; - for (const field of embedFields.filter(f => f.name !== '**' + interaction.client.strings.helpembed.build_in + '**')) { - fieldCount++; - fieldCache.push(field); - if (fieldCount % 3 === 0) { - addSite(fieldCache); - fieldCache = []; + const selectRow = new ActionRowBuilder().addComponents( + new StringSelectMenuBuilder() + .setCustomId('help-module-select') + .setPlaceholder(truncate(placeholder, 150)) + .addOptions(selectPages[selectPage]) + ); + container.addActionRowComponents(selectRow); + + const navRow = new ActionRowBuilder(); + if (selectPages.length > 1) { + navRow.addComponents( + new ButtonBuilder() + .setCustomId('help-page-prev') + .setLabel('◀') + .setStyle(ButtonStyle.Secondary) + .setDisabled(selectPage === 0), + new ButtonBuilder() + .setCustomId('help-page-next') + .setLabel('▶') + .setStyle(ButtonStyle.Secondary) + .setDisabled(selectPage >= selectPages.length - 1) + ); } + navRow.addComponents( + new ButtonBuilder() + .setCustomId('help-overview') + .setLabel(localize('help', 'back-to-overview')) + .setStyle(ButtonStyle.Secondary) + .setEmoji('🏠') + ); + container.addActionRowComponents(navRow); + + return [container]; } - if (fieldCache.length !== 0) addSite(fieldCache); /** - * Adds a site to the embed - * @param {Array} fields Fields to add - * @param atBeginning If this site needs to go at the beginning of the array + * Format a subcommand for display * @private + * @param {Object} command Subcommand object + * @param {String} prefix Line prefix + * @returns {string} */ - function addSite(fields, atBeginning = false) { - siteCount++; - const embed = new MessageEmbed().setColor(parseEmbedColor('GREEN')) - .setDescription(interaction.client.strings.helpembed.description) - .setThumbnail(interaction.client.user.avatarURL()) - .setAuthor({name: formatDiscordUserName(interaction.user), iconURL: interaction.user.avatarURL()}) - .setFooter({text: interaction.client.strings.footer, iconURL: interaction.client.strings.footerImgUrl}) - .setTitle(interaction.client.strings.helpembed.title.replaceAll('%site%', siteCount)) - .addFields(fields); - if (atBeginning) sites.unshift(embed); - else sites.push(embed); + function formatSubCommand(command, prefix = '\n') { + let result = `${prefix}> • \`${command.name}\`: ${command.description}`; + if (command.type === 'SUB_COMMAND_GROUP' && (command.options || []).filter(o => o.type === 'SUB_COMMAND').length !== 0) { + for (const c of command.options) { + result = result + formatSubCommand(c, '\n'); + } + } + return result; } - sendMultipleSiteButtonMessage(interaction.channel, sites, [interaction.user.id], interaction); + const overviewComponents = buildOverviewComponents(currentSelectPage); + const m = await interaction.reply({ + components: overviewComponents, + flags: MessageFlags.IsComponentsV2, + fetchReply: true + }); + + const collector = m.createMessageComponentCollector({time: 120000}); + collector.on('collect', async (i) => { + if (i.user.id !== interaction.user.id) return i.reply({ + ephemeral: true, + content: '⚠️ ' + localize('helpers', 'you-did-not-run-this-command') + }); + + if (i.isStringSelectMenu() && i.customId === 'help-module-select') { + const selectedModule = i.values[0]; + const moduleComponents = await buildModuleComponents(selectedModule); + await i.update({ + components: moduleComponents, + flags: MessageFlags.IsComponentsV2 + }); + } + + if (i.isButton() && i.customId === 'help-overview') { + await i.update({ + components: buildOverviewComponents(currentSelectPage), + flags: MessageFlags.IsComponentsV2 + }); + } + + if (i.isButton() && i.customId === 'help-page-prev') { + if (currentSelectPage > 0) currentSelectPage--; + await i.update({ + components: buildOverviewComponents(currentSelectPage), + flags: MessageFlags.IsComponentsV2 + }); + } + + if (i.isButton() && i.customId === 'help-page-next') { + if (currentSelectPage < selectPages.length - 1) currentSelectPage++; + await i.update({ + components: buildOverviewComponents(currentSelectPage), + flags: MessageFlags.IsComponentsV2 + }); + } + }); + + collector.on('end', () => { + m.edit({ + components: buildOverviewComponents(currentSelectPage), + flags: MessageFlags.IsComponentsV2 + }).catch(() => {}); + }); }; module.exports.config = { diff --git a/src/discordjs-fix.js b/src/discordjs-fix.js index ed5863f7..b70ef01d 100644 --- a/src/discordjs-fix.js +++ b/src/discordjs-fix.js @@ -88,6 +88,7 @@ function normalizeComponents(components) { if (!Array.isArray(components)) return components; return components.map(comp => { if (!comp || typeof comp !== 'object') return comp; + if (typeof comp.toJSON === 'function') return comp; const newComp = {...comp}; if (newComp.type) newComp.type = normalizeComponentType(newComp.type); if (newComp.style) newComp.style = normalizeStyle(newComp.style); diff --git a/src/events/interactionCreate.js b/src/events/interactionCreate.js index 49cbe0cb..99f0554e 100644 --- a/src/events/interactionCreate.js +++ b/src/events/interactionCreate.js @@ -33,6 +33,13 @@ module.exports.run = async (client, interaction) => { content: '⚠️ ' + localize('command', 'module-disabled', {m: command.module}) }); } + if (typeof command.disabled === 'function' && command.disabled(client)) { + if (interaction.isAutocomplete()) return interaction.respond([]); + return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('command', 'command-disabled') + }); + } if (command && typeof (command || {}).options === 'function') command.options = await command.options(interaction.client); const group = interaction.options['_group']; const subCommand = interaction.options['_subcommand']; diff --git a/src/functions/configuration.js b/src/functions/configuration.js index 178132b8..2e500f74 100644 --- a/src/functions/configuration.js +++ b/src/functions/configuration.js @@ -93,16 +93,16 @@ async function checkConfigFile(file, moduleName) { })); } let newConfig = exampleFile.configElements ? [] : {}; + if (exampleFile.configElements && !Array.isArray(configData)) { + client.logger.warn(`${builtIn ? '' : '/' + moduleName}/${exampleFile.filename}: This file should be a config-element, but is not. Converting to config-element.`); + if (typeof configData === 'object') configData = [configData]; + else configData = []; + } if (exampleFile.elementLimits) configData = require('./scnx-integration').verifyLimitedConfigElementFile(client, exampleFile, configData); let skipOverwrite = false; if (exampleFile.skipContentCheck) newConfig = configData; else if (exampleFile.configElements) { - if (!Array.isArray(configData)) { - client.logger.warn(`${builtIn ? '' : '/' + moduleName}/${exampleFile.filename}: This file should be a config-element, but is not. Converting to config-element.`); - if (typeof configData === 'object') configData = [configData]; - else configData = []; - } for (const object of configData) { const objectData = {}; for (const field of exampleFile.content) { diff --git a/src/functions/helpers.js b/src/functions/helpers.js index 94e140fa..9848e0aa 100644 --- a/src/functions/helpers.js +++ b/src/functions/helpers.js @@ -3,7 +3,27 @@ * @module Helpers */ -const {ChannelType, ComponentType, MessageEmbed, MessageAttachment, PermissionFlagsBits} = require('discord.js'); +const { + ChannelType, + ComponentType, + MessageEmbed, + MessageAttachment, + PermissionFlagsBits, + ContainerBuilder, + SectionBuilder, + TextDisplayBuilder, + SeparatorBuilder, + SeparatorSpacingSize, + ThumbnailBuilder, + MediaGalleryBuilder, + MediaGalleryItemBuilder, + FileBuilder, + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + StringSelectMenuBuilder, + MessageFlags +} = require('discord.js'); const {localize} = require('./localize'); const {PrivatebinClient} = require('@pixelfactory/privatebin'); const privatebin = new PrivatebinClient('https://paste.scootkit.com'); @@ -37,6 +57,31 @@ function formatDiscordUserName(userData) { module.exports.formatDiscordUserName = formatDiscordUserName; +/** + * Safely sets footer on an embed, handling null/undefined values + * @param {MessageEmbed} embed Embed to set footer on + * @param {Client} client Discord client instance + * @param {String} customText Optional custom footer text (overrides client.strings.footer) + * @param {String} customIconURL Optional custom footer icon URL (overrides client.strings.footerImgUrl) + * @returns {MessageEmbed} The embed with footer set (if valid values exist) + */ +function safeSetFooter(embed, client, customText = null, customIconURL = null) { + const footerText = customText || (client.strings && client.strings.footer) || null; + const footerIconURL = customIconURL || (client.strings && client.strings.footerImgUrl) || null; + + // Only set footer if we have valid text (Discord.js requires text to be non-empty) + if (footerText && footerText.trim().length > 0) { + embed.setFooter({ + text: footerText, + iconURL: footerIconURL + }); + } + + return embed; +} + +module.exports.safeSetFooter = safeSetFooter; + /** * Replaces every argument with a string * @param {Object} args Arguments to replace @@ -57,6 +102,24 @@ function inputReplacer(args, input, returnNull = false) { return input; } +function getGlobalArgs() { + if (!client || !client.user) return {}; + const guild = client.guild; + const globalArgs = { + '%botName%': client.user.displayName || client.user.username, + '%botID%': client.user.id, + '%botAvatar%': client.user.displayAvatarURL() || '', + '%botTag%': client.user.tag, + '%botMention%': client.user.toString() + }; + if (guild) { + globalArgs['%guildName%'] = guild.name; + globalArgs['%guildID%'] = guild.id; + globalArgs['%guildIcon%'] = guild.iconURL() || ''; + } + return globalArgs; +} + module.exports.inputReplacer = inputReplacer; const colors = { @@ -113,6 +176,7 @@ module.exports.parseEmbedColor = parseColor; * @return {object} Returns [MessageOptions](https://discord.js.org/#/docs/main/stable/typedef/MessageOptions) */ function embedType(input, args = {}, optionsToKeep = {}, mergeComponentsRows = []) { + args = {...getGlobalArgs(), ...args}; if (!optionsToKeep.allowedMentions) { optionsToKeep.allowedMentions = {parse: ['users', 'roles']}; if (client.config.disableEveryoneProtection) optionsToKeep.allowedMentions.parse.push('everyone'); @@ -123,15 +187,23 @@ function embedType(input, args = {}, optionsToKeep = {}, mergeComponentsRows = [ } const schemaVersion = input['_schema'] || 'v2'; if (schemaVersion === 'v2') return embedTypeSchemaV2(input, args, optionsToKeep, mergeComponentsRows); + if (schemaVersion === 'v4') return embedTypeSchemaV4(input, args, optionsToKeep, mergeComponentsRows); optionsToKeep.embeds = []; for (const embedData of input.embeds || []) { if (client.scnxSetup) embedData.footer = require('./scnx-integration').verifySchemaV3Embed(client, embedData.footer); let footer = null; - if (!embedData.footer?.disabled) footer = { - text: inputReplacer(args, embedData.footer?.text, true) || client.strings.footer, - iconURL: embedData.footer?.iconURL || client.strings.footerImgUrl - }; + if (!embedData.footer?.disabled) { + const footerText = inputReplacer(args, embedData.footer?.text, true) || (client.strings && client.strings.footer); + const footerIconURL = embedData.footer?.iconURL || (client.strings && client.strings.footerImgUrl); + // Only create footer object if we have valid text + if (footerText && footerText.trim().length > 0) { + footer = { + text: footerText, + iconURL: footerIconURL + }; + } + } const fields = []; for (const fieldData of embedData.fields || []) fields.push({ @@ -195,10 +267,16 @@ function embedTypeSchemaV2(input, args = {}, optionsToKeep = {}, mergeComponents } if (!client.strings.disableFooterTimestamp && !input.embedTimestamp) emb.setTimestamp(); if (input.embedTimestamp) emb.setTimestamp(input.embedTimestamp); - emb.setFooter({ - text: input.footer ? inputReplacer(args, input.footer) : client.strings.footer, - iconURL: (input.footerImgUrl || client.strings.footerImgUrl) - }); + + // Safely set footer with null checks + const footerText = input.footer ? inputReplacer(args, input.footer) : (client.strings && client.strings.footer); + const footerIconURL = input.footerImgUrl || (client.strings && client.strings.footerImgUrl); + if (footerText && footerText.trim().length > 0) { + emb.setFooter({ + text: footerText, + iconURL: footerIconURL + }); + } optionsToKeep.embeds = [emb]; } else optionsToKeep.embeds = []; if (!optionsToKeep.components && client.scnxSetup) optionsToKeep.components = require('./scnx-integration').returnSCNXComponents(input, mergeComponentsRows, args); @@ -206,11 +284,358 @@ function embedTypeSchemaV2(input, args = {}, optionsToKeep = {}, mergeComponents return optionsToKeep; } +/** + * Extracts a human-readable error description from discord.js builder validation errors. + * Handles CombinedPropertyError (nested errors array), ExpectedConstraintError, and plain Error. + * @param {Error} e The caught error + * @returns {string} Readable error description + * @private + */ +function formatV4BuilderError(e) { + if (Array.isArray(e.errors)) { + return e.errors.map(([key, err]) => { + const detail = err.given !== undefined ? ` (got ${JSON.stringify(err.given)})` : ''; + return `${key}: ${err.message}${detail}`; + }).join('; '); + } + const parts = [e.message]; + if (e.constraint) parts.push(`[${e.constraint}]`); + if (e.given !== undefined) parts.push(`(got ${JSON.stringify(e.given)})`); + if (e.expected) parts.push(`expected: ${Array.isArray(e.expected) ? e.expected.join(', ') : e.expected}`); + return parts.join(' '); +} + +/** + * Maps a v4 button style integer to a discord.js ButtonStyle enum value + * @param {number} style Button style integer (1-5) + * @returns {number} ButtonStyle enum value + * @private + */ +function mapButtonStyle(style) { + const map = { + 1: ButtonStyle.Primary, + 2: ButtonStyle.Secondary, + 3: ButtonStyle.Success, + 4: ButtonStyle.Danger, + 5: ButtonStyle.Link + }; + return map[style] || ButtonStyle.Secondary; +} + +/** + * Builds a discord.js ButtonBuilder from a v4 button component object + * @param {Object} comp V4 button component data + * @param {Object} args Variable replacement args + * @returns {ButtonBuilder|null} Built button or null if invalid + * @private + */ +function buildV4Button(comp, args) { + const btn = new ButtonBuilder(); + const style = comp.style || 2; + btn.setStyle(mapButtonStyle(style)); + + const label = inputReplacer(args, comp.label, true); + if (label) btn.setLabel(truncate(label, 80)); + + if (comp.emoji) { + const emoji = typeof comp.emoji === 'string' ? comp.emoji.trim() : comp.emoji; + if (emoji && emoji !== '' && emoji !== 'null') btn.setEmoji(emoji); + } + + if (comp.disabled) btn.setDisabled(true); + + if (comp.scnx_action) { + const action = comp.scnx_action; + if (action.type === 'roleButton') { + const actionChar = { + add: 'a', + remove: 'r', + toggle: 't' + }[action.action || 'toggle']; + btn.setCustomId(`srb-${actionChar}-${action.id}`); + } else if (action.type === 'customCommandButton') { + btn.setCustomId(`cc-${action.id}`); + } else if (action.type === 'disabledButton') { + btn.setDisabled(true); + btn.setCustomId(`disabled-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`); + } else if (action.type === 'linkButton') { + btn.setStyle(ButtonStyle.Link); + if (comp.url) btn.setURL(inputReplacer(args, comp.url)); + } + } else if (style === 5 && comp.url) { + btn.setURL(inputReplacer(args, comp.url)); + } else if (comp.custom_id) { + btn.setCustomId(comp.custom_id); + } + + if (!label && !comp.emoji) return null; + return btn; +} + +/** + * Builds a discord.js StringSelectMenuBuilder from a v4 select component object + * @param {Object} comp V4 string select component data + * @param {Object} args Variable replacement args + * @returns {StringSelectMenuBuilder|null} Built select menu or null if invalid + * @private + */ +function buildV4StringSelect(comp, args) { + if (!Array.isArray(comp.options) || comp.options.length === 0) return null; + + const select = new StringSelectMenuBuilder(); + + if (comp.scnx_action) { + if (comp.scnx_action.type === 'roleElement') { + select.setCustomId('select-roles'); + } else if (comp.scnx_action.type === 'customCommandElement') { + select.setCustomId('cc-select'); + } + } else if (comp.custom_id) { + select.setCustomId(comp.custom_id); + } + + const placeholder = inputReplacer(args, comp.placeholder, true); + if (placeholder) select.setPlaceholder(truncate(placeholder, 150)); + + if (typeof comp.min_values === 'number') select.setMinValues(comp.min_values); + if (typeof comp.max_values === 'number') select.setMaxValues(comp.max_values); + + const options = []; + for (const opt of comp.options) { + if (!opt.label || !opt.value) continue; + const option = { + label: truncate(inputReplacer(args, opt.label), 100), + value: String(opt.value) + }; + const desc = inputReplacer(args, opt.description, true); + if (desc) option.description = truncate(desc, 100); + if (opt.emoji && opt.emoji !== '' && opt.emoji !== 'null') option.emoji = opt.emoji; + options.push(option); + } + if (options.length === 0) return null; + select.addOptions(options); + return select; +} + +/** + * Builds a discord.js component builder from a v4 component object. + * Used recursively for nested components (Container, Section children). + * @param {Object} comp V4 component data + * @param {Object} args Variable replacement args + * @returns {Object|null} A discord.js builder instance or null if invalid/skipped + * @private + */ +function buildV4Component(comp, args) { + if (!comp || typeof comp !== 'object' || !comp.type) return null; + + try { + switch (comp.type) { + case 10: { // TextDisplay + const content = inputReplacer(args, comp.content, true); + if (!content) return null; + return new TextDisplayBuilder().setContent(truncate(content, 4000)); + } + case 14: { // Separator + const sep = new SeparatorBuilder(); + if (typeof comp.divider === 'boolean') sep.setDivider(comp.divider); + if (comp.spacing === 2) sep.setSpacing(SeparatorSpacingSize.Large); + else sep.setSpacing(SeparatorSpacingSize.Small); + return sep; + } + case 12: { // MediaGallery + if (!Array.isArray(comp.items) || comp.items.length === 0) return null; + const gallery = new MediaGalleryBuilder(); + let galleryItemCount = 0; + for (const item of comp.items) { + if (!item.media || !item.media.url) continue; + try { + const galleryItem = new MediaGalleryItemBuilder() + .setURL(inputReplacer(args, item.media.url)); + if (item.description) galleryItem.setDescription(truncate(inputReplacer(args, item.description), 1024)); + if (item.spoiler) galleryItem.setSpoiler(true); + gallery.addItems(galleryItem); + galleryItemCount++; + } catch (e) { + client.logger.error(`[embedType/v4] Skipping invalid media gallery item (url: ${JSON.stringify(item.media.url)}): ${formatV4BuilderError(e)}`); + } + } + if (galleryItemCount === 0) return null; + return gallery; + } + case 13: { // File + if (!comp.file || !comp.file.url) return null; + const file = new FileBuilder().setURL(inputReplacer(args, comp.file.url)); + if (comp.spoiler) file.setSpoiler(true); + return file; + } + case 1: { // ActionRow + if (!Array.isArray(comp.components) || comp.components.length === 0) return null; + const row = new ActionRowBuilder(); + const firstChild = comp.components[0]; + if (firstChild && firstChild.type === 3) { + // String select menu (max 1 per row) + const select = buildV4StringSelect(firstChild, args); + if (!select) return null; + row.addComponents(select); + } else { + // Buttons (max 5 per row) + const buttons = []; + for (const btnComp of comp.components.slice(0, 5)) { + if (btnComp.type !== 2) continue; + try { + const btn = buildV4Button(btnComp, args); + if (btn) buttons.push(btn); + } catch (e) { + client.logger.error(`[embedType/v4] Skipping invalid button (label: ${JSON.stringify(btnComp.label || null)}): ${formatV4BuilderError(e)}`); + } + } + if (buttons.length === 0) return null; + row.addComponents(...buttons); + } + return row; + } + case 9: { // Section + if (!Array.isArray(comp.components) || comp.components.length === 0) return null; + if (!comp.accessory) return null; + const section = new SectionBuilder(); + const textDisplays = []; + for (const child of comp.components.slice(0, 3)) { + if (child.type !== 10) continue; + const content = inputReplacer(args, child.content, true); + if (content) textDisplays.push(new TextDisplayBuilder().setContent(truncate(content, 4000))); + } + if (textDisplays.length === 0) return null; + section.addTextDisplayComponents(...textDisplays); + + if (comp.accessory.type === 11) { // Thumbnail + if (comp.accessory.media && comp.accessory.media.url) { + const thumb = new ThumbnailBuilder().setURL(inputReplacer(args, comp.accessory.media.url)); + if (comp.accessory.description) thumb.setDescription(truncate(inputReplacer(args, comp.accessory.description), 1024)); + if (comp.accessory.spoiler) thumb.setSpoiler(true); + section.setThumbnailAccessory(thumb); + } else { + return null; + } + } else if (comp.accessory.type === 2) { // Button + try { + const btn = buildV4Button(comp.accessory, args); + if (btn) section.setButtonAccessory(btn); + else return null; + } catch (e) { + client.logger.error(`[embedType/v4] Skipping section due to invalid button accessory (label: ${JSON.stringify(comp.accessory.label || null)}): ${formatV4BuilderError(e)}`); + return null; + } + } else { + return null; + } + return section; + } + case 17: { // Container + const container = new ContainerBuilder(); + if (typeof comp.accent_color === 'number') container.setAccentColor(comp.accent_color); + else if (comp.accent_color) container.setAccentColor(parseColor(comp.accent_color)); + if (comp.spoiler) container.setSpoiler(true); + + if (!Array.isArray(comp.components) || comp.components.length === 0) return null; + + let addedChildren = 0; + for (const child of comp.components) { + try { + const built = buildV4Component(child, args); + if (!built) continue; + switch (child.type) { + case 10: + container.addTextDisplayComponents(built); + addedChildren++; + break; + case 14: + container.addSeparatorComponents(built); + addedChildren++; + break; + case 12: + container.addMediaGalleryComponents(built); + addedChildren++; + break; + case 13: + container.addFileComponents(built); + addedChildren++; + break; + case 1: + container.addActionRowComponents(built); + addedChildren++; + break; + case 9: + container.addSectionComponents(built); + addedChildren++; + break; + } + } catch (e) { + client.logger.error(`[embedType/v4] Failed to build container child (type ${child.type}): ${formatV4BuilderError(e)}`); + } + } + if (addedChildren === 0) return null; + return container; + } + default: + return null; + } + } catch (e) { + client.logger.error(`[embedType/v4] Failed to build component (type ${comp.type}): ${formatV4BuilderError(e)}`); + return null; + } +} + +/** + * Handles the V4 (Components V2) message schema + * @param {Object} input V4 schema input with components array + * @param {Object} args Variable replacement args + * @param {Object} optionsToKeep Options to keep in the output + * @param {Array} mergeComponentsRows Additional ActionRows to merge + * @returns {Object} Discord.js MessageOptions + * @private + */ +function embedTypeSchemaV4(input, args = {}, optionsToKeep = {}, mergeComponentsRows = []) { + // Set IS_COMPONENTS_V2 flag, preserving any existing flags + const existingFlags = optionsToKeep.flags ? (typeof optionsToKeep.flags === 'number' ? optionsToKeep.flags : Number(optionsToKeep.flags)) : 0; + optionsToKeep.flags = existingFlags | MessageFlags.IsComponentsV2; + + const components = []; + for (const comp of input.components || []) { + try { + const built = buildV4Component(comp, args); + if (built) components.push(built); + } catch (e) { + client.logger.error(`[embedType/v4] Failed to build top-level component (type ${(comp || {}).type}): ${formatV4BuilderError(e)}`); + } + } + + for (const row of mergeComponentsRows) { + components.push(row); + } + + // Add SCNX branding for non-paid plans + if (client.scnxSetup && !['PROFESSIONAL', 'PRO', 'ENTERPRISE'].includes(client.scnxData.plan)) { + components.push(new TextDisplayBuilder().setContent('-# Powered by scnx.xyz \u26A1')); + } + + optionsToKeep.components = components; + optionsToKeep.content = null; + optionsToKeep.embeds = []; + return optionsToKeep; +} + module.exports.embedType = embedType; module.exports.embedTypeV2 = async function (input, args, otP, mergeComponentsRows) { let optionsToKeep = embedType(input, args, otP, mergeComponentsRows); - if (!optionsToKeep.attachments && client.scnxSetup && (input.dynamicImage || {}).enabled) optionsToKeep = await require('./scnx-integration').returnDynamicImages(input, optionsToKeep, args); + if (!optionsToKeep.attachments && client.scnxSetup && (input.dynamicImage || {}).enabled) { + optionsToKeep = await require('./scnx-integration').returnDynamicImages(input, optionsToKeep, args); + // For v4, dynamic image was added to files but embeds don't exist; add a File component to display it + if ((input._schema || 'v2') === 'v4' && optionsToKeep.files && optionsToKeep.files.length > 0) { + if (!optionsToKeep.components) optionsToKeep.components = []; + optionsToKeep.components.push(new FileBuilder().setURL('attachment://image.png')); + } + } return optionsToKeep; }; From f0f4bc591f2b711ec3fa3b807534d0abd7bc8d60 Mon Sep 17 00:00:00 2001 From: jateute Date: Tue, 3 Mar 2026 16:08:17 +0100 Subject: [PATCH 08/27] feat(economy): implements "Artikel bearbeiten" (#179) * feat(economy): implements "Artikel bearbeiten" Implements https://featureboard.net/suggestions/95f5a741-6d35-4b86-9c67-abd900dca76e * fix(economy): Adressed the issues copilot pointed out and also fixed them in createShopItem --- locales/en.json | 6 +- modules/economy-system/commands/shop.js | 47 ++++++++- modules/economy-system/configs/strings.json | 28 ++++++ modules/economy-system/economy-system.js | 104 +++++++++++++++++++- 4 files changed, 177 insertions(+), 8 deletions(-) diff --git a/locales/en.json b/locales/en.json index 5cc2e895..a0637a9b 100644 --- a/locales/en.json +++ b/locales/en.json @@ -774,16 +774,19 @@ "item-duplicate": "The item already exist", "role-to-high": "The specified role is higher than the highest role of the bot. Therefore the bot can't give the role to users. The item was **not** created.", "delete-item": "The user %u has deleted the shop item %i", + "edit-item": "The user %u has edited the item %i. Possible changes are:\nNew name: %n\nNew price: %p\nNew role: %r", "user-purchase": "The user %u has purchased the shop item %i for %p.", "shop-command-description": "Use the shop-system", "shop-command-description-add": "Create a new item in the shop (admins only)", "shop-option-description-itemName": "Name of the item", + "shop-option-description-newItemName": "New name of the Item", "shop-option-description-itemID": "ID of the Item", "shop-option-description-price": "Price of the item", "shop-option-description-role": "Role to give to users who buy the item", "shop-command-description-buy": "Buy an item", "shop-command-description-list": "List all items in the shop", "shop-command-description-delete": "Remove an item from the shop", + "shop-command-description-edit": "Edit an item", "channel-not-found": "Can't find the leaderboard channel with the ID %c", "command-description-deposit": "Deposit xyz to your bank", "option-description-amount-deposit": "Amount to deposit", @@ -800,7 +803,8 @@ "migration-happening": "Database not up-to-date. Migrating database...", "migration-done": "Migrated database successfully.", "nothing-selected": "Select an item to buy it", - "select-menu-price": "Price: %p" + "select-menu-price": "Price: %p", + "price-less-than-zero": "The price can't be less or equal to zero" }, "status-role": { "fulfilled": "Status-role condition is fulfilled", diff --git a/modules/economy-system/commands/shop.js b/modules/economy-system/commands/shop.js index a65eacc5..7dd5e64e 100644 --- a/modules/economy-system/commands/shop.js +++ b/modules/economy-system/commands/shop.js @@ -1,11 +1,11 @@ -const {createShopItem, createShopMsg, deleteShopItem, shopMsg, buyShopItem} = require('../economy-system'); +const {createShopItem, createShopMsg, deleteShopItem, shopMsg, buyShopItem, updateShopItem} = require('../economy-system'); const {localize} = require('../../../src/functions/localize'); /** * @param {*} interaction Interaction * @returns {Promise} Result */ -async function checkPerms(interaction) { +async function checkPermsAndSendReplyOnFail(interaction) { const result = interaction.client.configurations['economy-system']['config']['shopManagers'].includes(interaction.user.id) || interaction.client.config['botOperators'].includes(interaction.user.id); if (!result) await interaction.reply({ content: interaction.client.strings['not_enough_permissions'], @@ -16,7 +16,7 @@ async function checkPerms(interaction) { module.exports.subcommands = { 'add': async function (interaction) { - if (!await checkPerms(interaction)) return; + if (!await checkPermsAndSendReplyOnFail(interaction)) return; await interaction.deferReply({ephemeral: !interaction.client.configurations['economy-system']['config']['publicCommandReplies']}); await createShopItem(interaction); await shopMsg(interaction.client); @@ -32,10 +32,16 @@ module.exports.subcommands = { interaction.reply(msg); }, 'delete': async function (interaction) { - if (!await checkPerms(interaction)) return; + if (!await checkPermsAndSendReplyOnFail(interaction)) return; await interaction.deferReply({ephemeral: !interaction.client.configurations['economy-system']['config']['publicCommandReplies']}); await deleteShopItem(interaction); await shopMsg(interaction.client); + }, + 'edit': async function (interaction) { + if (!await checkPermsAndSendReplyOnFail(interaction)) return; + await interaction.deferReply({ephemeral: !interaction.client.configurations['economy-system']['config']['publicCommandReplies']}); + await updateShopItem(interaction); + await shopMsg(interaction.client); } }; @@ -117,6 +123,37 @@ module.exports.config = { required: false } ] - } + }, + { + type: 'SUB_COMMAND', + name: 'edit', + description: localize('economy-system', 'shop-command-description-edit'), + options: [ + { + type: 'STRING', + required: true, + name: 'item-id', + description: localize('economy-system', 'shop-option-description-itemID') + }, + { + type: 'STRING', + required: false, + name: 'item-new-name', + description: localize('economy-system', 'shop-option-description-newItemName') + }, + { + type: 'INTEGER', + required: false, + name: 'new-price', + description: localize('economy-system', 'shop-option-description-price') + }, + { + type: 'ROLE', + required: false, + name: 'new-role', + description: localize('economy-system', 'shop-option-description-role') + } + ] + }, ] }; \ No newline at end of file diff --git a/modules/economy-system/configs/strings.json b/modules/economy-system/configs/strings.json index 23be38be..f4ff4528 100644 --- a/modules/economy-system/configs/strings.json +++ b/modules/economy-system/configs/strings.json @@ -451,6 +451,34 @@ } ] }, + { + "name": "itemEdit", + "humanName": {}, + "default": { + "en": "Successfully edited the item %name%. Check it out using `/shop list`" + }, + "description": { + "en": "Message that gets sent when a shop item gets edited" + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "name", + "description": { + "en": "Name of the edited item", + "de": "Name des bearbeiteten Items" + } + }, + { + "name": "id", + "description": { + "en": "Id of the edited item", + "de": "ID des bearbeiteten Items" + } + } + ] + }, { "name": "depositMsg", "humanName": { diff --git a/modules/economy-system/economy-system.js b/modules/economy-system/economy-system.js index 7871e9aa..0167b685 100644 --- a/modules/economy-system/economy-system.js +++ b/modules/economy-system/economy-system.js @@ -183,7 +183,16 @@ async function createShopItem(interaction) { const role = await interaction.options.getRole('role', true); const price = await interaction.options.getInteger('price'); const model = interaction.client.models['economy-system']['Shop']; - if (interaction.guild.me.roles.highest.comparePositionTo(role) <= 0) return await interaction.editReply(localize('economy-system', 'role-to-high')); + if (interaction.guild.members.me.roles.highest.comparePositionTo(role) <= 0) { + await interaction.editReply(localize('economy-system', 'role-to-high')); + return resolve(localize('economy-system', 'role-to-high')); + } + + if(price<=0) { + await interaction.editReply(localize('economy-system', 'price-less-than-zero')); + return resolve(localize('economy-system', 'price-less-than-zero')); + } + const itemModel = await model.findOne({ where: { [Op.or]: [ @@ -382,12 +391,102 @@ async function deleteShopItem(interaction) { }); } +/** +* Function to update a shop-item +* @param {*} interaction Interaction +* @returns {Promise} +*/ +async function updateShopItem(interaction) { + return new Promise(async (resolve) => { + const id = interaction.options.get('item-id')['value']; + + if (!id) { + await interaction.editReply('Please use the id!'); //IDK how this should happen + return resolve(); + } + + const item = await interaction.client.models['economy-system']['Shop'].findOne({ + where: { + id: id + } + }); + + if (!item) { + await interaction.editReply(embedType(interaction.client.configurations['economy-system']['strings']['noMatches'], { + '%id%': id, + '%name%': '-' + })); + return resolve(); + } + + const newNameOption = interaction.options.get('item-new-name'); + const newPrice = interaction.options.getInteger('new-price'); + const newRole = interaction.options.getRole('new-role'); + if (newRole && interaction.guild.members.me.roles.highest.comparePositionTo(newRole) <= 0) { + await interaction.editReply(localize('economy-system', 'role-to-high')); + return resolve(localize('economy-system', 'role-to-high')); + } + + if(newPrice !== null && newPrice<=0) { + await interaction.editReply(localize('economy-system', 'price-less-than-zero')); + return resolve(localize('economy-system', 'price-less-than-zero')); + } + + if (newNameOption) { + const collidingItem = await interaction.client.models['economy-system']['Shop'].findOne({ + where: { + name: newNameOption['value'] + } + }); + if (collidingItem && collidingItem['id'] !== id) { + await interaction.editReply(embedType(interaction.client.configurations['economy-system']['strings']['itemDuplicate'], { + '%id%': id, + '%name%': "-" + })); + return resolve(localize('economy-system', 'item-duplicate')); + } + } + + if (newNameOption) { + item.name = newNameOption['value']; + } + if (newPrice !== null) { + item.price = newPrice; + } + if (newRole) { + item.role = newRole['id']; + } + + await item.save(); + + await interaction.editReply(embedType(interaction.client.configurations['economy-system']['strings']['itemEdit'], { + '%name%': item.name, + '%id%': item.id + })); + interaction.client.logger.info(`[economy-system] ` + localize('economy-system', 'edit-item', { + u: interaction.user.tag, + i: id, + n: newNameOption ? newNameOption['value'] : "-", + p: newPrice ? newPrice : "-", + r: newRole ? newRole['name'] : "-", + })); + if (interaction.client.logChannel) await interaction.client.logChannel.send(`[economy-system] ` + localize('economy-system', 'edit-item', { + u: interaction.user.tag, + i: id, + n: newNameOption ? newNameOption['value'] : "-", + p: newPrice ? newPrice : "-", + r: newRole ? newRole['name'] : "-", + })); + resolve(`Edited the item ${item.name} successfully`); + }); +} + /** * Create the shop message * @param {Client} client Client * @param {object} guild Object of the guild * @param {boolean} ephemeral Should the message be ephemeral? - * @returns {string} + * @returns {Promise} */ async function createShopMsg(client, guild, ephemeral) { const items = await client.models['economy-system']['Shop'].findAll(); @@ -515,6 +614,7 @@ module.exports.createShopItemAPI = createShopItemAPI; module.exports.createShopItem = createShopItem; module.exports.deleteShopItemAPI = deleteShopItemAPI; module.exports.deleteShopItem = deleteShopItem; +module.exports.updateShopItem = updateShopItem; module.exports.createShopMsg = createShopMsg; module.exports.shopMsg = shopMsg; module.exports.createLeaderboard = leaderboard; \ No newline at end of file From e72ed641abf938e2edceb05925ba3b9f2d7f85d5 Mon Sep 17 00:00:00 2001 From: hfgd <46094961+hfgd123@users.noreply.github.com> Date: Sat, 14 Mar 2026 18:41:18 +0100 Subject: [PATCH 09/27] Added name-list-cleaner AGAIN (#183) * Weird things are happening. Tried to rebuild it * Weird things are happening. Tried to rebuild it * Next try, please work this time * temp-removed locales --- locales/en.json | 997 ------------------ modules/name-list-cleaner/configs/config.json | 90 ++ modules/name-list-cleaner/events/botReady.js | 7 + .../events/guildMemberUpdate.js | 9 + modules/name-list-cleaner/module.json | 24 + modules/name-list-cleaner/renameMember.js | 70 ++ 6 files changed, 200 insertions(+), 997 deletions(-) delete mode 100644 locales/en.json create mode 100644 modules/name-list-cleaner/configs/config.json create mode 100644 modules/name-list-cleaner/events/botReady.js create mode 100644 modules/name-list-cleaner/events/guildMemberUpdate.js create mode 100644 modules/name-list-cleaner/module.json create mode 100644 modules/name-list-cleaner/renameMember.js diff --git a/locales/en.json b/locales/en.json deleted file mode 100644 index a0637a9b..00000000 --- a/locales/en.json +++ /dev/null @@ -1,997 +0,0 @@ -{ - "main": { - "startup-info": "SCNX-CustomBot v2 - Log-Level: %l", - "missing-moduleconf": "Missing moduleConfig-file. Automatically disabling all modules and overwriting modules.json later", - "sync-db": "Synced database", - "login-error": "Bot could not log in. Error: %e", - "login-error-token": "Bot could not log in because the provided token is invalid. Please update your token.", - "login-error-intents": "Bot could not log in because the intents were not enabled correctly. Please enable \"PRESENCE INTENT\", \"SERVER MEMBERS INTENT\" and \"MESSAGE CONTENT INTENT\" in your Discord-Developer-Dashboard: %url", - "not-invited": "Please invite the bot to your Discord server before continuing: %inv", - "require-code-grant-active": "You might be unable to invite your bot to your server as you have enabled the \"Require public code grant\" option in your Discord Developer Dashboard. Please disable this option: %d", - "interactions-endpoint-active": "You bot will be unable to respond to interactions, because the field \"Interactions Endpoint URL\" has a value in your Discord Developer Dashboard. Please remove any content from this field and restart your bot: %d", - "logged-in": "Bot logged in as %tag and is now online.", - "logchannel-wrong-type": "There is no Log-Channel set or it has the wrong type (only text-channels are supported).", - "config-check-failed": "Configuration-Check failed. You can find more information in your log. The bot exited.", - "bot-ready": "The bot initiated successfully and is now listening to commands", - "no-command-permissions": "Could not update server commands. Please give us permissions to performe this critical action: %inv", - "perm-sync": "Synced permissions for /%c", - "perm-sync-failed": "Failed to synced permissions for /%c: %e", - "loading-module": "Loading module %m", - "hidden-module": "Module %m is hidden, meaning that it is not available. Skipping…", - "module-disabled": "Module %m is disabled", - "command-loaded": "Loaded command %d/%f", - "command-dir": "Loading commands in %d/%f", - "global-command-sync": "Synced global application commands", - "guild-command-sync": "Synced server application commands", - "guild-command-no-sync-required": "Server application commands are up to date - no syncing required", - "global-command-no-sync-required": "Global application commands are up to date - no syncing required", - "event-loaded": "Loaded events %d/%f", - "event-dir": "Loading events in %d/%f", - "model-loaded": "Loaded database model %d/%f", - "model-dir": "Loading database model in %d/%f", - "loaded-cli": "Loaded API-Action %c in %p", - "channel-lock": "Locked channel", - "channel-unlock": "Unlocked channel", - "channel-unlock-data-not-found": "Unlocking channel with ID %c failed because it was never locked (which is weird to begin with).", - "module-disable": "Module %m got disabled because %r", - "migrate-success": "Migration from %o to %m finished successfully.", - "migrate-start": "Migration from %o to %m started... Please do not stop the bot" - }, - "reload": { - "reloading-config": "Reloading configuration…", - "reloading-config-with-name": "User %tag is reloading the configuration…", - "reloaded-config": "Configuration reloaded successfully.\nOut of %totalModules modules, %enabled were enabled, %configDisabled were disabled because their configuration was wrong.", - "reload-failed": "Configuration reloaded failed. Bot shutting down.", - "reload-successful-syncing-commands": "Configuration reloaded successfully, syncing commands, to make sure permissions are up-to-date…", - "reload-failed-message": "**FAILED**\n```%r```\n**Please read your log to find more information**\nThe bot will kill itself now, bye :wave:", - "command-description": "Reloads the configuration" - }, - "config": { - "checking-config": "Checking configurations...", - "done-with-checking": "Done with checking. Out of %totalModules modules, %enabled were enabled, %configDisabled were disabled because their configuration was wrong.", - "creating-file": "Config %m/%f does not exist - I'm going to create it, please stand by...", - "checking-of-field-failed": "An error occurred while checking the content of field \"%fieldName\" in %m/%f", - "saved-file": "Configuration-File %f in %m was saved successfully.", - "moduleconf-regeneration": "Regenerating module configuration, no settings will be overwritten, don't worry.", - "moduleconf-regeneration-success": "Module configuration regeneration successfully finished.", - "channel-not-found": "Channel with ID \"%id\" could not be found", - "user-not-found": "User with ID \"%id\" could not be found", - "channel-not-on-guild": "Channel with ID \"%id\" is not on your server", - "channel-invalid-type": "Channel with ID \"%id\" has a type that can not be used for this field", - "role-not-found": "Role with ID \"%id\" could not be found on your server", - "config-reload": "Reloading all configuration..." - }, - "helpers": { - "timestamp": "%dd.%mm.%yyyy at %hh:%min", - "you-did-not-run-this-command": "You did not run this command. If you want to use the buttons, try running the command yourself.", - "next": "Next", - "back": "Back", - "toggle-data-fetch-error": "SC Network Release: Toggle-Data could not be fetched", - "toggle-data-fetch": "SC Network Release: Toggle-Data fetched successfully" - }, - "command": { - "startup": "The bot is currently starting up. Please try again in a few minutes.", - "not-found": "Command not found", - "used": "%tag (%id) used command /%c", - "message-used": "%tag (%id) used command %p%c", - "execution-failed": "Execution of command /%c %g %s failed (Tracing: %t): %e", - "message-execution-failed": "Execution of command %p%c failed (Tracing: %t): %e", - "wrong-guild": "This command is only available on the server **%g**.", - "autcomplete-execution-failed": "Execution of auto-complete on command /%c %g %s with option %f failed: %e", - "execution-failed-message": "## 🔴 Command execution failed 🔴\nThis usually happens either due to misconfiguration or due to an error on our site. Please send a screenshot of this message in #support on the [ScootKit Discord](https://scootk.it/dc-en), to resolve this.\n\n### Internal Tracing ID\n`%t`\n### Debugging-Information\n```%e```", - "error-giving-role": "An error occurred when trying to give you your roles ):\nPlease ask the server administrators to confirm that the highest role of the bot is above the role that the bot is supposed to assign.", - "description-too-long": "The following command description of %c was too long to sync: %s", - "module-disabled": "This command is part of the \"%m\" which is disabled. This can either be intended by the server-admins (and slash-commands haven't synced yet) or this could be caused by a configuration error. Please check (or ask the admins) to check the bot's configuration and logs for details.", - "command-disabled": "This command is currently disabled by the server configuration. If you believe this is an error, please contact a server administrator." - }, - "help": { - "bot-info-titel": "ℹ️ Bot-Info", - "bot-info-description": "This bot is part of [SCNX](https://scnx.xyz/de?ref=custombot_help_embed), a plattform from [ScootKit](https://scootkit.net) allowing the creation of fully customizable for Discord communities, and is being hosted for \"%g\".", - "stats-title": "📊 Stats", - "stats-content": "Active modules: %am\nRegistered commands: %rc\nBot-Version: %v\nRunning on server %si\n[Server-Plan](https://scnx.xyz/plan): %pl\nLast restart: %lr\nLast reload: %lR", - "command-description": "Show every commands", - "slash-commands-title": "Slash-Commands", - "select-module-placeholder": "Select a module to view its commands", - "select-module-hint": "👇 Use the dropdown below to browse commands by module.", - "back-to-overview": "Back to overview", - "modules-overview": "📋 Modules & Commands", - "built-in-description": "Core commands built into the bot" - }, - "bot-feedback": { - "command-description": "Send feedback about the bot to the bot developer", - "submitted-successfully": "Thanks so much for your feedback! It has been carefully recorded and our team will review it soon. If we have any questions, we may contact you via DM (or if you are on our [Support Server]() we'll open a ticket). Thank you for making [CustomBots]() better for everyone <3\n\nYour feedback is subject to our [Terms of service]() and [Privacy Policy]().", - "failed-to-submit": "Sorry, but I couldn't send your feedback to our staff. This could be, because you got blocked or because of some server issue we are having. You can always report bugs and submit feedback in our [Feature-Board](https://features.sc-network.net). Thank you.", - "feedback-description": "Your feedback. Make sure it's neutral, constructive and helpful" - }, - "admin-tools": { - "position": "%i has the position %p.", - "position-changed": "Changed %i's position to %p.", - "category-can-not-have-category": "A Category can not have a category", - "not-category": "Can not change category of channel to a not category channel", - "changed-category": "%c's category got set to %cat", - "command-description": "Execute some actions for admins via commands", - "new-position-description": "New position", - "movechannel-description": "See the position of a channel or change the position of a channel", - "moverole-description": "See the position of a role or change the position of a role", - "setcategory-description": "Sets the category of a channel", - "channel-description": "Channel on which this action should be executed", - "role-description": "Role on which this action should be executed", - "category-description": "New category of the channel", - "emoji-too-much-data": "Please **only** enter one emoji and nothing else", - "emoji-import": "Imported \"%e\" successfully.", - "stealemote-description": "Steals a emote from another server", - "emote-description": "Emote to steal", - "role-command-description": "Assign or remove roles permanently or temporarily", - "role-give-description": "Assign someone a role permanently or temporarily", - "role-user-add-description": "Member that you want to assign the role to", - "role-add-role-description": "Role you want to assign to the member", - "role-add-duration-description": "If you set this parameter, the role will be removed from this user after this duration expires", - "role-user-status-description": "User you want to see temporary roles from", - "role-remove-description": "Remove a role from someone permanently or temporarily", - "role-user-remove-description": "Member that you want to remove the role from", - "role-remove-role-description": "Role you want to remove from the member", - "role-remove-duration-description": "If you set this parameter, the role will be added back to this user after this duration expires", - "role-status-description": "Shows which roles of a user are temporary and when they will be removed", - "role-not-high-enough": "The highest role of the bot is not above %e. The highest role of the bot needs to be above the role you want to remove or assign.", - "unable-to-change-roles": "Changing role %r to %u failed. Error message obtained by Discord:\n```%e```", - "user-not-found": "The user has not been found on your server.", - "duration-wrong": "The value of the duration argument is wrong. Learn more [in our docs]()", - "audit-log-add": "[admin-tools] %u added a role using a command.", - "audit-log-remove": "[admin-tools] %u removed a role using a command.", - "audit-log-add-duration": "[admin-tools] %u added a temporary role using a command that will be removed at %t.", - "audit-log-remove-duration": "[admin-tools] %u removed a temporary role using a command that will be added back at %t.", - "audit-log-temporary-remove": "[admin-tools] This role was added temporarily and has removed since the temporary timeframe expired.", - "audit-log-temporary-add": "[admin-tools] This role has been removed temporarily and has been added back since the temporary timeframe expired.", - "role-add": "%u has been given the role %r.", - "role-remove": "%u has removed the role %r.", - "role-add-duration": "%u has been given the role %r. It will be removed at %t.", - "role-remove-duration": "%r has been removed from %u. It will be given back at %t.", - "user-without-temporary-action": "%u has no roles that are temporary.", - "user-temporary-action-header": "Temporary roles of %u", - "status-remove": "%r will be removed on %t.", - "status-add": "%r will be added back on %t.", - "users-trying-to-manage-higher-role": "Your highest role, %t, is not below %e. To manage a user's role, you the role you are managing needs to be below your highest role." - }, - "welcomer": { - "channel-not-found": "[welcomer] Channel not found: %c", - "welcome-yourself-error": "Welcome, nice to meet you! This button is reversed for a special member of this server who want's to say \"Hi\" to you ^^" - }, - "birthdays": { - "channel-not-found": "[birthdays] Channel not found: %c", - "sync-error": "[birthdays] %u's state was set to \"sync\", but there was no syncing candidate, so I disabled the synchronization", - "age-hover": "%a years old", - "sync-enabled-hover": "Birthday synchronized", - "verified-hover": "Birthday verified", - "no-bd-this-month": "No birthdays this month ):", - "no-birthday-set": "You don't currently have a registered birthday on this server. Set a birthday with `/birthday set`.", - "birthday-status": "Your birthday is currently set to **%dd.%mm%yyyy**%age.", - "your-age": "which means that you are **%age** years old", - "sync-on": "Your birthday is being synced via your [SC Network Account](https://sc-network.net/dashboard).", - "sync-off": "Your birthday is set locally on this server and will not be synchronized", - "no-sync-account": "It seems like you either don't have an [SC Network Account]() or you haven't entered any information about your birthday in it yet.", - "auto-sync-on": "It seems that you have autoSync in your [SC Network Account]() enabled. This means that your birthday will be synchronized all the time on every server. [Learn more]().\nYour birthday isn't showing up? It can take up to 24 hours (usually it's less than two hours) for it to be synced, so stay calm and wait just a bit longer.", - "enabled-sync": "Successfully set. The synchronization is now enabled :+1:", - "disabled-sync": "Successfully set. The synchronization is disabled, you can now change or remove your birthday from this server.", - "delete-but-sync-is-on": "You currently have sync enabled. Please disable sync to delete your birthday.", - "deleted-successfully": "Birthday deleted successfully.", - "only-sync-allowed": "This server only allows synchronization of your birthday with a [SC Network Account]()", - "invalid-date": "Invalid date provided", - "against-tos": "You have to be at least 13 years old to use Discord. Please read Discord's [Terms of Service]() and if you are under the age of 13 please [delete your account]() to comply with Discord's [Terms of Service]() and wait %waitTime (or for the age for your country, listed [here]()) years before creating a new account.", - "too-old": "It seems like you are too old to be alive", - "command-description": "View, edit and delete your birthday", - "status-command-description": "Shows the current status of your birthday", - "sync-command-description": "Manage the synchronization on this server", - "sync-command-action-description": "Action which should be performed on your synchronization", - "sync-command-action-enable-description": "Enable synchronization", - "sync-command-action-disable-description": "Disable synchronization", - "set-command-description": "Sets your birthday", - "set-command-day-description": "Day of your birthday", - "set-command-month-description": "Month of your birthday", - "set-command-year-description": "Year of your birthday", - "delete-command-description": "Deletes your birthday from this server", - "migration-happening": "Database-Schema not up-to-date. Migration database... This could take a while. Do not restart your bot to avoid data loss.", - "migration-done": "Successfully migrated database to newest version." - }, - "months": { - "1": "January", - "2": "February", - "3": "March", - "4": "April", - "5": "May", - "6": "June", - "7": "July", - "8": "August", - "9": "September", - "10": "October", - "11": "November", - "12": "December" - }, - "levels": { - "leaderboard-channel-not-found": "Leaderboard-Channel not found or wrong type", - "leaderboard-notation": "%p. %u: Level %l - %xp XP", - "list-location": "[Level System] The live leaderboard is currently located here: %l. Delete the message and restart the bot, to re-send it.", - "leaderboard": "Leaderboard", - "no-user-on-leaderboard": "Can't generate a leaderboard, because no one has any XP which is odd, but that's how it is ¯\\_(ツ)_/¯", - "and-x-other-users": "and %uc other users", - "level": "Level %l", - "users": "Users", - "leaderboard-command-description": "Shows the leaderboard of this server", - "leaderboard-sortby-description": "How to sort the leaderboard (default: %d)", - "profile-command-description": "Shows the profile of you or an an user", - "profile-user-description": "User to see the profile from (default: you)", - "please-send-a-message": "Please send some messages before I can show you some data", - "no-role": "None", - "are-you-sure-you-want-to-delete-user-xp": "Okay, do you really want to screw with %u? If you hate them so much, feel free to run `/manage-levels reset-xp confirm:True user:%ut` to run this irreversible action.", - "are-you-sure-you-want-to-delete-server-xp": "Do you really want to delete all XP and Levels from this server? This action is irreversible and everyone on this server will hate you. Decided that it's worth it? Enter `/manage-levels reset-xp confirm:True`", - "user-not-found": "User not found", - "user-deleted-users-xp": "%t deleted the XP of the user with id %u", - "removed-xp-successfully": "`Removed %u's XP and level successfully.`", - "deleted-server-xp": "%u deleted the XP of all users", - "successfully-deleted-all-xp-of-users": "Successfully deleted all the XP of all users", - "cheat-no-profile": "This user doesn't have a profile (yet), please force them to write a message before trying to betrayal your community by manipulating level scores.", - "manipulated": "%u manipulated the XP of %m to %v (level %l)", - "successfully-changed": "Successfully edited the XP of %u - they are now **level %l** with **%x XP**.\nRemember, every change you make destroys the experience of other users on this server as the levelsystem isn't fair anymore.", - "edit-xp-command-description": "Manage the levels of your server", - "negative-xp": "This user would have a negative XP value which is not possible", - "negative-level": "This user would have a level below one which is not possible", - "reset-xp-description": "Reset the XP of a user or of the whole server", - "reset-xp-user-description": "User to reset the XP from (default: whole server)", - "reset-xp-confirm-description": "Do you really want to delete the data?", - "edit-xp-user-description": "User to edit", - "edit-xp-value-description": "New XP value of the user", - "edit-xp-description": "Betrays your community and edits a user's XP", - "no-custom-formula": "No valid custom formula was entered. Using default formula.", - "invalid-custom-formula": "Invalid custom formula was entered. Please either fix the syntax of your custom formula or remove the value of the custom formula field.", - "role-factors-total": "Multiplied together, the user receives **%f times more XP** for every message.", - "edit-level-description": "Betrays your community and edits a user's levels", - "random-messages-enabled-but-non-configured": "You have random messages enabled, but have non configured. Ignoring config.randomMessages configuration.", - "granted-rewards-audit-log": "Updated roles to make sure, they have the level role they need" - }, - "team-list": { - "channel-not-found": "Could not find channel with ID %c or the channel has a wrong type (only text-channels supported)", - "role-not-found": "Could not find role with ID %r", - "no-users-with-role": "No users on this server have the %r role yet.", - "no-roles-selected": "No roles listed yet.", - "offline": "Offline", - "dnd": "Do not disturb", - "idle": "Away", - "online": "Online" - }, - "ping-on-vc-join": { - "channel-not-found": "Notify channel %c not found", - "could-not-send-pn": "Could not send PN to %m" - }, - "suggestions": { - "suggestion-not-found": "Suggestion not found", - "updated-suggestion": "Successfully updated suggestion", - "suggest-description": "Create and comment on suggestions", - "suggest-content": "Content you want to suggest", - "loading": "A wild new suggestion appeared, loading..", - "manage-suggestion-command-description": "Manage suggestions as an admin", - "manage-suggestion-accept-description": "Accepts a suggestion", - "manage-suggestion-deny-description": "Denies a suggestion", - "manage-suggestion-id-description": "ID of the suggestion", - "manage-suggestion-comment-description": "Explain why you made this choice" - }, - "auto-delete": { - "could-not-fetch-channel": "Could not fetch channel with ID %c", - "could-not-fetch-messages": "Could not fetch messages from channel with ID %c" - }, - "auto-thread": { - "thread-create-reason": "This thread got created, because you configured auto-thread to do so" - }, - "auto-messager": { - "channel-not-found": "Channel with ID %id not found" - }, - "polls": { - "what-have-i-votet": "What have I voted?", - "vote": "Vote!", - "vote-this": "Click on this option to place your vote here", - "voted-successfully": "Successfully voted. Thanks for your participation.", - "not-voted-yet": "You have not voted yet, so I can't show you what you voted.", - "you-voted": "You have voted for **%o**.", - "remove-vote": "Remove my vote", - "removed-vote": "Your vote was removed successfully.", - "change-opinion": "You can change your opinion anytime by just selecting something else above the button you just clicked.", - "command-poll-description": "Create and end polls", - "command-poll-create-description": "Create a new poll", - "command-poll-end-description": "Ends an existing poll", - "command-poll-end-msgid-description": "ID of the poll", - "command-poll-create-description-description": "Topic / Description of this poll", - "command-poll-create-channel-description": "Channel in which the poll should get created", - "command-poll-create-option-description": "Option number %o", - "command-poll-create-endAt-description": "Duration of the poll (if not set the poll will not end automatically)", - "command-poll-create-public-description": "If enabled (disabled by default) the votes of users will be displayed publicly", - "created-poll": "Successfully created poll in %c.", - "not-found": "Poll could not be found", - "no-votes-for-this-option": "Nobody voted this option yet", - "ended-poll": "Poll ended successfully", - "view-public-votes": "View current voters", - "not-public": "This poll does not appear to be public, no results can be displayed.", - "poll-private": "\uD83D\uDD12 This poll is **anonymous**, meaning that no one can see what you voted (not even the admins).", - "poll-public": "\uD83D\uDD13 This poll is **public**, meaning that everyone can see what you voted.", - "not-text-channel": "You need to select a text-channel that is not an announcement-channel." - }, - "channel-stats": { - "audit-log-reason-interval": "Updated channel because of interval", - "audit-log-reason-startup": "Updated channel because of startup", - "not-voice-channel-info": "Channel \"%c\" (%id) is a %t and not a voice-channel as recommended" - }, - "info-commands": { - "info-command-description": "Find information about parts of this server", - "command-userinfo-description": "Find more information about a user on this server", - "argument-userinfo-user-description": "User you want to see information about (default: you)", - "command-roleinfo-description": "Find more information about a role on this server", - "argument-roleinfo-role-description": "Role you want to see information about", - "command-channelinfo-description": "Find more information about a channel on this server", - "argument-channelinfo-channel-description": "Channel you want to see information about", - "command-serverinfo-description": "Find more information about this server", - "information-about-role": "Information about the role %r", - "hoisted": "Hoisted", - "mentionable": "Mentionable", - "managed": "Managed", - "information-about-channel": "Information about the channel %c", - "information-about-user": "Information about the user %u", - "information-about-server": "Information about %s", - "boostLevel": "Level", - "boostCount": "Boosts", - "userCount": "Users", - "memberCount": "Members", - "onlineCount": "Online", - "textChannel": "Text", - "voiceChannel": "Voice", - "categoryChannel": "Categories", - "otherChannel": "Other", - "total-invites": "Total", - "active-invites": "Active", - "left-invites": "Left" - }, - "channelType": { - "GUILD_TEXT": "Text-Channel", - "GUILD_VOICE": "Voice-Channel", - "GUILD_CATEGORY": "Category", - "GUILD_NEWS": "News-Channel", - "GUILD_STORE": "Store-Channel", - "GUILD_NEWS_THREAD": "News-Channel-Thread", - "GUILD_PUBLIC_THREAD": "Public Thread", - "GUILD_PRIVATE_THREAD": "Private Thread", - "GUILD_STAGE_VOICE": "Stage-Channel", - "DM": "Direct-Message", - "GROUP_DM": "Group-Direct-Message", - "UNKNOWN": "Unknown" - }, - "stagePrivacy": { - "PUBLIC": "Publicly accessible", - "GUILD_ONLY": "Only server members can join" - }, - "guildVerification": { - "0": "None", - "1": "Low", - "2": "Medium", - "3": "High", - "4": "Very high" - }, - "boostTier": { - "0": "None", - "1": "Level 1", - "2": "Level 2", - "3": "Level 3" - }, - "temp-channels": { - "removed-audit-log-reason": "Removed temp channel, because no one was in it", - "permission-update-audit-log-reason": "Updated permissions, to make sure only people in the VC can see the no-mic-channel", - "created-audit-log-reason": "Created Temp-Channel for %u", - "move-audit-log-reason": "Moved user to their voice channel", - "no-mic-channel-topic": "Welcome to %u's no-mic-channel. You will see this channel as long as you are connected to this temp-channel.", - "disconnect-audit-log-reason": "The old channel of the user could not be found - disconnecting them - hopefully they join again", - "command-description": "Manage your temp-channel", - "mode-subcommand-description": "Change the mode of your channel", - "public-option-description": "If enabled, anyone can join your temp-channel", - "add-subcommand-description": "Add users, that will be able to join your channel, while it is private", - "remove-subcommand-description": "Remove users from you channel", - "add-user-option-description": "The user to be added", - "remove-user-option-description": "The user to be removed", - "list-subcommand-description": "List the users with access to your channel", - "edit-subcommand-description": "Edit various settings of your channel", - "user-limit-option-description": "Change the user-limit of your channel", - "bitrate-option-description": "Change the bitrate of your channel (min. 8000)", - "name-option-description": "Change the name of your channel", - "nsfw-option-description": "Change, whether your channel is age-restricted or not", - "no-added-user": "There are no users to be displayed here", - "nothing-changed": "Your channel already had these settings.", - "no-disconnect": "Couldn't disconnect the user from your channel. This could be due to missing permissions, or the user not being in your voice-channel", - "edit-error": "An error occurred while editing your channel. one or more of your settings couldn't be applied. This could be due to missing permissions or an invalid value.", - "add-user": "Add user", - "remove-user": "Remove user", - "list-users": "List users", - "private-channel": "Private", - "public-channel": "Public", - "edit-channel": "Edit channel", - "add-modal-title": "Add an user to your temp-channel", - "add-modal-prompt": "The user you want to add (tag or user-id)", - "remove-modal-title": "Remove an user from your temp-channel", - "remove-modal-prompt": "The user you want to remove (tag or user-id)", - "edit-modal-title": "Edit your temp-channel", - "edit-modal-nsfw-prompt": "Mark temp-channel as age-restricted?", - "edit-modal-nsfw-placeholder": "\"true\" (yes) or \"false\" (no)", - "edit-modal-bitrate-prompt": "Bitrate of your Temp-channel?", - "edit-modal-bitrate-placeholder": "A number over 8000", - "edit-modal-limit-prompt": "Limit of users in your temp-channel", - "edit-modal-limit-placeholder": "Number between 0 and 99; 0 = unlimited", - "edit-modal-name-prompt": "How should your channel be called?", - "edit-modal-name-placeholder": "A very creative channel name", - "edit-modal-username-placeholder": "Username of the user", - "user-not-found": "User not found" - }, - "massrole": { - "command-description": "Manage roles for all members", - "add-subcommand-description": "Add a role to all members", - "remove-subcommand-description": "Remove a role from all members", - "remove-all-subcommand-description": "Remove all roles from all members", - "role-option-add-description": "The role, that will be given to all members", - "role-option-remove-description": "The role, that will be removed from all members", - "target-option-description": "Determines whether bots should be included or not", - "all-users": "All Users", - "bots": "Bots", - "humans": "Humans", - "not-admin": "⚠ To use this command, you need to be added to the adminRoles option in the SCNX-Dashboard. If you are the owner of this bot please remember to create an override in the server settings to prevent abuse of this command.", - "add-reason": "Mass role addition by %u", - "remove-reason": "Mass role removal by %u" - }, - "twitch-notifications": { - "channel-not-found": "Channel with ID %c could not be found", - "user-not-on-twitch": "Could not find user %u on twitch" - }, - "hunt-the-code": { - "admin-command-description": "Manage the current Code-Hunt", - "create-code-description": "Create a new code for the current code-hunt", - "display-name-description": "Name of the code that will be displayed to user when they redeem the code", - "code-description": "Set the code that will be used to redeem it (default: randomly generated)", - "code-created": "Code \"%displayName\" successfully created: \"%code\"", - "error-creating-code": "Error creating code \"{{displayName}}\". Maybe the entered code is already in the database?", - "successful-reset": "Successfully ended the current Code-Hunt-Game - [here](%url)'s your report - save the URL if you want to access it later.", - "end-description": "Ends the current Code-Hunt (will clear users and codes and generates a report)", - "command-description": "Redeem or see data about the current Code-Hunt", - "redeem-description": "Redeem a code you found", - "code-redeem-description": "The code you want to redeem", - "leaderboard-description": "See the current leaderboard", - "profile-description": "See your current count of found codes", - "no-codes-found": "No codes redeemed yet ):", - "no-users": "No users redeemed codes yet ):", - "report-header": "Report for the Hunt-The-Code game on %s", - "user-header": "Participating users", - "code-header": "Codes", - "report-description": "Generates a report", - "report": "You can find the report [here](%url)." - }, - "fun": { - "slap-command-description": "Slap a user in the face", - "user-argument-description": "User to performe this action on", - "no-no-not-slapping-yourself": "You can not punch yourself lol (well technically you can, but our gifs do not support that, so deal with it ¯\\_(ツ)_/¯)", - "pat-command-description": "Pat someone nicely", - "no-no-not-patting-yourself": "Well, good try, but we don't do this here", - "no-no-not-kissing-yourself": "Uah, that's gross, you should try paying somebody to do that (well you should not, but better then kissing yourself)", - "kiss-command-description": "Kiss someone", - "hug-command-description": "Hug someone <3", - "no-no-not-hugging-yourself": "You are quite lonely aren't you? Try hugging a tree, that should work. Unless you live in a desert. Then hug a cactus. That's a bit more painful, but trust me.", - "random-command-description": "Helps you select random things", - "random-number-command-description": "Selects a random number", - "min-argument-description": "Minimal number (default: 1)", - "max-argument-description": "Maximal number (default: 42)", - "random-ikeaname-command-description": "Generates a random name for a IKEA-Name", - "syllable-count-argument-description": "Count of syllables to generate name from (default: random)", - "random-dice-command-description": "Roll a dice", - "random-coinflip-command-description": "Flip a coin", - "random-8ball-command-description": "Generates an answer to a yes/no question", - "dice-site-1": "Heads", - "dice-site-2": "Tails" - }, - "moderation": { - "moderate-command-description": "Moderate users on your server", - "moderate-notes-command-description": "Set or see moderator's notes of a user", - "moderate-notes-command-view": "View a user's notes", - "moderate-notes-command-create": "Create a new note about a user", - "moderate-notes-command-edit": "Edit one of your existing notes about a user", - "moderate-notes-command-delete": "Delete one of your existing notes about a user", - "moderate-ban-command-description": "Ban a user on your server", - "moderate-reason-description": "Reason for your action", - "moderate-proof-description": "Proof for your action", - "report-user-not-found-on-guild": "This user could not be found on \"%s\". You can only report users that are members of our server.", - "proof": "Proof", - "report-proof-description": "Attach an optional (image) proof to your report", - "file": "File uploaded", - "anti-grief-reason": "Too many actions of type \"%type\" in the last %h hours. Maximum amount allowed: %n", - "anti-grief-user-message": "Sorry, but it seems like you are abusing your moderative powers. We've taken actions to prevent this from happening.", - "moderate-duration-description": "Duration of the action (max: 28 days, default: 14 days)", - "mute-max-duration": "Discord limits the maximal duration of a timeout to 28 days. Please enter an amount equal or less than this", - "moderate-quarantine-command-description": "Quarantine a user on your server", - "moderate-unquarantine-command-description": "Removes a user from the quarantine", - "moderate-unban-command-description": "Revokes an existing ban", - "moderate-clear-command-description": "Clears messages in the current channel", - "moderate-clear-amount-description": "How many messages should get cleared?", - "moderate-kick-command-description": "Kick a user from your server", - "moderate-unwarn-command-description": "Revokes a warning", - "moderate-mute-command-description": "Mute a user on your server", - "moderate-unmute-command-description": "Unmutes a user on your server", - "moderate-warn-command-description": "Warn a user", - "moderate-channel-mute-description": "Mutes a user from the current channel", - "moderate-unchannel-mute-description": "Removes a channel-mute from this channel", - "moderate-lock-command-description": "Lock the current channel", - "moderate-unlock-command-description": "Unlock the current channel", - "moderate-lockdown-command-description": "Activate or lift server-wide lockdown", - "moderate-lockdown-enable-description": "True to activate lockdown, false to lift it", - "lockdown-not-enabled": "The lockdown system is not enabled. Enable it in the lockdown configuration.", - "lockdown-already-active": "A lockdown is already active.", - "lockdown-not-active": "No lockdown is currently active.", - "lockdown-activated": "Server Lockdown Activated", - "lockdown-lifted": "Server Lockdown Lifted", - "lockdown-activated-reply": "Lockdown activated. %c channels have been locked.", - "lockdown-lifted-reply": "Lockdown lifted. %c channels have been restored.", - "lockdown-log-description": "**Reason:** %r\n**Triggered by:** %u\n**Type:** %t\n**Affected channels:** %c", - "lockdown-lift-log-description": "**Reason:** %r\n**Lifted by:** %u\n**Restored channels:** %c", - "lockdown-automatic": "Automatic", - "lockdown-manual": "Manual", - "lockdown-system": "System", - "lockdown-auto-lift-reason": "Auto-lift timer expired", - "lockdown-restored": "Lockdown state restored from database after restart", - "lockdown-joinraid-trigger": "Join raid detected", - "lockdown-spam-trigger": "Excessive spam detected", - "lockdown-joingate-trigger": "Excessive join-gate violations detected", - "lockdown-restore-failed": "Failed to restore permissions for channel %c: %e", - "lockdown-users-kicked": "Users Kicked", - "lockdown-users-kicked-description": "%k non-moderator users were disconnected from voice channels.", - "moderate-user-description": "User on who the action should get performed", - "moderate-userid-description": "ID of a user", - "moderate-days-description": "Number of days of messages to delete", - "invalid-days": "Days can only be between 0 and 7 (inclusive)", - "moderate-notes-description": "Notes to set / update", - "moderate-note-id-description": "ID of one of your notes you want to edit (leave blank to create a new one)", - "moderate-warnid-description": "ID of a warn (run /moderate actions to get it)", - "moderate-actions-command-description": "Show all recorded actions against a user", - "report-command-description": "Reports a user and sends a snapshot of the chat to server staff", - "report-reason-description": "Please describe what the user did wrong", - "report-user-description": "User you want to report", - "no-reason": "Not set", - "muterole-not-found": "Could not find muterole. Can not perform this action", - "quarantinerole-not-found": "Could not find quarantinerole. Can not perform this action", - "mute-audit-log-reason": "Got muted by %u because of \"%r\"", - "unmute-audit-log-reason": "Got unmuted by %u because of \"%r\"", - "quarantine-audit-log-reason": "Got quarantined by %u because of \"%r\"", - "kicked-audit-log-reason": "Got kicked by %u because of \"%r\"", - "banned-audit-log-reason": "Got banned by %u because of \"%r\"", - "channelmute-audit-log-reason": "Got channel-mutet by %u because of \"%r\"", - "unchannelmute-audit-log-reason": "The Channel-Mute got removed by %u because of \"%r\"", - "unbanned-audit-log-reason": "Got unbanned by %u because of \"%r\"", - "unquarantine-audit-log-reason": "Got unquarantined by %u because of \"%r\"", - "action-expired": "Action expired", - "auto-mod": "Auto-Mod", - "batch-role-remove-failed": "Could not remove all roles from %i (trying to remove roles one by one): %e", - "batch-role-add-failed": "Could not add all roles to %i (trying to remove roles one by one): %e", - "could-not-remove-role": "Could not remove role %r from %i: %e", - "could-not-add-role": "Could not add role %r to %i: %e", - "reason": "Reason", - "join-gate": "Join-Gate", - "expires-at": "Action expires on", - "action": "Action", - "case": "Case", - "victim": "Victim", - "missing-logchannel": "LogChannel could not be found", - "reached-warns": "Reached %w warns", - "restored-punishment-audit-log-reason": "Restored punishment", - "anti-join-raid": "ANTI-JOIN-RAID", - "raid-detected": "Raid detected", - "joingate-for-everyone": "Join-Gate-Modus: Catch all users", - "account-age-to-low": "Account creation age of %a days is to low (required are more then %c)", - "no-profile-picture": "Account has no profile picture (required)", - "join-gate-fail": "Account failed Join-Gate (%r)", - "blacklisted-word": "Posted blacklisted word in %c", - "invite-sent": "Sent invite in %c", - "scam-url-sent": "Sent scam-url in %c", - "anti-spam": "Anti-Spam", - "reached-messages-in-timeframe": "Reached %m (normal) messages in less than %t seconds", - "reached-duplicated-content-messages": "Reached %m messages with the same content in less than %t", - "reached-ping-messages": "Reached %m messages with (user) pings in less then %t seconds", - "reached-massping-messages": "Reached %m messages with mass pings in less than %t seconds", - "action-done": "Executed action successfully. Action-ID: #%i", - "expiring-action-done": "Done. Action will expire on %d. Action-ID: #%i", - "cleared-channel": "Cleared channel successfully.\nNote: Messages older than 14 days can not be deleted using this method.", - "clear-failed": "An error occurred. You can only delete 100 messages at once.", - "no-quarantine-action-found": "Sorry, but I couldn't find any records of quarantining this users.", - "locked-channel-successfully": "Locked channel successfully. Only moderators (and admins) can write messages here now.", - "unlocked-channel-successfully": "Unlocked channel successfully. Permissions got restored to the permission-state before the lock occurred.", - "unlock-audit-log-reason": "User %u unlocked this channel by running /moderate unlock", - "warning-not-found": "I could not find this warning. Please make sure you are actually using a warning-id and not a userid.", - "can-not-report-mod": "You can not report moderators.", - "action-description-format": "%reason\nby %u on %t", - "no-actions-title": "None found", - "no-actions-value": "No actions against %u found.", - "actions-embed-title": "Mod-Actions against %u - Site %i", - "actions-embed-description": "You can find every action against %u here.", - "report-embed-title": "New report", - "report-embed-description": "A user reported another user. Please review the case and take actions if needed.", - "reported-user": "Reported user", - "report-reason": "Reason for the report", - "report-user": "User who submitted report", - "message-log": "Last 100 messages", - "message-log-description": "You can find an encrypted message-log [here](%u).", - "channel": "Channel", - "no-report-pings": "No pings configured. Check your configuration to ping your staff.", - "not-allowed-to-see-own-notes": "Sorry, but you are not allowed to see your own notes.", - "note-added": "Note added successfully", - "note-edited": "Edited note successfully", - "note-deleted": "Note deleted successfully", - "note-not-found-or-no-permissions": "Note not found or no permissions to edit this note.", - "notes-embed-title": "Notes about %u", - "info-field-title": "ℹ️ Information", - "no-notes-found": "No notes about this user. Create a new note with `/moderate notes create` and set the notes attribute.", - "more-notes": "%x other moderator also added notes about this user. Notes are sorted in reverse chronology, so you will see the newest notes first.", - "user-notes-field-title": "%t's notes", - "user-not-on-server": "I can't perform this action on this user, as they are not currently on your server.", - "verification": "VERIFICATION", - "verification-failed": "Verification failed", - "verification-started": "Verification got started", - "verification-completed": "Verification completed", - "user": "User", - "manual-verification-needed": "Manual verification needed", - "verification-deny": "Deny verification", - "verification-approve": "Approve verification", - "verification-skip": "Skip verification", - "captcha-verification-pending": "Captcha-Verification is pending. You can either wait for the user to complete it or skip it manually.", - "verification-update-proceeded": "Successfully update verification status", - "verify-channel-set-but-not-found-or-wrong-type": "The configured verify-channel could not be found or it's type is not supported.", - "generating-message": "We are preparing some stuff, this message should get edited shortly...", - "restart-verification-button": "Restart verification process", - "member-not-found": "This user could not be found, maybe they already left?", - "already-verified": "Seems like you are already verified... Why would you want to repeat this process?", - "restarted-verification": "I have sent you another DM about your verification process. Please read it carefully and follow the actions described in it. Please not that this action did not re-trigger the manual-verification (if enabled), so spamming this button is useless.", - "dms-still-disabled": "It seems like your DMs are still disabled. Please enable your DMs to start the verification. This is not optional, you need to do this in order to get access to %g.", - "dms-not-enabled-ping": "%p, it seems like you have your DMs disabled. Please enable them and hit the button below this message to verify yourself. You have two minutes to complete this process." - }, - "counter": { - "created-db-entry": "Initialized database entry for %i", - "not-a-number": "This is not a number. You can not chat here. Try creating a thread if your message is that important.", - "restriction-audit-log": "This user proceeded to abuse the counter channel after five warnings, so we locked them out.", - "only-one-message-per-person": "Users have to take turns counting: You can not count two times in a row.", - "not-the-next-number": "That's not the next number. The next number is **%n**, please make sure you are counting up one by one.", - "channel-topic-change-reason": "Someone counted, so we updated the description as required by the configuration" - }, - "tickets": { - "channel-not-found": "Ticket-Create-Channel could not be found", - "existing-ticket": "You already have a ticket open: %c", - "ticket-created-audit-log": "%u created a new ticket by clicking the button", - "ticket-created": "Successfully created ticket and notified staff. Head over to it: %c", - "no-admin-pings": "No pings configured. Check your configuration to ping your staff.", - "ticket-closed-successfully": "Closed ticket successfully. This channel will be deleted in a few seconds, thanks for reaching out to our support.", - "ticket-closed-audit-log": "%u closed ticket", - "closing-ticket": "Closing ticket as requested by %u...", - "ticket-with-user": "👤 Ticket-User", - "could-not-dm": "Could not DM %u: %r", - "no-log-channel": "Log-Channel not found", - "ticket-log-embed-title": "📎 Ticket %i closed", - "ticket-log": "Ticket-Log", - "ticket-type": "☕ Ticket-Topic", - "ticket-log-value": "Transcript with %n messages can be found [here](%u).", - "closed-by": "👷 Ticket closed by" - }, - "reminders": { - "command-description": "Set a reminder for yourself", - "in-description": "After what time should we remind you? (eg. \"2h 30m\")", - "what-description": "What should we remind you about?", - "dm-description": "Should we send you a DM instead of reminding your in this channel?", - "one-minute-in-future": "Your reminder needs to be at least one minute in the future", - "reminder-set": "Reminder set. We'll remind you at %d." - }, - "afk-system": { - "command-description": "Manage your AFK-Status on this server", - "end-command-description": "End your current AFK-Session", - "start-command-description": "Start a new AFK-Session", - "reason-option-description": "Explain why you started this session", - "autoend-option-description": "If enabled, the bot will auto-end your AFK Session when your write a message (default: enabled)", - "no-running-session": "You don't have any session running.", - "already-running-session": "You already have an AFK-Session running, try ending it with `/afk-system end`.", - "afk-nickname-change-audit-log": "Updated user nickname because they started an AFK-Session", - "can-not-edit-nickname": "Can not edit nickname of %u: %e" - }, - "tic-tac-toe": { - "command-description": "Play tic-tac-toe against someone in the chat", - "user-description": "User to play against", - "challenge-message": "%t, %u challenged you to a game of tic-tac-toe! Hit the button below to join the battle! This invitation will expire in about 2 minutes, so don't hesitate to much.", - "accept-invite": "Join game", - "deny-invite": "No thanks", - "self-invite-not-possible": "Are you really that lonely? Even Simon, a complete introvert with no friends and developer of this bot, can find another user to play tic-tac-toe with... You should be able to do that too, try inviting %r for example, maybe they want to play a round?", - "invite-expired": "Sorry, %u, %i didn't accept your request to play tic-tac-toe in time ):", - "invite-denied": "Sorry, %u, but %i denied your request to play a round of tic-tac-toe ):", - "you-are-not-the-invited-one": "Sorry, but this invite doesn't belong to you. You can start your own game with `/tic-tac-toe`.", - "playing-header": "**TIC-TAC-TOE GAME IS RUNNING**\n\n%u (🟢) VS %i (🟡)\nCurrently on turn: %t\n\n%t, click a button with a white circle below to place your marker", - "win-header": "**TIC-TOE-GAME ENDED**\n\n%u (🟢) VS %i (🟡)\n\n%w won the game - GG!\n\n*You can start a new round by using `/tic-tac-toe`*", - "draw-header": "**TIC-TOE-GAME ENDED**\n\n%u (🟢) VS %i (🟡)\n\nDraw - no one won this game.", - "not-your-turn": "It's not your turn, take a coffee and return later" - }, - "duel": { - "command-description": "Play duel against someone in the chat", - "user-description": "User to play against", - "challenge-message": "%t, %u challenged you to a game of duel! Hit the button below to join the battle! This invitation will expire in about 2 minutes, so don't hesitate to much.", - "accept-invite": "Join game", - "deny-invite": "No thanks", - "self-invite-not-possible": "Are you really that lonely? Even Simon, a complete introvert with no friends and developer of this bot, can find another user to play duel with... You should be able to do that too, try inviting %r for example, maybe they want to play a round?", - "invite-expired": "Sorry, %u, %i didn't accept your request to play duel in time ):", - "invite-denied": "Sorry, %u, but %i denied your request to play a round of duel ):", - "you-are-not-the-invited-one": "Sorry, but this invite doesn't belong to you. You can start your own game with `/duel`.", - "game-running-header": "🎮 Game running", - "what-do-you-want-to-do": "**Select your action!**", - "pending": "⏳ Waiting for selection…", - "ready": "✅ Ready", - "continues-info": "The game continues once both parties have selected their next action.", - "how-does-this-game-work": "Wondering how this game works? Read our short explanation [here]().", - "use-gun": "Use gun", - "guard": "Guard", - "reload": "Load gun", - "game-ended": "🎮 Game ended", - "no-bullets": "Sorry, but you haven't loaded any bullets yet, so you can't use your gun yet.", - "bullets-full": "Sorry, but your gun only has place for 5 bullets at a time.", - "gun-gun": "Both %g1 and %g1 draw their guns. They stare each other and their eyes and slowly lower their weapons. No, the duell won't be resolved if both die - there can only be one winner.", - "guard-gun": "%g1 draws their gun and shoot - %d1 dodged the bullet successfully.", - "guard-guard": "Both %d1 and %d2 wait for each other to fire the shot - but nothing happens.", - "reload-gun": "While %r1 starts reloading their gun, %g1 draws their weapon and shoots - it's a head-shot. %r1 drops to the ground. %g1 should celebrate because they won, but they are left feeling bad for murdering their old friend.", - "guard-over-reload-gun": "As this is %r1's fifth guard in a row, they are tired and are to slow - %g1 shoots them directly into their head and %r1 drops to the ground. It's a win for %g1 - but at what price?", - "reload-reload": "Both %r1 and %r2 stare each other in the eyes while taking a short break to load one bullet each in their chamber.", - "reload-guard": "%d1 prepares to doge a bullet - but %r1 uses the time to load their weapon - no shots get fired.", - "ended-state": "This game ended. You can start a new duel with `/duel`.", - "not-your-game": "You are not one of players - you can start a new game with `/duel`." - }, - "economy-system": { - "work-earned-money": "The user %u gained %m %c by working", - "crime-earned-money": "The user %u gained %m %c by committing a crime", - "message-drop-earned-money": "The user %u gained %m %c by getting a message drop", - "rob-earned-money": "The user %u gained %m %c by robbing from %v", - "weekly-earned-money": "The user %u gained %m %c by cashing in their weekly reward", - "daily-earned-money": "The user %u gained %m %c by cashing in their daily reward", - "admin-self-abuse": "The admin %a wanted to abuse their permissions by giving them self even more money! This can't and should not be ignored!", - "admin-self-abuse-answer": "What a bad admin you are, %u. I'm disappointed with you! I need to report this. If I wish I could ban you!", - "added-money": "%i %c has been added to the balance of %u", - "removed-money": "%i %c has been removed from the balance of %u", - "set-money": "The balance of %u has been set to %i.", - "added-money-log": "The user %u added %i %c to the balance of %v", - "removed-money-log": "The user %u removed %i %c from the balance of %v", - "set-money-log": "The user %u set %v's balance to %i %c", - "command-description-main": "Use the economy-system", - "command-description-work": "Earn some cash by working", - "command-description-crime": "Earn some cash by committing a crime", - "command-description-rob": "Rob some cash from another user", - "option-description-rob-user": "User to rob from", - "crime-loose-money": "The user %u lost %m %c by committing a crime", - "command-description-daily": "Cash in your daily rewards", - "command-description-weekly": "Cash in your weekly rewards", - "command-description-balance": "Show the balance of a user", - "option-description-user": "User to execute action upon", - "command-description-add": "Add some cash to a user", - "command-description-remove": "Remove some cash from a user", - "option-description-amount": "Amount to manipulate", - "command-description-set": "Set a user's balance", - "option-description-balance": "Balance to set user to", - "message-drop": "Message-Drop: You earned %m %c simply by chatting!", - "created-item": "The user %u has created a new shop item: %i", - "item-duplicate": "The item already exist", - "role-to-high": "The specified role is higher than the highest role of the bot. Therefore the bot can't give the role to users. The item was **not** created.", - "delete-item": "The user %u has deleted the shop item %i", - "edit-item": "The user %u has edited the item %i. Possible changes are:\nNew name: %n\nNew price: %p\nNew role: %r", - "user-purchase": "The user %u has purchased the shop item %i for %p.", - "shop-command-description": "Use the shop-system", - "shop-command-description-add": "Create a new item in the shop (admins only)", - "shop-option-description-itemName": "Name of the item", - "shop-option-description-newItemName": "New name of the Item", - "shop-option-description-itemID": "ID of the Item", - "shop-option-description-price": "Price of the item", - "shop-option-description-role": "Role to give to users who buy the item", - "shop-command-description-buy": "Buy an item", - "shop-command-description-list": "List all items in the shop", - "shop-command-description-delete": "Remove an item from the shop", - "shop-command-description-edit": "Edit an item", - "channel-not-found": "Can't find the leaderboard channel with the ID %c", - "command-description-deposit": "Deposit xyz to your bank", - "option-description-amount-deposit": "Amount to deposit", - "command-description-withdraw": "Withdraw xyz from your Bank", - "option-description-amount-withdraw": "Amount to withdraw", - "command-group-description-msg-drop-msg": "Enable/ Disable the Message-Drop-Message", - "command-description-msg-drop-msg-enable": "Enable the Message-Drop-Message", - "command-description-msg-drop-msg-disable": "Disable the Message-Drop-Message", - "command-description-destroy": "Destroy the whole economy (deletes all Database-Entries)", - "option-description-confirm": "Confirm, that you really want to destroy the whole economy", - "destroy-cancel-reply": "You're lucky. You stopped me in the last moment before I destroyed the economy", - "destroy-reply": "Ok... I'll destroy the whole economy", - "destroy": "%u destroyed the economy", - "migration-happening": "Database not up-to-date. Migrating database...", - "migration-done": "Migrated database successfully.", - "nothing-selected": "Select an item to buy it", - "select-menu-price": "Price: %p", - "price-less-than-zero": "The price can't be less or equal to zero" - }, - "status-role": { - "fulfilled": "Status-role condition is fulfilled", - "not-fulfilled": "Status-role condition is no longer fulfilled" - }, - "color-me": { - "create-log-reason": "%user redeemed their boosting-rewards by requesting the creation of this role", - "edit-log-reason": "%user edited their boosting-reward-role", - "delete-unboost-log-reason": "%user stopped boosting, so their role got deleted", - "delete-manual-log-reason": "%user deleted their role manually", - "command-description": "Request a Custom role as a reward for boosting. This has a cooldown of 24 hours", - "manage-subcommand-description": "Create or edit your custom role", - "name-option-description": "The name of your custom role", - "color-option-description": "The color of your custom role", - "remove-subcommand-description": "Remove your custom role", - "icon-option-description": "Your role-icon", - "confirm-option-remove-description": "Do you really want to delete your custom role? This will not reset any running cooldowns" - }, - "rock-paper-scissors": { - "stone": "Stone", - "paper": "Paper", - "scissors": "Scissors", - "won": "won", - "lost": "lost", - "tie": "tie", - "play-again": "Play again", - "challenge-message": "%t, %u challenged you to a game of rock-paper-scissors! Hit the button below to join the game! This invitation will expire in about 2 minutes, so don't hesitate to much.", - "invite-expired": "Sorry, %u, %i didn't accept your request to play rock-paper-scissors in time ):", - "invite-denied": "Sorry, %u, but %i denied your request to play a round of rock-paper-scissors ):", - "rps-title": "Rock Paper Scissors", - "rps-description": "Choose your weapon!", - "its-a-tie-try-again": "It's a tie! Try again!", - "command-description": "Play rock-paper-scissors against the bot or someone in the chat" - }, - "connect-four": { - "tie": "It's a tie!", - "win": "%u has won the game!", - "not-turn": "Sorry, but it's not your turn!", - "game-message": "Connect Four game of %u1 and %u2\nCurrent turn: %c %t.\n\n%g", - "challenge-message": "%t, %u challenged you to a game of Connect Four! Hit the button below to join the game! This invitation will expire in about 2 minutes, so don't hesitate to much.", - "invite-expired": "Sorry, %u, %i didn't accept your request to play Connect Four in time ):", - "invite-denied": "Sorry, %u, but %i denied your request to play a round of Connect Four ):", - "command-description": "Play Connect Four against someone in the chat", - "field-size-description": "The size of the playfield (default: 7)", - "challenge-yourself": "You cannot challenge yourself!", - "challenge-bot": "You cannot challenge bots!" - }, - "uno": { - "command-description": "Play Uno against users in the chat", - "challenge-message": "%u invites to a round of Uno! Click the button below this message to join! The game starts %timestamp with %count players.", - "not-enough-players": "Not enough players joined for a round of Uno!", - "user-cards": "%u: %cards cards", - "already-joined": "You're already in!", - "view-deck": "View deck", - "draw": "Draw card", - "uno": "Uno!", - "turn": "It's %u turn!", - "update-button": "Update", - "use-drawn": "Do you want to use the drawn card?", - "dont-use-drawn": "Dont use", - "win": "%u won the game! %turns cards were played.", - "win-you": "You've won the game!", - "missing-uno": "⚠️ You must use the Uno! button before you use your second last card!", - "choose-color": "Select a color:", - "pending-draws": "Use a Draw 2/4 card, otherwise you have to draw %count cards!", - "not-ingame": "You're not in this game!", - "skip": "Skip", - "reverse": "Reverse", - "color": "Color choice", - "draw2": "Draw 2", - "colordraw4": "Color choice and draw 4", - "cant-uno": "You cannot use Uno currently.", - "done-uno": "You've called Uno!", - "auto-drawn-skip": "Your turn was skipped because you would have had to draw the cards anyway.", - "start-game": "Start game now", - "not-host": "You're not the host of the game!", - "max-players": "The game is full!", - "previous-cards": "Previous cards: ", - "used-card": "You've already used the card %c! Use the Update button and play a valid card.", - "invalid-card": "You cannot play the card %c right now! Please select a valid card.", - "inactive-warn": "%u, it's your turn in the uno game!", - "inactive-win": "The uno game has ended. %u won as all others have been eliminated!" - }, - "quiz": { - "what-have-i-voted": "What have I voted?", - "vote": "Vote!", - "vote-this": "Select this option if you think it's correct.", - "voted-successfully": "Selected successfully.", - "not-voted-yet": "You have not selected an option yet, so I can't show you what you selected.", - "you-voted": "You've selected **%o** as correct answer.", - "change-opinion": "You can change your opinion at any time by selecting another option above the button you just clicked.", - "cannot-change-opinion": "You cannot change your selection as the creator of this quiz disabled it.", - "select-correct": "Select all correct answers", - "this-correct": "Mark this answer as correct", - "cmd-description": "Create or play server quiz", - "cmd-create-normal-description": "Create a quiz with up to 10 answers", - "cmd-create-bool-description": "Create a quiz with true or false answers", - "cmd-play-description": "Play a server quiz", - "cmd-leaderboard-description": "Shows the quiz leaderboard of the server", - "cmd-create-description-description": "Title / description of the quiz", - "cmd-create-channel-description": "Channel in which the quiz should be created", - "cmd-create-endAt-description": "How long the quiz will last", - "cmd-create-option-description": "Option number %o", - "cmd-create-canchange-description": "If the players can change their opinion after voting (default: no)", - "daily-quiz-limit": "You've reached the limit of **%l** daily playable quizzes. You can play again %timestamp.", - "created": "Quiz created successfully in %c.", - "correct-highlighted": "All correct answers were highlighted.", - "answer-correct": "✅ Your answer was correct and you've received one point for the leaderboard!", - "answer-wrong": "❌ Your answer was wrong!", - "bool-true": "Statement is correct", - "bool-false": "Statement is wrong", - "leaderboard-channel-not-found": "The leaderboard channel couldn't be found or it's type is invalid.", - "leaderboard-notation": "**%p. %u**: %xp XP", - "your-rank": "You've collected **%xp** points in quiz!", - "no-rank": "You've never finished a quiz successfully!", - "no-quiz": "No quizzes have been created for this server. Trusted admins can create them on https://scnx.app/glink?page=bot/configuration?query=quiz&file=quiz%7Cconfigs%2FquizList .", - "no-permission": "You don't have enough permissions to create quiz using the command." - }, - "starboard": { - "invalid-minstars": "Invalid minimum stars %stars", - "star-limit": "You've reached the hourly starboard limit of %limitEmoji on the server which is why you cannot react on the message %msgUrl .\nTry again %time!" - }, - "nicknames": { - "owner-cannot-be-renamed": "The owner of the server (%u) cannot be renamed.", - "nickname-error": "An error occurred while trying to change the nickname of %u: %e" - }, - "ping-protection": { - "log-not-a-member": "[Ping Protection] Punishment failed: The pinger is not a member.", - "log-punish-role-error": "[Ping Protection] Punishment failed: I cannot punish %tag because their role is higher than or equal to my highest role.", - "log-mute-error": "[Ping Protection] Punishment failed: I cannot mute %tag: %e", - "log-kick-error": "[Ping Protection] Punishment failed: I cannot kick %tag: %e", - "log-action-log-failed": "[Ping Protection] Punishment logging failed: %e", - "log-data-deletion": "[Ping Protection] All data for the user with ID %u has been deleted successfully.", - "log-automod-keyword-limit": "[Ping Protection] Automod keywords exceed 1000 characters limit. Keywords were truncated.", - "punish-log-failed-title": "Punishment failed for user %u", - "punish-log-failed-desc": "An error occured while trying to punish the user %m. Please check the bot's permissions and role hierarchy. See the message below for the error.", - "punish-log-error": "Error: ```%e```", - "punish-role-error": "I cannot punish %tag because their role is higher than or equal to my highest role.", - "reason-basic": "User reached %c pings in the last %w weeks.", - "reason-advanced": "User reached %c pings in the last %d days (Custom timeframe).", - "cmd-desc-module": "Ping protection related commands", - "cmd-desc-group-user": "Every command related to the users", - "cmd-desc-history": "View the ping history of a user", - "cmd-opt-user": "The user to check", - "cmd-desc-actions": "View the moderation action history of a user", - "cmd-desc-panel": "Admin: Open the user management panel", - "cmd-desc-group-list": "Lists protected or whitelisted entities", - "cmd-desc-list-protected": "List of all the protected users and roles", - "cmd-desc-list-wl": "List of all the whitelisted roles, channels and users", - "embed-history-title": "Ping history of %u", - "no-data-found": "No logs found for this user.", - "embed-actions-title": "Moderation history of %u", - "label-reason": "Reason", - "actions-retention-note": "Note: Moderation actions are retained for 1 - 12 months based on the configuration.", - "no-permission": "You don't have sufficient permissions to use this command.", - "panel-title": "User Panel: %u", - "panel-description": "Manage and view data for %u (%i). View a quick recap of their ping and moderation history, or delete all data stored for this user (Risky).", - "btn-history": "Ping history", - "btn-actions": "Actions history", - "btn-delete": "Delete all data (Risky)", - "list-protected-title": "Protected Users and Roles", - "list-protected-desc": "View all protected users and roles here. When someone pings one of these protected user(s)/role(s), a warning will be sent. Exceptions are when pinged by someone with a whitelisted role/as a whitelisted user or when it's sent in a whitelisted channel.", - "field-protected-users": "Protected Users", - "field-protected-roles": "Protected Roles", - "list-whitelist-title": "Whitelisted Roles, Users and Channels", - "list-whitelist-desc": "View all whitelisted roles, users and channels here. Whitelisted roles and users will not get a warning for pinging a protected entity, and pings from them or in whitelisted channels will be ignored.", - "field-wl-roles": "Whitelisted Roles", - "field-wl-channels": "Whitelisted Channels", - "field-wl-users": "Whitelisted Users", - "list-none": "None are configured.", - "modal-title": "Confirm data deletion for this user", - "modal-label": "Confirm data deletion by typing this phrase:", - "modal-phrase": "I understand that all data of this user will be deleted and that this action cannot be undone.", - "modal-failed": "The phrase you entered is incorrect. Data deletion cancelled.", - "modal-success-data-deletion": "All data for the user <@%u> (%u) has been deleted successfully", - "field-quick-history": "Quick history view (Last %w weeks)", - "field-quick-desc": "Pings history amount: %p\nModeration actions amount: %m", - "history-disabled": "History logging has been disabled by a bot-configurator.\nAre you (one of) the bot-configurators? You can enable history logging in the \"Data Storage\" tab in the 'ping-protection' module ^^", - "leaver-warning-long": "This user left the server at %d. These logs will stay until automatic deletion.", - "leaver-warning-short": "This user left the server at %d.", - "meme-why": "😐 [Why are you the way that you are?]() - You just pinged yourself..", - "meme-played": "🔑 [Congratulations, you played yourself.]()", - "meme-spider": "🕷️ [Is this you?]() - You just pinged yourself.", - "meme-rick": "🎵 [Never gonna give you up, never gonna let you down...]() You just Rick Rolled yourself. Also congrats you unlocked the secret easter egg that only has a 1% chance of appearing!!1!1!!", - "meme-grind": "Why are you even pinging yourself 5 times in a row? Anyways continue some more to possibly get the secret meme\n-# (good luck grinding, only a 1% chance of getting it and during testing I had it once after 83 pings)", - "label-jump": "Jump to Message", - "no-message-link": "This ping was blocked by AutoMod", - "list-entry-text": "%index. **Pinged %target** at %time\n%link" - } -} diff --git a/modules/name-list-cleaner/configs/config.json b/modules/name-list-cleaner/configs/config.json new file mode 100644 index 00000000..b726c2d2 --- /dev/null +++ b/modules/name-list-cleaner/configs/config.json @@ -0,0 +1,90 @@ +{ + "description": { + "en": "Configure the function of the module here", + "de": "Stelle hier die Funktionen des Modules ein" + }, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + }, + "filename": "config.json", + "content": [ + { + "name": "keepNickname", + "humanName": { + "en": "Keep nickname", + "de": "Nickname behalten" + }, + "default": { + "en": true + }, + "description": { + "en": "When activated, special characters will be removed from nicknames. If left disabled, the nickname will be removed and the username will be shown instead.", + "de": "Wenn aktiviert, werden Sonderzeichen aus den Nicknamen entfernt. Wenn deaktiviert, wird der Nickname entfernt und stattdessen der Benutzername angezeigt." + }, + "type": "boolean" + }, + { + "name": "symbolWhitelist", + "humanName": { + "en": "Character Whitelist/Blacklist", + "de": "Zeichen Whitelist/Blacklist" + }, + "default": { + "en": [] + }, + "description": { + "en": "A list of characters that should be allowed (whitelist) or blocked (blacklist) at the start of usernames. If the list is empty, all non-alphanumeric characters will be removed.", + "de": "Eine Liste von Zeichen, die am Anfang von Benutzernamen erlaubt (Whitelist) oder blockiert (Blacklist) werden sollen. Wenn die Liste leer ist, werden alle nicht-alphanumerischen Zeichen entfernt." + }, + "type": "array", + "content": "string" + }, + { + "name": "isBlacklist", + "humanName": { + "en": "Use blacklist instead of whitelist", + "de": "Blacklist statt Whitelist verwenden" + }, + "default": { + "en": false + }, + "description": { + "en": "When activated, the list of characters will be treated as a blacklist (characters in the list will be blocked). If left disabled, the list will be treated as a whitelist (only characters in the list and alphanumeric characters will be allowed).", + "de": "Wenn aktiviert, wird die Liste der Zeichen als Blacklist behandelt (Zeichen in der Liste werden blockiert). Wenn deaktiviert, wird die Liste als Whitelist behandelt (nur Zeichen in der Liste und alphanumerische Zeichen werden erlaubt)." + }, + "type": "boolean" + }, + { + "name": "userWhitelist", + "humanName": { + "en": "User Whitelist", + "de": "Benutzer-Whitelist" + }, + "default": { + "en": [] + }, + "description": { + "en": "A list of user IDs that should be exempt from the username check. Usernames of these users will not be modified.", + "de": "Eine Liste von Benutzer-IDs, die von der Benutzernamenprüfung ausgenommen werden sollen. Die Benutzernamen dieser Benutzer werden nicht geändert." + }, + "type": "array", + "content": "userID" + }, + { + "name": "alsoCheckUsernames", + "humanName": { + "en": "Also check usernames", + "de": "Auch Benutzernamen überprüfen" + }, + "default": { + "en": false + }, + "description": { + "en": "When activated, not only nicknames but also usernames will be checked for special characters and modified accordingly. If left disabled, only nicknames will be checked and modified, usernames will be left unchanged.", + "de": "Wenn aktiviert, werden nicht nur Nicknamen, sondern auch Benutzernamen auf Sonderzeichen überprüft und entsprechend geändert. Wenn deaktiviert, werden nur Nicknamen überprüft und geändert, Benutzernamen bleiben unverändert." + }, + "type": "boolean" + } + ] +} \ No newline at end of file diff --git a/modules/name-list-cleaner/events/botReady.js b/modules/name-list-cleaner/events/botReady.js new file mode 100644 index 00000000..aeb44e66 --- /dev/null +++ b/modules/name-list-cleaner/events/botReady.js @@ -0,0 +1,7 @@ +const {renameMember} = require('../renameMember'); + +module.exports.run = async function (client) { + for (const member of client.guild.members.cache.values()) { + await renameMember(client, member); + } +} \ No newline at end of file diff --git a/modules/name-list-cleaner/events/guildMemberUpdate.js b/modules/name-list-cleaner/events/guildMemberUpdate.js new file mode 100644 index 00000000..1e8d39d7 --- /dev/null +++ b/modules/name-list-cleaner/events/guildMemberUpdate.js @@ -0,0 +1,9 @@ +const {renameMember} = require("../renameMember"); +module.exports.run = async function (client, oldGuildMember, newGuildMember) { + + if (!client.botReadyAt) return; + if (newGuildMember.guild.id !== client.guild.id) return; + if (oldGuildMember.nickname === newGuildMember.nickname && oldGuildMember.user.username === newGuildMember.user.username) return; + await renameMember(client, newGuildMember); +} + diff --git a/modules/name-list-cleaner/module.json b/modules/name-list-cleaner/module.json new file mode 100644 index 00000000..890ebd10 --- /dev/null +++ b/modules/name-list-cleaner/module.json @@ -0,0 +1,24 @@ +{ + "name": "name-list-cleaner", + "humanReadableName": { + "en": "Name List Cleaner", + "de": "Namenslisten Cleaner" + }, + "author": { + "name": "hfgd", + "link": "https://github.com/hfgd123", + "scnxOrgID": "2" + }, + "openSourceURL": "https://github.com/hfgd123/CustomDCBot/tree/main/modules/name-list-cleaner", + "config-example-files": [ + "configs/config.json" + ], + "events-dir": "/events", + "tags": [ + "tools" + ], + "description": { + "en": "Remove special characters from the beginning of usernames", + "de": "Entferne Sonderzeichen vom Anfang von Benutzernamen" + } +} \ No newline at end of file diff --git a/modules/name-list-cleaner/renameMember.js b/modules/name-list-cleaner/renameMember.js new file mode 100644 index 00000000..6455bc05 --- /dev/null +++ b/modules/name-list-cleaner/renameMember.js @@ -0,0 +1,70 @@ +const {localize} = require("../../src/functions/localize"); +renameMember = async function (client, guildMember) { + const moduleConf = client.configurations['name-list-cleaner']['config']; + if (moduleConf.userWhitelist.includes(guildMember.user.id)) return; + let hasNickname = guildMember.nickname !== null; + + + if (hasNickname) { + let newName = checkUsername(client, guildMember.nickname, false); + if (newName === guildMember.nickname) return; + } else if (moduleConf.alsoCheckUsernames) { + let newName = checkUsername(client, guildMember.user.username, true); + if (newName === guildMember.user.username) return; + } else { + return; + } + + if (guildMember.guild.ownerId === guildMember.id) { + client.logger.error('[name-list-cleaner] ' + localize('name-list-cleaner', 'owner-cannot-be-renamed', {u: guildMember.user.username})) + return; + } + + if (moduleConf.keepNickname) { + try { + await guildMember.setNickname(newName, localize('name-list-cleaner', 'nickname-changed', {u: guildMember.user.username})); + } catch (e) { + client.logger.error('[name-list-cleaner] ' + localize('name-list-cleaner', 'nickname-error', {u: guildMember.user.username, e: e})) + } + } else { + if (!hasNickname) { + return; + } + try { + await guildMember.setNickname(null, localize('name-list-cleaner', 'nickname-reset', {u: guildMember.user.username})); + } catch (e) { + client.logger.error('[name-list-cleaner] ' + localize('name-list-cleaner', 'nickname-error', {u: guildMember.user.username, e: e})) + } + } +} + +module.exports.renameMember = renameMember; + +function checkUsername(client, name, isUsername) { + const moduleConf = client.configurations['name-list-cleaner']['config']; + const regEx = /^[a-zA-Z0-9]$/; + if (name.length === 0) { + if (isUsername) { + return 'User' + } else { + return null; + } + } + if (moduleConf.symbolWhitelist.length === 0) { + if (name.charAt(0).match(regEx)) { + return name; + } else { + return checkUsername(client, name.substring(1), isUsername); + } + } else if (!moduleConf.symbolWhitelist.includes(name.charAt(0)) && !moduleConf.isBlacklist) { + if (name.charAt(0).match(regEx)) { + return name; + } else { + return checkUsername(client, name.substring(1), isUsername); + } + } else if (moduleConf.symbolWhitelist.includes(name.charAt(0)) && moduleConf.isBlacklist) { + return checkUsername(client, name.substring(1), isUsername); + } else { + return name; + } +} \ No newline at end of file From 29addb8986a9ca5c71102d496475ecfcefc9331b Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Mon, 16 Mar 2026 08:29:39 +0100 Subject: [PATCH 10/27] Staff Management System --- locales/en.json | 418 +++ .../staff-management-system/commands/duty.js | 1086 ++++++++ .../commands/staff-management.js | 717 ++++++ .../commands/status.js | 221 ++ .../configs/activity-checks.json | 316 +++ .../configs/configuration.json | 86 + .../configs/infractions.json | 454 ++++ .../configs/profiles.json | 144 ++ .../configs/promotions.json | 247 ++ .../configs/reviews.json | 147 ++ .../configs/shifts.json | 179 ++ .../configs/status.json | 195 ++ .../events/botReady.js | 81 + .../events/guildMemberRemove.js | 38 + .../events/interactionCreate.js | 498 ++++ .../models/ActivityCheck.js | 46 + .../models/Infraction.js | 54 + .../models/LoaRequest.js | 54 + .../models/Promotion.js | 42 + .../models/StaffProfile.js | 63 + .../models/StaffReview.js | 43 + .../models/StaffShift.js | 37 + modules/staff-management-system/module.json | 33 + .../staff-management.js | 2289 +++++++++++++++++ src/events/interactionCreate.js | 2 +- 25 files changed, 7489 insertions(+), 1 deletion(-) create mode 100644 modules/staff-management-system/commands/duty.js create mode 100644 modules/staff-management-system/commands/staff-management.js create mode 100644 modules/staff-management-system/commands/status.js create mode 100644 modules/staff-management-system/configs/activity-checks.json create mode 100644 modules/staff-management-system/configs/configuration.json create mode 100644 modules/staff-management-system/configs/infractions.json create mode 100644 modules/staff-management-system/configs/profiles.json create mode 100644 modules/staff-management-system/configs/promotions.json create mode 100644 modules/staff-management-system/configs/reviews.json create mode 100644 modules/staff-management-system/configs/shifts.json create mode 100644 modules/staff-management-system/configs/status.json create mode 100644 modules/staff-management-system/events/botReady.js create mode 100644 modules/staff-management-system/events/guildMemberRemove.js create mode 100644 modules/staff-management-system/events/interactionCreate.js create mode 100644 modules/staff-management-system/models/ActivityCheck.js create mode 100644 modules/staff-management-system/models/Infraction.js create mode 100644 modules/staff-management-system/models/LoaRequest.js create mode 100644 modules/staff-management-system/models/Promotion.js create mode 100644 modules/staff-management-system/models/StaffProfile.js create mode 100644 modules/staff-management-system/models/StaffReview.js create mode 100644 modules/staff-management-system/models/StaffShift.js create mode 100644 modules/staff-management-system/module.json create mode 100644 modules/staff-management-system/staff-management.js diff --git a/locales/en.json b/locales/en.json index a0637a9b..a86e475c 100644 --- a/locales/en.json +++ b/locales/en.json @@ -423,6 +423,36 @@ "edit-modal-username-placeholder": "Username of the user", "user-not-found": "User not found" }, + "guess-the-number": { + "command-description": "Manage your guess-the-number-games", + "status-command-description": "Shows the current status of a guess-the-number-game in this channel", + "create-command-description": "Create a new guess-the-number-game in this channel", + "create-min-description": "Minimal value users can guess", + "create-max-description": "Maximal value users can guess", + "create-number-description": "Number users should guess to win", + "end-command-description": "Ends the current game", + "session-already-running": "There is a session already running in this channel. Please end it with /guess-the-number end", + "session-not-running": "There is currently no session running.", + "gamechannel-modus": "You can't use this command in a gamechannel. To end a game, disable the gamechannel modus and try using the end command.", + "session-ended-successfully": "Ended session successfully. Locked channel successfully.", + "current-session": "Current session", + "number": "Number", + "min-val": "Min-Value", + "max-val": "Max-Value", + "owner": "Owner", + "guess-count": "Count of guesses", + "min-max-discrepancy": "`min` can't be bigger or equal to `max`", + "max-discrepancy": "`number` can't be bigger than `max`.", + "min-discrepancy": "`number` can't be smaller than `min`.", + "emoji-guide-button": "What does the reaction under my guess mean?", + "guide-wrong-guess": "Your guess was wrong (but your entry was valid)", + "guide-win": "You guessed correctly - you win :tada:", + "guide-admin-guess": "Your guess was invalid, because you are an admin - admins can't participate because they can see the correct number", + "guide-invalid-guess": "Your guess was invalid (e.g. below the minimal / over the maximal number, not a number, …)", + "created-successfully": "Created game successfully. Users can now start guessing in this channel. The winning number is **%n**. You can always check the status by running `/guess-the-number-status`. Note that you as an admin can not guess.", + "game-ended": "Game ended", + "game-started": "Game started" + }, "massrole": { "command-description": "Manage roles for all members", "add-subcommand-description": "Add a role to all members", @@ -993,5 +1023,393 @@ "label-jump": "Jump to Message", "no-message-link": "This ping was blocked by AutoMod", "list-entry-text": "%index. **Pinged %target** at %time\n%link" + }, + "staff-management-system": { + "time-zero": "0 seconds", + "time-hours": "hours", + "time-hour": "hour", + "time-mins": "minutes", + "time-min": "minute", + "time-secs": "seconds", + "time-sec": "second", + "stat-brk": "🟡 On Break", + "stat-on": "🟢 On-Duty", + "stat-off": "🔴 Off-Duty", + "duty-panel-title": "Duty Panel - %type", + "duty-stats": "📊 Statistics", + "duty-stat-desc": "**Total Shift Duration:** %duration\n**Total Shifts:** %count\n**Average Shift Duration:** %average", + "btn-duty-on": "On-Duty", + "btn-duty-res": "Resume Duty", + "btn-duty-brk": "Toggle Break", + "btn-duty-off": "Off-Duty", + "duty-breakdown": "Shift Breakdown", + "duty-quota-str": "\n\n**Quota (%timeframe):** %duration / %hours hours\n*%result*", + "quota-met": "✅ Quota Met", + "quota-fail": "❌ Quota Not Met", + "duty-time-title": "Shift Time - %type", + "duty-time-desc": "**Total Shifts:** %count\n**Total Duration:** %duration", + "btn-hist": "View History", + "err-no-lb": "ℹ️ No shift data found for **%type**.", + "duty-lb-title": "Leaderboard - %type", + "duty-lb-desc": "**%lookback Top Shifts**\n\n%lines", + "page-count": "Page %page/%total", + "info-no-sh-hi": "ℹ️ No completed shifts found.", + "duty-hi-title": "Shift History - %type", + "duty-adm-title": "Admin Duty Panel - %user", + "btn-f-off": "Force Off-Duty", + "btn-v-act": "Void Active Shift", + "btn-add-t": "Add Time", + "btn-v-all": "Void All Shifts", + "err-not-yours": "❌ This panel is not yours.", + "err-alr-on": "❌ You are already on a shift.", + "err-not-on": "❌ You are not on a shift.", + "err-hist-oth": "❌ You can only view your own history.", + "mod-v-all-title": "Confirm: Void All Shifts", + "mod-v-all-lbl": "Type CONFIRM to delete all shift data", + "err-conf-fail": "❌ Data deletion confirmation failed. You must type the phrase exactly.", + "succ-v-all": "All shift data for <@%user> has been deleted successfully.", + "mod-add-t": "Add Duty Time", + "mod-add-min": "Minutes to add", + "mod-add-type": "Shift Type", + "err-inv-min": "❌ Invalid number of minutes.", + "err-inv-type": "❌ Invalid shift type. Available: %types", + "err-sh-dis": "❌ Shift tracking is disabled.", + "info-no-act-sh": "ℹ️ There are no active shifts right now.", + "duty-act-title": "Active Shifts", + "duty-act-desc": "**Total Shifts:** %count", + "err-no-perm": "❌ You do not have permission to do this.", + "err-no-mem": "❌ Could not find that member.", + "ph-sel-type": "Select a Shift Type", + "msg-sel-type": "👇 Please choose your shift type:", + "err-prof-dis": "❌ Staff Profiles are disabled.", + "err-prof-cfg": "❌ Configuration is missing. Please make sure the message is not empty.", + "err-prof-no-own": "❌ You do not have a staff profile.", + "err-prof-no-tgt": "❌ That user does not have a profile.", + "rev-dis-text": "*Reviews disabled*", + "rev-no-rate": "No ratings yet", + "stat-offl": "⚫ Offline", + "stat-onl": "🟢 Online", + "stat-idl": "🟡 Away", + "stat-dnd": "🔴 Do Not Disturb", + "stat-prof-ond": "⏱️ On duty", + "stat-prof-loa": "🌙 On LoA", + "stat-prof-ra": "⛱️ On RA", + "prof-no-intro": "*No introduction set.*", + "err-prof-empty": "❌ Profile embed is empty.", + "err-prof-perm": "❌ You must be a staff member to have a profile.", + "prof-edit-title": "Edit Profile", + "prof-edit-nick": "Custom Nickname", + "prof-edit-intro": "Introduction", + "succ-prof-wipe": "✅ Profile wiped for %u.", + "succ-prof-upd": "✅ Profile updated!", + "general-chan": "Channel", + "general-ends": "Ends", + "ac-tot-res": "Total Responded", + "err-ac-noact": "❌ There is no active activity check.", + "succ-ac-end": "✅ Activity check ended manually.", + "err-gen-no-user": "❌ Could not find that user.", + "del-conf-phrase": "I understand that this will delete the specified data for this user and it cannot be undone.", + "mod-del-title": "Confirm Data Deletion", + "mod-del-lbl": "Type confirmation phrase:", + "del-all-title": "Confirm total data deletion", + "del-all-desc": "You are about to delete ALL data for this user. Reminder that this ***cannot be undone***. This is the last chance to back out. If you are sure, click the button below.\nThis action will automatically cancel in 30 seconds.", + "btn-conf-del": "Confirm deletion", + "btn-cancel": "Cancel", + "succ-del-canc": "✅ Data deletion cancelled.", + "succ-del-all": "✅ ALL data has been permanently wiped.", + "err-del-time": "⏳ Data deletion timed out.", + "succ-del-tgt": "✅ Target data has been permanently wiped.", + "err-gen-no-perm": "❌ You do not have permission.", + "err-no-req": "❌ Request not found.", + "err-req-hndl": "❌ Request is already %status.", + "mod-deny-req": "Deny Request", + "general-rsn": "Reason", + "label-appr-by": "This was approved by", + "req-appr-by": "✅ Approved by %user", + "req-deny-by": "❌ Denied by %user", + "general-stat": "Status", + "err-ac-alr-end": "❌ This activity check has already ended.", + "err-ac-notreq": "❌ You are not required to respond to this.", + "info-ac-alr-conf": "ℹ️ You already confirmed your activity!", + "succ-ac-log": "✅ Activity logged successfully!", + "err-internal": "❌ An internal error occurred.", + "dm-appr-title": "Your %label request got approved!", + "dm-appr-desc": "Your %label request got approved by %approver!\nYou are now on LoA until %endFmt.\nYou can view your LoA status by using the %viewCmd command.", + "dm-deny-title": "Your %label request was denied", + "dm-deny-desc": "Your %label request was denied by %denier.\n**Reason:** %reason", + "dm-ext-title": "Your %label got extended", + "dm-ext-desc": "Your %label got extended by %extender.\nThis extension is for **%days day(s)** - your %label now ends at %endFmt.\n**Reason for extension:** %reason\nYou can view your updated %label status by using the %viewCmd command.", + "dm-early-title": "Your %label ended early", + "dm-early-desc": "Your %label got ended early by %ender - your %label is now over and your role has been removed.\n**Reason for early end:** %reason.", + "dm-end-title": "Your %label has ended", + "dm-end-desc": "Your %label has now ended and your role has been removed.", + "log-start-title": "%label started for %username", + "log-start-desc": "%label started for %mention.%apprText", + "log-info-hdr": "%label Information", + "general-start": "Start", + "general-end": "End", + "log-end-title": "%label ended for %username", + "log-end-desc": "%label ended for %mention.", + "general-started": "Started", + "general-ended": "Ended", + "log-adj-title": "%label adjusted for %username", + "log-adj-desc": "The %label of %mention was adjusted by <@%executor>.", + "log-changes": "Changes made:", + "err-feat-disabled": "❌ %feature disabled.", + "err-use-susp": "❌ Please use `/staff-management infraction suspend`.", + "err-inv-dur": "❌ Invalid duration format or value.", + "label-never": "Never", + "succ-infract": "✅ Issued **%type** (Case #%caseId) to %user.", + "label-days": "days", + "succ-susp": "✅ Issued Suspension (Case #%caseId) to %user for %duration.", + "err-no-case": "❌ Case #%caseId does not exist.", + "err-case-inact": "⚠️ Case #%caseId is inactive.", + "succ-void-fail": "✅ Case #%caseId voided, role restore failed.", + "succ-void": "✅ Voided Case #%caseId.", + "info-clean-rec": "ℹ️ %username has a clean record.", + "rec-title": "Record: %username", + "icon-voided": "⚪", + "label-exp": "Expires", + "label-case": "Case", + "label-date": "Date", + "label-iss": "Issuer", + "err-role-hier": "❌ I cannot assign a role higher than my highest role.", + "err-add-role": "❌ Failed to add role: %e", + "succ-promo": "✅ Promoted %user to %role.", + "info-no-promo": "ℹ️ No promotion history found for %username.", + "prom-hist-title": "Promotion History: %username", + "label-role": "Role", + "label-prom-by": "Promoted by", + "panel-title": "User Panel: %username", + "panel-desc": "Manage and view all data for the user %mention (%id).", + "panel-ph": "Select a category...", + "opt-over": "Overview", + "opt-act": "Activity Checks", + "opt-inf": "Infractions", + "opt-prom": "Promotions", + "opt-rev": "Reviews", + "opt-shi": "Shifts", + "opt-sta": "Status", + "opt-del": "Data Deletion", + "p-inf-title": "Infractions: %username", + "p-inf-desc": "Total: **%count**\n%types\n", + "info-none": "*None*", + "p-no-hist": "*No history on this page.*", + "p-prom-title": "Promotions: %username", + "p-prom-desc": "Total: **%count**\n", + "p-rev-title": "Reviews: %username", + "p-rev-desc": "Total: **%count**\nAverage rating: **%avg ⭐**\n", + "label-by": "by", + "p-sta-title": "Status: %username", + "p-sta-desc": "Total requests: **%count**\nActive: %active\n", + "p-act-title": "Activity Checks: %username", + "p-act-desc": "Responses: **%count**\n", + "label-chk": "Check on", + "label-end": "Ends", + "label-chan": "Channel", + "p-shi-title": "Shifts: %username", + "no-quota-configured": "No quota", + "duty-quota-met": "✅ Quota Met", + "duty-quota-failed": "❌ Quota Not Met", + "label-unranked": "Unranked", + "panel-shifts-desc": "**Total Shifts:** %totalShifts\n**Duration:** %totalSeconds\n**Rank:** %lbRank\n**Breakdown:**\n%breakdownStr\n\n%quotaStr", + "err-shift-data-unavailable": "Shift data unavailable: %error", + "btn-view-history": "View History", + "panel-deletion-title": "Data Deletion: %tag", + "panel-deletion-desc": "⚠️ DANGEROUS AREA ⚠️\nYou are now entering a dengrous zone. At this place, you are able to delete specific or all data for the selected user. These actions ***CANNOT BE UNDONE*** and should only be used if you are absolutely sure about what you are doing. If you only want to delete specific entries, please use the respective command for that entry instead.\nIf you are unsure, click 'Go Back' from the dropdown now.\n\nUse the dropdown below to choose which data you want to delete or delete all data. Choose wisely and gracefully.", + "panel-deletion-placeholder": "Select data to delete...", + "panel-opt-back": "Go Back", + "panel-opt-del-act": "Delete Activity Checks", + "panel-opt-del-inf": "Delete Infractions", + "panel-opt-del-prom": "Delete Promotions", + "panel-opt-del-rev": "Delete Reviews", + "panel-opt-del-shifts": "Delete Shifts", + "panel-opt-del-status": "Delete Status", + "panel-opt-del-all": "Delete ALL data", + "status-active-loa": "🟢 On LoA", + "status-active-ra": "🟠 On RA", + "status-hist-loa": "LoA History", + "status-hist-ra": "RA History", + "err-status-disabled": "❌ %type system disabled.", + "err-invalid-duration": "❌ Invalid duration.", + "err-duration-max": "❌ Max duration is %max days.", + "err-status-exists": "❌ You have an active %type request.", + "status-request-title": "New %type Request", + "status-req-user": "User", + "status-req-duration": "Duration", + "btn-approve": "Approve", + "btn-deny": "Deny", + "success-status-request": "✅ %type request created (%state).", + "state-pending": "Pending", + "state-auto": "Auto-Approved", + "no-active-status": "ℹ️ %user has no active %type.", + "label-stat": "Status", + "filter-active": " (Active)", + "filter-expired": " (Expired)", + "filter-history": " (History)", + "err-no-recs": "No records found.", + "manage-status-title": "Manage %label - %username", + "manage-stat-desc": "%status\nPrevious %label's: %count", + "no-act-stat": "⚫ No active %label", + "manage-active-details": "📋 Active %label Details", + "label-auto": "Auto", + "manage-no-active-user": "No active %label.", + "btn-end-early": "End %label Early", + "btn-extend": "Extend %label", + "err-no-active-end": "❌ No active %label to end.", + "modal-end-early-title": "End %label Early", + "modal-end-early-reason": "Reason for ending", + "err-stat-inact": "❌ This %label is inactive.", + "status-ended-embed-desc": "⚫ %label ended by %user\nReason: %reason", + "err-no-active-extend": "❌ No active %label.", + "modal-extend-title": "Extend %label", + "modal-extend-days": "Additional days, maximum of 180 days", + "modal-extend-reason": "Reason for extension", + "status-adjusted-log": "**%label extended** - the %label now ends at %newEnd.\n**Reason:** %reason", + "mod-stat-ext": "**Start:** %s\n**End:** %e (+%d days)\n**Status:** %t\n**Approved by:** %a\n**Reason:** %r", + "info-no-status-history": "ℹ️ No %label history.", + "status-history-desc": "Showing %count of %total %label records.", + "err-ac-act": "❌ Active check already running.", + "err-ac-norole": "❌ No target roles configured.", + "err-ac-invchan": "❌ Invalid channel.", + "ac-confirm-btn": "Confirm Activity", + "succ-ac-start": "✅ Check started in <#%channel> for %hours hours.", + "err-ac-perms": "❌ Missing permissions in <#%channel>.", + "ac-title-end": "📋 Activity Check (Ended)", + "ac-res-title": "📊 Activity Results", + "ac-f-res": "✅ Responded (%count)", + "ac-f-fail": "❌ Failed (%count)", + "ac-f-exc": "🛡️ Exceptions (%count)", + "err-not-mem": "❌ Not a member.", + "err-self-rate": "❌ Cannot rate yourself.", + "err-staff-rate": "❌ Can only rate staff.", + "succ-review": "✅ Rated %tag %stars stars.", + "rev-title": "Reviews: %username", + "rev-desc": "**Average:** %avg ⭐ (%count reviews)", + "label-hist": "History", + "info-ac-none": "There are no active activity checks. Please check recent results in %c.", + "log-sched-loa": "[Staff Management] Successfully scheduled %count active LoA/RA expirations.", + "log-sched-fail": "[Staff Management] Failed to init expiry schedules: %error", + "log-susp-end": "[Staff Management] Automatically ended suspension for %tag", + "log-susp-err": "[Staff Management] Error expiring suspension: %error", + "log-shift-leave": "[Staff Management] Auto-ended shift for user %tag (User left guild).", + "log-leave-err": "[Staff Management] Error handling member leave: %error", + "log-del-all": "[Staff Management] Data deletion (ALL) executed for user %target by admin %admin.", + "log-del-type": "[Staff Management] Data deletion (%type) executed for user %target by admin %admin.", + "log-int-error": "[Staff Management] Interaction Error: %error", + "log-void-all": "[Staff management] All shift data for the user with ID %target has been deleted by admin %admin.", + "log-add-time": "[Staff Management] %admin added %min mins of %type duty time to %target.", + "log-panel-shift-err": "[Staff Management] User panel error: %error", + "log-ac-auto": "[Staff Management] Automated activity check is being initiated.", + "log-stat-dm-error": "[Staff Management] Failed to send status DM to %u: %e", + "log-status-adj-error": "[Staff Management] Logging status adjustment failed: %e", + "log-promo-msg-error": "[Staff Management] Failed to send promotion announcement: %e", + "label-user": "User", + "label-dur": "Duration", + "btn-appr": "Approve", + "stat-pend": "Pending Approval", + "stat-auto": "Auto-Approved", + "filter-act": " (Active)", + "filter-exp": " (Expired)", + "filter-hist": " (History)", + "manage-stat-title": "Manage %l - %u", + "manage-stat-f1": "📋 Active %l Details", + "no-act-user": "This staff member does not have an active %l.", + "btn-ext": "Extend %l", + "err-no-act-end": "❌ No active %l to end.", + "modal-end-early": "End %l Early", + "label-reason-end": "Reason for ending early", + "err-stat-not-act": "❌ This %l is no longer active.", + "mod-stat-end-desc": "**Status:** ⚫ %l ended early by %u\n**Reason:** %r", + "err-no-act-ext": "❌ No active %l to extend.", + "modal-ext": "Extend %l", + "label-add-days": "Additional days (e.g. 3, 7, 14)", + "label-ext-reason": "Reason for extension", + "log-adj-text": "1. **%l extended:** it now ends at %n. Old end date: %o.\n2. **Reason:** %r", + "label-ext-by": "extended by %dd", + "info-no-stat-hist": "ℹ️ This staff member has no %l history.", + "stat-hist-desc": "Showing %r of %c %l records.", + "err-no-user": "❌ Could not find that user.", + "btn-conf-act": "Confirm Activity", + "duty-hi-line": "Start: | End: ", + "err-gen": "❌ Error: %e", + "lbl-log-chan": "the configured log channel", + "ac-live-title": "Live Activity Check Status", + "lbl-ends": "Ends", + "del-conf-phr": "I understand that this will delete the specified data for this user and it cannot be undone.", + "err-ac-ended": "❌ This activity check has already ended.", + "err-ac-not-req": "❌ You are not required to respond to this activity check.", + "info-ac-alr": "ℹ️ You have already confirmed your activity!", + "cmd-desc-status": "Manage Leave of Absence (LoA) and Reduced Activity (RA).", + "cmd-desc-loa": "Manage Leave of Absence (LoA).", + "cmd-desc-loa-request": "Request a Leave of Absence.", + "cmd-desc-loar-duration": "The duration for your LoA (e.g. 3d, 2w, 1m)", + "cmd-desc-loar-reason": "Reason for your LoA", + "cmd-desc-loa-view": "View your Leave of Absence status.", + "cmd-desc-loav-user": "The user to view the LoA status", + "cmd-desc-loa-list": "List of all Leave of Abscences", + "cmd-desc-loal-filter": "Filter the LoA list on active, expired or all", + "cmd-desc-loa-admin": "Manage a user's Leave of Absence.", + "cmd-desc-loaa-user": "The user to manage their LoA", + "cmd-desc-ra": "Manage Reduced Activity (RA).", + "cmd-desc-ra-request": "Request Reduced Activity.", + "cmd-desc-rar-duration": "The duration for your RA (e.g. 3d, 2w, 1m)", + "cmd-desc-rar-reason": "Reason for your RA", + "cmd-desc-ra-view": "View your Reduced Activity status.", + "cmd-desc-rav-user": "The user to view the RA status", + "cmd-desc-ra-list": "List of all Reduced Activities", + "cmd-desc-ral-filter": "Filter the RA list on active, expired or all", + "cmd-desc-ra-admin": "Manage a user's Reduced Activity.", + "cmd-desc-raa-user": "The user to manage their RA", + "cmd-desc-duty": "Manage your duty status and view statistics.", + "cmd-desc-duty-manage": "Manage your duty status.", + "cmd-desc-duty-manage-type": "The duty type", + "cmd-desc-duty-active": "View all staff currently on duty.", + "cmd-desc-duty-history": "View your duty history.", + "cmd-desc-duty-lb": "View the duty time leaderboard.", + "cmd-desc-duty-lb-type": "The duty type for the leaderboard.", + "cmd-desc-duty-time": "View your total duty time.", + "cmd-desc-duty-time-type": "The duty type", + "cmd-desc-duty-admin": "Manage a user's shift.", + "cmd-desc-duty-admin-user": "The user to manage their shift", + "cmd-desc-smg": "Access the staff management system.", + "cmd-desc-panel": "Open the staff management panel for a user.", + "cmd-desc-panel-user": "The user to open the staff panel for.", + "cmd-desc-infractions": "Manage staff infractions.", + "cmd-desc-issue": "Issue an infraction to a staff member.", + "cmd-desc-issue-user": "The user receiving the infraction.", + "cmd-desc-issue-type": "The type of infraction to issue.", + "cmd-desc-issue-reason": "The reason for issuing this infraction.", + "cmd-desc-issue-expiry": "When the infraction should expire.", + "cmd-desc-suspend": "Suspend a staff member.", + "cmd-desc-suspend-user": "The user to suspend.", + "cmd-desc-suspend-duration": "How long the suspension should last.", + "cmd-desc-suspend-reason": "The reason for the suspension.", + "cmd-desc-history": "View a user's history.", + "cmd-desc-history-user": "The user whose history you want to view.", + "cmd-desc-void": "Void an infraction case.", + "cmd-desc-void-case-id": "The case ID of the infraction to void.", + "cmd-desc-promotion": "Manage staff promotions.", + "cmd-desc-promote": "Promote a staff member to a new rank.", + "cmd-desc-promote-user": "The user to promote.", + "cmd-desc-promote-rank": "The rank to promote the user to.", + "cmd-desc-promote-reason": "The reason for the promotion.", + "cmd-desc-promote-channel": "The channel to announce the promotion in.", + "cmd-desc-ac": "Manage activity checks.", + "cmd-desc-ac-start": "Start a new activity check.", + "cmd-desc-ac-start-channel": "The channel where the activity check will be posted.", + "cmd-desc-ac-view": "View the current activity check status.", + "cmd-desc-ac-end": "End the current activity check.", + "cmd-desc-profile": "Manage staff profiles.", + "cmd-desc-profile-view": "View a staff member's profile.", + "cmd-desc-profile-view-user": "The user whose profile you want to view.", + "cmd-desc-profile-edit": "Edit your staff profile.", + "cmd-desc-profile-wipe": "Wipe a staff member's profile data.", + "cmd-desc-profile-wipe-user": "The user whose profile will be wiped.", + "cmd-desc-review": "Manage staff reviews.", + "cmd-desc-submit": "Submit a review for a staff member.", + "cmd-desc-submit-user": "The user you are reviewing.", + "cmd-desc-submit-stars": "The star rating for the review.", + "cmd-desc-submit-comment": "Your review comment." } } diff --git a/modules/staff-management-system/commands/duty.js b/modules/staff-management-system/commands/duty.js new file mode 100644 index 00000000..194d37bc --- /dev/null +++ b/modules/staff-management-system/commands/duty.js @@ -0,0 +1,1086 @@ +const { MessageFlags, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, StringSelectMenuBuilder, ModalBuilder, TextInputBuilder, TextInputStyle } = require('discord.js'); +const { Op, fn, col, literal } = require('sequelize'); +const { getConfig, applyFooter, formatDuration, buildPaginationRow } = require('../staff-management'); +const { localize } = require('../../../src/functions/localize'); + +function getLookbackDate(config) { + const lookback = config.leaderboardLookback || 'Weekly'; + if (lookback === 'All-time') return null; + const date = new Date(); + if (lookback === 'Weekly') date.setDate(date.getDate() - 7); + else if (lookback === 'Monthly') date.setMonth(date.getMonth() - 1); + return date; +} + +function getQuotaForMember(member, config) { + if (!config.enableQuotas || !config.quotas || Object.keys(config.quotas).length === 0) return null; + + let bestQuota = null; + let highestPosition = -1; + + for (const [roleId, hoursStr] of Object.entries(config.quotas)) { + const hours = parseFloat(hoursStr); + if (isNaN(hours)) continue; + + const role = member.guild.roles.cache.get(roleId); + if (role && member.roles.cache.has(roleId) && role.position > highestPosition) { + highestPosition = role.position; + bestQuota = { roleId, hours }; + } + } + + return bestQuota; +} + +async function buildDutyManagePayload(client, userId, guild, shiftType) { + const Profile = client.models['staff-management-system']['StaffProfile']; + const Shift = client.models['staff-management-system']['StaffShift']; + + const user = await client.users.fetch(userId).catch(() => null); + const profile = await Profile.findByPk(userId); + + const onDuty = profile?.onDuty || false; + const onBreak = profile?.onBreak || false; + + let statusText, statusColor; + if (onDuty && onBreak) { statusText = localize('staff-management-system', 'stat-brk'); statusColor = 'Yellow'; } + else if (onDuty) { statusText = localize('staff-management-system', 'stat-on'); statusColor = 'Green'; } + else { statusText = localize('staff-management-system', 'stat-off'); statusColor = 'Red'; } + + const completedShifts = await Shift.findAll({ + where: { + userId, + type: shiftType, + endTime: { [Op.not]: null }, + duration: { [Op.not]: null } + } + }); + const totalShifts = completedShifts.length; + const totalSeconds = completedShifts.reduce((sum, s) => sum + (parseInt(s.duration) || 0), 0); + const avgSeconds = totalShifts > 0 + ? Math.floor(totalSeconds / totalShifts) + : 0; + + const embed = applyFooter(client, new EmbedBuilder() + .setTitle(localize('staff-management-system', 'duty-panel-title', { type: shiftType })) + .setColor(statusColor) + .setThumbnail(user?.displayAvatarURL({ dynamic: true }) || null) + .setDescription(`**${user?.username || userId}**\n${statusText}`) + .addFields( + { + name: localize('staff-management-system', 'duty-stats'), + value: localize('staff-management-system', 'duty-stat-desc', + { + duration: formatDuration(totalSeconds), + count: totalShifts, + average: formatDuration(avgSeconds) + } + ) + } + ) + ); + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(`duty-mgmt_start_${userId}_${shiftType}`) + .setLabel(localize('staff-management-system', 'btn-duty-on')) + .setStyle(ButtonStyle.Success) + .setDisabled(onDuty), + new ButtonBuilder() + .setCustomId(`duty-mgmt_break_${userId}`) + .setLabel(onBreak ? localize('staff-management-system', 'btn-duty-res') : localize('staff-management-system', 'btn-duty-brk')) + .setStyle(ButtonStyle.Secondary) + .setDisabled(!onDuty), + new ButtonBuilder() + .setCustomId(`duty-mgmt_end_${userId}`) + .setLabel(localize('staff-management-system', 'btn-duty-off')) + .setStyle(ButtonStyle.Danger) + .setDisabled(!onDuty) + ); + + return { + embeds: [embed.toJSON()], + components: [row.toJSON()] + }; +} + +async function buildDutyTimePayload(client, interaction, shiftType) { + const config = getConfig(client, 'shifts'); + const Shift = client.models['staff-management-system']['StaffShift']; + const user = interaction.user; + + const whereClause = { + userId: user.id, + endTime: { [Op.not]: null }, + duration: { [Op.not]: null } + }; + if (shiftType !== 'All') whereClause.type = shiftType; + + const shifts = await Shift.findAll({ where: whereClause }); + + const totalSeconds = shifts.reduce((sum, s) => sum + (parseInt(s.duration) || 0), 0); + const shiftCount = shifts.length; + + let breakdownText = ''; + if (shiftType === 'All' && shiftCount > 0) { + const grouped = {}; + for (const s of shifts) { + const t = s.type || 'Staff'; + grouped[t] = (grouped[t] || 0) + (parseInt(s.duration) || 0); + } + breakdownText = `\n\n**${localize('staff-management-system', 'duty-breakdown')}:**\n` + Object.entries(grouped) + .sort((a, b) => b[1] - a[1]) + .map(([t, sec]) => `• ${t}: ${formatDuration(sec)}`) + .join('\n'); + } + + let quotaText = ''; + const member = await interaction.guild.members.fetch(user.id).catch(() => null); + if (member) { + const quota = getQuotaForMember(member, config); + if (quota) { + const timeframe = config.quotaTimeframe || 'Weekly'; + const cutoff = new Date(); + if (timeframe === 'Weekly') cutoff.setDate(cutoff.getDate() - 7); + else cutoff.setMonth(cutoff.getMonth() - 1); + + const recentWhere = { + userId: user.id, + startTime: { [Op.gt]: cutoff }, + endTime: { [Op.not]: null }, + duration: { [Op.not]: null } + }; + if (shiftType !== 'All') recentWhere.type = shiftType; + + const recentShifts = await Shift.findAll({ where: recentWhere }); + const recentSeconds = recentShifts.reduce((sum, s) => sum + (parseInt(s.duration) || 0), 0); + const requiredSeconds = quota.hours * 3600; + const metQuota = recentSeconds >= requiredSeconds; + quotaText = localize('staff-management-system', 'duty-quota-str', { + timeframe, + duration: formatDuration(recentSeconds), + hours: quota.hours, + result: metQuota + ? localize('staff-management-system', 'quota-met') + : localize('staff-management-system', 'quota-fail') + }); + } + } + + const embed = applyFooter(client, new EmbedBuilder() + .setTitle(localize('staff-management-system', 'duty-time-title', { type: shiftType })) + .setColor('Blue') + .setThumbnail(user.displayAvatarURL({ dynamic: true })) + .setDescription(localize('staff-management-system', 'duty-time-desc', { + count: shiftCount, + duration: formatDuration(totalSeconds) + }) + breakdownText + quotaText) + ); + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(`duty-mgmt_hist_${user.id}_1_${shiftType}`) + .setLabel(localize('staff-management-system', 'btn-hist')) + .setStyle(ButtonStyle.Secondary) + .setDisabled(shiftCount === 0) + ); + + return { + embeds: [embed.toJSON()], + components: [row.toJSON()] + }; +} + +async function buildLeaderboardPayload(client, page = 1, shiftType) { + const config = getConfig(client, 'shifts'); + const Shift = client.models['staff-management-system']['StaffShift']; + const limit = 15; + const offset = (page - 1) * limit; + + const whereClause = { + endTime: { [Op.not]: null }, + duration: { [Op.not]: null } + }; + if (shiftType !== 'All') whereClause.type = shiftType; + + const lookbackDate = getLookbackDate(config); + if (lookbackDate) whereClause.startTime = { [Op.gt]: lookbackDate }; + + const allResults = await Shift.findAll({ + attributes: [ + 'userId', + [fn('SUM', col('duration')), 'totalDuration'], + [fn('COUNT', col('id')), 'shiftCount'] + ], + where: whereClause, + group: ['userId'], + order: [[literal('totalDuration'), 'DESC']] + }); + + const total = allResults.length; + if (total === 0) return { + content: localize('staff-management-system', 'err-no-lb', { + type: shiftType + }) + }; + + const totalPages = Math.ceil(total / limit) || 1; + const paginated = allResults.slice(offset, offset + limit); + + const lines = []; + for (let i = 0; i < paginated.length; i++) { + const entry = paginated[i]; + const dur = formatDuration(parseInt(entry.dataValues.totalDuration)); + lines.push(`${offset + i + 1}. **<@${entry.userId}>** • ${dur}`); + } + + const lookbackLabel = config.leaderboardLookback || 'Weekly'; + const embed = new EmbedBuilder() + .setTitle(localize('staff-management-system', 'duty-lb-title', { + type: shiftType + })) + .setColor('Gold') + .setDescription(localize('staff-management-system', 'duty-lb-desc', { + lookback: lookbackLabel, + lines: lines.join('\n') + })) + .setFooter({ text: localize('staff-management-system', 'page-count', { + page, + total: totalPages + }) + }); + + const row = buildPaginationRow( + `duty-mgmt_lb_${page - 1}_${shiftType}`, + 'duty_lb_count', + `duty-mgmt_lb_${page + 1}_${shiftType}`, + page, totalPages, 'back', 'next' + ); + + return { + embeds: [embed.toJSON()], + components: [row.toJSON()] + }; +} + +async function buildShiftHistoryPayload(client, userId, page = 1, shiftType) { + const Shift = client.models['staff-management-system']['StaffShift']; + const limit = 10; + const offset = (page - 1) * limit; + + const whereClause = { + userId, + endTime: { [Op.not]: null }, + duration: { [Op.not]: null } + }; + if (shiftType !== 'All') whereClause.type = shiftType; + + const { count, rows } = await Shift.findAndCountAll({ + where: whereClause, + order: [['startTime', 'DESC']], + limit, + offset + }); + + if (count === 0) return { content: localize('staff-management-system', 'info-no-sh-hi') }; + const totalPages = Math.ceil(count / limit) || 1; + + const lines = rows.map((shift, i) => { + const dur = formatDuration(shift.duration); + const startTs = Math.floor(new Date(shift.startTime).getTime() / 1000); + const endTs = Math.floor(new Date(shift.endTime).getTime() / 1000); + const typeBadge = shiftType === 'All' ? ` \`[${shift.type || 'Staff'}]\`` : ''; + + return `**${offset + i + 1}. ${dur}${typeBadge}:**\nStart: | End: `; + }); + + const embed = new EmbedBuilder() + .setTitle(localize('staff-management-system', 'duty-hi-title', { + type: shiftType + })) + .setColor('Blue') + .setDescription(lines.join('\n\n')) + .setFooter({ text: localize('staff-management-system', 'page-count', { + page, + total: totalPages + }) + }); + + const row = buildPaginationRow( + `duty-mgmt_hist_${userId}_${page - 1}_${shiftType}`, + 'duty_hist_count', + `duty-mgmt_hist_${userId}_${page + 1}_${shiftType}`, + page, totalPages + ); + + return { + embeds: [embed.toJSON()], + components: [row.toJSON()] + }; +} + +async function buildDutyAdminPayload(client, targetMember, requestingMember) { + const Profile = client.models['staff-management-system']['StaffProfile']; + const Shift = client.models['staff-management-system']['StaffShift']; + + const targetUser = targetMember.user; + const profile = await Profile.findByPk(targetUser.id); + + const onDuty = profile?.onDuty || false; + const onBreak = profile?.onBreak || false; + + let statusText, statusColor; + if (onDuty && onBreak) { + statusText = localize('staff-management-system', 'stat-brk'); + statusColor = 'Yellow'; + } + else if (onDuty) { + statusText = localize('staff-management-system', 'stat-on'); + statusColor = 'Green'; + } + else { + statusText = localize('staff-management-system', 'stat-off'); + statusColor = 'Red'; + } + + const completedShifts = await Shift.findAll({ + where: { + userId: targetUser.id, + endTime: { [Op.not]: null }, + duration: { [Op.not]: null } + } + }); + const totalShifts = completedShifts.length; + const totalSeconds = completedShifts.reduce((sum, s) => sum + (parseInt(s.duration) || 0), 0); + const avgSeconds = totalShifts > 0 + ? Math.floor(totalSeconds / totalShifts) + : 0; + + const embed = applyFooter(client, new EmbedBuilder() + .setTitle(localize('staff-management-system', 'duty-adm-title', { + user: targetUser.username + })) + .setColor(statusColor) + .setThumbnail(targetUser.displayAvatarURL({ dynamic: true })) + .setDescription(`**${targetUser.username}**\n${statusText}`) + .addFields( + { + name: localize('staff-management-system', 'duty-stats'), + value: localize('staff-management-system', 'duty-stat-desc', { + duration: formatDuration(totalSeconds), + count: totalShifts, + average: formatDuration(avgSeconds) + }) + } + ) + ); + + const generalConfig = client.configurations['staff-management-system']['configuration']; + const isManagement = requestingMember.roles.cache.some(r => (generalConfig.managementRoles || []).includes(r.id)) || requestingMember.permissions.has('Administrator'); + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(`duty-mgmt_admin-forceend_${targetUser.id}`) + .setLabel(localize('staff-management-system', 'btn-f-off')) + .setEmoji('🔴') + .setStyle(ButtonStyle.Danger) + .setDisabled(!onDuty), + new ButtonBuilder() + .setCustomId(`duty-mgmt_admin-voidactive_${targetUser.id}`) + .setLabel(localize('staff-management-system', 'btn-v-act')) + .setEmoji('🗑️') + .setStyle(ButtonStyle.Danger) + .setDisabled(!onDuty), + new ButtonBuilder() + .setCustomId(`duty-mgmt_admin-addtime_${targetUser.id}`) + .setLabel(localize('staff-management-system', 'btn-add-t')) + .setEmoji('⏱️') + .setStyle(ButtonStyle.Success), + new ButtonBuilder() + .setCustomId(`duty-mgmt_admin-voidall_${targetUser.id}`) + .setLabel(localize('staff-management-system', 'btn-v-all')) + .setEmoji('⚠️') + .setStyle(ButtonStyle.Danger) + .setDisabled(!isManagement) + ); + + return { + embeds: [embed.toJSON()], + components: [row.toJSON()] + }; +} + +// ----- Button handlers ----- +async function handleDutyStartButton(client, interaction) { + const parts = interaction.customId.split('_'); + const userId = parts[2]; + const shiftType = parts[3] || 'Staff'; + + if (interaction.user.id !== userId) return interaction.followUp({ + content: localize('staff-management-system', 'err-not-yours'), + flags: MessageFlags.Ephemeral + }); + + const config = getConfig(client, 'shifts'); + const Profile = client.models['staff-management-system']['StaffProfile']; + const Shift = client.models['staff-management-system']['StaffShift']; + + const profile = await Profile.findByPk(userId); + if (profile?.onDuty) return interaction.followUp({ + content: localize('staff-management-system', 'err-alr-on'), + flags: MessageFlags.Ephemeral + }); + + await Shift.create({ + userId, + startTime: new Date(), + type: shiftType + }); + await Profile.upsert({ + userId, + onDuty: true, + onBreak: false, + lastClockIn: new Date() + }); + + if (config.onDutyRole) { + const member = await interaction.guild.members.fetch(userId).catch(() => null); + if (member) await member.roles.add(config.onDutyRole).catch(() => {}); + } + + const payload = await buildDutyManagePayload(client, userId, interaction.guild, shiftType); + return interaction.editReply(payload); +} + +async function handleDutyBreakButton(client, interaction) { + const userId = interaction.customId.split('_')[2]; + if (interaction.user.id !== userId) return interaction.followUp({ + content: localize('staff-management-system', 'err-not-yours'), + flags: MessageFlags.Ephemeral + }); + + const Profile = client.models['staff-management-system']['StaffProfile']; + const Shift = client.models['staff-management-system']['StaffShift']; + const profile = await Profile.findByPk(userId); + + if (!profile?.onDuty) return interaction.followUp({ + content: localize('staff-management-system', 'err-not-on'), + flags: MessageFlags.Ephemeral + }); + + const activeShift = await Shift.findOne({ + where: { userId, endTime: null } + }); + const shiftType = activeShift?.type || 'Staff'; + + const nowOnBreak = !profile.onBreak; + await Profile.update({ + onBreak: nowOnBreak, + breakStartTime: nowOnBreak + ? new Date() + : null }, { + where: { userId } + } + ); + + const payload = await buildDutyManagePayload(client, userId, interaction.guild, shiftType); + return interaction.editReply(payload); +} + +async function handleDutyEndButton(client, interaction) { + const userId = interaction.customId.split('_')[2]; + if (interaction.user.id !== userId) return interaction.followUp({ + content: localize('staff-management-system', 'err-not-yours'), + flags: MessageFlags.Ephemeral + }); + + const config = getConfig(client, 'shifts'); + const Profile = client.models['staff-management-system']['StaffProfile']; + const Shift = client.models['staff-management-system']['StaffShift']; + + const profile = await Profile.findByPk(userId); + if (!profile?.onDuty) return interaction.followUp({ + content: localize('staff-management-system', 'err-not-on'), + flags: MessageFlags.Ephemeral + }); + + const activeShifts = await Shift.findAll({ where: { userId, endTime: null } }); + const shiftType = activeShifts.length > 0 ? activeShifts[0].type : 'Staff'; + + for (const activeShift of activeShifts) { + const endTime = new Date(); + const durationSeconds = Math.floor((endTime.getTime() - new Date(activeShift.startTime).getTime()) / 1000); + + if (config.minShiftDuration && (durationSeconds / 60) < config.minShiftDuration) { + await activeShift.destroy(); + } else { + await activeShift.update({ endTime, duration: durationSeconds }); + } + } + + await Profile.update({ + onDuty: false, + onBreak: false, + breakStartTime: null }, { + where: { userId } + }); + if (config.onDutyRole) { + const member = await interaction.guild.members.fetch(userId).catch(() => null); + if (member) await member.roles.remove(config.onDutyRole).catch(() => {}); + } + + const payload = await buildDutyManagePayload(client, userId, interaction.guild, shiftType); + return interaction.editReply(payload); +} + +async function handleDutyHistPageButton(client, interaction) { + const parts = interaction.customId.split('_'); + const userId = parts[2]; + const page = parseInt(parts[3]); + const shiftType = parts[4] || 'Staff'; + + if (interaction.user.id !== userId) return interaction.followUp({ + content: localize('staff-management-system', 'err-hist-oth'), + flags: MessageFlags.Ephemeral + }); + + const payload = await buildShiftHistoryPayload(client, userId, page, shiftType); + if (payload.content) return interaction.followUp({ + ...payload, + flags: MessageFlags.Ephemeral + }); + + const isOnHistEmbed = interaction.message?.embeds?.[0]?.title?.startsWith(localize('staff-management-system', 'duty-hi-title', { type: '' }).replace(' - ', '')); + if (isOnHistEmbed) { + return interaction.editReply(payload); + } else { + return interaction.followUp({ + ...payload, + flags: MessageFlags.Ephemeral + }); + } +} + +async function handleDutyLbPageButton(client, interaction) { + const parts = interaction.customId.split('_'); + const page = parseInt(parts[2]); + const shiftType = parts[3] || 'Staff'; + + const payload = await buildLeaderboardPayload(client, page, shiftType); + if (payload.content) return interaction.editReply({ ...payload, flags: MessageFlags.Ephemeral }); + return interaction.editReply(payload); +} + +// ----- Admin handler ----- +async function handleDutyAdminForceEnd(client, interaction) { + const targetUserId = interaction.customId.split('_')[2]; + const config = getConfig(client, 'shifts'); + const Profile = client.models['staff-management-system']['StaffProfile']; + const Shift = client.models['staff-management-system']['StaffShift']; + + const activeShifts = await Shift.findAll({ + where: { userId: targetUserId, endTime: null } + }); + for (const activeShift of activeShifts) { + const endTime = new Date(); + const durationSeconds = Math.floor((endTime.getTime() - new Date(activeShift.startTime).getTime()) / 1000); + await activeShift.update({ + endTime, + duration: durationSeconds + }); + } + + await Profile.update({ + onDuty: false, + onBreak: false, + breakStartTime: null }, { + where: { userId: targetUserId } + }); + if (config.onDutyRole) { + const member = await interaction.guild.members.fetch(targetUserId).catch(() => null); + if (member) await member.roles.remove(config.onDutyRole).catch(() => {}); + } + + const targetMember = await interaction.guild.members.fetch(targetUserId).catch(() => null); + const payload = await buildDutyAdminPayload(client, targetMember, interaction.member); + return interaction.editReply(payload); +} + +async function handleDutyAdminVoidActive(client, interaction) { + const targetUserId = interaction.customId.split('_')[2]; + const config = getConfig(client, 'shifts'); + const Profile = client.models['staff-management-system']['StaffProfile']; + const Shift = client.models['staff-management-system']['StaffShift']; + + const activeShifts = await Shift.findAll({ + where: { userId: targetUserId, endTime: null } + }); + for (const activeShift of activeShifts) await activeShift.destroy(); + + await Profile.update({ + onDuty: false, + onBreak: false, + breakStartTime: null }, { + where: { userId: targetUserId } + }); + if (config.onDutyRole) { + const member = await interaction.guild.members.fetch(targetUserId).catch(() => null); + if (member) await member.roles.remove(config.onDutyRole).catch(() => {}); + } + + const targetMember = await interaction.guild.members.fetch(targetUserId).catch(() => null); + const payload = await buildDutyAdminPayload(client, targetMember, interaction.member); + return interaction.editReply(payload); +} + +async function handleDutyAdminVoidAll(client, interaction) { + const targetUserId = interaction.customId.split('_')[2]; + const modal = new ModalBuilder() + .setCustomId(`duty-mgmt_admin-voidall-submit_${targetUserId}`) + .setTitle(localize('staff-management-system', 'mod-v-all-title')); + modal.addComponents( + new ActionRowBuilder() + .addComponents(new TextInputBuilder() + .setCustomId('confirm') + .setLabel(localize('staff-management-system', 'mod-v-all-lbl')) + .setStyle(TextInputStyle.Short) + .setPlaceholder('CONFIRM') + .setRequired(true)) + ); + return interaction.showModal(modal); +} + +async function handleDutyAdminVoidAllSubmit(client, interaction) { + const targetUserId = interaction.customId.split('_')[2]; + + if (interaction.fields.getTextInputValue('confirm') !== 'CONFIRM') { + return interaction.reply({ + content: localize('staff-management-system', 'err-conf-fail'), + flags: MessageFlags.Ephemeral + }); + } + + const config = getConfig(client, 'shifts'); + const Profile = client.models['staff-management-system']['StaffProfile']; + const Shift = client.models['staff-management-system']['StaffShift']; + + await Shift.destroy({ + where: { userId: targetUserId } + }); + await Profile.update({ + onDuty: false, + onBreak: false, + breakStartTime: null + }, { + where: { userId: targetUserId } + }); + + if (config.onDutyRole) { + const member = await interaction.guild.members.fetch(targetUserId).catch(() => null); + if (member) await member.roles.remove(config.onDutyRole).catch(() => {}); + } + + client.logger.info(localize('staff-management-system', 'log-void-all', { + target: targetUserId, + admin: interaction.user.id + })); + + return interaction.reply({ + content: localize('staff-management-system', 'succ-v-all', { user: targetUserId }), + flags: MessageFlags.Ephemeral + }); +} + +async function handleDutyAdminAddTimeButton(client, interaction) { + const targetUserId = interaction.customId.split('_')[2]; + const config = getConfig(client, 'shifts'); + const dutyTypes = config.dutyTypes && config.dutyTypes.length > 0 + ? config.dutyTypes + : ['Staff']; + + const modal = new ModalBuilder() + .setCustomId(`duty-mgmt_admin-addtime-submit_${targetUserId}`) + .setTitle(localize('staff-management-system', 'mod-add-t')); + modal.addComponents( + new ActionRowBuilder() + .addComponents( + new TextInputBuilder() + .setCustomId('minutes') + .setLabel(localize('staff-management-system', 'mod-add-min')) + .setStyle(TextInputStyle.Short) + .setPlaceholder('e.g. 60') + .setRequired(true) + ), + new ActionRowBuilder() + .addComponents( + new TextInputBuilder() + .setCustomId('type') + .setLabel(localize('staff-management-system', 'mod-add-type')) + .setStyle(TextInputStyle.Short) + .setPlaceholder(dutyTypes.join(', ')) + .setValue(dutyTypes[0]) + .setRequired(true) + ) + ); + return interaction.showModal(modal); +} + +async function handleDutyAdminAddTimeSubmit(client, interaction) { + const targetUserId = interaction.customId.split('_')[2]; + const minutesRaw = interaction.fields.getTextInputValue('minutes'); + const shiftType = interaction.fields.getTextInputValue('type'); + + const minutes = parseInt(minutesRaw); + if (isNaN(minutes) || minutes <= 0) { + return interaction.reply({ + content: localize('staff-management-system', 'err-inv-min'), + flags: MessageFlags.Ephemeral + }); + } + + const config = getConfig(client, 'shifts'); + const dutyTypes = config.dutyTypes && config.dutyTypes.length > 0 + ? config.dutyTypes + : ['Staff']; + + if (!dutyTypes.includes(shiftType)) { + return interaction.reply({ + content: localize('staff-management-system', 'err-inv-type', { + types: dutyTypes.join(', ') + }), + flags: MessageFlags.Ephemeral + }); + } + + const Shift = client.models['staff-management-system']['StaffShift']; + + const durationSeconds = minutes * 60; + const endTime = new Date(); + const startTime = new Date(endTime.getTime() - (durationSeconds * 1000)); + + await Shift.create({ + userId: targetUserId, + startTime: startTime, + endTime: endTime, + duration: durationSeconds, + type: shiftType + }); + + client.logger.info(localize('staff-management-system', 'log-add-time', { + admin: interaction.user.tag, + min: minutes, + type: shiftType, + target: targetUserId + })); + + const targetMember = await interaction.guild.members.fetch(targetUserId).catch(() => null); + const payload = await buildDutyAdminPayload(client, targetMember, interaction.member); + + return interaction.update(payload); +} + +// ----- Dropdown handler ----- +async function handleDutyDropdown(client, interaction, action, selectedType) { + if (action === 'manage') { + const payload = await buildDutyManagePayload(client, interaction.user.id, interaction.guild, selectedType); + return interaction.editReply({ content: '', ...payload }); + } + if (action === 'leaderboard') { + const payload = await buildLeaderboardPayload(client, 1, selectedType); + return interaction.editReply({ content: '', ...payload }); + } + if (action === 'time') { + const payload = await buildDutyTimePayload(client, interaction, selectedType); + return interaction.editReply({ content: '', ...payload }); + } +} + +async function handleCommonDutyCommand(i, action) { + const config = getConfig(i.client, 'shifts'); + if (!config || !config.enableShifts) return i.editReply({ content: localize('staff-management-system', 'err-sh-dis') }); + + const dutyTypes = config.dutyTypes && config.dutyTypes.length > 0 ? config.dutyTypes : ['Staff']; + let shiftType = i.options.getString('type'); + + const allowedTypes = (action === 'leaderboard' || action === 'time') ? ['All', ...dutyTypes] : dutyTypes; + + if (action === 'manage') { + const Profile = i.client.models['staff-management-system']['StaffProfile']; + const Shift = i.client.models['staff-management-system']['StaffShift']; + const profile = await Profile.findByPk(i.user.id); + if (profile?.onDuty) { + const activeShift = await Shift.findOne({ where: { userId: i.user.id, endTime: null } }); + shiftType = activeShift?.type || dutyTypes[0]; + } + } + + if (!shiftType) { + if (dutyTypes.length === 1 && action === 'manage') { + shiftType = dutyTypes[0]; + } else if (dutyTypes.length === 1 && (action === 'leaderboard' || action === 'time')) { + shiftType = 'All'; + } else { + const selectMenu = new StringSelectMenuBuilder() + .setCustomId(`duty-mgmt_dropdown_${action}`) + .setPlaceholder(localize('staff-management-system', 'ph-sel-type')); + + allowedTypes.forEach(t => selectMenu.addOptions({ label: t, value: t })); + const row = new ActionRowBuilder().addComponents(selectMenu); + return i.editReply({ content: localize('staff-management-system', 'msg-sel-type'), components: [row.toJSON()] }); + } + } else if (!allowedTypes.includes(shiftType)) { + return i.editReply({ content: localize('staff-management-system', 'err-inv-type', { types: allowedTypes.join(', ') }) }); + } + + if (action === 'manage') { + const payload = await buildDutyManagePayload(i.client, i.user.id, i.guild, shiftType); + await i.editReply(payload); + } else if (action === 'leaderboard') { + const payload = await buildLeaderboardPayload(i.client, 1, shiftType); + await i.editReply(payload); + } else if (action === 'time') { + const payload = await buildDutyTimePayload(i.client, i, shiftType); + await i.editReply(payload); + } +} + +module.exports.autoComplete = { + 'manage': { + 'type': async function (interaction) { + const config = getConfig(interaction.client, 'shifts'); + const dutyTypes = config.dutyTypes && config.dutyTypes.length > 0 + ? config.dutyTypes + : ['Staff']; + const focusedValue = interaction.value || ''; + + const filtered = dutyTypes.filter(choice => choice.toLowerCase().startsWith(focusedValue.toLowerCase())); + await interaction.respond(filtered.slice(0, 25).map(choice => ({ + name: choice, + value: choice + }))); + } + }, + 'leaderboard': { + 'type': async function (interaction) { + const config = getConfig(interaction.client, 'shifts'); + const dutyTypes = config.dutyTypes && config.dutyTypes.length > 0 + ? config.dutyTypes + : ['Staff']; + const options = ['All', ...dutyTypes]; + const focusedValue = interaction.value || ''; + + const filtered = options.filter(choice => choice.toLowerCase().startsWith(focusedValue.toLowerCase())); + await interaction.respond(filtered.slice(0, 25).map(choice => ({ + name: choice, + value: choice + }))); + } + }, + 'time': { + 'type': async function (interaction) { + const config = getConfig(interaction.client, 'shifts'); + const dutyTypes = config.dutyTypes && config.dutyTypes.length > 0 + ? config.dutyTypes + : ['Staff']; + const options = ['All', ...dutyTypes]; + const focusedValue = interaction.value || ''; + + const filtered = options.filter(choice => choice.toLowerCase().startsWith(focusedValue.toLowerCase())); + await interaction.respond(filtered.slice(0, 25).map(choice => ({ + name: choice, + value: choice + }))); + } + } +}; + +module.exports.beforeSubcommand = async function (interaction) { + await interaction.deferReply({ + flags: MessageFlags.Ephemeral + }); +}; + +module.exports.subcommands = { + 'manage': async function (i) { + await handleCommonDutyCommand(i, 'manage'); + }, + 'active': async function (i) { + const config = getConfig(i.client, 'shifts'); + if (!config || !config.enableShifts) return i.editReply({ + content: localize('staff-management-system', 'err-sh-dis') + }); + + const Shift = i.client.models['staff-management-system']['StaffShift']; + const activeShifts = await Shift.findAll({ + where: { endTime: null }, + order: [['startTime', 'ASC']] + }); + + if (activeShifts.length === 0) return i.editReply({ + content: localize('staff-management-system', 'info-no-act-sh') + }); + + const dutyTypes = config.dutyTypes && config.dutyTypes.length > 0 + ? config.dutyTypes + : ['Staff']; + + const grouped = {}; + for (const shift of activeShifts) { + const type = shift.type || dutyTypes[0]; + if (!grouped[type]) grouped[type] = []; + grouped[type].push(shift); + } + + const embed = applyFooter(i.client, new EmbedBuilder() + .setTitle(localize('staff-management-system', 'duty-act-title')) + .setColor('Green') + .setDescription(localize('staff-management-system', 'duty-act-desc', { + count: activeShifts.length + })) + ); + + let index = 1; + for (const type of dutyTypes) { + if (grouped[type]) { + const lines = []; + for (const shift of grouped[type]) { + const elapsed = Math.floor((Date.now() - new Date(shift.startTime).getTime()) / 1000); + lines.push(`${index}. **<@${shift.userId}>** • ${formatDuration(elapsed)}`); + index++; + } + embed.addFields({ + name: `${type} (${grouped[type].length})`, + value: lines.join('\n') + }); + delete grouped[type]; + } + } + for (const [type, shifts] of Object.entries(grouped)) { + const lines = []; + for (const shift of shifts) { + const elapsed = Math.floor((Date.now() - new Date(shift.startTime).getTime()) / 1000); + lines.push(`${index}. **<@${shift.userId}>** • ${formatDuration(elapsed)}`); + index++; + } + embed.addFields({ + name: `${type} (${shifts.length}) [Legacy]`, + value: lines.join('\n') + }); + } + await i.editReply({ + embeds: [embed.toJSON()] + }); + }, + 'leaderboard': async function (i) { + await handleCommonDutyCommand(i, 'leaderboard'); + }, + 'time': async function (i) { + await handleCommonDutyCommand(i, 'time'); + }, + 'admin': async function (i) { + const config = getConfig(i.client, 'shifts'); + if (!config || !config.enableShifts) return i.editReply({ + content: localize('staff-management-system', 'err-sh-dis') + }); + + const generalConfig = getConfig(i.client, 'configuration'); + const canManage = i.member.roles.cache.some(r => [...(generalConfig.supervisorRoles || []), ...(generalConfig.managementRoles || [])].includes(r.id)) || i.member.permissions.has('Administrator'); + if (!canManage) return i.editReply({ + content: localize('staff-management-system', 'err-no-perm') + }); + + const target = i.options.getMember('user'); + if (!target) return i.editReply({ + content: localize('staff-management-system', 'err-no-mem') + }); + + const payload = await buildDutyAdminPayload(i.client, target, i.member); + await i.editReply(payload); + } +}; + +module.exports.config = { + name: 'duty', + description: localize('staff-management-system', 'cmd-desc-duty'), + usage: '/duty', + type: 'slash', + defaultPermission: false, + options: [ + { + type: 'SUB_COMMAND', + name: 'manage', + description: localize('staff-management-system', 'cmd-desc-duty-manage'), + options: [ + { + type: 'STRING', + name: 'type', + description: localize('staff-management-system', 'cmd-desc-duty-manage-type'), + required: false, + autocomplete: true + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'active', + description: localize('staff-management-system', 'cmd-desc-duty-active') + }, + { + type: 'SUB_COMMAND', + name: 'leaderboard', + description: localize('staff-management-system', 'cmd-desc-duty-lb'), + options: [ + { + type: 'STRING', + name: 'type', + description: localize('staff-management-system', 'cmd-desc-duty-lb-type'), + required: false, + autocomplete: true + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'time', + description: localize('staff-management-system', 'cmd-desc-duty-time'), + options: [ + { + type: 'STRING', + name: 'type', + description: localize('staff-management-system', 'cmd-desc-duty-time-type'), + required: false, + autocomplete: true + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'admin', + description: localize('staff-management-system', 'cmd-desc-duty-admin'), + options: [ + { + type: 'USER', + name: 'user', + description: localize('staff-management-system', 'cmd-desc-duty-admin-user'), + required: true + } + ] + } + ] +}; + +// Export handlers +module.exports.buttonHandlers = { + handleDutyStartButton, + handleDutyAdminAddTimeButton, + handleDutyBreakButton, + handleDutyEndButton, + handleDutyDropdown, + handleDutyHistPageButton, + handleDutyLbPageButton, + handleDutyAdminForceEnd, + handleDutyAdminVoidActive, + handleDutyAdminVoidAll, + handleDutyAdminVoidAllSubmit, + handleDutyAdminAddTimeSubmit +}; \ No newline at end of file diff --git a/modules/staff-management-system/commands/staff-management.js b/modules/staff-management-system/commands/staff-management.js new file mode 100644 index 00000000..56c4cc08 --- /dev/null +++ b/modules/staff-management-system/commands/staff-management.js @@ -0,0 +1,717 @@ +const { MessageFlags, EmbedBuilder, ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder } = require('discord.js'); +const { embedTypeV2 } = require('../../../src/functions/helpers'); +const { localize } = require('../../../src/functions/localize'); +const { + issueInfraction, + getInfractionHistory, + issueSuspension, + voidInfraction, + promoteUser, + getPromotionHistory, + submitReview, + getReviewHistory, + startActivityCheck, + endActivityCheckProcess, + generateUserPanel +} = require('../staff-management'); + +function canManageChecks(client, member) { + if (member.permissions.has('Administrator')) return true; + const config = client.configurations['staff-management-system']['configuration'] || {}; + const supRoles = config.supervisorRoles || []; + const mgmtRoles = config.managementRoles || []; + return member.roles.cache.some(r => supRoles.includes(r.id) || mgmtRoles.includes(r.id)); +} + +async function handleProfileView(client, interaction, targetUser) { + const config = client.configurations['staff-management-system']['profiles']; + if (!config || !config.enableProfiles) return interaction.editReply({ + content: localize('staff-management-system', 'err-prof-dis') + }); + + if (!config.profileEmbedMessage) { + return interaction.editReply({ + content: localize('staff-management-system', 'err-prof-cfg') + }); + } + + const user = targetUser || interaction.user; + const member = await interaction.guild.members.fetch(user.id).catch(() => null); + if (!member) return interaction.editReply({ + content: localize('staff-management-system', 'err-no-mem') + }); + + const restrictToStaff = config.onlyAllowStaffProfile !== false; + if (restrictToStaff) { + const generalConfig = client.configurations['staff-management-system']['configuration'] || {}; + + const staffRoles = Array.isArray(generalConfig.staffRoles) + ? generalConfig.staffRoles + : (generalConfig.staffRoles + ? [generalConfig.staffRoles] + : [] + ); + const supRoles = Array.isArray(generalConfig.supervisorRoles) + ? generalConfig.supervisorRoles + : (generalConfig.supervisorRoles + ? [generalConfig.supervisorRoles] + : [] + ); + const mgmtRoles = Array.isArray(generalConfig.managementRoles) + ? generalConfig.managementRoles + : (generalConfig.managementRoles + ? [generalConfig.managementRoles] + : [] + ); + + const allStaffRoles = [...staffRoles, ...supRoles, ...mgmtRoles]; + const isAdmin = member.permissions.has('Administrator'); + const isStaff = allStaffRoles.length > 0 && member.roles.cache.some(r => allStaffRoles.includes(r.id)); + + if (!isAdmin && !isStaff) { + if (user.id === interaction.user.id) { + return interaction.editReply({ + content: localize('staff-management-system', 'err-prof-no-own') + }); + } else { + return interaction.editReply({ + content: localize('staff-management-system', 'err-prof-no-tgt') + }); + } + } + } + + const Profile = client.models['staff-management-system']['StaffProfile']; + const Review = client.models['staff-management-system']['StaffReview']; + + const [profile] = await Profile.findOrCreate({ + where: { userId: user.id } + }); + + const reviewsConfig = client.configurations['staff-management-system']['reviews']; + const reviewsEnabled = reviewsConfig && reviewsConfig.enableReviews; + + let ratingDisplay = localize('staff-management-system', 'rev-dis-text'); + if (reviewsEnabled) { + let avgRatingText = localize('staff-management-system', 'rev-no-rate'); + const allReviews = await Review.findAll({ + where: { targetId: user.id }, + attributes: ['stars'] + }); + if (allReviews.length > 0) { + avgRatingText = (allReviews.reduce((a, b) => a + b.stars, 0) / allReviews.length).toFixed(1); + } + ratingDisplay = `⭐ ${avgRatingText}`; + } + + let discordStatus = localize('staff-management-system', 'stat-offl'); + if (member.presence) { + switch (member.presence.status) { + case 'online': discordStatus = localize('staff-management-system', 'stat-onl'); break; + case 'idle': discordStatus = localize('staff-management-system', 'stat-idl'); break; + case 'dnd': discordStatus = localize('staff-management-system', 'stat-dnd'); break; + case 'offline': discordStatus = localize('staff-management-system', 'stat-offl'); break; + } + } + + const statusLines = [discordStatus]; + if (profile.onDuty) statusLines.push(localize('staff-management-system', 'stat-prof-ond')); + if (profile.activityStatus === 'LOA') statusLines.push(localize('staff-management-system', 'stat-prof-loa')); + if (profile.activityStatus === 'RA') statusLines.push(localize('staff-management-system', 'stat-prof-ra')); + + const introText = profile.customIntro || localize('staff-management-system', 'prof-no-intro'); + const nicknameText = profile.customNickname || user.username; + + const placeholders = { + '%user%': user.toString(), + '%username%': user.username, + '%nickname%': nicknameText, + '%intro%': introText, + '%status%': statusLines.join('\n'), + '%rating%': ratingDisplay, + '%pfp%': user.displayAvatarURL({ + dynamic: true, + format: 'png', + size: 1024 + }) || '' + }; + + let embedTemplate = config.profileEmbedMessage; + if (typeof embedTemplate === 'string') { + try { embedTemplate = JSON.parse(embedTemplate); } catch (e) {} + } + + let msgOpts = await embedTypeV2(embedTemplate, placeholders); + + if (!msgOpts || (!msgOpts.content && (!msgOpts.embeds || msgOpts.embeds.length === 0))) { + return interaction.editReply({ + content: localize('staff-management-system', 'err-prof-empty') + }); + } + + await interaction.editReply(msgOpts); +} + +async function handleProfileEdit(client, interaction) { + const config = client.configurations['staff-management-system']['profiles']; + if (!config || !config.enableProfiles) return interaction.reply({ + content: localize('staff-management-system', 'err-prof-dis'), + flags: MessageFlags.Ephemeral + }); + + const restrictToStaff = config.onlyAllowStaffProfile !== false; + if (restrictToStaff) { + const generalConfig = client.configurations['staff-management-system']['configuration'] || {}; + + const staffRoles = Array.isArray(generalConfig.staffRoles) + ? generalConfig.staffRoles + : (generalConfig.staffRoles + ? [generalConfig.staffRoles] + : [] + ); + const supRoles = Array.isArray(generalConfig.supervisorRoles) + ? generalConfig.supervisorRoles + : (generalConfig.supervisorRoles + ? [generalConfig.supervisorRoles] + : [] + ); + const mgmtRoles = Array.isArray(generalConfig.managementRoles) + ? generalConfig.managementRoles + : (generalConfig.managementRoles + ? [generalConfig.managementRoles] + : [] + ); + + const allStaffRoles = [ + ...staffRoles, + ...supRoles, + ...mgmtRoles + ]; + + const isAdmin = interaction.member.permissions.has('Administrator'); + const hasStaffRole = allStaffRoles.length > 0 && interaction.member.roles.cache.some(r => allStaffRoles.includes(r.id)); + + if (!isAdmin && !hasStaffRole) { + return interaction.reply({ + content: localize('staff-management-system', 'err-prof-perm'), + flags: MessageFlags.Ephemeral + }); + } + } + + const Profile = client.models['staff-management-system']['StaffProfile']; + const profile = await Profile.findByPk(interaction.user.id); + + const modal = new ModalBuilder() + .setCustomId(`staff-mgmt_profile-edit`) + .setTitle(localize('staff-management-system', 'prof-edit-title')); + + modal.addComponents( + new ActionRowBuilder() + .addComponents( + new TextInputBuilder() + .setCustomId('nickname') + .setLabel(localize('staff-management-system', 'prof-edit-nick')) + .setStyle(TextInputStyle.Short) + .setRequired(false) + .setValue(profile?.customNickname || '') + ), + new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId('intro') + .setLabel(localize('staff-management-system', 'prof-edit-intro')) + .setStyle(TextInputStyle.Paragraph) + .setRequired(false) + .setValue(profile?.customIntro || '') + ) + ); + + return interaction.showModal(modal); +} + +async function handleProfileAdminWipe(client, interaction, targetUser) { + const profilesConfig = client.configurations['staff-management-system']['profiles']; + const generalConfig = client.configurations['staff-management-system']['configuration'] || {}; + + if (!profilesConfig || !profilesConfig.enableProfiles) { + return interaction.editReply({ + content: localize('staff-management-system', 'err-prof-dis') + }); + } + + const mRoles = Array.isArray(generalConfig.managementRoles) + ? generalConfig.managementRoles + : (generalConfig.managementRoles + ? [generalConfig.managementRoles] + : [] + ); + const sRoles = Array.isArray(generalConfig.supervisorRoles) + ? generalConfig.supervisorRoles + : (generalConfig.supervisorRoles + ? [generalConfig.supervisorRoles] + : [] + ); + + const requiredRoles = profilesConfig.managePermission === 'Management' + ? mRoles + : [...sRoles, ...mRoles]; + + const isAdmin = interaction.member.permissions.has('Administrator'); + const hasRequiredRole = requiredRoles.length > 0 && interaction.member.roles.cache.some(r => requiredRoles.includes(r.id)); + + if (!isAdmin && !hasRequiredRole) { + return interaction.editReply({ + content: localize('staff-management-system', 'err-no-perm') + }); + } + + const Profile = client.models['staff-management-system']['StaffProfile']; + await Profile.update({ + customNickname: null, + customIntro: null + }, + { + where: { userId: targetUser.id } + }); + + await interaction.editReply({ + content: localize('staff-management-system', 'succ-prof-wipe', { u: targetUser.username }) + }); +} + +module.exports.autoComplete = { + 'infraction': { + 'issue': { + 'type': async function (interaction) { + const config = interaction.client.configurations['staff-management-system']['infractions'] || {}; + const types = config.infractionTypes && config.infractionTypes.length > 0 + ? config.infractionTypes + : ['Warning', 'Strike']; + + const focusedValue = interaction.options.getFocused() || ''; + const filtered = types.filter(choice => choice.toLowerCase().startsWith(focusedValue.toLowerCase())); + await interaction.respond(filtered.slice(0, 25).map(choice => ({ name: choice, value: choice }))); + } + } + } +}; + +module.exports.subcommands = { + 'panel': async (i) => { + const user = i.options.getUser('user'); + const payload = await generateUserPanel(i.client, user); + await i.reply({ + ...payload, + flags: MessageFlags.Ephemeral + }); + }, + 'infraction': { + 'issue': async (i) => { + const user = i.options.getMember('user'); + const type = i.options.getString('type'); + const reason = i.options.getString('reason'); + const expiry = i.options.getString('expiry'); + await issueInfraction(i.client, i, user, type, reason, expiry); + }, + 'suspend': async (i) => { + const user = i.options.getMember('user'); + const duration = i.options.getString('duration'); + const reason = i.options.getString('reason'); + await issueSuspension(i.client, i, user, duration, reason); + }, + 'history': async (i) => { + const user = i.options.getUser('user'); + await getInfractionHistory(i.client, i, user); + }, + 'void': async (i) => { + const caseId = i.options.getInteger('case_id'); + await voidInfraction(i.client, i, caseId); + } + }, + 'promotion': { + 'promote': async (i) => { + const user = i.options.getMember('user'); + const role = i.options.getRole('rank'); + const reason = i.options.getString('reason'); + await promoteUser(i.client, i, user, role, reason); + }, + 'history': async (i) => { + const user = i.options.getUser('user'); + await getPromotionHistory(i.client, i, user); + } + }, + 'activity-check': { + 'start': async (i) => { + await i.deferReply({ flags: MessageFlags.Ephemeral }); + if (!canManageChecks(i.client, i.member)) return i.editReply({ + content: localize('staff-management-system', 'err-no-perm') + }); + await startActivityCheck(i.client, i, false); + }, + 'view': async (i) => { + await i.deferReply({ flags: MessageFlags.Ephemeral }); + if (!canManageChecks(i.client, i.member)) return i.editReply({ + content: localize('staff-management-system', 'err-no-perm') + }); + + const ActivityCheck = i.client.models['staff-management-system']['ActivityCheck']; + const activeCheck = await ActivityCheck.findOne({ + where: { status: 'ACTIVE' } + }); + + if (!activeCheck) { + const config = i.client.configurations['staff-management-system']['activity-checks'] || {}; + const generalConfig = i.client.configurations['staff-management-system']['configuration'] || {}; + let logChannelId = config.logChannel; + if (!logChannelId || (Array.isArray(logChannelId) && logChannelId.length === 0)) logChannelId = generalConfig.generalLogChannel; + if (Array.isArray(logChannelId)) logChannelId = logChannelId[0]; + + const channelPing = logChannelId + ? `<#${logChannelId}>` + : localize('staff-management-system', 'lbl-log-chan'); + return i.editReply({ + content: localize('staff-management-system', 'info-ac-none', { c: channelPing }) + }); + } + + const responded = JSON.parse(activeCheck.respondedUsers || '[]'); + const embed = new EmbedBuilder() + .setTitle(localize('staff-management-system', 'ac-live-title')) + .setColor('Blue') + .setDescription(`**${localize('staff-management-system', 'general-ends')}:** \n**${localize('staff-management-system', 'general-chan')}:** <#${activeCheck.channelId}>\n**${localize('staff-management-system', 'ac-tot-res')}:** ${responded.length}`) + .setFooter({ + text: `${i.client.strings.footer}`, + iconURL: i.client.strings.footerImgUrl + }); + + if (!i.client.strings.disableFooterTimestamp) embed.setTimestamp(); + await i.editReply({ + embeds: [embed] + }); + }, + 'end': async (i) => { + await i.deferReply({ flags: MessageFlags.Ephemeral }); + if (!canManageChecks(i.client, i.member)) return i.editReply({ + content: localize('staff-management-system', 'err-no-perm') + }); + + const ActivityCheck = i.client.models['staff-management-system']['ActivityCheck']; + const activeCheck = await ActivityCheck.findOne({ where: { status: 'ACTIVE' } }); + + if (!activeCheck) return i.editReply({ + content: localize('staff-management-system', 'err-ac-noact') + }); + + await endActivityCheckProcess(i.client, activeCheck); + await i.editReply({ + content: localize('staff-management-system', 'succ-ac-end') + }); + } + }, + 'profile': { + 'view': async (i) => { + await i.deferReply({ + flags: MessageFlags.Ephemeral + }); + const user = i.options.getUser('user') || i.user; + await handleProfileView(i.client, i, user); + }, + 'edit': async (i) => { + await handleProfileEdit(i.client, i); + }, + 'wipe': async (i) => { + await i.deferReply({ + flags: MessageFlags.Ephemeral + }); + const user = i.options.getUser('user'); + await handleProfileAdminWipe(i.client, i, user); + } + }, + 'review': { + 'submit': async (i) => { + const user = i.options.getUser('user'); + const stars = i.options.getInteger('stars'); + const comment = i.options.getString('comment'); + await submitReview(i.client, i, user, stars, comment); + }, + 'history': async (i) => { + const user = i.options.getUser('user') || i.user; + await getReviewHistory(i.client, i, user); + } + } +}; + +module.exports.config = { + name: 'staff-management', + description: localize('staff-management-system', 'cmd-desc-smg'), + usage: '/staff-management', + type: 'slash', + defaultPermission: false, + options: [ + { + type: 'SUB_COMMAND', + name: 'panel', + description: localize('staff-management-system', 'cmd-desc-panel'), + options: [ + { + type: 'USER', + name: 'user', + description: localize('staff-management-system', 'cmd-desc-panel-user'), + required: true + } + ] + }, + { + type: 'SUB_COMMAND_GROUP', + name: 'infraction', + description: localize('staff-management-system', 'cmd-desc-infractions'), + options: [ + { + type: 'SUB_COMMAND', + name: 'issue', + description: localize('staff-management-system', 'cmd-desc-issue'), + options: [ + { + type: 'USER', + name: 'user', + description: localize('staff-management-system', 'cmd-desc-issue-user'), + required: true + }, + { + type: 'STRING', + name: 'type', + description: localize('staff-management-system', 'cmd-desc-issue-type'), + required: true, + autocomplete: true + }, + { + type: 'STRING', + name: 'reason', + description: localize('staff-management-system', 'cmd-desc-issue-reason'), + required: true + }, + { + type: 'STRING', + name: 'expiry', + description: localize('staff-management-system', 'cmd-desc-issue-expiry'), + required: false + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'suspend', + description: localize('staff-management-system', 'cmd-desc-suspend'), + options: [ + { + type: 'USER', + name: 'user', + description: localize('staff-management-system', 'cmd-desc-suspend-user'), + required: true + }, + { + type: 'STRING', + name: 'duration', + description: localize('staff-management-system', 'cmd-desc-suspend-duration'), + required: true + }, + { + type: 'STRING', + name: 'reason', + description: localize('staff-management-system', 'cmd-desc-suspend-reason'), + required: true + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'history', + description: localize('staff-management-system', 'cmd-desc-history'), + options: [ + { + type: 'USER', + name: 'user', + description: localize('staff-management-system', 'cmd-desc-history-user'), + required: true + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'void', + description: localize('staff-management-system', 'cmd-desc-void'), + options: [ + { + type: 'INTEGER', + name: 'case_id', + description: localize('staff-management-system', 'cmd-desc-void-case-id'), + required: true + } + ] + } + ] + }, + { + type: 'SUB_COMMAND_GROUP', + name: 'promotion', + description: localize('staff-management-system', 'cmd-desc-promotion'), + options: [ + { + type: 'SUB_COMMAND', + name: 'promote', + description: localize('staff-management-system', 'cmd-desc-promote'), + options: [ + { + type: 'USER', + name: 'user', + description: localize('staff-management-system', 'cmd-desc-promote-user'), + required: true + }, + { + type: 'ROLE', + name: 'rank', + description: localize('staff-management-system', 'cmd-desc-promote-rank'), + required: true + }, + { + type: 'STRING', + name: 'reason', + description: localize('staff-management-system', 'cmd-desc-promote-reason'), + required: false + }, + { + type: 'CHANNEL', + name: 'channel', + description: localize('staff-management-system', 'cmd-desc-promote-channel'), + required: false, + channelTypes: [0, 5] + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'history', + description: localize('staff-management-system', 'cmd-desc-history'), + options: [{ + type: 'USER', + name: 'user', + description: localize('staff-management-system', 'cmd-desc-history-user'), + required: true + }] + } + ] + }, + { + type: 'SUB_COMMAND_GROUP', + name: 'activity-check', + description: localize('staff-management-system', 'cmd-desc-ac'), + options: [ + { + type: 'SUB_COMMAND', + name: 'start', + description: localize('staff-management-system', 'cmd-desc-ac-start'), + options: [ + { + type: 'CHANNEL', + name: 'channel', + description: localize('staff-management-system', 'cmd-desc-ac-start-channel'), + required: false, + channelTypes: [0] + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'view', + description: localize('staff-management-system', 'cmd-desc-ac-view') + }, + { + type: 'SUB_COMMAND', + name: 'end', + description: localize('staff-management-system', 'cmd-desc-ac-end') + } + ] + }, + { + type: 'SUB_COMMAND_GROUP', + name: 'profile', + description: localize('staff-management-system', 'cmd-desc-profile'), + options: [ + { + type: 'SUB_COMMAND', + name: 'view', + description: localize('staff-management-system', 'cmd-desc-profile-view'), + options: [{ + type: 'USER', + name: 'user', + description: localize('staff-management-system', 'cmd-desc-profile-view-user'), + required: false + }] + }, + { + type: 'SUB_COMMAND', + name: 'edit', + description: localize('staff-management-system', 'cmd-desc-profile-edit') + }, + { + type: 'SUB_COMMAND', + name: 'wipe', + description: localize('staff-management-system', 'cmd-desc-profile-wipe'), + options: [ + { + type: 'USER', + name: 'user', + description: localize('staff-management-system', 'cmd-desc-profile-wipe-user'), + required: true + } + ] + } + ] + }, + { + type: 'SUB_COMMAND_GROUP', + name: 'review', + description: localize('staff-management-system', 'cmd-desc-review'), + options: [ + { + type: 'SUB_COMMAND', + name: 'submit', + description: localize('staff-management-system', 'cmd-desc-submit'), + options: [ + { + type: 'USER', + name: 'user', + description: localize('staff-management-system', 'cmd-desc-submit-user'), + required: true + }, + { + type: 'INTEGER', + name: 'stars', + description: localize('staff-management-system', 'cmd-desc-submit-stars'), + required: true, + minValue: 1, + maxValue: 5 + }, + { + type: 'STRING', + name: 'comment', + description: localize('staff-management-system', 'cmd-desc-submit-comment'), + required: true + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'history', + description: localize('staff-management-system', 'cmd-desc-history'), + options: [{ + type: 'USER', + name: 'user', + description: localize('staff-management-system', 'cmd-desc-history-user'), + required: false + }] + } + ] + } + ] +}; \ No newline at end of file diff --git a/modules/staff-management-system/commands/status.js b/modules/staff-management-system/commands/status.js new file mode 100644 index 00000000..44177d1e --- /dev/null +++ b/modules/staff-management-system/commands/status.js @@ -0,0 +1,221 @@ +const { MessageFlags } = require('discord.js'); +const { handleStatusRequest, handleStatusView, handleStatusList, handleStatusManage } = require('../staff-management'); +const { localize } = require('../../../src/functions/localize'); + +module.exports.beforeSubcommand = async function (interaction) { + if (!interaction.replied && !interaction.deferred) { + await interaction.deferReply({ + flags: MessageFlags.Ephemeral + }); + } +}; + +module.exports.subcommands = { + 'loa': { + 'request': async function (interaction) { + const duration = interaction.options.getString('duration'); + const reason = interaction.options.getString('reason'); + await handleStatusRequest(interaction.client, interaction, 'LOA', duration, reason); + }, + 'view': async function (interaction) { + const user = interaction.options.getUser('user') || interaction.user; + await handleStatusView(interaction.client, interaction, 'LOA', user); + }, + 'list': async function (interaction) { + const filter = interaction.options.getString('filter'); + await handleStatusList(interaction.client, interaction, 'LOA', filter); + }, + 'admin': async function (interaction) { + const user = interaction.options.getMember('user'); + if (!user) return interaction.editReply({ + content: localize('staff-management-system', 'err-no-mem') + }); + await handleStatusManage(interaction.client, interaction, user, 'LOA'); + } + }, + 'ra': { + 'request': async function (interaction) { + const duration = interaction.options.getString('duration'); + const reason = interaction.options.getString('reason'); + await handleStatusRequest(interaction.client, interaction, 'RA', duration, reason); + }, + 'view': async function (interaction) { + const user = interaction.options.getUser('user') || interaction.user; + await handleStatusView(interaction.client, interaction, 'RA', user); + }, + 'list': async function (interaction) { + const filter = interaction.options.getString('filter'); + await handleStatusList(interaction.client, interaction, 'RA', filter); + }, + 'admin': async function (interaction) { + const user = interaction.options.getMember('user'); + if (!user) return interaction.editReply({ + content: localize('staff-management-system', 'err-no-mem') + }); + await handleStatusManage(interaction.client, interaction, user, 'RA'); + } + } +}; + +module.exports.config = { + name: 'status', + description: localize('staff-management-system', 'cmd-desc-status'), + usage: '/status', + type: 'slash', + defaultPermission: false, + options: [ + { + type: 'SUB_COMMAND_GROUP', + name: 'loa', + description: localize('staff-management-system', 'cmd-desc-loa'), + options: [ + { + type: 'SUB_COMMAND', + name: 'request', + description: localize('staff-management-system', 'cmd-desc-loa-request'), + options: [ + { + type: 'STRING', + name: 'duration', + description: localize('staff-management-system', 'cmd-desc-loar-duration'), + required: true + }, + { + type: 'STRING', + name: 'reason', + description: localize('staff-management-system', 'cmd-desc-loar-reason'), + required: true + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'view', + description: localize('staff-management-system', 'cmd-desc-loa-view'), + options: [ + { + type: 'USER', + name: 'user', + description: localize('staff-management-system', 'cmd-desc-loav-user'), + required: false + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'list', + description: localize('staff-management-system', 'cmd-desc-loa-list'), + options: [{ + type: 'STRING', + name: 'filter', + description: localize('staff-management-system', 'cmd-desc-loal-filter'), + required: true, + choices: [ + { + name: 'Active', + value: 'active' + }, + { + name: 'Expired', + value: 'expired' + }, + { + name: 'All', + value: 'all' + }] + }] + }, + { + type: 'SUB_COMMAND', + name: 'admin', + description: localize('staff-management-system', 'cmd-desc-loa-admin'), + options: [ + { + type: 'USER', + name: 'user', + description: localize('staff-management-system', 'cmd-desc-loaa-user'), + required: true + } + ] + } + ] + }, + { + type: 'SUB_COMMAND_GROUP', + name: 'ra', + description: localize('staff-management-system', 'cmd-desc-ra'), + options: [ + { + type: 'SUB_COMMAND', + name: 'request', + description: localize('staff-management-system', 'cmd-desc-ra-request'), + options: [ + { + type: 'STRING', + name: 'duration', + description: localize('staff-management-system', 'cmd-desc-rar-duration'), + required: true + }, + { + type: 'STRING', + name: 'reason', + description: localize('staff-management-system', 'cmd-desc-rar-reason'), + required: true + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'view', + description: localize('staff-management-system', 'cmd-desc-ra-view'), + options: [ + { + type: 'USER', + name: 'user', + description: localize('staff-management-system', 'cmd-desc-rav-user'), + required: false + }] + }, + { + type: 'SUB_COMMAND', + name: 'list', + description: localize('staff-management-system', 'cmd-desc-ra-list'), + options: [ + { + type: 'STRING', + name: 'filter', + description: localize('staff-management-system', 'cmd-desc-ral-filter'), + required: true, + choices: [ + { + name: 'Active', + value: 'active' + }, + { + name: 'Expired', + value: 'expired' + }, + { + name: 'All', + value: 'all' + } + ] + }] + }, + { + type: 'SUB_COMMAND', + name: 'admin', + description: localize('staff-management-system', 'cmd-desc-ra-admin'), + options: [ + { + type: 'USER', + name: 'user', + description: localize('staff-management-system', 'cmd-desc-raa-user'), + required: true + } + ] + } + ] + } + ] +}; \ No newline at end of file diff --git a/modules/staff-management-system/configs/activity-checks.json b/modules/staff-management-system/configs/activity-checks.json new file mode 100644 index 00000000..5e18b796 --- /dev/null +++ b/modules/staff-management-system/configs/activity-checks.json @@ -0,0 +1,316 @@ +{ + "filename": "activity-checks.json", + "humanName": { + "en": "Activity Checks" + }, + "description": { + "en": "Configure automated staff activity checks and response logging." + }, + "categories": [ + { + "id": "general", + "icon": "fas fa-clipboard-user", + "displayName": { + "en": "General Settings" + } + }, + { + "id": "exceptions", + "icon": "fa-solid fa-badge-check", + "displayName": { + "en": "Exceptions" + } + }, + { + "id": "automation", + "icon": "far fa-robot", + "displayName": { + "en": "Automation" + } + }, + { + "id": "results", + "icon": "fa-solid fa-check-to-slot", + "displayName": { + "en": "Results & Logging" + } + } + ], + "content": [ + { + "name": "enableActivityChecks", + "category": "general", + "humanName": { + "en": "Enable Activity Checks" + }, + "description": { + "en": "Allows admins to start an activity check to see who is active." + }, + "type": "boolean", + "default": { + "en": true + }, + "elementToggle": true + }, + { + "name": "targetRoles", + "category": "general", + "humanName": { + "en": "Roles to Check" + }, + "description": { + "en": "The roles required to respond to the activity check. Anyone with these roles will be expected to click the button. Leave empty to default to the General Staff Roles." + }, + "type": "array", + "content": "roleID", + "default": { + "en": [] + }, + "allowNull": true + }, + { + "name": "timeframe", + "category": "general", + "humanName": { + "en": "Check Duration (Hours)" + }, + "description": { + "en": "How long staff have to respond to the activity check (Max 168 hours / 1 week)." + }, + "type": "integer", + "minValue": 1, + "maxValue": 168, + "default": { + "en": 24 + } + }, + { + "name": "checkMessage", + "category": "general", + "humanName": { + "en": "Activity Check Embed" + }, + "description": { + "en": "The message sent when an activity check starts." + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "endtime", + "description": { + "en": "The Discord timestamp when the check ends." + } + }, + { + "name": "duration", + "description": { + "en": "The configured duration in hours." + } + } + ], + "default": { + "en": { + "title": "📋 Staff Activity Check", + "description": "Please click the button below to confirm your activity before %endtime%.", + "color": "#3498db" + } + } + }, + { + "name": "sendingChannel", + "category": "general", + "humanName": { + "en": "Default Sending Channel" + }, + "description": { + "en": "The default channel where the activity check message will be posted. This can manually be overridden with the command." + }, + "type": "channelID", + "channelTypes": [ + "GUILD_TEXT" + ], + "default": { + "en": "" + }, + "allowNull": true + }, + { + "name": "exceptionsType", + "category": "exceptions", + "humanName": { + "en": "Exceptions Rule" + }, + "description": { + "en": "Who are excused from the activity checks?" + }, + "type": "select", + "content": [ + "No exceptions", + "Only LoA", + "Only RA", + "LoA and RA", + "Custom role(s)" + ], + "default": { + "en": "LoA and RA" + } + }, + { + "name": "customExceptionRoles", + "category": "exceptions", + "humanName": { + "en": "Custom Exception Roles" + }, + "description": { + "en": "Only applies if 'Custom role(s)' is selected above." + }, + "type": "array", + "content": "roleID", + "default": { + "en": [] + }, + "allowNull": true + }, + { + "name": "automatedChecks", + "category": "automation", + "humanName": { + "en": "Automated Checks" + }, + "description": { + "en": "If enabled, the bot will automatically start activity checks at configured intervals." + }, + "type": "boolean", + "default": { + "en": false + } + }, + { + "name": "automatedCheckInterval", + "category": "automation", + "humanName": { + "en": "Automated Check Interval" + }, + "description": { + "en": "On which interval to start automatic checks. Choose cronjob for full customzation." + }, + "type": "select", + "content": [ + "Weekly", + "Biweekly", + "Monthly", + "Cronjob" + ], + "default": { + "en": "Biweekly" + }, + "dependsOn": "automatedChecks" + }, + { + "name": "automatedCheckCronjob", + "category": "automation", + "humanName": { + "en": "Automated Check Cronjob" + }, + "description": { + "en": "The cronjob schedule for automatic checks. Only applies if 'Cronjob' is selected above." + }, + "type": "string", + "default": { + "en": "" + }, + "dependsOn": "automatedChecks", + "allowNull": true + }, + { + "name": "automatedCheckWeekDay", + "category": "automation", + "humanName": { + "en": "Automated Check Week Day" + }, + "description": { + "en": "The week day to start automatic checks." + }, + "type": "select", + "content": [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday" + ], + "default":{ + "en": "Monday" + }, + "dependsOn": "automatedChecks" + }, + { + "name": "automatedCheckMonthWeek", + "category": "automation", + "humanName": { + "en": "Automated Check Month Week" + }, + "description": { + "en": "The week of the month to start automatic checks. Only applies if 'Monthly' is selected above." + }, + "type": "integer", + "minValue": 1, + "maxValue": 4, + "default": { + "en": 1 + }, + "dependsOn": "automatedChecks" + }, + { + "name": "logChannel", + "category": "results", + "humanName": { + "en": "Results Channel" + }, + "description": { + "en": "Where the final results are posted. Leave empty if you want to use the general log channel." + }, + "type": "channelID", + "default": { + "en": "" + }, + "channelTypes": [ + "GUILD_TEXT" + ], + "allowNull": true + }, + { + "name": "pingResults", + "category": "results", + "humanName": { + "en": "Ping on Results" + }, + "description": { + "en": "Ping specific roles when the results are posted." + }, + "type": "boolean", + "default": { + "en": false + } + }, + { + "name": "pingRoles", + "category": "results", + "humanName": { + "en": "Roles to Ping" + }, + "description": { + "en": "The roles to ping with the results message." + }, + "type": "array", + "content": "roleID", + "default": { + "en": [] + }, + "dependsOn": "pingResults" + } + ] +} \ No newline at end of file diff --git a/modules/staff-management-system/configs/configuration.json b/modules/staff-management-system/configs/configuration.json new file mode 100644 index 00000000..600aeff7 --- /dev/null +++ b/modules/staff-management-system/configs/configuration.json @@ -0,0 +1,86 @@ +{ + "filename": "configuration.json", + "humanName": { + "en": "General Configuration" + }, + "description": { + "en": "Configure the main staff roles and the default log channel." + }, + "categories": [ + { + "id": "roles", + "icon": "fas fa-clipboard-user", + "displayName": { + "en": "Staff Roles" + } + }, + { + "id": "logging", + "icon": "fa-solid fa-clipboard-list", + "displayName": { + "en": "Logging" + } + } + ], + "content": [ + { + "name": "staffRoles", + "category": "roles", + "humanName": { + "en": "Staff Roles" + }, + "description": { + "en": "Roles that can use basic staff commands (Shifts, LoA Request and RA Request, reviews etc.)." + }, + "type": "array", + "content": "roleID", + "default": { + "en": [] + } + }, + { + "name": "supervisorRoles", + "category": "roles", + "humanName": { + "en": "Supervisor Roles" + }, + "description": { + "en": "Roles that can manage other staff members (Approve/Deny/Manage LoA's and RA's, Manage Shifts, promote and infract users)." + }, + "type": "array", + "content": "roleID", + "default": { + "en": [] + } + }, + { + "name": "managementRoles", + "category": "roles", + "humanName": { + "en": "Management Roles" + }, + "description": { + "en": "Roles with full access, including data deletion abilities." + }, + "type": "array", + "content": "roleID", + "default": { + "en": [] + } + }, + { + "name": "generalLogChannel", + "category": "logging", + "humanName": { + "en": "General Log Channel" + }, + "description": { + "en": "The default channel where logs happen such as status changes/request and more. This can be overridden in some features." + }, + "type": "channelID", + "default": { + "en": "" + } + } + ] +} \ No newline at end of file diff --git a/modules/staff-management-system/configs/infractions.json b/modules/staff-management-system/configs/infractions.json new file mode 100644 index 00000000..9d33f68a --- /dev/null +++ b/modules/staff-management-system/configs/infractions.json @@ -0,0 +1,454 @@ +{ + "filename": "infractions.json", + "humanName": { + "en": "Infractions & Suspensions" + }, + "description": { + "en": "Configure how staff infractions, strikes, and suspensions are handled." + }, + "categories": [ + { + "id": "logic", + "icon": "fas fa-hammer", + "displayName": { + "en": "General Logic" + } + }, + { + "id": "suspensions", + "icon": "fa fa-bell-exclamation", + "displayName": { + "en": "Suspensions Logic" + } + }, + { + "id": "messages", + "icon": "fa fa-messages", + "displayName": { + "en": "Messages & Embeds" + } + } + ], + "content": [ + { + "name": "enableInfractions", + "category": "logic", + "humanName": { + "en": "Enable Infractions System" + }, + "description": { + "en": "Enabling this will unlock features such as issuing infractions to staff members, suspensions and more." + }, + "type": "boolean", + "elementToggle": true, + "default": { + "en": true + } + }, + { + "name": "infractionTypes", + "category": "logic", + "humanName": { + "en": "Infraction Types" + }, + "description": { + "en": "These are the types of infractions that can be issued to staff members. You can customize these to fit your infractions system." + }, + "type": "array", + "content": "string", + "default": { + "en": [ + "Warning", + "Strike", + "Demotion", + "Termination", + "Under Investigation" + ] + } + }, + { + "name": "enableSuspensions", + "category": "suspensions", + "humanName": { + "en": "Enable Suspensions System" + }, + "description": { + "en": "Suspensions temporarily strip a staff member of their roles." + }, + "type": "boolean", + "elementToggle": true, + "default": { + "en": true + } + }, + { + "name": "suspensionHierarchyRole", + "category": "suspensions", + "humanName": { + "en": "Hierarchy Base Role" + }, + "description": { + "en": "When suspending, the bot will remove all roles above and including this one. This would usually be your lowest 'Staff' role." + }, + "type": "roleID", + "allowNull": true, + "dependsOn": "enableSuspensions", + "default": { + "en": "" + } + }, + { + "name": "suspensionRole", + "category": "suspensions", + "humanName": { + "en": "Suspended Role (Optional)" + }, + "description": { + "en": "A role to assign the user while they are suspended (e.g., 'Suspended Staff')." + }, + "type": "roleID", + "allowNull": true, + "dependsOn": "enableSuspensions", + "default": { + "en": "" + } + }, + { + "name": "suspensionMessage", + "category": "suspensions", + "humanName": { + "en": "Suspension Announcement Embed" + }, + "description": { + "en": "The embed sent to the log channel when a staff member is suspended." + }, + "type": "string", + "allowEmbed": true, + "dependsOn": "enableSuspensions", + "params": [ + { + "name": "user", + "description": { + "en": "Mention of the staff member" + } + }, + { + "name": "userPfp", + "description": { + "en": "Avatar of the staff member" + }, + "isImage": true + }, + { + "name": "issuerMention", + "description": { + "en": "Mention of the manager issuing it" + } + }, + { + "name": "issuerName", + "description": { + "en": "Name of the issuer" + } + }, + { + "name": "issuerPfp", + "description": { + "en": "Avatar of the issuer" + }, + "isImage": true + }, + { + "name": "duration", + "description": { + "en": "Duration of suspension" + } + }, + { + "name": "endDate", + "description": { + "en": "Timestamp of when the suspension ends" + } + }, + { + "name": "reason", + "description": { + "en": "Reason provided" + } + }, + { + "name": "caseId", + "description": { + "en": "Database Case ID" + } + } + ], + "default": { + "en": { + "content": "%user%", + "embeds": [ + { + "author": { + "name": "Signed, %issuerName% • Case #%caseId%", + "iconURL": "%issuerPfp%" + }, + "title": "⛔ Staff Suspension", + "description": "**Staff Member:** %user%\n**Duration:** %duration%\n**Ends:** %endDate%\n**Reason:** %reason%", + "color": "#ed4245", + "thumbnailURL": "%userPfp%" + } + ] + } + } + }, + { + "name": "infractionLogChannel", + "category": "messages", + "humanName": { + "en": "Infraction Log Channel" + }, + "description": { + "en": "Where should infractions and suspensions be announced?" + }, + "type": "channelID", + "content": [ + "GUILD_TEXT", + "GUILD_NEWS" + ], + "default": { + "en": "" + } + }, + { + "name": "infractionMessage", + "category": "messages", + "humanName": { + "en": "Infraction Announcement Embed" + }, + "description": { + "en": "The embed sent to the log channel for regular infractions." + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "user", + "description": { + "en": "Mention of the staff member" + } + }, + { + "name": "userPfp", + "description": { + "en": "Avatar of the staff member" + }, + "isImage": true + }, + { + "name": "issuerMention", + "description": { + "en": "Mention of the manager issuing it" + } + }, + { + "name": "issuerName", + "description": { + "en": "Name of the issuer" + } + }, + { + "name": "issuerPfp", + "description": { + "en": "Avatar of the issuer" + }, + "isImage": true + }, + { + "name": "duration", + "description": { + "en": "Duration of suspension" + } + }, + { + "name": "endDate", + "description": { + "en": "Timestamp of when the suspension ends" + } + }, + { + "name": "reason", + "description": { + "en": "Reason provided" + } + }, + { + "name": "caseId", + "description": { + "en": "Database Case ID" + } + } + ], + "default": { + "en": { + "content": "%user%", + "embeds": [ + { + "author": { + "name": "Signed, %issuerName% • Case #%caseId%", + "iconURL": "%issuerPfp%" + }, + "title": "⚠️ New %type%", + "description": "**Staff Member:** %user%\n**Action Taken:** %type%\n**Expires:** %endDate%\n**Reason:** %reason%", + "color": "#e67e22", + "thumbnailURL": "%userPfp%" + } + ] + } + } + }, + { + "name": "dmInfractedUser", + "category": "messages", + "humanName": { + "en": "DM User on Infraction?" + }, + "description": { + "en": "If enabled, the bot will DM the staff member when they receive an infraction or suspension." + }, + "type": "boolean", + "default": { + "en": true + } + }, + { + "name": "infractionDmMessage", + "category": "messages", + "humanName": { + "en": "Infraction DM Embed" + }, + "description": { + "en": "The message sent directly to the staff member." + }, + "type": "string", + "allowEmbed": true, + "dependsOn": "dmInfractedUser", + "params": [ + { + "name": "user", + "description": { + "en": "Mention of the staff member" + } + }, + { + "name": "issuerName", + "description": { + "en": "Name of the issuer" + } + }, + { + "name": "type", + "description": { + "en": "Type of infraction (e.g., Warning, Strike)" + } + }, + { + "name": "endDate", + "description": { + "en": "Timestamp of when this infraction expires" + } + }, + { + "name": "reason", + "description": { + "en": "Reason provided" + } + }, + { + "name": "caseId", + "description": { + "en": "Database Case ID" + } + } + ], + "default": { + "en": { + "embeds": [ + { + "author": { + "name": "Signed, %issuerName% • Case #%caseId%" + }, + "title": "⚠️ Staff Notice: %type%", + "description": "You have received a formal **%type%** from the management team.\n\n**Reason:** %reason%\n**Expires:** %endDate%", + "color": "#e67e22" + } + ] + } + } + }, + { + "name": "suspensionDmMessage", + "category": "messages", + "humanName": { + "en": "Suspension DM Embed" + }, + "description": { + "en": "The message sent directly to the staff member when suspended." + }, + "type": "string", + "allowEmbed": true, + "dependsOn": "dmInfractedUser", + "params": [ + { + "name": "user", + "description": { + "en": "Mention of the staff member" + } + }, + { + "name": "issuerName", + "description": { + "en": "Name of the issuer" + } + }, + { + "name": "type", + "description": { + "en": "Type of infraction (e.g., Warning, Strike)" + } + }, + { + "name": "endDate", + "description": { + "en": "Timestamp of when this infraction expires" + } + }, + { + "name": "reason", + "description": { + "en": "Reason provided" + } + }, + { + "name": "caseId", + "description": { + "en": "Database Case ID" + } + } + ], + "default": { + "en": { + "embeds": [ + { + "author": { + "name": "Signed, %issuerName% • Case #%caseId%" + }, + "title": "⛔ Staff Suspension", + "description": "Your staff privileges have been temporarily suspended.\n\n**Duration:** %duration%\n**Returns:** %endDate%\n**Reason:** %reason%\n\nDuring this time, your roles have been removed and you are expected to step away from all staff duties.", + "color": "#ed4245" + } + ] + } + } + } + ] +} \ No newline at end of file diff --git a/modules/staff-management-system/configs/profiles.json b/modules/staff-management-system/configs/profiles.json new file mode 100644 index 00000000..bbbaacfd --- /dev/null +++ b/modules/staff-management-system/configs/profiles.json @@ -0,0 +1,144 @@ +{ + "filename": "profiles.json", + "humanName": { + "en": "Staff Profiles" + }, + "description": { + "en": "Configure the staff profile system (Intros, custom nicknames, and stats)." + }, + "categories": [ + { + "id": "settings", + "icon": "fa-user-tie", + "displayName": { + "en": "Profile Settings" + } + } + ], + "content": [ + { + "name": "enableProfiles", + "category": "settings", + "humanName": { + "en": "Enable Staff Profiles" + }, + "description": { + "en": "Allows staff to have a profile tracking their shifts, reviews, and a custom introduction." + }, + "type": "boolean", + "default": { + "en": true + }, + "elementToggle": true + }, + { + "name": "onlyAllowStaffProfile", + "category": "settings", + "humanName": { + "en": "Only allow staff and higher to have their own customizable profile" + }, + "description": { + "en": "If enabled, only staff members and higher will be able to set a custom profile nickname and introduction. If disabled, all members will be able to set a custom profile nickname and introduction." + }, + "type": "boolean", + "default": { + "en": true + } + }, + { + "name": "managePermission", + "category": "settings", + "humanName": { + "en": "Profile Moderation Permission" + }, + "description": { + "en": "Which group is allowed to forcibly wipe another staff member's profile?" + }, + "type": "select", + "content": [ + "Supervisor", + "Management" + ], + "default": { + "en": "Supervisor" + } + }, + { + "name": "profileEmbedMessage", + "category": "settings", + "humanName": { + "en": "Profile Embed" + }, + "description": { + "en": "Customize the embed shown when viewing a staff profile." + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "user", + "description": { + "en": "The user's mention." + } + }, + { + "name": "username", + "description": { + "en": "The user's standard Discord username." + } + }, + { + "name": "nickname", + "description": { + "en": "The user's custom profile nickname (or default username if not set)." + } + }, + { + "name": "intro", + "description": { + "en": "The user's custom introduction." + } + }, + { + "name": "status", + "description": { + "en": "The user's current status (On Duty, Off Duty, LoA, etc.)." + } + }, + { + "name": "rating", + "description": { + "en": "The user's average review rating." + } + }, + { + "name": "pfp", + "description": { + "en": "The user's avatar URL." + }, + "isImage": true + } + ], + "default": { + "en": { + "title": "Staff Profile: %nickname%", + "description": "%intro%", + "color": "#2b2d31", + "thumbnail": "%pfp%", + "fields": [ + { + "name": "Status", + "value": "%status%", + "inline": true + }, + { + "name": "Average Rating", + "value": "%rating%", + "inline": true + } + ] + } + } + } + ] +} \ No newline at end of file diff --git a/modules/staff-management-system/configs/promotions.json b/modules/staff-management-system/configs/promotions.json new file mode 100644 index 00000000..3b351caa --- /dev/null +++ b/modules/staff-management-system/configs/promotions.json @@ -0,0 +1,247 @@ +{ + "filename": "promotions.json", + "humanName": { + "en": "Promotions" + }, + "description": { + "en": "Configure how staff promotions are handled and announced." + }, + "categories": [ + { + "id": "logic", + "icon": "fas fa-gears", + "displayName": { + "en": "General logic" + } + }, + { + "id": "messages", + "icon": "fas fa-comment-dots", + "displayName": { + "en": "Announcements" + } + } + ], + "content": [ + { + "name": "enablePromotions", + "category": "logic", + "humanName": { + "en": "Enable Promotions System" + }, + "description": { + "en": "If disabled, the /staff-management promote command will not work." + }, + "type": "boolean", + "default": { + "en": true + }, + "elementToggle": true + }, + { + "name": "autoAddRole", + "category": "logic", + "humanName": { + "en": "Auto-Add New Role?" + }, + "description": { + "en": "If enabled, the bot will automatically give the user the new rank role. Note: This makes your server prone to raids by promoting someone to a role with more dangerous permissions which can be used to do malicious actions. It is recommended to keep this setting disabled." + }, + "type": "boolean", + "default": { + "en": true + } + }, + { + "name": "promotionsChannel", + "category": "messages", + "humanName": { + "en": "Promotions Channel" + }, + "description": { + "en": "The channel where promotion announcements will be sent." + }, + "type": "channelID", + "channelTypes": [ + "GUILD_TEXT", + "GUILD_ANNOUNCEMENT" + ], + "default": { + "en": "" + } + }, + { + "name": "promotionMessage", + "category": "messages", + "humanName": { + "en": "Promotion Announcement Embed" + }, + "description": { + "en": "This will be the message sent when someone is promoted." + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "user", + "description": { + "en": "Pings the promoted user." + } + }, + { + "name": "newRoleName", + "description": { + "en": "The plain text name of the new role." + } + }, + { + "name": "newRoleMention", + "description": { + "en": "The pingable mention of the new role." + } + }, + { + "name": "promoterMention", + "description": { + "en": "Pings the staff member who issued the promotion." + } + }, + { + "name": "promoterName", + "description": { + "en": "The username of the staff member who issued the promotion." + } + }, + { + "name": "%reason%", + "description": { + "en": "The reason for the promotion." + } + }, + { + "name": "userPfp", + "description": { + "en": "The avatar URL of the promoted user." + } + }, + { + "name": "promoterPfp", + "description": { + "en": "The avatar URL of the promoter." + } + } + ], + "default": { + "en": { + "content": "%user%", + "embeds": [ + { + "author": { + "name": "Signed, %promoterName%", + "imageURL": "%promoterPfp%" + }, + "title": "🎉 New promotion!", + "description": "Congratulations, you have been promoted to **%newRoleName%**!\n\n**Promoted to:** %newRoleMention%\n**On behalf of:** %promoterMention%\n**Reason:** %reason%", + "color": "#f1c40f", + "thumbnailURL": "%userPfp%" + } + ] + } + } + }, + { + "name": "dmPromotedUser", + "category": "messages", + "humanName": { + "en": "DM Promoted User?" + }, + "description": { + "en": "If enabled, the user will receive a direct message when promoted." + }, + "type": "boolean", + "default": { + "en": false + } + }, + { + "name": "promotionDmMessage", + "category": "messages", + "humanName": { + "en": "Promotion DM Embed" + }, + "description": { + "en": "The message sent directly to the user." + }, + "type": "string", + "allowEmbed": true, + "dependsOn": "dmPromotedUser", + "params": [ + { + "name": "user", + "description": { + "en": "Pings the promoted user." + } + }, + { + "name": "newRoleName", + "description": { + "en": "The plain text name of the new role." + } + }, + { + "name": "%ewRoleMention", + "description": { + "en": "The pingable mention of the new role." + } + }, + { + "name": "promoterMention", + "description": { + "en": "Pings the staff member who issued the promotion." + } + }, + { + "name": "promoterName", + "description": { + "en": "The username of the staff member who issued the promotion." + } + }, + { + "name": "reason", + "description": { + "en": "The reason for the promotion." + } + }, + { + "name": "userPfp", + "description": { + "en": "The avatar URL of the promoted user." + } + }, + { + "name": "promoterPfp", + "description": { + "en": "The avatar URL of the promoter." + } + } + ], + "default": { + "en": { + "content": "%user%", + "embeds": [ + { + "author": { + "name": "Signed, %promoterName%", + "imageURL": "%promoterPfp%" + }, + "title": "🎉 New promotion!", + "description": "Congratulations, you have been promoted to **%newRoleName%**!\n\n**Promoted to:** %newRoleMention%\n**On behalf of:** %promoterMention%\n**Reason:** %reason%", + "color": "#f1c40f", + "thumbnailURL": "%userPfp%" + } + ] + } + } + } + ] +} \ No newline at end of file diff --git a/modules/staff-management-system/configs/reviews.json b/modules/staff-management-system/configs/reviews.json new file mode 100644 index 00000000..b8685ebc --- /dev/null +++ b/modules/staff-management-system/configs/reviews.json @@ -0,0 +1,147 @@ +{ + "filename": "reviews.json", + "humanName": { + "en": "Staff Ratings" + }, + "description": { + "en": "Configure the staff rating system and feedback channels." + }, + "categories": [ + { + "id": "settings", + "icon": "fas fa-gears", + "displayName": { + "en": "Settings" + } + }, + { + "id": "messages", + "icon": "fa fa-messages", + "displayName": { + "en": "Notifications" + } + } + ], + "content": [ + { + "name": "enableReviews", + "category": "settings", + "humanName": { + "en": "Enable Reviews System" + }, + "description": { + "en": "Enabling this unlocks the staff review system, allowing users to submit ratings and feedback for staff members." + }, + "type": "boolean", + "default": { + "en": true + } + }, + { + "name": "reviewLogChannel", + "category": "settings", + "humanName": { + "en": "Reviews Log Channel" + }, + "description": { + "en": "Channel where new reviews are posted." + }, + "type": "channelID", + "default": { + "en": "" + } + }, + { + "name": "allowSelfRating", + "category": "settings", + "humanName": { + "en": "Allow Self-Rating?" + }, + "description": { + "en": "If enabled, staff can review themselves. This is not recommended to keep a fair ratings system." + }, + "type": "boolean", + "default": { + "en": false + } + }, + { + "name": "onlyAllowStaffReview", + "category": "settings", + "humanName": { + "en": "Only let users review staff" + }, + "description": { + "en": "If enabled, only staff members can review other staff members." + }, + "type": "boolean", + "default": { + "en": true + } + }, + { + "name": "ratingMessage", + "category": "messages", + "humanName": { + "en": "Review Message" + }, + "description": { + "en": "The message sent when a review is submitted." + }, + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "target", + "description": { + "en": "The staff member" + } + }, + { + "name": "author", + "description": { + "en": "The reviewer" + } + }, + { + "name": "stars", + "description": { + "en": "Star emoji string (⭐⭐⭐⭐⭐)" + } + }, + { + "name": "rating", + "description": { + "en": "Number (1-5)" + } + }, + { + "name": "comment", + "description": { + "en": "The review text" + } + }, + { + "name": "staff-profile-picture", + "description": { + "en": "The staff member's profile picture (URL)" + } + }, + { + "name": "reviewer-profile-picture", + "description": { + "en": "The reviewer's profile picture (URL)" + } + } + ], + "default": { + "en": { + "title": "🌟 New Staff Rating", + "description": "**Staff:** %target%\n**Rated by:** %author%\n\n**Rating:** %stars% (%rating%/5)\n**Comment:**\n%comment%", + "color": "#f1c40f", + "thumbnail": "%staff-profile-picture%" + } + } + } + ] +} \ No newline at end of file diff --git a/modules/staff-management-system/configs/shifts.json b/modules/staff-management-system/configs/shifts.json new file mode 100644 index 00000000..ee7b250c --- /dev/null +++ b/modules/staff-management-system/configs/shifts.json @@ -0,0 +1,179 @@ +{ + "filename": "shifts.json", + "humanName": { + "en": "Shift Management" + }, + "description": { + "en": "Configure shift requirements, duty roles, leaderboards, and quotas." + }, + "categories": [ + { + "id": "settings", + "icon": "fas fa-gears", + "displayName": { + "en": "Shift Settings" + } + }, + { + "id": "leaderboard", + "icon": "fas fa-ranking-stars", + "displayName": { + "en": "Leaderboard" + } + }, + { + "id": "quotas", + "icon": "fa-solid fa-check-to-slot", + "displayName": { + "en": "Quotas" + } + } + ], + "content": [ + { + "name": "enableShifts", + "category": "settings", + "humanName": { + "en": "Enable Shifts" + }, + "description": { + "en": "This unlocks the ability for staff to use a shifts system, where they can get on-duty, off-duty, take a break and see their total duty time." + }, + "type": "boolean", + "default": { + "en": true + }, + "elementToggle": true + }, + { + "name": "onDutyRole", + "category": "settings", + "humanName": { + "en": "On-Duty Role" + }, + "description": { + "en": "Role given to users when they are on-duty. This is optional, but recommended to easily identify who is on-duty." + }, + "type": "roleID", + "allowNull": true, + "default": { + "en": "" + } + }, + { + "name": "dutyTypes", + "category": "settings", + "humanName": { + "en": "Duty Types" + }, + "description": { + "en": "The types of duty a staff member can select when going on-duty." + }, + "type": "array", + "content": "string", + "default": { + "en": ["Staff"] + } + }, + { + "name": "minShiftDuration", + "category": "settings", + "humanName": { + "en": "Minimum Shift Duration (minutes)" + }, + "description": { + "en": "A minimum shift duration for a shift to count towards their duty time. Default is 0, which means all shift time counts." + }, + "type": "integer", + "default": { + "en": 0 + } + }, + { + "name": "enableLeaderboard", + "category": "leaderboard", + "humanName": { + "en": "Enable duty leaderboard" + }, + "description": { + "en": "If enabled, staff can see a leaderboard of who has the most duty time in the configured timeframe." + }, + "type": "boolean", + "default": { + "en": true + } + }, + { + "name": "leaderboardLookback", + "category": "leaderboard", + "humanName": { + "en": "Leaderboard Timeframe" + }, + "description": { + "en": "The timeframe of the duty time shown on the leaderboard." + }, + "type": "select", + "content": [ + "Weekly", + "Monthly", + "All-time" + ], + "default": { + "en": "Weekly" + }, + "dependsOn": "enableLeaderboard" + }, + { + "name": "enableQuotas", + "category": "quotas", + "humanName": { + "en": "Enable Quota System" + }, + "description": { + "en": "If enabled, you can set a custom quota of hours for staff to meet in the configured timeframe." + }, + "type": "boolean", + "default": { + "en": false + } + }, + { + "name": "quotaTimeframe", + "category": "quotas", + "humanName": { + "en": "Quota Timeframe" + }, + "description": { + "en": "The timeframe in which the quota must be met." + }, + "type": "select", + "content": [ + "Weekly", + "Monthly" + ], + "default": { + "en": "Weekly" + }, + "dependsOn": "enableQuotas" + }, + { + "name": "quotas", + "category": "quotas", + "humanName": { + "en": "Role Quotas" + }, + "description": { + "en": "Set required hours per role - the left side will be the role, and the right side is a number which is the hours for the quota. The user's highest role counts as their quota." + }, + "type": "keyed", + "content": { + "key": "roleID", + "value": "integer" + }, + "default": { + "en": {} + }, + "dependsOn": "enableQuotas" + } + ] +} \ No newline at end of file diff --git a/modules/staff-management-system/configs/status.json b/modules/staff-management-system/configs/status.json new file mode 100644 index 00000000..1c40f79b --- /dev/null +++ b/modules/staff-management-system/configs/status.json @@ -0,0 +1,195 @@ +{ + "filename": "status.json", + "humanName": { + "en": "LoA & RA Status" + }, + "description": { + "en": "Configure Leave of Absence (LoA) and Reduced Activity (RA) settings." + }, + "categories": [ + { + "id": "loa", + "icon": "fas fa-door-open", + "displayName": { + "en": "LoA Settings" + } + }, + { + "id": "ra", + "icon": "fa-user-tie", + "displayName": { + "en": "RA Settings" + } + }, + { + "id": "logging", + "icon": "fa-solid fa-clipboard-list", + "displayName": { + "en": "Requests Log" + } + } + ], + "content": [ + { + "name": "enableLoa", + "category": "loa", + "humanName": { + "en": "Enable LoA System" + }, + "description": { + "en": "If enabled, staff can request a Leave of Absence (LoA)." + }, + "type": "boolean", + "default": { + "en": false + }, + "elementToggle": true + }, + { + "name": "loaRole", + "category": "loa", + "humanName": { + "en": "LoA Role" + }, + "description": { + "en": "Role given to users when they are on a Leave of Absence. This is optional, but recommended to easily identify who is on LoA." + }, + "type": "roleID", + "allowNull": true, + "default": { + "en": "" + } + }, + { + "name": "loaMaxDays", + "category": "loa", + "humanName": { + "en": "Maximum LoA Duration (days)" + }, + "description": { + "en": "The maximum duration for a Leave of Absence in days. This limits how long staff can request to be on LoA for." + }, + "type": "integer", + "default": { + "en": 60 + } + }, + { + "name": "requireLoaApproval", + "category": "loa", + "humanName": { + "en": "Require Approval for LoA?" + }, + "description": { + "en": "If enabled, LoA requests will require approval from staff who have supervisor permissions or higher." + }, + "type": "boolean", + "default": { + "en": true + } + }, + { + "name": "enableRa", + "category": "ra", + "humanName": { + "en": "Enable RA System" + }, + "description": { + "en": "If enabled, staff can request Reduced Activity (RA) status for when they are still working but at a reduced load." + }, + "type": "boolean", + "default": { + "en": false + }, + "elementToggle": true + }, + { + "name": "raRole", + "category": "ra", + "humanName": { + "en": "RA Role" + }, + "description": { + "en": "Role given to users when they are on Reduced Activity. This is optional, but recommended to easily identify who is on RA." + }, + "type": "roleID", + "allowNull": true, + "default": { + "en": "" + } + }, + { + "name": "raMaxDays", + "category": "ra", + "humanName": { + "en": "Maximum RA Duration (days)" + }, + "description": { + "en": "The maximum duration for RA in days. This limits how long staff can request to be on RA for." + }, + "type": "integer", + "default": { + "en": 30 + } + }, + { + "name": "requireRaApproval", + "category": "ra", + "humanName": { + "en": "Require Approval for RA?" + }, + "description": { + "en": "If enabled, RA requests will require approval from staff who have supervisor permissions or higher." + }, + "type": "boolean", + "default": { + "en": true + } + }, + { + "name": "statusLogChannel", + "category": "logging", + "humanName": { + "en": "Status Request Channel" + }, + "description": { + "en": "Channel where requests are sent for approval." + }, + "type": "channelID", + "allowNull": true, + "default": { + "en": "" + } + }, + { + "name": "logStatusChanges", + "category": "logging", + "humanName": { + "en": "Log status changes" + }, + "description": { + "en": "If enabled, any changes in staff status (going on/off LoA or RA) will be logged in the configured channel." + }, + "type": "boolean", + "default": { + "en": true + } + }, + { + "name": "statusChangeLogChannel", + "category": "logging", + "humanName": { + "en": "Status Change Log Channel" + }, + "description": { + "en": "Channel where status changes are logged. By default this uses your main log channel, but you can set a seperate channel here." + }, + "type": "channelID", + "allowNull": true, + "default": { + "en": "" + }, + "dependsOn": "logStatusChanges" + } + ] +} \ No newline at end of file diff --git a/modules/staff-management-system/events/botReady.js b/modules/staff-management-system/events/botReady.js new file mode 100644 index 00000000..615cd05a --- /dev/null +++ b/modules/staff-management-system/events/botReady.js @@ -0,0 +1,81 @@ +const schedule = require('node-schedule'); +const { localize } = require('../../../src/functions/localize'); +const { Op } = require('sequelize'); +const { scheduleStatusExpiry } = require('../staff-management'); + +module.exports.run = async (client) => { + try { + const LoaRequest = client.models['staff-management-system']['LoaRequest']; + const activeRequests = await LoaRequest.findAll({ + where: { status: 'APPROVED' } + }); + + let loaded = 0; + for (const req of activeRequests) { + scheduleStatusExpiry(client, req); + loaded++; + } + } catch (e) { + client.logger.error(localize('staff-management-system', 'log-sched-fail', { error: e.message })); + } + + const jobName = 'staff-management-checks'; + const existingJob = schedule.scheduledJobs[jobName]; + if (existingJob) existingJob.cancel(); + + const job = schedule.scheduleJob(jobName, '0 * * * *', async function() { + if (!client.botReadyAt) return; + + const guild = client.guilds.cache.get(client.guildID); + if (!guild) return; + + await checkExpiredSuspensions(client, guild); + }); + if (!client.intervals) client.intervals = []; + client.intervals.push(job); +}; + +async function checkExpiredSuspensions(client, guild) { + const Infraction = client.models['staff-management-system']['Infraction']; + const StaffProfile = client.models['staff-management-system']['StaffProfile']; + const config = client.configurations['staff-management-system']['infractions']; + const activeSuspensions = await Infraction.findAll({ + where: { type: 'Suspension', active: true } + }); + + for (const susp of activeSuspensions) { + const startDate = new Date(susp.createdAt); + const expireDate = new Date(startDate.getTime() + (susp.durationDays * 24 * 60 * 60 * 1000)); + + if (new Date() >= expireDate) { + const member = await guild.members.fetch(susp.userId).catch(() => null); + const profile = await StaffProfile.findByPk(susp.userId); + + if (member && profile && profile.suspendedRoles) { + try { + const rolesToAdd = JSON.parse(profile.suspendedRoles); + if (Array.isArray(rolesToAdd)) { + await member.roles.add(rolesToAdd).catch(e => client.logger.warn(`Failed to restore roles for ${member.user.tag}: ${e.message}`)); + } + + if (config.suspensionRole) { + await member.roles.remove(config.suspensionRole).catch(() => {}); + } + + await susp.update({ active: false }); + await profile.update({ + isSuspended: false, + suspendedRoles: null + }); + + client.logger.info(localize('staff-management-system', 'log-susp-end', { tag: member.user.tag })); + + } catch (e) { + client.logger.error(localize('staff-management-system', 'log-susp-err', { error: e.message })); + } + } else { + await susp.update({ active: false }); + } + } + } +} \ No newline at end of file diff --git a/modules/staff-management-system/events/guildMemberRemove.js b/modules/staff-management-system/events/guildMemberRemove.js new file mode 100644 index 00000000..38e0f6f3 --- /dev/null +++ b/modules/staff-management-system/events/guildMemberRemove.js @@ -0,0 +1,38 @@ +const { Op } = require('sequelize'); +const { localize } = require('../../../src/functions/localize'); + +module.exports.run = async (client, member) => { + if (member.guild.id !== client.guildID) return; + + const StaffShift = client.models['staff-management-system']['StaffShift']; + const StaffProfile = client.models['staff-management-system']['StaffProfile']; + + try { + const openShift = await StaffShift.findOne({ + where: { + userId: member.id, + endTime: null + } + }); + + if (openShift) { + const now = new Date(); + const duration = Math.floor((now - openShift.startTime) / 1000); + + await openShift.update({ + endTime: now, + duration: duration + }); + + client.logger.info(localize('staff-management-system', 'log-shift-leave', { tag: member.user.tag })); + } + + await StaffProfile.update( + { onDuty: false }, + { where: { userId: member.id } } + ); + + } catch (e) { + client.logger.error(localize('staff-management-system', 'log-leave-err', { error: e.message })); + } +}; \ No newline at end of file diff --git a/modules/staff-management-system/events/interactionCreate.js b/modules/staff-management-system/events/interactionCreate.js new file mode 100644 index 00000000..1f989315 --- /dev/null +++ b/modules/staff-management-system/events/interactionCreate.js @@ -0,0 +1,498 @@ +const { ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags, EmbedBuilder } = require('discord.js'); +const { + generateReviewHistoryResponse, + handleStatusEnd, + scheduleStatusExpiry, + handleStatusEndSubmit, + handleStatusExtend, + handleStatusExtendSubmit, + handleStatusHistPage, + sendStatusDm, + generatePromotionHistoryResponse, + generateInfractionHistoryResponse, + generateUserPanel, + generatePanelInfractions, + generatePanelPromotions, + generatePanelReviews, + generatePanelStatus, + generatePanelActivity, + generatePanelShifts, + generatePanelDeletion, + executeDataDeletion, + generatePanelSubpage, + logStatusChange +} = require('../staff-management'); +const { localize } = require('../../../src/functions/localize'); +const dutyHandlers = require('../commands/duty.js').buttonHandlers; + +module.exports.run = async (client, interaction) => { + if (!client.botReadyAt) return; + if (!interaction.guild || interaction.guild.id !== client.guildID) return; + if (!interaction.customId || (!interaction.customId.startsWith('staff-mgmt_') && !interaction.customId.startsWith('duty-mgmt_'))) return; + + try { + const parts = interaction.customId.split('_'); + const action = parts[1]; + + // ----- Duty manage handlers ----- + if (interaction.customId.startsWith('duty-mgmt_')) { + const dutyAction = parts[1]; + + if (interaction.isStringSelectMenu() && dutyAction === 'dropdown') { + await interaction.deferUpdate(); + return await dutyHandlers.handleDutyDropdown(client, interaction, parts[2], interaction.values[0]); + } + + if (['start', 'break', 'end', 'hist', 'lb', 'admin-forceend', 'admin-voidactive'].includes(dutyAction)) { + await interaction.deferUpdate(); + } + + if (dutyAction === 'start') return await dutyHandlers.handleDutyStartButton(client, interaction); + if (dutyAction === 'break') return await dutyHandlers.handleDutyBreakButton(client, interaction); + if (dutyAction === 'end') return await dutyHandlers.handleDutyEndButton(client, interaction); + if (dutyAction === 'hist') return await dutyHandlers.handleDutyHistPageButton(client, interaction); + if (dutyAction === 'lb') return await dutyHandlers.handleDutyLbPageButton(client, interaction); + if (dutyAction === 'admin-forceend') return await dutyHandlers.handleDutyAdminForceEnd(client, interaction); + if (dutyAction === 'admin-voidactive') return await dutyHandlers.handleDutyAdminVoidActive(client, interaction); + if (dutyAction === 'admin-voidall') return await dutyHandlers.handleDutyAdminVoidAll(client, interaction); + if (dutyAction === 'admin-voidall-submit') return await dutyHandlers.handleDutyAdminVoidAllSubmit(client, interaction); + if (dutyAction === 'admin-addtime') return await dutyHandlers.handleDutyAdminAddTimeButton(client, interaction); + if (dutyAction === 'admin-addtime-submit') return await dutyHandlers.handleDutyAdminAddTimeSubmit(client, interaction); + return; + } + + // ----- Review history pagination ----- + if (action === 'rev-page') { + const targetUser = await client.users.fetch(parts[2]).catch(() => null); + if (!targetUser) return interaction.reply({ + content: localize('staff-management-system', 'err-gen-no-user'), + flags: MessageFlags.Ephemeral + }); + + const payload = await generateReviewHistoryResponse(client, targetUser, parseInt(parts[3])); + if (payload.content) return interaction.reply(payload); + return interaction.update(payload); + } + + // ----- LOA/RA handlers ----- + const loaActions = ['loa-end', 'loa-end-submit', 'loa-extend', 'loa-extend-submit', 'loa-hist']; + const raActions = ['ra-end', 'ra-end-submit', 'ra-extend', 'ra-extend-submit', 'ra-hist']; + + if (loaActions.includes(action) || raActions.includes(action)) { + const type = action.startsWith('loa-') ? 'LOA' : 'RA'; + const base = action.replace(/^(loa|ra)-/, ''); + + if (base === 'end') return handleStatusEnd(client, interaction, type); + if (base === 'end-submit') return handleStatusEndSubmit(client, interaction, type); + if (base === 'extend') return handleStatusExtend(client, interaction, type); + if (base === 'extend-submit') return handleStatusExtendSubmit(client, interaction, type); + if (base === 'hist') return handleStatusHistPage(client, interaction, type); + } + + // ----- Promotion history pagination ----- + if (action === 'prom-hist') { + const targetUser = await client.users.fetch(parts[2]).catch(() => null); + if (!targetUser) return interaction.reply({ + content: localize('staff-management-system', 'err-gen-no-user'), + flags: MessageFlags.Ephemeral + }); + + const payload = await generatePromotionHistoryResponse(client, targetUser, parseInt(parts[3], 10)); + if (payload.content) return interaction.reply(payload); + return interaction.update(payload); + } + + // ----- Infraction history pagination ----- + if (action === 'inf-hist') { + const targetUser = await client.users.fetch(parts[2]).catch(() => null); + if (!targetUser) return interaction.reply({ + content: localize('staff-management-system', 'err-gen-no-user'), + flags: MessageFlags.Ephemeral + }); + const payload = await generateInfractionHistoryResponse(client, targetUser, parseInt(parts[3], 10)); + if (payload.content) return interaction.reply(payload); + return interaction.update(payload); + } + + // ----- User panel dropdown ----- + if (interaction.customId.startsWith('staff-mgmt_panel-menu_')) { + const targetId = interaction.customId.split('_')[2]; + const targetUser = await client.users.fetch(targetId).catch(() => null); + if (!targetUser) return interaction.reply({ + content: localize('staff-management-system', 'err-gen-no-user'), + flags: MessageFlags.Ephemeral + }); + + const selection = interaction.values[0]; + let payload; + if (selection === 'overview') payload = await generateUserPanel(client, targetUser); + else if (selection === 'infractions') payload = await generatePanelInfractions(client, targetUser, 1); + else if (selection === 'promotions') payload = await generatePanelPromotions(client, targetUser, 1); + else if (selection === 'reviews') payload = await generatePanelReviews(client, targetUser, 1); + else if (selection === 'status') payload = await generatePanelStatus(client, targetUser, 1); + else if (selection === 'activity') payload = await generatePanelActivity(client, targetUser, 1); + else if (selection === 'shifts') payload = await generatePanelShifts(client, targetUser); + else if (selection === 'deletion') payload = await generatePanelDeletion(client, targetUser); + + return interaction.update(payload); + } + + // ----- User panel deletion dropdown ----- + if (interaction.customId.startsWith('staff-mgmt_delete-menu_')) { + const targetId = interaction.customId.split('_')[2]; + const selection = interaction.values[0]; + + if (selection === 'back') { + const targetUser = await client.users.fetch(targetId).catch(() => null); + if (!targetUser) return interaction.reply({ + content: localize('staff-management-system', 'err-gen-no-user'), + flags: MessageFlags.Ephemeral + }); + + const payload = await generateUserPanel(client, targetUser); + return interaction.update(payload); + } + + const confirmPhrase = localize('staff-management-system', 'del-conf-phrase'); + const modal = new ModalBuilder() + .setCustomId(`staff-mgmt_del-confirm_${targetId}_${selection}`) + .setTitle(localize('staff-management-system', 'mod-del-title')); + modal.addComponents( + new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId('confirm') + .setLabel(localize('staff-management-system', 'mod-del-lbl')) + .setStyle(TextInputStyle.Paragraph) + .setPlaceholder(confirmPhrase) + .setRequired(true) + ) + ); + return interaction.showModal(modal); + } + + // ----- Data deletion modal submission ----- + if (interaction.isModalSubmit() && interaction.customId.startsWith('staff-mgmt_del-confirm_')) { + const parts = interaction.customId.split('_'); + const targetId = parts[2]; + const selection = parts.slice(3).join('_'); + + const confirmPhrase = localize('staff-management-system', 'del-conf-phrase'); + + if (interaction.fields.getTextInputValue('confirm').trim() !== confirmPhrase) { + return interaction.reply({ + content: localize('staff-management-system', 'err-conf-fail'), + flags: MessageFlags.Ephemeral + }); + } + + if (selection === 'del_all') { + const embed = new EmbedBuilder() + .setTitle(localize('staff-management-system', 'del-all-title')) + .setDescription(localize('staff-management-system', 'del-all-desc')) + .setColor('DarkRed'); + + const row = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId(`staff-mgmt_del-all-confirm_${targetId}`) + .setLabel(localize('staff-management-system', 'btn-conf-del')) + .setStyle(ButtonStyle.Danger), + new ButtonBuilder() + .setCustomId(`staff-mgmt_del-all-cancel_${targetId}`) + .setLabel(localize('staff-management-system', 'btn-cancel')) + .setStyle(ButtonStyle.Secondary) + ); + + await interaction.reply({ + embeds: [embed.toJSON()], + components: [row.toJSON()], + flags: MessageFlags.Ephemeral + }); + + const reply = await interaction.fetchReply(); + const collector = reply.createMessageComponentCollector({ time: 30000 }); + + collector.on('collect', async (btnInt) => { + if (btnInt.customId.includes('cancel')) { + await btnInt.update({ + content: localize('staff-management-system', 'succ-del-canc'), + embeds: [], + components: [] + }); + collector.stop('cancelled'); + } else if (btnInt.customId.includes('confirm')) { + await executeDataDeletion(client, targetId, 'del_all'); + + client.logger.info(localize('staff-management-system', 'log-del-all', { + target: targetId, + admin: btnInt.user.id + })); + + const targetUser = await client.users.fetch(targetId).catch(() => null); + if (targetUser) { + const payload = await generateUserPanel(client, targetUser); + await interaction.message.edit(payload).catch(()=>{}); + } + + await btnInt.update({ + content: localize('staff-management-system', 'succ-del-all'), + embeds: [], + components: [] + }); + collector.stop('confirmed'); + } + }); + + collector.on('end', (reason) => { + if (reason === 'time') { + interaction.editReply({ + content: localize('staff-management-system', 'err-del-time'), + embeds: [], + components: [] + }).catch(()=>{}); + } + }); + return; + } + + await executeDataDeletion(client, targetId, selection); + client.logger.info(localize('staff-management-system', 'log-del-type', { + type: selection, + target: targetId, + admin: interaction.user.id + })); + const targetUser = await client.users.fetch(targetId).catch(() => null); + if (targetUser) { + const payload = await generateUserPanel(client, targetUser); + await interaction.message.edit(payload).catch(()=>{}); + } + + return interaction.reply({ + content: localize('staff-management-system', 'succ-del-tgt'), + flags: MessageFlags.Ephemeral + }); + } + + // ----- User panel buttons ----- + if (interaction.customId.startsWith('staff-mgmt_panel-')) { + const parts = interaction.customId.split('_'); + const targetId = parts[2]; + const page = parseInt(parts[3], 10); + + const targetUser = await client.users.fetch(targetId).catch(() => null); + if (!targetUser) return interaction.reply({ + content: localize('staff-management-system', 'err-gen-no-user'), + flags: MessageFlags.Ephemeral + }); + + const typeMap = { + 'inf': 'infractions', + 'prom': 'promotions', + 'rev': 'reviews', + 'stat': 'status', + 'act': 'activity' + }; + const fullType = typeMap[parts[1].split('-')[1]]; + + if (fullType) { + const payload = await generatePanelSubpage(client, interaction, targetUser, fullType, page); + if (payload) return interaction.update(payload); + } + } + + // ----- Status buttons ----- + const LoARequest = client.models['staff-management-system']['LoaRequest']; + const StaffProfile = client.models['staff-management-system']['StaffProfile']; + const config = client.configurations['staff-management-system']['configuration']; + const statusConfig = client.configurations['staff-management-system']['status']; + + if (action === 'approve' || action === 'deny') { + const isSupervisor = interaction.member.roles.cache.some(r => config.supervisorRoles.includes(r.id)) || + interaction.member.roles.cache.some(r => config.managementRoles.includes(r.id)) || + interaction.member.permissions.has('Administrator'); + + if (!isSupervisor) return interaction.reply({ + content: localize('staff-management-system', 'err-gen-no-perm'), + flags: MessageFlags.Ephemeral + }); + + const request = await LoARequest.findByPk(parts[2]); + if (!request) return interaction.reply({ + content: localize('staff-management-system', 'err-no-req'), + flags: MessageFlags.Ephemeral + }); + if (request.status !== 'PENDING') return interaction.reply({ + content: localize('staff-management-system', 'err-req-hndl', { status: request.status }), + flags: MessageFlags.Ephemeral + }); + + if (action === 'deny') { + const modal = new ModalBuilder() + .setCustomId(`staff-mgmt_loa-deny_${parts[2]}`) + .setTitle(localize('staff-management-system', 'mod-deny-req')); + modal.addComponents( + new ActionRowBuilder() + .addComponents( + new TextInputBuilder() + .setCustomId('reason') + .setLabel(localize('staff-management-system', 'general-rsn')) + .setStyle(TextInputStyle.Paragraph) + .setRequired(true) + ) + ); + return interaction.showModal(modal); + } + + if (action === 'approve') { + await request.update({ + status: 'APPROVED', + approverId: interaction.user.id + }); + await StaffProfile.upsert({ + userId: request.userId, + activityStatus: request.type + }); + scheduleStatusExpiry(client, request); + + const member = await interaction.guild.members.fetch(request.userId).catch(() => null); + if (member) { + const roleId = request.type === 'LOA' + ? statusConfig.loaRole + : statusConfig.raRole; + if (roleId) await member.roles.add(roleId).catch(() => {}); + await sendStatusDm(member.user, request.type, 'approved', { + approver: interaction.user.tag, + endDate: request.endDate + }); + } + + await logStatusChange(client, request.type, 'start', { + userId: request.userId, + startDate: request.startDate, + endDate: request.endDate, + reason: request.reason, + approverId: interaction.user.id + }); + + const embed = EmbedBuilder + .from(interaction.message.embeds[0]) + .setColor('Green') + .addFields({ + name: localize('staff-management-system', 'general-stat'), + value: localize('staff-management-system', 'req-appr-by', { + user: interaction.user.tag + }) + }); + return interaction.update({ + embeds: [embed.toJSON()], + components: [] + }); + } + } + + // ----- Deny modal submission ----- + if (interaction.isModalSubmit() && action === 'loa-deny') { + const reason = interaction.fields.getTextInputValue('reason'); + const request = await LoARequest.findByPk(parts[2]); + + if (request) { + await request.update({ + status: 'DENIED', + approverId: interaction.user.id, + rejectionReason: reason + }); + const member = await interaction.guild.members.fetch(request.userId).catch(() => null); + if (member) await sendStatusDm(member.user, request.type, 'denied', { + denier: interaction.user.tag, + reason + }); + + const embed = EmbedBuilder + .from(interaction.message.embeds[0]) + .setColor('Red') + .addFields({ + name: localize('staff-management-system', 'general-stat'), + value: localize('staff-management-system', 'req-deny-by', { + user: interaction.user.tag + }) + }, { + name: localize('staff-management-system', 'general-rsn'), + value: reason + }); + return interaction.update({ + embeds: [embed.toJSON()], + components: [] + }); + } + } + + // ----- Profile edit submission ----- + if (interaction.isModalSubmit() && action === 'profile-edit') { + const nickname = interaction.fields.getTextInputValue('nickname'); + const intro = interaction.fields.getTextInputValue('intro'); + + const Profile = client.models['staff-management-system']['StaffProfile']; + await Profile.upsert({ + userId: interaction.user.id, + customNickname: nickname || null, + customIntro: intro || null + }); + return interaction.reply({ + content: localize('staff-management-system', 'succ-prof-upd'), + flags: MessageFlags.Ephemeral + }); + } + + // ----- Activity checks button ----- + if (action === 'ac-respond') { + const ActivityCheck = client.models['staff-management-system']['ActivityCheck']; + const activeCheck = await ActivityCheck.findOne({ + where: { + status: 'ACTIVE', + messageId: interaction.message.id + } + }); + + if (!activeCheck) return interaction.reply({ + content: localize('staff-management-system', 'err-ac-alr-end'), + flags: MessageFlags.Ephemeral + }); + + const targetRoles = JSON.parse(activeCheck.targetRoles || '[]'); + const hasRole = targetRoles.length === 0 || interaction.member.roles.cache.some(r => targetRoles.includes(r.id)); + if (!hasRole) return interaction.reply({ + content: localize('staff-management-system', 'err-ac-notreq'), + flags: MessageFlags.Ephemeral + }); + + let responded = JSON.parse(activeCheck.respondedUsers || '[]'); + if (responded.includes(interaction.user.id)) return interaction.reply({ + content: localize('staff-management-system', 'info-ac-alr-conf'), + flags: MessageFlags.Ephemeral + }); + + responded.push(interaction.user.id); + await activeCheck.update({ + respondedUsers: JSON.stringify(responded) + }); + return interaction.reply({ + content: localize('staff-management-system', 'succ-ac-log'), + flags: MessageFlags.Ephemeral + }); + } + + } catch (e) { + client.logger.error(localize('staff-management-system', 'log-int-error', { error: e.stack })); + if (!interaction.replied && !interaction.deferred) { + try { await interaction.reply({ + content: localize('staff-management-system', 'err-internal'), + flags: MessageFlags.Ephemeral + }); } catch (err) {} + } else { + try { await interaction.followUp({ + content: localize('staff-management-system', 'err-internal'), + flags: MessageFlags.Ephemeral + }); } catch (err) {} + } + } +}; \ No newline at end of file diff --git a/modules/staff-management-system/models/ActivityCheck.js b/modules/staff-management-system/models/ActivityCheck.js new file mode 100644 index 00000000..5d0dacea --- /dev/null +++ b/modules/staff-management-system/models/ActivityCheck.js @@ -0,0 +1,46 @@ +const { DataTypes, Model } = require('sequelize'); + +module.exports = class StaffManagementActivityCheck extends Model { + static init(sequelize) { + return super.init({ + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + messageId: { + type: DataTypes.STRING, + allowNull: false + }, + channelId: { + type: DataTypes.STRING, + allowNull: false + }, + endTime: { + type: DataTypes.DATE, + allowNull: false + }, + targetRoles: { + type: DataTypes.TEXT, + allowNull: false + }, + respondedUsers: { + type: DataTypes.TEXT, + defaultValue: '[]' + }, + status: { + type: DataTypes.STRING, + defaultValue: 'ACTIVE' + } + }, { + tableName: 'staff_management_activity_checks', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + name: 'ActivityCheck', + module: 'staff-management-system' +}; \ No newline at end of file diff --git a/modules/staff-management-system/models/Infraction.js b/modules/staff-management-system/models/Infraction.js new file mode 100644 index 00000000..2822e9b6 --- /dev/null +++ b/modules/staff-management-system/models/Infraction.js @@ -0,0 +1,54 @@ +const { DataTypes, Model } = require('sequelize'); + +module.exports = class StaffManagementInfraction extends Model { + static init(sequelize) { + return super.init({ + caseId: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true + }, + userId: { + type: DataTypes.STRING, + allowNull: false + }, + issuerId: { + type: DataTypes.STRING, + allowNull: false + }, + type: { + type: DataTypes.STRING, + allowNull: false + }, + reason: { + type: DataTypes.TEXT, + allowNull: true + }, + durationDays: { + type: DataTypes.INTEGER, + allowNull: true + }, + active: { + type: DataTypes.BOOLEAN, + defaultValue: true + }, + messageUrl: { + type: DataTypes.STRING, + allowNull: true + }, + expiresAt: { + type: DataTypes.DATE, + allowNull: true + } + }, { + tableName: 'staff_management_infractions', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + name: 'Infraction', + module: 'staff-management-system' +}; \ No newline at end of file diff --git a/modules/staff-management-system/models/LoaRequest.js b/modules/staff-management-system/models/LoaRequest.js new file mode 100644 index 00000000..83f71288 --- /dev/null +++ b/modules/staff-management-system/models/LoaRequest.js @@ -0,0 +1,54 @@ +const { DataTypes, Model } = require('sequelize'); + +module.exports = class StaffManagementLoaRequest extends Model { + static init(sequelize) { + return super.init({ + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true + }, + userId: { + type: DataTypes.STRING, + allowNull: false + }, + type: { + type: DataTypes.STRING, + allowNull: false + }, + reason: { + type: DataTypes.TEXT, + allowNull: false + }, + startDate: { + type: DataTypes.DATE, + allowNull: false + }, + endDate: { + type: DataTypes.DATE, + allowNull: false + }, + status: { + type: DataTypes.STRING, + defaultValue: "PENDING" + }, + approverId: { + type: DataTypes.STRING, + allowNull: true + }, + rejectionReason: { + type: DataTypes.TEXT, + allowNull: true + } + }, { + tableName: 'staff_management_loa_requests', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + name: 'LoaRequest', + module: 'staff-management-system' +}; \ No newline at end of file diff --git a/modules/staff-management-system/models/Promotion.js b/modules/staff-management-system/models/Promotion.js new file mode 100644 index 00000000..491dbe45 --- /dev/null +++ b/modules/staff-management-system/models/Promotion.js @@ -0,0 +1,42 @@ +const { DataTypes, Model } = require('sequelize'); + +module.exports = class StaffManagementPromotion extends Model { + static init(sequelize) { + return super.init({ + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + userId: { + type: DataTypes.STRING, + allowNull: false + }, + issuerId: { + type: DataTypes.STRING, + allowNull: false + }, + newRole: { + type: DataTypes.STRING, + allowNull: false + }, + reason: { + type: DataTypes.TEXT, + allowNull: true + }, + messageUrl: { + type: DataTypes.STRING, + allowNull: true + } + }, { + tableName: 'staff_management_promotions', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + name: 'Promotion', + module: 'staff-management-system' +}; \ No newline at end of file diff --git a/modules/staff-management-system/models/StaffProfile.js b/modules/staff-management-system/models/StaffProfile.js new file mode 100644 index 00000000..0f66976b --- /dev/null +++ b/modules/staff-management-system/models/StaffProfile.js @@ -0,0 +1,63 @@ +const { DataTypes, Model } = require('sequelize'); + +module.exports = class StaffManagementProfile extends Model { + static init(sequelize) { + return super.init({ + userId: { + type: DataTypes.STRING, + primaryKey: true, + allowNull: false + }, + points: { + type: DataTypes.INTEGER, + defaultValue: 0, + allowNull: false + }, + onDuty: { + type: DataTypes.BOOLEAN, + defaultValue: false + }, + lastClockIn: { + type: DataTypes.DATE, + allowNull: true + }, + activityStatus: { + type: DataTypes.STRING, + defaultValue: 'ACTIVE' + }, + isSuspended: { + type: DataTypes.BOOLEAN, + defaultValue: false + }, + suspendedRoles: { + type: DataTypes.TEXT, + allowNull: true + }, + customNickname: { + type: DataTypes.STRING, + allowNull: true + }, + customIntro: { + type: DataTypes.STRING(1024), + allowNull: true + }, + onBreak: { + type: DataTypes.BOOLEAN, + defaultValue: false + }, + breakStartTime: { + type: DataTypes.DATE, + allowNull: true + } + }, { + tableName: 'staff_management_profiles', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + name: 'StaffProfile', + module: 'staff-management-system' +}; \ No newline at end of file diff --git a/modules/staff-management-system/models/StaffReview.js b/modules/staff-management-system/models/StaffReview.js new file mode 100644 index 00000000..1c2d379b --- /dev/null +++ b/modules/staff-management-system/models/StaffReview.js @@ -0,0 +1,43 @@ +const { DataTypes, Model } = require('sequelize'); + +module.exports = class StaffManagementReview extends Model { + static init(sequelize) { + return super.init({ + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true + }, + targetId: { + type: DataTypes.STRING, + allowNull: false + }, + authorId: { + type: DataTypes.STRING, + allowNull: false + }, + stars: { + type: DataTypes.INTEGER, + allowNull: false, + validate: { min: 1, max: 5 } + }, + comment: { + type: DataTypes.TEXT, + allowNull: true + }, + messageUrl: { + type: DataTypes.STRING, + allowNull: true + } + }, { + tableName: 'staff_management_reviews', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + name: 'StaffReview', + module: 'staff-management-system' +}; \ No newline at end of file diff --git a/modules/staff-management-system/models/StaffShift.js b/modules/staff-management-system/models/StaffShift.js new file mode 100644 index 00000000..bc5789bc --- /dev/null +++ b/modules/staff-management-system/models/StaffShift.js @@ -0,0 +1,37 @@ +const { DataTypes, Model } = require('sequelize'); + +module.exports = class StaffManagementShift extends Model { + static init(sequelize) { + return super.init({ + userId: { + type: DataTypes.STRING, + allowNull: false + }, + startTime: { + type: DataTypes.DATE, + allowNull: false + }, + endTime: { + type: DataTypes.DATE, + allowNull: true + }, + duration: { + type: DataTypes.INTEGER, + allowNull: true + }, + type: { + type: DataTypes.STRING, + defaultValue: "General" + } + }, { + tableName: 'staff_management_shifts', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + name: 'StaffShift', + module: 'staff-management-system' +}; \ No newline at end of file diff --git a/modules/staff-management-system/module.json b/modules/staff-management-system/module.json new file mode 100644 index 00000000..416068a5 --- /dev/null +++ b/modules/staff-management-system/module.json @@ -0,0 +1,33 @@ +{ + "name": "staff-management-system", + "author": { + "scnxOrgID": "148", + "name": "Kevin", + "link": "https://github.com/Kevinking500" + }, + "openSourceURL": "https://github.com/Kevinking500/CustomDCBot/tree/main/modules/ping-protection", + "commands-dir": "/commands", + "events-dir": "/events", + "models-dir": "/models", + "config-example-files": [ + "configs/configuration.json", + "configs/infractions.json", + "configs/promotions.json", + "configs/reviews.json", + "configs/shifts.json", + "configs/status.json", + "configs/profiles.json", + "configs/activity-checks.json" + ], + "tags": [ + "moderation" + ], + "humanReadableName": { + "en": "Staff Management System", + "de": "Mitarbeiter-Management-System" + }, + "description": { + "en": "A powerful, highly customizable staff management system designed to track activity, moderate personnel, and maintain detailed staff records seamlessly.", + "de": "Ein leistungsstarkes, hochgradig anpassbares Mitarbeiter-Management-System, das entwickelt wurde, um die Aktivität zu verfolgen, das Personal zu moderieren und detaillierte Mitarbeiterakten nahtlos zu pflegen." + } +} \ No newline at end of file diff --git a/modules/staff-management-system/staff-management.js b/modules/staff-management-system/staff-management.js new file mode 100644 index 00000000..3d8bb5f4 --- /dev/null +++ b/modules/staff-management-system/staff-management.js @@ -0,0 +1,2289 @@ +/** + * Logic for the Staff Management module + * @module staff-management + * @author itskevinnn + */ +const { ModalBuilder, TextInputBuilder, TextInputStyle, EmbedBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags } = require('discord.js'); +const { Op } = require('sequelize'); +const schedule = require('node-schedule'); +const { embedTypeV2, formatDate } = require('../../src/functions/helpers'); +const { localize } = require('../../src/functions/localize'); + +// --- Local helpers --- +const getConfig = (client, file) => client.configurations['staff-management-system'][file]; +const getSafeChannelId = (val) => Array.isArray(val) && val.length > 0 // Helper to get safe channel ID from config +? val[0] +: (typeof val === 'string' + ? val + : null +); +const parseDurationToDays = (input) => { + if (!input) return null; + const match = input.toString().match(/^(\d+)([dDwWmM])?$/); + if (!match) return null; + const value = parseInt(match[1], 10); + const unit = match[2]?.toLowerCase() || 'd'; + return unit === 'm' + ? value * 30 + : (unit === 'w' + ? value * 7 + : value + ); +}; + +const applyFooter = (client, embed) => { + embed.setFooter({ text: client.strings.footer, iconURL: client.strings.footerImgUrl }); + if (!client.strings.disableFooterTimestamp) embed.setTimestamp(); + return embed; +}; + +const buildPaginationRow = (backId, countId, nextId, page, totalPages) => { + return new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(backId) + .setLabel(localize('helpers', 'back')) + .setStyle(ButtonStyle.Primary) + .setDisabled(page <= 1), + new ButtonBuilder() + .setCustomId(countId) + .setLabel(`${page}/${totalPages}`) + .setStyle(ButtonStyle.Secondary) + .setDisabled(true), + new ButtonBuilder() + .setCustomId(nextId) + .setLabel(localize('helpers', 'next')) + .setStyle(ButtonStyle.Primary) + .setDisabled(page >= totalPages) + ); +}; + +function formatDuration(seconds) { + if (!seconds || seconds <= 0) return localize('staff-management-system', 'time-zero'); + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = seconds % 60; + const parts = []; + if (h > 0) parts.push(`${h} ${localize('staff-management-system', h !== 1 + ? 'time-hours' + : 'time-hour' + )}`); + if (m > 0) parts.push(`${m} ${localize('staff-management-system', m !== 1 + ? 'time-mins' + : 'time-min' + )}`); + if (s > 0) parts.push(`${s} ${localize('staff-management-system', s !== 1 + ? 'time-secs' + : 'time-sec' + )}`); + return parts.join(', ') || localize('staff-management-system', 'time-zero'); +} + +// ---------- Status DM's and logging ---------- + +async function sendStatusDm(user, type, dmType, data = {}) { + const label = type === 'LOA' + ? 'LoA' + : 'RA'; + const viewCmd = type === 'LOA' + ? '`/loa view`' + : '`/ra view`'; + const endFmt = data.endDate + ? `` + : ''; + + // These messages use the locales key to be easily used later + const messages = { + approved: { + title: 'dm-appr-title', + color: 'Green', + desc: 'dm-appr-desc', + params: { label, approver: data.approver, endFmt, viewCmd } + }, + denied: { + title: 'dm-deny-title', + color: 'Red', + desc: 'dm-deny-desc', + params: { label, denier: data.denier, reason: data.reason } + }, + extended: { + title: 'dm-ext-title', + color: 'Yellow', + desc: 'dm-ext-desc', + params: { label, extender: data.extender, days: data.days, endFmt, reason: data.reason, viewCmd } + }, + ended_early: { + title: 'dm-early-title', + color: 'Red', + desc: 'dm-early-desc', + params: { label, ender: data.ender, reason: data.reason } + }, + ended: { + title: 'dm-end-title', + color: 'Black', + desc: 'dm-end-desc', + params: { label } + } + }; + + const msg = messages[dmType]; + if (!msg) return; + + const embed = new EmbedBuilder() + .setTitle(localize('staff-management-system', msg.title, msg.params)) + .setDescription(localize('staff-management-system', msg.desc, msg.params)) + .setColor(msg.color); + applyFooter(user.client, embed); + + try { + await user.send({ + embeds: [embed.toJSON()] + }); + } catch (e) { + user.client.logger.error( + localize('staff-management-system', 'log-stat-dm-error', { + e: e.message, + u: user.tag + }) + ); +} +} + +async function logStatusChange(client, type, action, data) { + const statusConfig = getConfig(client, 'status'); + if (!statusConfig?.logStatusChanges) return; + + const channelId = getSafeChannelId(statusConfig.statusChangeLogChannel) || getSafeChannelId(getConfig(client, 'configuration')?.generalLogChannel); + if (!channelId) return; + + const guild = client.guilds.cache.get(client.guildID); + if (!guild) return; + const channel = await guild.channels.fetch(channelId).catch(() => null); + if (!channel) return; + + const label = type === 'LOA' + ? 'LoA' + : 'RA'; + const targetUserObj = data.targetUser || await client.users.fetch(data.userId).catch(() => null); + const mention = targetUserObj + ? targetUserObj.toString() + : `<@${data.userId}>`; + const username = targetUserObj + ? targetUserObj.username + : data.userId; + + const embed = new EmbedBuilder() + .setThumbnail(targetUserObj + ?.displayAvatarURL({ dynamic: true }) || null); + + if (action === 'start') { + embed.setTitle(localize('staff-management-system', 'log-start-title', { label, username })) + .setColor('Green') + .setDescription(localize('staff-management-system', 'log-start-desc', + { label, mention, apprText: data.approverId + ? ` ${localize('staff-management-system', 'label-appr-by')}: <@${data.approverId}>.` + : '' + })) + .addFields({ + name: localize('staff-management-system', 'log-info-hdr', { label }), + value: `**${localize('staff-management-system', 'general-start')}:** \n**${localize('staff-management-system', 'general-end')}:** \n**${localize('staff-management-system', 'general-rsn')}:** ${data.reason || localize('staff-management-system', 'none-provided')}` + }); + + } else if (action === 'end') { + embed.setTitle(localize('staff-management-system', 'log-end-title', { label, username })) + .setColor('Red') + .setDescription(localize('staff-management-system', 'log-end-desc', { label, mention })) + .addFields({ + name: localize('staff-management-system', 'log-info-hdr', { label }), + value: `**${localize('staff-management-system', 'general-started')}:** \n**${localize('staff-management-system', 'general-ended')}:** \n**${localize('staff-management-system', 'general-rsn')}:** ${data.reason || localize('staff-management-system', 'none-provided')}` + }); + + } else if (action === 'adjusted') { + embed.setTitle(localize('staff-management-system', 'log-adj-title', { label, username })) + .setColor('Yellow') + .setDescription(localize('staff-management-system', 'log-adj-desc', { label, mention, executor: data.executorId })) + .addFields({ + name: localize('staff-management-system', 'log-changes'), + value: data.changesText + }); + } + + applyFooter(client, embed); + try { + await channel.send({ + embeds: [embed.toJSON()] + }); + } catch (e) { + client.logger.error( + localize('staff-management-system', 'log-status-adj-error', { + e: e.message + }) + ); + } +} + +// ---------- Infractions ---------- +async function issueInfraction(client, interaction, targetMember, type, reason, expiryInput) { + const config = getConfig(client, 'infractions'); + if (!config?.enableInfractions) return interaction.reply({ + content: localize('staff-management-system', 'err-feat-disabled', { feature: 'Infractions' }), + flags: MessageFlags.Ephemeral + }); + if (type.toLowerCase() === 'suspension') { + return interaction.reply({ + content: localize('staff-management-system', 'err-use-susp'), + flags: MessageFlags.Ephemeral + }); + } + + let expiresAt = null; + if (expiryInput) { + const days = parseDurationToDays(expiryInput); + if (!days) return interaction.reply({ + content: localize('staff-management-system', 'err-inv-dur'), + flags: MessageFlags.Ephemeral + }); + expiresAt = new Date(Date.now() + days * 24 * 60 * 60 * 1000); + } + + const record = await client.models['staff-management-system']['Infraction'].create({ + userId: targetMember.id, + issuerId: interaction.user.id, + type, reason, expiresAt, + active: true + }); + + const placeholders = { + '%user%': targetMember.user.toString(), + '%userPfp%': targetMember.user.displayAvatarURL({ dynamic: true, format: 'png', size: 1024 }) || '', + '%issuerMention%': interaction.user.toString(), + '%issuerName%': interaction.user.username, + '%issuerPfp%': interaction.user.displayAvatarURL({ dynamic: true, format: 'png', size: 1024 }) || '', + '%type%': type, + '%reason%': reason, + '%caseId%': record.caseId.toString(), + '%endDate%': expiresAt + ? `` + : localize('staff-management-system', 'label-never') + }; + + const channelId = getSafeChannelId(config.infractionLogChannel); + if (channelId) { + const channel = await interaction.guild.channels.fetch(channelId).catch(() => null); + if (channel) { + let template = config.infractionMessage; + if (typeof template === 'string') { + try { template = JSON.parse(template); } + catch (e) {} + } + else if (typeof template === 'object') { + template = JSON.parse(JSON.stringify(template)); + } + + if (template && template.embeds && !template._schema) template._schema = 'v3'; + let msgOpts = await embedTypeV2(template, placeholders); + if (msgOpts?.content?.trim() === '') delete msgOpts.content; + + if (msgOpts?.embeds?.length > 0) { + const parsedEmbed = EmbedBuilder.from(msgOpts.embeds[0]); + applyFooter(client, parsedEmbed); + msgOpts.embeds[0] = parsedEmbed.toJSON(); + } + + const sentMsg = await channel.send(msgOpts).catch(()=>{}); + if (sentMsg) await record.update({ messageUrl: sentMsg.url }); + } + } + + if (config.dmInfractedUser) { + let dmTemplate = config.infractionDmMessage; + if (typeof dmTemplate === 'string') { + try { dmTemplate = JSON.parse(dmTemplate); } + catch (e) {} + } + else if (typeof dmTemplate === 'object') { + dmTemplate = JSON.parse(JSON.stringify(dmTemplate)); + } + + if (dmTemplate && dmTemplate.embeds && !dmTemplate._schema) dmTemplate._schema = 'v3'; + let dmOpts = await embedTypeV2(dmTemplate, placeholders); + if (dmOpts?.content?.trim() === '') delete dmOpts.content; + if (dmOpts && (dmOpts.content || dmOpts.embeds?.length > 0)) + await targetMember.send(dmOpts).catch(()=>{}); + } + + await interaction.reply({ + content: localize('staff-management-system', 'succ-infract', { + type, caseId: record.caseId, user: targetMember.user.tag + }), + flags: MessageFlags.Ephemeral + }); +} + +// ---------- Suspensions ---------- +async function issueSuspension(client, interaction, targetMember, durationInput, reason) { + const config = getConfig(client, 'infractions'); + if (!config?.enableInfractions) + return interaction.reply({ + content: localize('staff-management-system', 'err-feat-disabled', { + feature: 'Infractions' + }), + flags: MessageFlags.Ephemeral + }); + + if (!config?.enableSuspensions) + return interaction.reply({ + content: localize('staff-management-system', 'err-feat-disabled', { + feature: 'Suspensions' + }), + flags: MessageFlags.Ephemeral + }); + + const durationDays = parseDurationToDays(durationInput); + if (!durationDays) + return interaction.reply({ + content: localize('staff-management-system', 'err-inv-dur'), + flags: MessageFlags.Ephemeral + }); + + const expiresAt = new Date(Date.now() + durationDays * 24 * 60 * 60 * 1000); + const durationString = `${durationDays} ${localize('staff-management-system', 'label-days')}`; + + const hierarchyRole = interaction.guild.roles.cache.get(config.suspensionHierarchyRole); + if (hierarchyRole) { + const rolesToRemove = targetMember.roles.cache.filter(r => r.position >= hierarchyRole.position && r.id !== interaction.guild.id && !r.managed).map(r => r.id); + if (rolesToRemove.length) { + await targetMember.roles.remove(rolesToRemove).catch(() => {}); + await client.models['staff-management-system']['StaffProfile'].upsert({ + userId: targetMember.id, + isSuspended: true, + suspendedRoles: JSON.stringify(rolesToRemove) + }); + } + } + if (config.suspensionRole) await targetMember.roles.add(config.suspensionRole).catch(() => {}); + + const record = await client.models['staff-management-system']['Infraction'].create({ + userId: targetMember.id, + issuerId: interaction.user.id, + type: 'Suspension', + reason, durationDays, expiresAt, + active: true + }); + + const placeholders = { + '%user%': targetMember.user.toString(), + '%userPfp%': targetMember.user.displayAvatarURL({ dynamic: true, format: 'png', size: 1024 }) || '', + '%issuerMention%': interaction.user.toString(), + '%issuerName%': interaction.user.username, + '%issuerPfp%': interaction.user.displayAvatarURL({ dynamic: true, format: 'png', size: 1024 }) || '', + '%duration%': durationString, + '%reason%': reason, + '%caseId%': record.caseId.toString(), + '%endDate%': `` + }; + + const channelId = getSafeChannelId(config.infractionLogChannel); + if (channelId) { + const channel = await interaction.guild.channels.fetch(channelId).catch(() => null); + if (channel) { + let template = config.suspensionMessage; + if (typeof template === 'string') { + try { + template = JSON.parse(template); + } + catch (e) {} + } + else if (typeof template === 'object') { + template = JSON.parse(JSON.stringify(template)); + } + + if (template && template.embeds && !template._schema) template._schema = 'v3'; + let msgOpts = await embedTypeV2(template, placeholders); + if (msgOpts?.content?.trim() === '') delete msgOpts.content; + + if (msgOpts?.embeds?.length > 0) { + const parsedEmbed = EmbedBuilder.from(msgOpts.embeds[0]); + applyFooter(client, parsedEmbed); + msgOpts.embeds[0] = parsedEmbed.toJSON(); + } + + const sentMsg = await channel.send(msgOpts).catch(()=>{}); + if (sentMsg) await record.update({ messageUrl: sentMsg.url }); + } + } + + if (config.dmInfractedUser) { + let dmTemplate = config.suspensionDmMessage; + if (typeof dmTemplate === 'string') { + try { + dmTemplate = JSON.parse(dmTemplate); + } + catch (e) {} + } + else if (typeof dmTemplate === 'object') { + dmTemplate = JSON.parse(JSON.stringify(dmTemplate)); + } + + if (dmTemplate && dmTemplate.embeds && !dmTemplate._schema) dmTemplate._schema = 'v3'; + let dmOpts = await embedTypeV2(dmTemplate, placeholders); + if (dmOpts?.content?.trim() === '') delete dmOpts.content; + if (dmOpts && (dmOpts.content || dmOpts.embeds?.length > 0)) await targetMember.send(dmOpts).catch(()=>{}); + } + + await interaction.reply({ + content: localize('staff-management-system', 'succ-susp', { + caseId: record.caseId, + user: targetMember.user.tag, + duration: durationString + }), + flags: MessageFlags.Ephemeral + }); +} + +// ----- Infractions voiding ----- +async function voidInfraction(client, interaction, caseId) { + const config = getConfig(client, 'infractions'); + if (!config?.enableInfractions) return interaction.reply({ + content: localize('staff-management-system', 'err-feat-disabled', { + feature: 'Infractions' + }), + flags: MessageFlags.Ephemeral + }); + + const generalConfig = getConfig(client, 'configuration'); + const canManage = interaction.member.roles.cache.some(r => [...(generalConfig.supervisorRoles || []), ...(generalConfig.managementRoles || [])].includes(r.id)) || interaction.member.permissions.has('Administrator'); + if (!canManage) return interaction.reply({ + content: localize('staff-management-system', 'err-gen-no-perm'), + flags: MessageFlags.Ephemeral + }); + + const record = await client.models['staff-management-system']['Infraction'].findByPk(caseId); + if (!record) return interaction.reply({ + content: localize('staff-management-system', 'err-no-case', { caseId }), + flags: MessageFlags.Ephemeral + }); + if (!record.active) return interaction.reply({ + content: localize('staff-management-system', 'err-case-inact', { caseId }), + flags: MessageFlags.Ephemeral + }); + + await record.update({ active: false }); + + if (record.type.toLowerCase() === 'suspension') { + const Profile = client.models['staff-management-system']['StaffProfile']; + const profile = await Profile.findOne({ + where: { userId: record.userId } + }); + const member = await interaction.guild.members.fetch(record.userId).catch(() => null); + + if (member && profile && profile.isSuspended) { + try { + const rolesToRestore = JSON.parse(profile.suspendedRoles || '[]'); + if (rolesToRestore.length > 0) await member.roles.add(rolesToRestore); + if (config.suspensionRole) await member.roles.remove(config.suspensionRole); + await profile.update({ isSuspended: false, suspendedRoles: '[]' }); + } catch (e) { + return interaction.reply({ + content: localize('staff-management-system', 'succ-void-fail', { caseId }), + flags: MessageFlags.Ephemeral + }); + } + } + } + await interaction.reply({ + content: localize('staff-management-system', 'succ-void', { caseId }), + flags: MessageFlags.Ephemeral + }); +} + +// ----- Generates infractions history embed ----- +async function generateInfractionHistoryResponse(client, targetUser, page = 1) { + const limit = 5; + const offset = (page - 1) * limit; + const { count, rows } = await client.models['staff-management-system']['Infraction'].findAndCountAll({ + where: { userId: targetUser.id }, + order: [['createdAt', 'DESC']], + limit, offset + }); + + if (count === 0) + return { + content: localize('staff-management-system', 'info-clean-rec', { + username: targetUser.username + }), + flags: MessageFlags.Ephemeral + }; + + const totalPages = Math.ceil(count / limit) || 1; + const embed = applyFooter(client, new EmbedBuilder() + .setTitle(localize('staff-management-system', 'rec-title', { username: targetUser.username })) + .setThumbnail(targetUser.displayAvatarURL({ dynamic: true })) + .setColor('Red') + ); + + const desc = rows.map(r => { + const link = r.messageUrl + ? ` • [Jump](${r.messageUrl})` + : ''; + const statusIcon = r.active + ? '🔴' + : localize('staff-management-system', 'icon-voided'); + const expiry = r.expiresAt + ? `\n**${localize('staff-management-system', 'label-exp')}:** ` + : ''; + + return `**${statusIcon} ${localize('staff-management-system', 'label-case')} #${r.caseId} - ${r.type}**\n**${localize('staff-management-system', 'label-date')}:** \n**${localize('staff-management-system', 'label-iss')}:** <@${r.issuerId}>\n**${localize('staff-management-system', 'general-rsn')}:** ${r.reason}${expiry}${link}`; + }).join('\n\n'); + + embed.setDescription(desc); + embed.addFields({ name: '\u200b', value: localize('staff-management-system', 'page-count', { + page, + total: totalPages + }) }); + + const row = buildPaginationRow( + `staff-mgmt_inf-hist_${targetUser.id}_${page - 1}`, + 'inf_hist_count', + `staff-mgmt_inf-hist_${targetUser.id}_${page + 1}`, + page, totalPages + ); + + return { embeds: [embed.toJSON()], components: [row.toJSON()] }; +} + +// ----- Gets infraction history ----- +async function getInfractionHistory(client, interaction, targetUser) { + const response = await generateInfractionHistoryResponse(client, targetUser, 1); + if (response.content && response.content.startsWith('ℹ️')) return interaction.reply(response); + await interaction.reply({ + ...response, + flags: MessageFlags.Ephemeral + }); +} + +// ---------- Promotions ---------- +async function promoteUser(client, interaction, targetMember, newRole, reason) { + const config = getConfig(client, 'promotions'); + if (!config?.enablePromotions) return interaction.reply({ + content: localize('staff-management-system', 'err-feat-disabled', { feature: 'Promotions' }), + flags: MessageFlags.Ephemeral + }); + + const finalReason = reason && reason.trim() !== '' + ? reason + : localize('staff-management-system', 'none-provided'); + const channelOverride = interaction.options.getChannel('channel'); + + if (config.autoAddRole) { + if (interaction.guild.members.me.roles.highest.position <= newRole.position) { + return interaction.reply({ + content: localize('staff-management-system', 'err-role-hier'), + flags: MessageFlags.Ephemeral + }); + } + try { + await targetMember.roles.add(newRole); + } + catch (e) { + return interaction.reply({ + content: localize('staff-management-system', 'err-add-role', { e: e.message }), + flags: MessageFlags.Ephemeral + }); } + } + + const record = await client.models['staff-management-system']['Promotion'].create({ + userId: targetMember.id, + issuerId: interaction.user.id, + newRole: newRole.id, + reason: finalReason + }); + + const placeholders = { + '%user%': targetMember.user.toString(), + '%newRoleName%': newRole.name, + '%newRoleMention%': newRole.toString(), + '%promoterMention%': interaction.user.toString(), + '%promoterName%': interaction.user.username, + '%reason%': finalReason, + '%userPfp%': targetMember.user.displayAvatarURL({ dynamic: true, format: 'png', size: 1024 }) || '', + '%promoterPfp%': interaction.user.displayAvatarURL({ dynamic: true, format: 'png', size: 1024 }) || '' + }; + + const targetChannelId = channelOverride + ? channelOverride.id + : getSafeChannelId(config.promotionsChannel); + + if (targetChannelId) { + const channel = await interaction.guild.channels.fetch(targetChannelId).catch(() => null); + if (channel) { + let embedTemplate = config.promotionMessage; + if (typeof embedTemplate === 'string') { + try { + embedTemplate = JSON.parse(embedTemplate); + } + catch (e) {} } + + else if (typeof embedTemplate === 'object') { + embedTemplate = JSON.parse(JSON.stringify(embedTemplate)); + } + + if (embedTemplate && embedTemplate.embeds && !embedTemplate._schema) embedTemplate._schema = 'v3'; + let msgOpts = await embedTypeV2(embedTemplate, placeholders); + if (msgOpts?.content?.trim() === '') delete msgOpts.content; + + if (msgOpts.embeds && msgOpts.embeds.length > 0) { + const parsedEmbed = EmbedBuilder.from(msgOpts.embeds[0]); + applyFooter(client, parsedEmbed); + msgOpts.embeds[0] = parsedEmbed.toJSON(); + } + + const sentMessage = await channel + .send(msgOpts) + .catch(e => { + client.logger.error(localize('staff-management-system', 'log-promo-msg-error', { + e: e.message, + })); + return null; + }); + + if (sentMessage) await record.update({ messageUrl: sentMessage.url }); + } + } + + if (config.dmPromotedUser && config.promotionDmMessage) { + try { + let dmTemplate = config.promotionDmMessage; + if (typeof dmTemplate === 'string') { + try { + dmTemplate = JSON.parse(dmTemplate); + } catch (e) {} } + else if (typeof dmTemplate === 'object') { + dmTemplate = JSON.parse(JSON.stringify(dmTemplate)); + } + + if (dmTemplate && dmTemplate.embeds && !dmTemplate._schema) dmTemplate._schema = 'v3'; + let dmOpts = await embedTypeV2(dmTemplate, placeholders); + if (dmOpts?.content?.trim() === '') delete dmOpts.content; + + if (dmOpts && (dmOpts.content || (dmOpts.embeds && dmOpts.embeds.length > 0))) { + await targetMember.send(dmOpts).catch(()=>{}); + } + } catch (e) {} + } + + await interaction.reply({ + content: localize('staff-management-system', 'succ-promo', { + user: targetMember.user.tag, + role: newRole.name + }), + flags: MessageFlags.Ephemeral + }); +} + +// ----- Generates promotion history & embed ----- +async function generatePromotionHistoryResponse(client, targetUser, page = 1) { + const Promotion = client.models['staff-management-system']['Promotion']; + const limit = 5; + const offset = (page - 1) * limit; + + const { count, rows } = await Promotion.findAndCountAll({ + where: { + userId: targetUser.id + }, + order: [['createdAt', 'DESC']], + limit, + offset + }); + if (count === 0) return { + content: localize('staff-management-system', 'info-no-promo', { username: targetUser.username }), + flags: MessageFlags.Ephemeral + }; + + const totalPages = Math.ceil(count / limit) || 1; + const embed = applyFooter(client, new EmbedBuilder() + .setTitle(localize('staff-management-system', 'prom-hist-title', { username: targetUser.username })) + .setThumbnail(targetUser.displayAvatarURL({ dynamic: true })) + .setColor('Gold') + ); + + const desc = rows.map((r, i) => { + const link = r.messageUrl ? ` • [Jump](${r.messageUrl})` : ''; + return `**${offset + i + 1}. **\n**${localize('staff-management-system', 'label-role')}:** <@&${r.newRole}>\n**${localize('staff-management-system', 'label-prom-by')}:** <@${r.issuerId}>\n**${localize('staff-management-system', 'general-rsn')}:** ${r.reason}${link}`; + }).join('\n\n'); + + embed.setDescription(desc); + embed.addFields({ name: '\u200b', value: localize('staff-management-system', 'page-count', { page, total: totalPages }) }); + + const row = buildPaginationRow( + `staff-mgmt_prom-hist_${targetUser.id}_${page - 1}`, + 'prom_hist_count', + `staff-mgmt_prom-hist_${targetUser.id}_${page + 1}`, + page, totalPages + ); + + return { + embeds: [embed.toJSON()], + components: [row.toJSON()] + }; +} + +async function getPromotionHistory(client, interaction, targetUser) { + const response = await generatePromotionHistoryResponse(client, targetUser, 1); + if (response.content && response.content.startsWith('ℹ️')) return interaction.reply(response); + await interaction.reply({ ...response, flags: MessageFlags.Ephemeral }); +} + +// ---------- User Panel ---------- +async function generatePanelSubpage(client, targetUser, type, page) { + if (type === 'infractions') return await generatePanelInfractions(client, targetUser, page); + if (type === 'promotions') return await generatePanelPromotions(client, targetUser, page); + if (type === 'reviews') return await generatePanelReviews(client, targetUser, page); + if (type === 'status') return await generatePanelStatus(client, targetUser, page); + if (type === 'activity') return await generatePanelActivity(client, targetUser, page); + return null; +} + +// Overview page +async function generateUserPanel(client, targetUser) { + const embed = applyFooter(client, new EmbedBuilder() + .setTitle(localize('staff-management-system', 'panel-title', { + username: targetUser.username + })) + .setDescription(localize('staff-management-system', 'panel-desc', { + mention: targetUser.toString(), + id: targetUser.id + })) + .setThumbnail(targetUser.displayAvatarURL({ dynamic: true })) + .setColor('Blurple') + ); + + const menu = new StringSelectMenuBuilder() + .setCustomId(`staff-mgmt_panel-menu_${targetUser.id}`) + .setPlaceholder(localize('staff-management-system', 'panel-ph')) + .addOptions( + new StringSelectMenuOptionBuilder() + .setLabel(localize('staff-management-system', 'opt-over')) + .setValue('overview') + .setEmoji('🏠'), + new StringSelectMenuOptionBuilder() + .setLabel(localize('staff-management-system', 'opt-act')) + .setValue('activity') + .setEmoji('📋'), + new StringSelectMenuOptionBuilder() + .setLabel(localize('staff-management-system', 'opt-inf')) + .setValue('infractions') + .setEmoji('⚠️'), + new StringSelectMenuOptionBuilder() + .setLabel(localize('staff-management-system', 'opt-prom')) + .setValue('promotions') + .setEmoji('🎉'), + new StringSelectMenuOptionBuilder() + .setLabel(localize('staff-management-system', 'opt-rev')) + .setValue('reviews') + .setEmoji('⭐'), + new StringSelectMenuOptionBuilder() + .setLabel(localize('staff-management-system', 'opt-shi')) + .setValue('shifts') + .setEmoji('⏱️'), + new StringSelectMenuOptionBuilder() + .setLabel(localize('staff-management-system', 'opt-sta')) + .setValue('status') + .setEmoji('🌙'), + new StringSelectMenuOptionBuilder() + .setLabel(localize('staff-management-system', 'opt-del')) + .setValue('deletion') + .setEmoji('🗑️') + ); + + const row = new ActionRowBuilder().addComponents(menu); + return { + embeds: [embed.toJSON()], + components: [row.toJSON()] + }; +} + +// Infractions page +async function generatePanelInfractions(client, targetUser, page = 1) { + const Infraction = client.models['staff-management-system']['Infraction']; + const allInfractions = await Infraction.findAll({ + where: { userId: targetUser.id } + }); + const count = allInfractions.length; + + let totalPages = 1; + if (count > 3) totalPages = 1 + Math.ceil((count - 3) / 5); + + const limit = page === 1 ? 3 : 5; + const offset = page === 1 ? 0 : 3 + ((page - 2) * 5); + + const typeCounts = {}; + allInfractions.forEach(inf => { typeCounts[inf.type] = (typeCounts[inf.type] || 0) + 1; }); + const typeStrings = Object.entries(typeCounts).map(([type, qty]) => `${type}: **${qty}**`).join('\n'); + + const embed = applyFooter(client, new EmbedBuilder() + .setTitle(localize('staff-management-system', 'p-inf-title', { username: targetUser.username })) + .setColor('Red') + .setThumbnail(targetUser.displayAvatarURL({ dynamic: true })) + ); + + let desc = localize('staff-management-system', 'p-inf-desc', { + count: count, types: typeStrings || localize('staff-management-system', 'info-none') + }); + + const rows = await Infraction.findAll({ + where: { userId: targetUser.id }, + order: [['createdAt', 'DESC']], + limit, + offset + }); + + if (rows.length === 0) { + desc += localize('staff-management-system', 'p-no-hist'); + } else { + desc += rows.map(r => { + const statusIcon = r.active ? '🔴' : localize('staff-management-system', 'icon-voided'); + const expiry = r.expiresAt ? `\n**${localize('staff-management-system', 'label-exp')}:** ` : ''; + return `**${statusIcon} ${localize('staff-management-system', 'label-case')} #${r.caseId} - ${r.type}**\n**${localize('staff-management-system', 'label-date')}:** \n**${localize('staff-management-system', 'general-rsn')}:** ${r.reason}${expiry}`; + }).join('\n\n'); + } + + embed.setDescription(desc); + embed.addFields({ name: '\u200b', value: localize('staff-management-system', 'page-count', { page, total: totalPages }) }); + + const menu = ActionRowBuilder.from((await generateUserPanel(client, targetUser)).components[0]); + menu.components[0].options.find(opt => opt.data.value === 'infractions').data.default = true; + + const paginationRow = buildPaginationRow( + `staff-mgmt_panel-inf_${targetUser.id}_${page - 1}`, + 'panel_inf_count', + `staff-mgmt_panel-inf_${targetUser.id}_${page + 1}`, + page, totalPages + ); + + return { + embeds: [embed.toJSON()], + components: [menu.toJSON(), paginationRow.toJSON()] + }; +} + +// Promotions page +async function generatePanelPromotions(client, targetUser, page = 1) { + const Promotion = client.models['staff-management-system']['Promotion']; + const count = await Promotion.count({ + where: { userId: targetUser.id } + }); + + let totalPages = 1; + if (count > 3) totalPages = 1 + Math.ceil((count - 3) / 5); + + const limit = page === 1 + ? 3 + : 5; + const offset = page === 1 + ? 0 + : 3 + ((page - 2) * 5); + + const embed = applyFooter(client, new EmbedBuilder() + .setTitle(localize('staff-management-system', 'p-prom-title', { + username: targetUser.username + })) + .setColor('Gold') + .setThumbnail(targetUser.displayAvatarURL({ dynamic: true })) + ); + + let desc = localize('staff-management-system', 'p-prom-desc', { count: count }); + const rows = await Promotion.findAll({ + where: { userId: targetUser.id }, + order: [['createdAt', 'DESC']], + limit, + offset + }); + + if (rows.length === 0) { + desc += localize('staff-management-system', 'p-no-hist'); + } else { + desc += rows.map(r => `**${localize('staff-management-system', 'label-role')}:** <@&${r.newRole}>\n**${localize('staff-management-system', 'label-prom-by')}:** <@${r.issuerId}>\n**${localize('staff-management-system', 'label-date')}:** \n**${localize('staff-management-system', 'general-rsn')}:** ${r.reason}`).join('\n\n'); + } + + embed.setDescription(desc); + embed.addFields({ name: '\u200b', value: localize('staff-management-system', 'page-count', { page, total: totalPages }) }); + + const menu = ActionRowBuilder.from((await generateUserPanel(client, targetUser)).components[0]); + menu.components[0].options.find(opt => opt.data.value === 'promotions').data.default = true; + + const paginationRow = buildPaginationRow( + `staff-mgmt_panel-prom_${targetUser.id}_${page - 1}`, + 'panel_prom_count', + `staff-mgmt_panel-prom_${targetUser.id}_${page + 1}`, + page, totalPages + ); + + return { + embeds: [embed.toJSON()], + components: [menu.toJSON(), paginationRow.toJSON()] + }; +} + +// Reviews page +async function generatePanelReviews(client, targetUser, page = 1) { + const Review = client.models['staff-management-system']['StaffReview']; + const allReviews = await Review.findAll({ + where: { targetId: targetUser.id } + }); + const count = allReviews.length; + + let totalPages = 1; + if (count > 3) totalPages = 1 + Math.ceil((count - 3) / 5); + + const limit = page === 1 ? 3 : 5; + const offset = page === 1 ? 0 : 3 + ((page - 2) * 5); + + const avg = count + ? (allReviews.reduce((a, b) => a + b.stars, 0) / count).toFixed(1) + : 0; + + const embed = applyFooter(client, new EmbedBuilder() + .setTitle(localize('staff-management-system', 'p-rev-title', { + username: targetUser.username + })) + .setColor('Gold') + .setThumbnail(targetUser.displayAvatarURL({ dynamic: true })) + ); + + let desc = localize('staff-management-system', 'p-rev-desc', { count: count, avg: avg }); + + const rows = await Review.findAll({ + where: { targetId: targetUser.id }, + order: [['createdAt', 'DESC']], + limit, + offset + }); + if (rows.length === 0) desc += localize('staff-management-system', 'p-no-hist'); + else desc += rows.map(r => `**${"⭐".repeat(r.stars)}** ${localize('staff-management-system', 'label-by')} <@${r.authorId}>\n"${r.comment}"`).join('\n\n'); + + embed.setDescription(desc); + embed.addFields({ + name: '\u200b', + value: localize('staff-management-system', 'page-count', { + page, total: totalPages + }) + }); + + const menu = ActionRowBuilder.from((await generateUserPanel(client, targetUser)).components[0]); + menu.components[0].options.find(opt => opt.data.value === 'reviews').data.default = true; + + const paginationRow = buildPaginationRow( + `staff-mgmt_panel-rev_${targetUser.id}_${page - 1}`, + 'panel_rev_count', + `staff-mgmt_panel-rev_${targetUser.id}_${page + 1}`, + page, totalPages + ); + + return { + embeds: [embed.toJSON()], + components: [menu.toJSON(), paginationRow.toJSON()] + }; +} + +// Status page +async function generatePanelStatus(client, targetUser, page = 1) { + const LoaRequest = client.models['staff-management-system']['LoaRequest']; + const allStatuses = await LoaRequest.findAll({ + where: { userId: targetUser.id } + }); + const count = allStatuses.length; + + let totalPages = 1; + if (count > 3) totalPages = 1 + Math.ceil((count - 3) / 5); + const limit = page === 1 + ? 3 + : 5; + const offset = page === 1 + ? 0 + : 3 + ((page - 2) * 5); + + const activeStatus = allStatuses.find(s => ['APPROVED', 'PENDING'].includes(s.status) && new Date(s.endDate) > new Date()); + let activeText = localize('staff-management-system', 'info-none'); + if (activeStatus) { + activeText = `**${activeStatus.type}** (${activeStatus.status})\n${localize('staff-management-system', 'label-end')}: `; + } + + const embed = applyFooter(client, new EmbedBuilder() + .setTitle(localize('staff-management-system', 'p-sta-title', { + username: targetUser.username + })) + .setColor('Green') + .setThumbnail(targetUser.displayAvatarURL({ dynamic: true })) + ); + + let desc = localize('staff-management-system', 'p-sta-desc', { + count: count, active: activeText + }); + + const rows = await LoaRequest.findAll({ + where: { userId: targetUser.id }, + order: [['createdAt', 'DESC']], + limit, + offset + }); + if (rows.length === 0) desc += localize('staff-management-system', 'p-no-hist'); + else { + const icons = { + APPROVED: '✅', + DENIED: '❌', + ENDED: '⏹️', + PENDING: '🕐' + }; + desc += rows.map(r => `**${icons[r.status] || '❓'} ${r.type} - ${r.status}**\n**${localize('staff-management-system', 'general-start')}:** \n**${localize('staff-management-system', 'general-end')}:** \n**${localize('staff-management-system', 'general-rsn')}:** ${r.reason}`).join('\n\n'); + } + + embed.setDescription(desc); + embed.addFields({ + name: '\u200b', + value: localize('staff-management-system', 'page-count', { + page, + total: totalPages + }) + }); + + const menu = ActionRowBuilder.from((await generateUserPanel(client, targetUser)).components[0]); + menu.components[0].options.find(opt => opt.data.value === 'status').data.default = true; + + const paginationRow = buildPaginationRow( + `staff-mgmt_panel-stat_${targetUser.id}_${page - 1}`, + 'panel_stat_count', + `staff-mgmt_panel-stat_${targetUser.id}_${page + 1}`, + page, totalPages + ); + + return { + embeds: [embed.toJSON()], + components: [menu.toJSON(), paginationRow.toJSON()] + }; +} + +// Activity checks page +async function generatePanelActivity(client, targetUser, page = 1) { + const ActivityCheck = client.models['staff-management-system']['ActivityCheck']; + const allChecks = await ActivityCheck.findAll(); + + let userResponses = 0; + const historyRows = []; + allChecks.forEach(check => { + const responded = JSON.parse(check.respondedUsers || '[]'); + if (responded.includes(targetUser.id)) { + userResponses++; + historyRows.push(check); + } + }); + + historyRows.sort((a, b) => b.createdAt - a.createdAt); + const count = historyRows.length; + + let totalPages = 1; + if (count > 3) totalPages = 1 + Math.ceil((count - 3) / 5); + const limit = page === 1 + ? 3 + : 5; + const offset = page === 1 + ? 0 + : 3 + ((page - 2) * 5); + const paginatedRows = historyRows.slice(offset, offset + limit); + + const embed = applyFooter(client, new EmbedBuilder() + .setTitle(localize('staff-management-system', 'p-act-title', { + username: targetUser.username + })) + .setColor('Blue') + .setThumbnail(targetUser.displayAvatarURL({ dynamic: true })) + ); + + let desc = localize('staff-management-system', 'p-act-desc', { count: userResponses }); + + if (paginatedRows.length === 0) desc += localize('staff-management-system', 'p-no-hist'); + else { + desc += paginatedRows.map(r => `**${localize('staff-management-system', 'label-chk')} **\n**${localize('staff-management-system', 'label-end')}:** \n**${localize('staff-management-system', 'label-chan')}:** <#${r.channelId}>`).join('\n\n'); + } + + embed.setDescription(desc); + embed.addFields({ name: '\u200b', value: localize('staff-management-system', 'page-count', { page, total: totalPages }) }); + + const menu = ActionRowBuilder.from((await generateUserPanel(client, targetUser)).components[0]); + menu.components[0].options.find(opt => opt.data.value === 'activity').data.default = true; + + const paginationRow = buildPaginationRow( + `staff-mgmt_panel-act_${targetUser.id}_${page - 1}`, + 'panel_act_count', + `staff-mgmt_panel-act_${targetUser.id}_${page + 1}`, + page, totalPages + ); + + return { + embeds: [embed.toJSON()], + components: [menu.toJSON(), paginationRow.toJSON()] + }; +} + +// Shifts page +async function generatePanelShifts(client, targetUser) { + const embed = applyFooter(client, new EmbedBuilder() + .setTitle(localize('staff-management-system', 'p-shi-title', { + username: targetUser.username + })) + .setColor('Purple') + .setThumbnail(targetUser.displayAvatarURL({ dynamic: true })) + ); + + try { + const Shift = client.models['staff-management-system']['StaffShift']; + const config = getConfig(client, 'shifts') || {}; + const shifts = await Shift.findAll({ + where: { + userId: targetUser.id, + endTime: { [Op.not]: null }, + duration: { [Op.not]: null } + } + }); + + const totalShifts = shifts.length; + const totalSeconds = shifts.reduce((sum, s) => sum + (parseInt(s.duration) || 0), 0); + + const breakdown = {}; + shifts.forEach(log => { + const t = log.type || 'Staff'; + breakdown[t] = (breakdown[t] || 0) + (parseInt(log.duration) || 0); + }); + const breakdownStr = Object.entries(breakdown).sort((a, b) => b[1] - a[1]).map(([type, sec]) => `• ${type}: ${formatDuration(sec)}`).join('\n') || localize('staff-management-system', 'info-none'); + + let quotaStr = localize('staff-management-system', 'no-quota-configured'); + const guild = client.guilds.cache.get(client.guildID); + const member = await guild?.members.fetch(targetUser.id).catch(() => null); + + if (member && config.enableQuotas && config.quotas) { + let bestQuota = null; + let highestPosition = -1; + for (const [roleId, hoursStr] of Object.entries(config.quotas)) { + const hours = parseFloat(hoursStr); + const role = guild.roles.cache.get(roleId); + if (role && member.roles.cache.has(roleId) && role.position > highestPosition) { + highestPosition = role.position; + bestQuota = { hours }; + } + } + + if (bestQuota) { + const timeframe = config.quotaTimeframe || 'Weekly'; + const cutoff = new Date(); + if (timeframe === 'Weekly') cutoff.setDate(cutoff.getDate() - 7); + else cutoff.setMonth(cutoff.getMonth() - 1); + + const recentShifts = await Shift.findAll({ + where: { + userId: targetUser.id, + startTime: { [Op.gt]: cutoff }, + endTime: { [Op.not]: null }, + duration: { [Op.not]: null } + } + }); + const recentSeconds = recentShifts.reduce((sum, s) => sum + (parseInt(s.duration) || 0), 0); + const requiredSeconds = bestQuota.hours * 3600; + const isMet = recentSeconds >= requiredSeconds; + + quotaStr = localize('staff-management-system', 'duty-quota-str', { + timeframe, + duration: formatDuration(recentSeconds), + hours: bestQuota.hours, + result: isMet + ? localize('staff-management-system', 'duty-quota-met') + : localize('staff-management-system', 'duty-quota-failed') + }); + } + } + + const allResults = await Shift.findAll({ + attributes: ['userId', [Shift.sequelize.fn('SUM', Shift.sequelize.col('duration')), 'totalDuration']], + where: { endTime: { [Op.not]: null }, duration: { [Op.not]: null } }, + group: ['userId'], + order: [[Shift.sequelize.literal('totalDuration'), 'DESC']] + }); + + const lbIndex = allResults.findIndex(p => p.userId === targetUser.id); + const lbRank = lbIndex !== -1 + ? `${lbIndex + 1} / ${allResults.length}` + : localize('staff-management-system', 'label-unranked'); + + embed.setDescription(localize('staff-management-system', 'panel-shifts-desc', { + totalShifts, + totalSeconds: formatDuration(totalSeconds), + lbRank, + breakdownStr, + quotaStr + })); + + } catch (e) { + client.logger.error(`[Staff Management] User panel error: ${e.stack}`); + embed.setDescription(localize('staff-management-system', 'err-shift-data-unavailable', { error: e.message })); + } + + const menu = ActionRowBuilder.from((await generateUserPanel(client, targetUser)).components[0]); + menu.components[0].options.find(opt => opt.data.value === 'shifts').data.default = true; + + const historyBtnRow = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(`duty-mgmt_hist_${targetUser.id}_1_All`) + .setLabel(localize('staff-management-system', 'btn-view-history')) + .setStyle(ButtonStyle.Secondary) + ); + + return { + embeds: [embed.toJSON()], + components: [menu.toJSON(), historyBtnRow.toJSON()] + }; +} + +// Deletion page +async function generatePanelDeletion(client, targetUser) { + const embed = applyFooter(client, new EmbedBuilder() + .setTitle(localize('staff-management-system', 'panel-deletion-title', { tag: targetUser.username })) + .setDescription(localize('staff-management-system', 'panel-deletion-desc', { mention: targetUser.toString() })) + .setColor('DarkRed') + .setThumbnail(targetUser.displayAvatarURL({ dynamic: true })) + ); + + const menu = new StringSelectMenuBuilder() + .setCustomId(`staff-mgmt_delete-menu_${targetUser.id}`) + .setPlaceholder(localize('staff-management-system', 'panel-deletion-placeholder')) + .addOptions( + new StringSelectMenuOptionBuilder() + .setLabel(localize('staff-management-system', 'panel-opt-back')) + .setValue('back') + .setEmoji('◀️'), + new StringSelectMenuOptionBuilder() + .setLabel(localize('staff-management-system', 'panel-opt-del-act')) + .setValue('del_activity') + .setEmoji('📋'), + new StringSelectMenuOptionBuilder() + .setLabel(localize('staff-management-system', 'panel-opt-del-inf')) + .setValue('del_infractions') + .setEmoji('⚠️'), + new StringSelectMenuOptionBuilder() + .setLabel(localize('staff-management-system', 'panel-opt-del-prom')) + .setValue('del_promotions') + .setEmoji('🎉'), + new StringSelectMenuOptionBuilder() + .setLabel(localize('staff-management-system', 'panel-opt-del-rev')) + .setValue('del_reviews') + .setEmoji('⭐'), + new StringSelectMenuOptionBuilder() + .setLabel(localize('staff-management-system', 'panel-opt-del-shifts')) + .setValue('del_shifts') + .setEmoji('⏱️'), + new StringSelectMenuOptionBuilder() + .setLabel(localize('staff-management-system', 'panel-opt-del-status')) + .setValue('del_status') + .setEmoji('🌙'), + new StringSelectMenuOptionBuilder() + .setLabel(localize('staff-management-system', 'panel-opt-del-all')) + .setValue('del_all') + .setEmoji('💥') + ); + + return { + embeds: [embed.toJSON()], + components: [new ActionRowBuilder().addComponents(menu).toJSON()] + }; +} + +async function executeDataDeletion(client, targetId, dataType) { + const models = client.models['staff-management-system']; + + if (['del_infractions', 'del_all'].includes(dataType)) await models['Infraction'].destroy({ + where: { userId: targetId } + }); + if (['del_promotions', 'del_all'].includes(dataType)) await models['Promotion'].destroy({ + where: { userId: targetId } + }); + if (['del_reviews', 'del_all'].includes(dataType)) await models['StaffReview'].destroy({ + where: { targetId: targetId } + }); + if (['del_shifts', 'del_all'].includes(dataType)) { + await models['StaffShift'].destroy({ + where: { userId: targetId } + }); + await models['StaffProfile'].destroy({ + where: { userId: targetId } + }); + } + if (['del_status', 'del_all'].includes(dataType)) await models['LoaRequest'].destroy({ + where: { userId: targetId } + }); + if (['del_activity', 'del_all'].includes(dataType)) { + const allChecks = await models['ActivityCheck'].findAll(); + for (const check of allChecks) { + let responded = JSON.parse(check.respondedUsers || '[]'); + if (responded.includes(targetId)) { + responded = responded.filter(id => id !== targetId); + await check.update({ respondedUsers: JSON.stringify(responded) }); + } + } + } +} + +// ----- Status ----- +const getStatusMeta = (type) => ({ + isLoa: type === 'LOA', + label: type === 'LOA' + ? 'LoA' + : 'RA', + enableKey: type === 'LOA' + ? 'enableLoa' + : 'enableRa', + roleKey: type === 'LOA' + ? 'loaRole' + : 'raRole', + maxDaysKey: type === 'LOA' + ? 'loaMaxDays' + : 'raMaxDays', + color: type === 'LOA' + ? 'Green' + : 'Orange', + activeText: localize('staff-management-system', type === 'LOA' + ? 'status-active-loa' + : 'status-active-ra' + ), + histTitle: localize('staff-management-system', type === 'LOA' + ? 'status-hist-loa' + : 'status-hist-ra' + ), + actionPrefix: type === 'LOA' + ? 'loa' + : 'ra' +}); + +async function handleStatusRequest(client, interaction, type, durationInput, reason) { + const config = getConfig(client, 'status'); + const isLoa = type === 'LOA'; + if (!config[isLoa + ? 'enableLoa' + : 'enableRa']) return interaction.editReply({ + content: localize('staff-management-system', 'err-status-disabled', { type }) + } + ); + + const days = parseDurationToDays(durationInput?.trim()); + if (!days || isNaN(days) || days <= 0) return interaction.editReply({ + content: localize('staff-management-system', 'err-invalid-duration') + }); + + const maxDays = (isLoa ? config.loaMaxDays : config.raMaxDays) || (isLoa ? 60 : 30); + if (days > maxDays) return interaction.editReply({ + content: localize('staff-management-system', 'err-duration-max', { max: maxDays }) + }); + + const LoaRequest = client.models['staff-management-system']['LoaRequest']; + if (await LoaRequest.findOne({ + where: { userId: interaction.user.id, type, status: { [Op.in]: ['PENDING', 'APPROVED'] }, + endDate: { [Op.gt]: new Date() } } + })) { + return interaction.editReply({ + content: localize('staff-management-system', 'err-status-exists', { type }) + }); + } + + const startDate = new Date(); + const endDate = new Date(startDate.getTime() + days * 24 * 60 * 60 * 1000); + const needsApproval = isLoa + ? config.requireLoaApproval !== false + : config.requireRaApproval !== false; + + const req = await LoaRequest.create({ + userId: interaction.user.id, + type, + reason, + startDate, + endDate, + status: needsApproval + ? 'PENDING' + : 'APPROVED' + }); + + const logChannelId = getSafeChannelId(config.statusLogChannel); + if (logChannelId && needsApproval) { + const channel = await interaction.guild.channels.fetch(logChannelId).catch(() => null); + if (channel) { + const embed = new EmbedBuilder() + .setTitle(localize('staff-management-system', 'status-request-title', { type })) + .setColor('Blue') + .setAuthor({ name: `Request ID: ${req.id}`}) + .addFields( + { name: localize('staff-management-system', 'status-req-user'), + value: interaction.user.toString(), + inline: true + }, + { name: localize('staff-management-system', 'status-req-duration'), + value: `${days} ${localize('staff-management-system', 'label-days')}`, + inline: true + }, + { name: localize('staff-management-system', 'general-rsn'), + value: reason + } + ); + + applyFooter(client, embed); + const row = new ActionRowBuilder() + .addComponents(new ButtonBuilder() + .setCustomId(`staff-mgmt_approve_${req.id}`) + .setLabel(localize('staff-management-system', 'btn-approve')) + .setStyle(ButtonStyle.Success), + new ButtonBuilder() + .setCustomId(`staff-mgmt_deny_${req.id}`) + .setLabel(localize('staff-management-system', 'btn-deny')) + .setStyle(ButtonStyle.Danger)); + channel.send({ embeds: [embed.toJSON()], components: [row.toJSON()] }).catch(()=>{}); + } + } + + if (!needsApproval) { + const roleId = config[isLoa ? 'loaRole' : 'raRole']; + if (roleId) interaction.member.roles.add(roleId).catch(()=>{}); + await logStatusChange(client, type, 'start', { + targetUser: interaction.user, + startDate, + endDate, + reason, + approverId: null + }); + } + + await interaction.editReply({ + content: localize('staff-management-system', 'success-status-request', { + type, state: needsApproval + ? localize('staff-management-system', 'state-pending') + : localize('staff-management-system', 'state-auto') + }) + }); +} + +async function handleStatusView(client, interaction, type, targetUser) { + const user = targetUser || interaction.user; + const request = await client.models['staff-management-system']['LoaRequest'].findOne({ + where: { userId: user.id, type, status: { [Op.in]: ['APPROVED', 'PENDING'] }, + endDate: { [Op.gt]: new Date() } }, + order: [['createdAt', 'DESC']] + }); + + if (!request) return interaction.editReply({ + content: localize('staff-management-system', 'no-active-status', { + user: user.username, + type + }) + }); + + const embed = new EmbedBuilder() + .setTitle(`${type} Status: ${user.username}`) + .setColor(request.status === 'APPROVED' + ? 'Green' + : 'Yellow' + ) + .addFields( + { + name: localize('staff-management-system', 'label-stat'), + value: request.status, + inline: true }, + { + name: localize('staff-management-system', 'label-end'), + value: formatDate(request.endDate), + inline: true }, + { + name: localize('staff-management-system', 'general-rsn'), + value: request.reason || localize('staff-management-system', 'info-none') + }) + .setThumbnail(user.displayAvatarURL({ dynamic: true })); + applyFooter(client, embed); + await interaction.editReply({ embeds: [embed.toJSON()] }); +} + +async function handleStatusList(client, interaction, type, filter, page = 1) { + const limit = 10; + const offset = (page - 1) * limit; + + let whereClause = { type }; + let title = `${type} List`; + + if (filter === 'active') { + whereClause.status = 'APPROVED'; + whereClause.endDate = { [Op.gt]: new Date() }; + title += localize('staff-management-system', 'filter-active'); + } + else if (filter === 'expired') { + whereClause.endDate = { [Op.lt]: new Date() }; + title += localize('staff-management-system', 'filter-expired'); + } + else { + whereClause.status = { [Op.ne]: 'PENDING' }; + title += localize('staff-management-system', 'filter-history'); + } + + const { count, rows } = await client.models['staff-management-system']['LoaRequest'].findAndCountAll({ + where: whereClause, + limit, + offset, + order: [['endDate', 'DESC']] + }); + if (count === 0) return interaction.editReply({ + content: localize('staff-management-system', 'err-no-recs') + }); + + const totalPages = Math.ceil(count / limit) || 1; + const embed = new EmbedBuilder() + .setTitle(title) + .setColor('Blue') + .setDescription(rows.map(r => `**<@${r.userId}>** ${r.status === 'APPROVED' ? '✅' : (r.status === 'DENIED' ? '❌' : '⏹️')}\nEnds: ${formatDate(r.endDate)}\nReason: ${r.reason}`).join('\n\n')) + .addFields( + { + name: '\u200b', + value: localize('staff-management-system', 'page-count', { page, total: totalPages }) + } + ); + applyFooter(client, embed); + await interaction.editReply({ embeds: [embed.toJSON()] }); +} + +async function handleStatusManage(client, interaction, targetMember, type) { + const config = getConfig(client, 'status'); + const meta = getStatusMeta(type); + if (!config[meta.enableKey]) return interaction.editReply({ + content: localize('staff-management-system', 'err-status-disabled', { type }) + }); + + const generalConfig = getConfig(client, 'configuration'); + const canManage = interaction.member.roles.cache.some(r => [...(generalConfig.supervisorRoles || []), ...(generalConfig.managementRoles || [])].includes(r.id)) || interaction.member.permissions.has('Administrator'); + if (!canManage) return interaction.editReply({ + content: localize('staff-management-system', 'err-gen-no-perm') + }); + + const LoaRequest = client.models['staff-management-system']['LoaRequest']; + const activeRequest = await LoaRequest.findOne({ + where: { + userId: targetMember.user.id, + type, + status: { [Op.in]: ['APPROVED', 'PENDING'] }, + endDate: { [Op.gt]: new Date() } + }, + order: [['createdAt', 'DESC']] + } + ); + const totalCount = await LoaRequest.count({ + where: { userId: targetMember.user.id, type } + }); + + const embed = applyFooter(client, new EmbedBuilder() + .setTitle(localize('staff-management-system', 'manage-status-title', { + label: meta.label, + username: targetMember.user.username + })) + .setThumbnail(targetMember.user.displayAvatarURL({ dynamic: true })) + .setColor(activeRequest + ? meta.color + : 'Grey' + ) + .setDescription(localize('staff-management-system', 'manage-stat-desc', { + status: activeRequest + ? meta.activeText + : localize('staff-management-system', 'no-act-stat', { + label: meta.label + }), + label: meta.label, + count: Math.max(0, totalCount - (activeRequest ? 1 : 0)) + })) + ); + + embed.addFields({ + name: localize('staff-management-system', 'manage-active-details', { label: meta.label }), + value: activeRequest ? `**${localize('staff-management-system', 'general-start')}:** ${formatDate(activeRequest.startDate)}\n**${localize('staff-management-system', 'general-end')}:** ${formatDate(activeRequest.endDate)}\n**${localize('staff-management-system', 'label-stat')}:** ${activeRequest.status}\n**${localize('staff-management-system', 'label-appr-by')}:** ${activeRequest.approverId ? `<@${activeRequest.approverId}>` : localize('staff-management-system', 'label-auto')}\n**${localize('staff-management-system', 'general-rsn')}:** ${activeRequest.reason || localize('staff-management-system', 'info-none')}` : localize('staff-management-system', 'manage-no-active-user', { label: meta.label }) + }); + + const p = meta.actionPrefix; + const rid = activeRequest?.id ?? 'none'; + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(`staff-mgmt_${p}-end_${rid}`) + .setLabel(localize('staff-management-system', 'btn-end-early', { label: meta.label })) + .setEmoji('🚫').setStyle(ButtonStyle.Danger) + .setDisabled(!activeRequest), + new ButtonBuilder() + .setCustomId(`staff-mgmt_${p}-extend_${rid}`) + .setLabel(localize('staff-management-system', 'btn-extend', { label: meta.label })) + .setEmoji('⏳') + .setStyle(ButtonStyle.Primary) + .setDisabled(!activeRequest), + new ButtonBuilder() + .setCustomId(`staff-mgmt_${p}-hist_${targetMember.user.id}_1`) + .setLabel(localize('staff-management-system', 'btn-view-history')) + .setEmoji('📜') + .setStyle(ButtonStyle.Secondary) + .setDisabled(totalCount === 0) + ); + await interaction.editReply({ + embeds: [embed.toJSON()], + components: [row.toJSON()] + }); +} + +async function handleStatusEnd(interaction, type) { + const meta = getStatusMeta(type); + const requestId = interaction.customId.split('_')[2]; + if (requestId === 'none') return interaction.reply({ + content: localize('staff-management-system', 'err-no-active-end', { label: meta.label }), + flags: MessageFlags.Ephemeral + }); + + const modal = new ModalBuilder() + .setCustomId(`staff-mgmt_${meta.actionPrefix}-end-submit_${requestId}`) + .setTitle(localize('staff-management-system', 'modal-end-early-title', { label: meta.label })); + modal.addComponents(new ActionRowBuilder() + .addComponents( + new TextInputBuilder() + .setCustomId('end_reason') + .setLabel(localize('staff-management-system', 'modal-end-early-reason')) + .setStyle(TextInputStyle.Paragraph) + .setRequired(true) + )); + return interaction.showModal(modal); +} + +async function handleStatusEndSubmit(client, interaction, type) { + const meta = getStatusMeta(type); + const request = await client.models['staff-management-system']['LoaRequest'].findByPk(interaction.customId.split('_')[2]); + if (!request || request.status === 'ENDED' || request.status === 'DENIED') return interaction.reply({ + content: localize('staff-management-system', 'err-stat-inact', { label: meta.label }), + flags: MessageFlags.Ephemeral + }); + + const reason = interaction.fields.getTextInputValue('end_reason'); + const member = await interaction.guild.members.fetch(request.userId).catch(() => null); + + if (member && getConfig(client, 'status')[meta.roleKey]) await member.roles.remove(getConfig(client, 'status')[meta.roleKey]).catch(() => {}); + + await request.update({ status: 'ENDED', endDate: new Date() }); + await client.models['staff-management-system']['StaffProfile'].update({ activityStatus: 'ACTIVE' }, { + where: { userId: request.userId } + }); + + if (member) await sendStatusDm(member.user, type, 'ended_early', { + ender: interaction.user.tag, + reason + }); + await logStatusChange(client, type, 'end', { + userId: request.userId, + startDate: request.startDate, + reason: request.reason + }); + + const updatedEmbed = EmbedBuilder.from(interaction.message.embeds[0]) + .setColor('Grey') + .setDescription(localize('staff-management-system', 'status-ended-embed-desc', { + label: meta.label, user: interaction.user.tag, reason + })) + .spliceFields(0, 1, { + name: localize('staff-management-system', 'manage-active-details', { label: meta.label }), + value: localize('staff-management-system', 'manage-no-active-user', { label: meta.label }) + }); + + const p = meta.actionPrefix; + const disabledRow = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId(`${p}-end-done`) + .setLabel(localize('staff-management-system', 'btn-end-early', { label: meta.label })) + .setEmoji('🚫') + .setStyle(ButtonStyle.Danger) + .setDisabled(true), + new ButtonBuilder() + .setCustomId(`${p}-extend-done`) + .setLabel(localize('staff-management-system', 'btn-extend', { label: meta.label })) + .setEmoji('⏳') + .setStyle(ButtonStyle.Primary) + .setDisabled(true), + new ButtonBuilder() + .setCustomId(`staff-mgmt_${p}-hist_${request.userId}_1`) + .setLabel(localize('staff-management-system', 'btn-view-history')) + .setEmoji('📜') + .setStyle(ButtonStyle.Secondary) + ); + return interaction.update({ + embeds: [updatedEmbed.toJSON()], + components: [disabledRow.toJSON()] + }); +} + +async function handleStatusExtend(interaction, type) { + const meta = getStatusMeta(type); + const requestId = interaction.customId.split('_')[2]; + if (requestId === 'none') return interaction.reply({ + content: localize('staff-management-system', 'err-no-active-extend', { label: meta.label }), + flags: MessageFlags.Ephemeral + }); + + const modal = new ModalBuilder() + .setCustomId(`staff-mgmt_${meta.actionPrefix}-extend-submit_${requestId}`) + .setTitle(localize('staff-management-system', 'modal-extend-title', { + label: meta.label + })); + modal.addComponents( + new ActionRowBuilder() + .addComponents( + new TextInputBuilder() + .setCustomId('extend_days') + .setLabel(localize('staff-management-system', 'modal-extend-days')) + .setStyle(TextInputStyle.Short) + .setPlaceholder("7") + .setRequired(true) + ), + new ActionRowBuilder() + .addComponents( + new TextInputBuilder() + .setCustomId('extend_reason') + .setLabel(localize('staff-management-system', 'modal-extend-reason')) + .setStyle(TextInputStyle.Paragraph) + .setRequired(true) + ) + ); + return interaction.showModal(modal); +} + +function scheduleStatusExpiry(client, request) { + schedule.scheduleJob(new Date(request.endDate), async () => { + try { + const req = await client.models['staff-management-system']['LoaRequest'].findByPk(request.id); + if (req && req.status === 'APPROVED' && new Date(req.endDate) <= new Date()) { + await req.update({ status: 'ENDED' }); + await client.models['staff-management-system']['StaffProfile'].update({ activityStatus: 'ACTIVE' }, { + where: { userId: req.userId } + }); + + const member = await client.guilds.cache.get(client.guildID)?.members.fetch(req.userId).catch(() => null); + if (member) { + const roleKey = req.type === 'LOA' + ? 'loaRole' + : 'raRole'; + if (getConfig(client, 'status')[roleKey]) await member.roles.remove(getConfig(client, 'status')[roleKey]).catch(() => null); + await sendStatusDm(member.user, req.type, 'ended'); + } + await logStatusChange(client, req.type, 'end', { userId: req.userId, startDate: req.startDate, reason: req.reason }); + } + } catch (e) {} + }); +} + +async function handleStatusExtendSubmit(client, interaction, type) { + const meta = getStatusMeta(type); + const request = await client.models['staff-management-system']['LoaRequest'].findByPk(interaction.customId.split('_')[2]); + if (!request || request.status === 'ENDED' || request.status === 'DENIED') return interaction.reply({ + content: localize('staff-management-system', 'err-stat-inact', { + label: meta.label + }), + flags: MessageFlags.Ephemeral + }); + + const days = parseInt(interaction.fields.getTextInputValue('extend_days'), 10); + const reason = interaction.fields.getTextInputValue('extend_reason'); + if (isNaN(days) || days <= 0 || days > 180) return interaction.reply({ + content: localize('staff-management-system', 'err-inv-dur'), + flags: MessageFlags.Ephemeral + }); + + const newEndDate = new Date(new Date(request.endDate).getTime() + days * 24 * 60 * 60 * 1000); + await request.update({ endDate: newEndDate }); + scheduleStatusExpiry(client, request); + + const member = await interaction.guild.members.fetch(request.userId).catch(() => null); + if (member) await sendStatusDm(member.user, type, 'extended', { + extender: interaction.user.tag, + days, + endDate: newEndDate, + reason + }); + await logStatusChange(client, type, 'adjusted', { + userId: request.userId, + executorId: interaction.user.id, + changesText: localize('staff-management-system', 'status-adjusted-log', { + label: meta.label, + newEnd: ``, + oldEnd: ``, + reason + }) + }); + + const updatedEmbed = EmbedBuilder.from(interaction.message.embeds[0]) + .spliceFields(0, 1, { + name: localize('staff-management-system', 'manage-active-details', { label: meta.label }), + value: localize('staff-management-system', 'mod-stat-ext', { + s: formatDate(request.startDate), + e: formatDate(newEndDate), + d: days, + t: request.status, + a: request.approverId + ? `<@${request.approverId}>` + : localize('staff-management-system', 'label-auto'), + r: request.reason || localize('staff-management-system', 'info-none') + }) + }); + return interaction.update({ + embeds: [updatedEmbed.toJSON()], + components: interaction.message.components.map(c => c.toJSON()) + }); +} + +async function generateStatusHistoryResponse(client, targetUser, page = 1, type) { + const meta = getStatusMeta(type); + const limit = 5; + const offset = (page - 1) * limit; + + const { count, rows } = await client.models['staff-management-system']['LoaRequest'].findAndCountAll({ + where: { userId: targetUser.id, type }, + order: [['createdAt', 'DESC']], + limit, + offset + }); + if (count === 0) return { + content: localize('staff-management-system', 'info-no-status-history', { label: meta.label }), + flags: MessageFlags.Ephemeral + }; + + const totalPages = Math.ceil(count / limit) || 1; + const embed = applyFooter(client, new EmbedBuilder() + .setTitle(`${meta.histTitle} - ${targetUser.username}`) + .setThumbnail(targetUser.displayAvatarURL({ dynamic: true })) + .setColor(meta.color) + .setDescription(localize('staff-management-system', 'status-history-desc', { + count: rows.length, + total: count, + label: meta.label + } + )) + ); + + const statusIcons = { + APPROVED: '✅', + DENIED: '❌', + ENDED: '⏹️', + PENDING: '🕐' + }; + rows.forEach((req, index) => embed.addFields({ + name: `${statusIcons[req.status] ?? '❓'} ${meta.label} #${offset + index + 1} - ${req.status}`, + value: `**${localize('staff-management-system', 'general-start')}:** ${formatDate(req.startDate)}\n**${localize('staff-management-system', 'general-end')}:** ${formatDate(req.endDate)}\n**${localize('staff-management-system', 'label-appr-by')}:** ${req.approverId ? `<@${req.approverId}>` : localize('staff-management-system', 'label-auto')}\n**${localize('staff-management-system', 'general-rsn')}:** ${req.reason || localize('staff-management-system', 'info-none')}` })); + embed.addFields({ + name: '\u200b', + value: localize('staff-management-system', 'page-count', { page, total: totalPages }) + }); + + const row = buildPaginationRow( + `staff-mgmt_${meta.actionPrefix}-hist_${targetUser.id}_${page - 1}`, + `${meta.actionPrefix}_hist_page_count`, + `staff-mgmt_${meta.actionPrefix}-hist_${targetUser.id}_${page + 1}`, + page, + totalPages + ); + return { + embeds: [embed.toJSON()], + components: [row.toJSON()] + }; +} + +async function handleStatusHistPage(client, interaction, type) { + const parts = interaction.customId.split('_'); + const targetUser = await client.users.fetch(parts[2]).catch(() => null); + if (!targetUser) return interaction.reply({ + content: localize('staff-management-system', 'err-gen-no-user'), + flags: MessageFlags.Ephemeral + }); + + const payload = await generateStatusHistoryResponse(client, targetUser, parseInt(parts[3], 10), type); + if (payload.content) return interaction.reply({ + ...payload, + flags: MessageFlags.Ephemeral + }); + return interaction.message?.embeds?.[0]?.title?.startsWith(getStatusMeta(type).histTitle) + ? interaction.update(payload) + : interaction.reply({ ...payload, flags: MessageFlags.Ephemeral }); +} + +// ---------- Activity Checks ---------- +async function startActivityCheck(client, interactionOrChannel, isAutomated = false) { + const config = getConfig(client, 'activity-checks'); + const ActivityCheck = client.models['staff-management-system']['ActivityCheck']; + + if (await ActivityCheck.findOne({ + where: { status: 'ACTIVE' } + })) { + return !isAutomated && interactionOrChannel.editReply + ? interactionOrChannel.editReply({ content: localize('staff-management-system', 'err-ac-act') }) + : null; + } + + let rolesToCheck = config.targetRoles?.length + ? config.targetRoles + : (getConfig(client, 'configuration')?.staffRoles || []); + if (!rolesToCheck.length) return !isAutomated && interactionOrChannel.editReply + ? interactionOrChannel.editReply({ + content: localize('staff-management-system', 'err-ac-norole') + }) + : null; + + const targetChannel = isAutomated + ? interactionOrChannel + : (interactionOrChannel.options.getChannel('channel') || interactionOrChannel.guild.channels.cache.get(getSafeChannelId(config.sendingChannel)) || interactionOrChannel.channel); + if (!targetChannel) return !isAutomated && interactionOrChannel.editReply + ? interactionOrChannel.editReply({ + content: localize('staff-management-system', 'err-ac-invchan') + }) + : null; + + const durationHours = config.timeframe || 24; + const endTime = new Date(Date.now() + durationHours * 60 * 60 * 1000); + + let embedTemplate = typeof config.checkMessage === 'string' + ? JSON.parse(config.checkMessage) + : config.checkMessage; + let msgOpts = await embedTypeV2(embedTemplate, { + '%endtime%': ``, + '%duration%': durationHours.toString() + }); + + if (msgOpts?.content?.trim() === '') delete msgOpts.content; + msgOpts.components = [ + new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId('staff-mgmt_ac-respond') + .setLabel(localize('staff-management-system', 'ac-confirm-btn')) + .setStyle(ButtonStyle.Success) + .setEmoji('✅') + ) + .toJSON() + ]; + + try { + const checkMessage = await targetChannel.send(msgOpts); + if (!isAutomated && interactionOrChannel.editReply) await interactionOrChannel.editReply({ + content: localize('staff-management-system', 'succ-ac-start', { + channel: targetChannel.id, + hours: durationHours + }) + }); + + const record = await ActivityCheck.create({ + messageId: checkMessage.id, + channelId: targetChannel.id, + endTime, + targetRoles: JSON.stringify(rolesToCheck), + respondedUsers: '[]', + status: 'ACTIVE' + }); + schedule.scheduleJob(endTime, async () => { + const currentCheck = await ActivityCheck.findByPk(record.id); + if (currentCheck && currentCheck.status === 'ACTIVE') await endActivityCheckProcess(client, currentCheck); + }); + } catch (e) { + if (!isAutomated && interactionOrChannel.editReply) interactionOrChannel.editReply({ + content: localize('staff-management-system', 'err-ac-perms', { channel: targetChannel.id }) + }); + } +} + +async function endActivityCheckProcess(client, activeCheck) { + await activeCheck.update({ status: 'ENDED' }); + const guild = client.guilds.cache.get(client.guildID); + if (!guild) return; + + try { + const msg = await guild.channels.cache.get(activeCheck.channelId)?.messages.fetch(activeCheck.messageId); + if (msg && msg.embeds.length > 0) { + const originalEmbed = EmbedBuilder + .from(msg.embeds[0]) + .setColor('#ed4245'); + originalEmbed + .setTitle(localize('staff-management-system', 'ac-title-end')); + await msg.edit({ + embeds: [originalEmbed.toJSON()], + components: [] + }); + } + } catch (e) {} + + const config = getConfig(client, 'activity-checks'); + const logChannel = guild.channels.cache.get(getSafeChannelId(config.logChannel) || getSafeChannelId(getConfig(client, 'configuration')?.generalLogChannel)); + if (!logChannel) return; + + const targetRoles = JSON.parse(activeCheck.targetRoles || '[]'); + const respondedUsers = JSON.parse(activeCheck.respondedUsers || '[]'); + + const expectedMembers = guild.members.cache.filter(m => !m.user.bot && m.roles.cache.some(r => targetRoles.includes(r.id))); + const [responded, exceptions, failed] = [[], [], []]; + const profiles = await client.models['staff-management-system']['StaffProfile'].findAll(); + + expectedMembers.forEach(member => { + if (respondedUsers.includes(member.id)) return responded.push(member); + + let isException = false; + const prof = profiles.find(p => p.userId === member.id); + const isLoa = prof?.activityStatus === 'LOA'; + const isRa = prof?.activityStatus === 'RA'; + + if (config.exceptionsType === 'Only LoA' && isLoa) isException = true; + else if (config.exceptionsType === 'Only RA' && isRa) isException = true; + else if (config.exceptionsType === 'LoA and RA' && (isLoa || isRa)) isException = true; + else if (config.exceptionsType === 'Custom role(s)' && member.roles.cache.some(r => config.customExceptionRoles?.includes(r.id))) isException = true; + + isException + ? exceptions.push(member) + : failed.push(member); + }); + + const embed = applyFooter(client, new EmbedBuilder() + .setTitle(localize('staff-management-system', 'ac-res-title')) + .setColor('Blurple') + .addFields( + { + name: localize('staff-management-system', 'ac-f-res', { + count: responded.length } + ), + value: responded.length + ? responded.map(m => `<@${m.id}>`).join(', ').substring(0, 1024) + : localize('staff-management-system', 'info-none') + }, + { + name: localize('staff-management-system', 'ac-f-fail', { + count: failed.length + }), + value: failed.length + ? failed.map(m => `<@${m.id}>`).join(', ').substring(0, 1024) + : localize('staff-management-system', 'info-none') + }, + { + name: localize('staff-management-system', 'ac-f-exc', { + count: exceptions.length + }), + value: exceptions.length + ? exceptions.map(m => `<@${m.id}>`).join(', ').substring(0, 1024) + : localize('staff-management-system', 'info-none') + } + ) + ); + + const pingText = (config.pingResults && config.pingRoles?.length) + ? config.pingRoles.map(rId => `<@&${rId}>`).join(' ') + : null; + const finalMessage = { embeds: [embed.toJSON()] }; + if (pingText) finalMessage.content = pingText; + + await logChannel.send(finalMessage); +} + +function initActivityCheckAutomation(client) { + const config = getConfig(client, 'activity-checks'); + if (!config?.enableActivityChecks || !config?.automatedChecks) return; + + let cronString = config.automatedCheckInterval === 'Cronjob' + ? config.automatedCheckCronjob + : null; + if (!cronString) { + const dayMap = { + 'Monday': 1, + 'Tuesday': 2, + 'Wednesday': 3, + 'Thursday': 4, + 'Friday': 5, + 'Saturday': 6, + 'Sunday': 7 + }[config.automatedCheckWeekDay] || 1; + if (['Weekly', 'Biweekly'].includes(config.automatedCheckInterval)) cronString = `0 12 * * ${dayMap}`; + else if (config.automatedCheckInterval === 'Monthly') { + const startDay = [1, 8, 15, 22][(config.automatedCheckMonthWeek || 1) - 1]; + cronString = `0 12 ${startDay}-${startDay + 6} * ${dayMap}`; + } + } + if (!cronString) return; + + let toggleWeek = false; + schedule.scheduleJob('automated-activity-check', cronString, async () => { + if (config.automatedCheckInterval === 'Biweekly' && (toggleWeek = !toggleWeek, !toggleWeek)) return; + + const channel = client.guilds.cache.get(client.guildID)?.channels.cache.get(getSafeChannelId(config.sendingChannel)); + if (channel) { + client.logger.info(`[Activity Checks] Starting automated check.`); + await startActivityCheck(client, channel, true); + } + }); +} + +// ---------- Reviews ---------- +async function submitReview(client, interaction, targetUser, stars, comment) { + const config = getConfig(client, 'reviews'); + if (!config?.enableReviews) return interaction.reply({ + content: localize('staff-management-system', 'err-feat-disabled', { + feature: 'Reviews' + }), + flags: MessageFlags.Ephemeral + }); + + const targetMember = await interaction.guild.members.fetch(targetUser.id).catch(() => null); + if (!targetMember) return interaction.reply({ + content: localize('staff-management-system', 'err-not-mem'), + flags: MessageFlags.Ephemeral + }); + if (!config.allowSelfRating && targetUser.id === interaction.user.id) return interaction.reply({ + content: localize('staff-management-system', 'err-self-rate'), + flags: MessageFlags.Ephemeral + }); + + if (config.onlyAllowStaffReview !== false) { + const genCfg = getConfig(client, 'configuration'); + if (!targetMember.roles.cache.some(r => [...(genCfg?.staffRoles || []), ...(genCfg?.supervisorRoles || []), ...(genCfg?.managementRoles || [])].includes(r.id))) { + return interaction.reply({ + content: localize('staff-management-system', 'err-staff-rate'), + flags: MessageFlags.Ephemeral + }); + } + } + + const review = await client.models['staff-management-system']['StaffReview'].create({ + targetId: targetUser.id, + authorId: interaction.user.id, + stars, + comment + }); + const channelId = getSafeChannelId(config.reviewLogChannel); + + if (channelId) { + const channel = interaction.guild.channels.cache.get(channelId); + if (channel) { + let msgOpts = await embedTypeV2(config.ratingMessage, { + '%target%': targetUser.toString(), + '%author%': interaction.user.toString(), + '%stars%': "⭐".repeat(stars), + '%rating%': stars.toString(), + '%comment%': comment, + '%staff-profile-picture%': targetUser.displayAvatarURL({ dynamic: true }), + '%reviewer-profile-picture%': interaction.user.displayAvatarURL({ dynamic: true }) + }); + if (msgOpts?.content?.trim() === '') delete msgOpts.content; + const sentMessage = await channel.send(msgOpts).catch(()=>{}); + if (sentMessage) await review.update({ messageUrl: sentMessage.url }); + } + } + await interaction.reply({ + content: localize('staff-management-system', 'succ-review', { + tag: targetUser.tag, + stars + }), + flags: MessageFlags.Ephemeral + }); +} + +async function generateReviewHistoryResponse(client, targetUser, page = 1) { + if (!getConfig(client, 'reviews')?.enableReviews) return { + content: localize('staff-management-system', 'err-feat-disabled', { + feature: 'Reviews' + }), + flags: MessageFlags.Ephemeral + }; + + const limit = 8; + const offset = (page - 1) * limit; + const Review = client.models['staff-management-system']['StaffReview']; + + const { count, rows } = await Review.findAndCountAll({ + where: { targetId: targetUser.id }, + order: [['createdAt', 'DESC']], + limit, + offset + }); + const allReviews = await Review.findAll({ + where: { targetId: targetUser.id }, + attributes: ['stars'] + }); + const avg = allReviews.length + ? (allReviews.reduce((a, b) => a + b.stars, 0) / allReviews.length).toFixed(1) + : 0; + + const embed = applyFooter(client, new EmbedBuilder() + .setTitle(localize('staff-management-system', 'rev-title', { username: targetUser.username })) + .setColor('Gold') + .setDescription(localize('staff-management-system', 'rev-desc', { avg, count: allReviews.length })) + .setThumbnail(targetUser.displayAvatarURL({ dynamic: true })) + ); + + embed.addFields({ + name: localize('staff-management-system', 'label-hist'), + value: rows.length > 0 + ? rows.map(r => `**${"⭐".repeat(r.stars)}** ${localize('staff-management-system', 'label-by')} <@${r.authorId}>${r.messageUrl + ? ` • [Jump](${r.messageUrl})` + : ''}\n"${r.comment}"`).join('\n\n') + : localize('staff-management-system', 'p-no-hist') }); + + const row = buildPaginationRow( + `staff-mgmt_rev-page_${targetUser.id}_${page - 1}`, + 'page_count_disabled', + `staff-mgmt_rev-page_${targetUser.id}_${page + 1}`, + page, + Math.ceil(count / limit) || 1 + ); + return { + embeds: [embed.toJSON()], + components: [row.toJSON()] + }; +} + +async function getReviewHistory(client, interaction, targetUser) { + const response = await generateReviewHistoryResponse(client, targetUser, 1); + if (response.content && response.content.startsWith('❌')) return interaction.reply(response); + await interaction.reply({ + ...response, + flags: MessageFlags.Ephemeral + }); +} + +module.exports = { + logStatusChange, + getConfig, + applyFooter, + buildPaginationRow, + formatDuration, + issueInfraction, + issueSuspension, + getInfractionHistory, + voidInfraction, + generateInfractionHistoryResponse, + promoteUser, + generatePromotionHistoryResponse, + getPromotionHistory, + generateUserPanel, + generatePanelInfractions, + generatePanelPromotions, + generatePanelActivity, + generatePanelReviews, + generatePanelStatus, + generatePanelShifts, + generatePanelDeletion, + executeDataDeletion, + generatePanelSubpage, + handleStatusRequest, + handleStatusView, + handleStatusList, + handleStatusManage, + handleStatusEnd, + handleStatusEndSubmit, + handleStatusExtend, + handleStatusExtendSubmit, + handleStatusHistPage, + startActivityCheck, + initActivityCheckAutomation, + endActivityCheckProcess, + submitReview, + getReviewHistory, + generateReviewHistoryResponse, + sendStatusDm, + scheduleStatusExpiry +}; \ No newline at end of file diff --git a/src/events/interactionCreate.js b/src/events/interactionCreate.js index 99f0554e..b2c07ad3 100644 --- a/src/events/interactionCreate.js +++ b/src/events/interactionCreate.js @@ -82,7 +82,7 @@ module.exports.run = async (client, interaction) => { })); try { - if (command.options.filter(c => c.type === 'SUB_COMMAND').length === 0) return await command.run(interaction); + if (command.options.filter(c => c.type === 'SUB_COMMAND' || c.type === 'SUB_COMMAND_GROUP').length === 0) return await command.run(interaction); if (!command.subcommands) { interaction.client.logger.error(`Command ${interaction.commandName} has subcommands but does not use the subcommands handler (required).`); return interaction.reply({ From d569f1e3a0728c29a6b4fc98f00a4d3a54606f09 Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Thu, 26 Mar 2026 21:22:43 +0100 Subject: [PATCH 11/27] Fixed some changes and rewrote botReady to be better --- locales/en.json | 5 +- .../staff-management-system/commands/duty.js | 157 ++- .../commands/status.js | 774 ++++++++++++++- .../events/botReady.js | 184 +++- .../events/interactionCreate.js | 114 ++- .../staff-management.js | 902 +++--------------- 6 files changed, 1230 insertions(+), 906 deletions(-) diff --git a/locales/en.json b/locales/en.json index 26c54deb..deb020c1 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1280,6 +1280,7 @@ "ac-f-res": "✅ Responded (%count)", "ac-f-fail": "❌ Failed (%count)", "ac-f-exc": "🛡️ Exceptions (%count)", + "log-ac-send-fail": "Failed to send activity check results message: %error", "err-not-mem": "❌ Not a member.", "err-self-rate": "❌ Cannot rate yourself.", "err-staff-rate": "❌ Can only rate staff.", @@ -1411,6 +1412,8 @@ "cmd-desc-submit-user": "The user you are reviewing.", "cmd-desc-submit-stars": "The star rating for the review.", "cmd-desc-submit-comment": "Your review comment.", - "del-no-perm": "You do not have sufficient permissions to perform data deletion." + "del-no-perm": "You do not have sufficient permissions to perform data deletion.", + "log-err-break-recov": "Break recovery failed: %error", + "log-err-exp-susp": "Suspension check failed: %error" } } diff --git a/modules/staff-management-system/commands/duty.js b/modules/staff-management-system/commands/duty.js index 194d37bc..836bd271 100644 --- a/modules/staff-management-system/commands/duty.js +++ b/modules/staff-management-system/commands/duty.js @@ -12,6 +12,20 @@ function getLookbackDate(config) { return date; } +async function applyBreakElapsedToShift(activeShift, breakStartTime, now = new Date()) { + if (!activeShift || !breakStartTime) return; + + const breakStartedAt = new Date(breakStartTime); + if (Number.isNaN(breakStartedAt.getTime()) || breakStartedAt > now) return; + + const elapsedBreakMs = now.getTime() - breakStartedAt.getTime(); + if (elapsedBreakMs <= 0) return; + + await activeShift.update({ + startTime: new Date(new Date(activeShift.startTime).getTime() + elapsedBreakMs) + }); +} + function getQuotaForMember(member, config) { if (!config.enableQuotas || !config.quotas || Object.keys(config.quotas).length === 0) return null; @@ -235,20 +249,24 @@ async function buildLeaderboardPayload(client, page = 1, shiftType) { } const lookbackLabel = config.leaderboardLookback || 'Weekly'; - const embed = new EmbedBuilder() - .setTitle(localize('staff-management-system', 'duty-lb-title', { - type: shiftType + const embed = applyFooter(client, new EmbedBuilder() + .setTitle(localize('staff-management-system', 'duty-lb-title', { + type: shiftType })) .setColor('Gold') - .setDescription(localize('staff-management-system', 'duty-lb-desc', { - lookback: lookbackLabel, - lines: lines.join('\n') + .setDescription(localize('staff-management-system', 'duty-lb-desc', { + lookback: lookbackLabel, + lines: lines.join('\n') })) - .setFooter({ text: localize('staff-management-system', 'page-count', { - page, - total: totalPages - }) - }); + ); + + embed.addFields({ + name: '\u200b', + value: localize('staff-management-system', 'page-count', { + page, + total: totalPages + }) + }); const row = buildPaginationRow( `duty-mgmt_lb_${page - 1}_${shiftType}`, @@ -294,16 +312,20 @@ async function buildShiftHistoryPayload(client, userId, page = 1, shiftType) { return `**${offset + i + 1}. ${dur}${typeBadge}:**\nStart: | End: `; }); - const embed = new EmbedBuilder() - .setTitle(localize('staff-management-system', 'duty-hi-title', { - type: shiftType + const embed = applyFooter(client, new EmbedBuilder() + .setTitle(localize('staff-management-system', 'duty-hi-title', { + type: shiftType })) .setColor('Blue') .setDescription(lines.join('\n\n')) - .setFooter({ text: localize('staff-management-system', 'page-count', { - page, - total: totalPages - }) + ); + + embed.addFields({ + name: '\u200b', + value: localize('staff-management-system', 'page-count', { + page, + total: totalPages + }) }); const row = buildPaginationRow( @@ -474,14 +496,18 @@ async function handleDutyBreakButton(client, interaction) { const shiftType = activeShift?.type || 'Staff'; const nowOnBreak = !profile.onBreak; - await Profile.update({ - onBreak: nowOnBreak, + if (!nowOnBreak && profile.breakStartTime && activeShift) { + await applyBreakElapsedToShift(activeShift, profile.breakStartTime); + } + + await Profile.update({ + onBreak: nowOnBreak, breakStartTime: nowOnBreak ? new Date() - : null }, { - where: { userId } - } - ); + : null + }, { + where: { userId } + }); const payload = await buildDutyManagePayload(client, userId, interaction.guild, shiftType); return interaction.editReply(payload); @@ -508,9 +534,15 @@ async function handleDutyEndButton(client, interaction) { const shiftType = activeShifts.length > 0 ? activeShifts[0].type : 'Staff'; for (const activeShift of activeShifts) { + if (profile.onBreak && profile.breakStartTime) { + await applyBreakElapsedToShift(activeShift, profile.breakStartTime); + } + const endTime = new Date(); - const durationSeconds = Math.floor((endTime.getTime() - new Date(activeShift.startTime).getTime()) / 1000); - + const durationSeconds = Math.floor( + (endTime.getTime() - new Date(activeShift.startTime).getTime()) / 1000 + ); + if (config.minShiftDuration && (durationSeconds / 60) < config.minShiftDuration) { await activeShift.destroy(); } else { @@ -577,16 +609,24 @@ async function handleDutyAdminForceEnd(client, interaction) { const config = getConfig(client, 'shifts'); const Profile = client.models['staff-management-system']['StaffProfile']; const Shift = client.models['staff-management-system']['StaffShift']; + const profile = await Profile.findByPk(targetUserId); const activeShifts = await Shift.findAll({ where: { userId: targetUserId, endTime: null } }); for (const activeShift of activeShifts) { + if (profile?.onBreak && profile.breakStartTime) { + await applyBreakElapsedToShift(activeShift, profile.breakStartTime); + } + const endTime = new Date(); - const durationSeconds = Math.floor((endTime.getTime() - new Date(activeShift.startTime).getTime()) / 1000); - await activeShift.update({ - endTime, - duration: durationSeconds + const durationSeconds = Math.floor( + (endTime.getTime() - new Date(activeShift.startTime).getTime()) / 1000 + ); + + await activeShift.update({ + endTime, + duration: durationSeconds }); } @@ -911,6 +951,7 @@ module.exports.subcommands = { }); const Shift = i.client.models['staff-management-system']['StaffShift']; + const Profile = i.client.models['staff-management-system']['StaffProfile']; const activeShifts = await Shift.findAll({ where: { endTime: null }, order: [['startTime', 'ASC']] @@ -920,6 +961,13 @@ module.exports.subcommands = { content: localize('staff-management-system', 'info-no-act-sh') }); + const profiles = await Profile.findAll({ + where: { + userId: activeShifts.map(shift => shift.userId) + } + }); + const profileMap = new Map(profiles.map(profile => [profile.userId, profile])); + const dutyTypes = config.dutyTypes && config.dutyTypes.length > 0 ? config.dutyTypes : ['Staff']; @@ -944,8 +992,25 @@ module.exports.subcommands = { if (grouped[type]) { const lines = []; for (const shift of grouped[type]) { - const elapsed = Math.floor((Date.now() - new Date(shift.startTime).getTime()) / 1000); - lines.push(`${index}. **<@${shift.userId}>** • ${formatDuration(elapsed)}`); + const profile = profileMap.get(shift.userId); + const isOnBreak = profile?.onBreak && profile?.breakStartTime; + + let elapsed; + if (isOnBreak) { + elapsed = Math.floor( + (new Date(profile.breakStartTime).getTime() - new Date(shift.startTime).getTime()) / 1000 + ); + } else { + elapsed = Math.floor( + (Date.now() - new Date(shift.startTime).getTime()) / 1000 + ); + } + + const breakSuffix = isOnBreak + ? ` (${localize('staff-management-system', 'stat-brk')})` + : ''; + + lines.push(`${index}. **<@${shift.userId}>** • ${formatDuration(elapsed)}${breakSuffix}`); index++; } embed.addFields({ @@ -958,13 +1023,31 @@ module.exports.subcommands = { for (const [type, shifts] of Object.entries(grouped)) { const lines = []; for (const shift of shifts) { - const elapsed = Math.floor((Date.now() - new Date(shift.startTime).getTime()) / 1000); - lines.push(`${index}. **<@${shift.userId}>** • ${formatDuration(elapsed)}`); + const profile = profileMap.get(shift.userId); + const isOnBreak = profile?.onBreak && profile?.breakStartTime; + + let elapsed; + if (isOnBreak) { + elapsed = Math.floor( + (new Date(profile.breakStartTime).getTime() - new Date(shift.startTime).getTime()) / 1000 + ); + } else { + elapsed = Math.floor( + (Date.now() - new Date(shift.startTime).getTime()) / 1000 + ); + } + + const breakSuffix = isOnBreak + ? ` (${localize('staff-management-system', 'stat-brk')})` + : ''; + + lines.push(`${index}. **<@${shift.userId}>** • ${formatDuration(elapsed)}${breakSuffix}`); index++; } - embed.addFields({ - name: `${type} (${shifts.length}) [Legacy]`, - value: lines.join('\n') + + embed.addFields({ + name: `${type} (${shifts.length}) [Legacy]`, + value: lines.join('\n') }); } await i.editReply({ diff --git a/modules/staff-management-system/commands/status.js b/modules/staff-management-system/commands/status.js index 44177d1e..b9af2a2a 100644 --- a/modules/staff-management-system/commands/status.js +++ b/modules/staff-management-system/commands/status.js @@ -1,6 +1,761 @@ -const { MessageFlags } = require('discord.js'); -const { handleStatusRequest, handleStatusView, handleStatusList, handleStatusManage } = require('../staff-management'); +const { + MessageFlags, + EmbedBuilder, + ModalBuilder, + TextInputBuilder, + TextInputStyle, + ActionRowBuilder, + ButtonBuilder, + ButtonStyle +} = require('discord.js'); +const { Op } = require('sequelize'); +const schedule = require('node-schedule'); +const { formatDate } = require('../../../src/functions/helpers'); const { localize } = require('../../../src/functions/localize'); +const { + getConfig, + getSafeChannelId, + parseDurationToDays, + buildPaginationRow, + applyFooter, + checkStaffPermissions +} = require('../staff-management'); + +// ---------- Status DM's and logging ---------- +async function sendStatusDm(user, type, dmType, data = {}) { + const label = type === 'LOA' + ? 'LoA' + : 'RA'; + const viewCmd = type === 'LOA' + ? '`/loa view`' + : '`/ra view`'; + const endFmt = data.endDate + ? `` + : ''; + + // These messages use the locales key to be easily used later + const messages = { + approved: { + title: 'dm-appr-title', + color: 'Green', + desc: 'dm-appr-desc', + params: { label, approver: data.approver, endFmt, viewCmd } + }, + denied: { + title: 'dm-deny-title', + color: 'Red', + desc: 'dm-deny-desc', + params: { label, denier: data.denier, reason: data.reason } + }, + extended: { + title: 'dm-ext-title', + color: 'Yellow', + desc: 'dm-ext-desc', + params: { label, extender: data.extender, days: data.days, endFmt, reason: data.reason, viewCmd } + }, + ended_early: { + title: 'dm-early-title', + color: 'Red', + desc: 'dm-early-desc', + params: { label, ender: data.ender, reason: data.reason } + }, + ended: { + title: 'dm-end-title', + color: 'Black', + desc: 'dm-end-desc', + params: { label } + } + }; + + const msg = messages[dmType]; + if (!msg) return; + + const embed = new EmbedBuilder() + .setTitle(localize('staff-management-system', msg.title, msg.params)) + .setDescription(localize('staff-management-system', msg.desc, msg.params)) + .setColor(msg.color); + applyFooter(user.client, embed); + + try { + await user.send({ + embeds: [embed.toJSON()] + }); + } catch (e) { + user.client.logger.error( + localize('staff-management-system', 'log-stat-dm-error', { + e: e.message, + u: user.tag + }) + ); +} +} + +async function logStatusChange(client, type, action, data) { + const statusConfig = getConfig(client, 'status'); + if (!statusConfig?.logStatusChanges) return; + + const channelId = getSafeChannelId(statusConfig.statusChangeLogChannel) || getSafeChannelId(getConfig(client, 'configuration')?.generalLogChannel); + if (!channelId) return; + + const guild = client.guilds.cache.get(client.guildID); + if (!guild) return; + const channel = await guild.channels.fetch(channelId).catch(() => null); + if (!channel) return; + + const label = type === 'LOA' + ? 'LoA' + : 'RA'; + const targetUserObj = data.targetUser || await client.users.fetch(data.userId).catch(() => null); + const mention = targetUserObj + ? targetUserObj.toString() + : `<@${data.userId}>`; + const username = targetUserObj + ? targetUserObj.username + : data.userId; + + const embed = new EmbedBuilder() + .setThumbnail(targetUserObj + ?.displayAvatarURL({ dynamic: true }) || null); + + if (action === 'start') { + embed.setTitle(localize('staff-management-system', 'log-start-title', { label, username })) + .setColor('Green') + .setDescription(localize('staff-management-system', 'log-start-desc', + { label, mention, apprText: data.approverId + ? ` ${localize('staff-management-system', 'label-appr-by')}: <@${data.approverId}>.` + : '' + })) + .addFields({ + name: localize('staff-management-system', 'log-info-hdr', { label }), + value: `**${localize('staff-management-system', 'general-start')}:** \n**${localize('staff-management-system', 'general-end')}:** \n**${localize('staff-management-system', 'general-rsn')}:** ${data.reason || localize('staff-management-system', 'none-provided')}` + }); + + } else if (action === 'end') { + embed.setTitle(localize('staff-management-system', 'log-end-title', { label, username })) + .setColor('Red') + .setDescription(localize('staff-management-system', 'log-end-desc', { label, mention })) + .addFields({ + name: localize('staff-management-system', 'log-info-hdr', { label }), + value: `**${localize('staff-management-system', 'general-started')}:** \n**${localize('staff-management-system', 'general-ended')}:** \n**${localize('staff-management-system', 'general-rsn')}:** ${data.reason || localize('staff-management-system', 'none-provided')}` + }); + + } else if (action === 'adjusted') { + embed.setTitle(localize('staff-management-system', 'log-adj-title', { label, username })) + .setColor('Yellow') + .setDescription(localize('staff-management-system', 'log-adj-desc', { label, mention, executor: data.executorId })) + .addFields({ + name: localize('staff-management-system', 'log-changes'), + value: data.changesText + }); + } + + applyFooter(client, embed); + try { + await channel.send({ + embeds: [embed.toJSON()] + }); + } catch (e) { + client.logger.error( + localize('staff-management-system', 'log-status-adj-error', { + e: e.message + }) + ); + } +} + +// ----- Status ----- +const getStatusMeta = (type) => ({ + isLoa: type === 'LOA', + label: type === 'LOA' + ? 'LoA' + : 'RA', + enableKey: type === 'LOA' + ? 'enableLoa' + : 'enableRa', + roleKey: type === 'LOA' + ? 'loaRole' + : 'raRole', + maxDaysKey: type === 'LOA' + ? 'loaMaxDays' + : 'raMaxDays', + color: type === 'LOA' + ? 'Green' + : 'Orange', + activeText: localize('staff-management-system', type === 'LOA' + ? 'status-active-loa' + : 'status-active-ra' + ), + histTitle: localize('staff-management-system', type === 'LOA' + ? 'status-hist-loa' + : 'status-hist-ra' + ), + actionPrefix: type === 'LOA' + ? 'loa' + : 'ra' +}); + +async function handleStatusRequest(client, interaction, type, durationInput, reason) { + const config = getConfig(client, 'status'); + const isLoa = type === 'LOA'; + if (!config[isLoa + ? 'enableLoa' + : 'enableRa']) return interaction.editReply({ + content: localize('staff-management-system', 'err-status-disabled', { type }) + } + ); + + const days = parseDurationToDays(durationInput?.trim()); + if (!days || isNaN(days) || days <= 0) return interaction.editReply({ + content: localize('staff-management-system', 'err-invalid-duration') + }); + + const maxDays = (isLoa ? config.loaMaxDays : config.raMaxDays) || (isLoa ? 60 : 30); + if (days > maxDays) return interaction.editReply({ + content: localize('staff-management-system', 'err-duration-max', { max: maxDays }) + }); + + const LoaRequest = client.models['staff-management-system']['LoaRequest']; + if (await LoaRequest.findOne({ + where: { userId: interaction.user.id, type, status: { [Op.in]: ['PENDING', 'APPROVED'] }, + endDate: { [Op.gt]: new Date() } } + })) { + return interaction.editReply({ + content: localize('staff-management-system', 'err-status-exists', { type }) + }); + } + + const startDate = new Date(); + const endDate = new Date(startDate.getTime() + days * 24 * 60 * 60 * 1000); + const needsApproval = isLoa + ? config.requireLoaApproval !== false + : config.requireRaApproval !== false; + + const req = await LoaRequest.create({ + userId: interaction.user.id, + type, + reason, + startDate, + endDate, + status: needsApproval + ? 'PENDING' + : 'APPROVED' + }); + + const logChannelId = getSafeChannelId(config.statusLogChannel); + if (logChannelId && needsApproval) { + const channel = await interaction.guild.channels.fetch(logChannelId).catch(() => null); + if (channel) { + const embed = new EmbedBuilder() + .setTitle(localize('staff-management-system', 'status-request-title', { type })) + .setColor('Blue') + .setAuthor({ name: `Request ID: ${req.id}`}) + .addFields( + { name: localize('staff-management-system', 'status-req-user'), + value: interaction.user.toString(), + inline: true + }, + { name: localize('staff-management-system', 'status-req-duration'), + value: `${days} ${localize('staff-management-system', 'label-days')}`, + inline: true + }, + { name: localize('staff-management-system', 'general-rsn'), + value: reason + } + ); + + applyFooter(client, embed); + const row = new ActionRowBuilder() + .addComponents(new ButtonBuilder() + .setCustomId(`staff-mgmt_approve_${req.id}`) + .setLabel(localize('staff-management-system', 'btn-approve')) + .setStyle(ButtonStyle.Success), + new ButtonBuilder() + .setCustomId(`staff-mgmt_deny_${req.id}`) + .setLabel(localize('staff-management-system', 'btn-deny')) + .setStyle(ButtonStyle.Danger)); + channel.send({ embeds: [embed.toJSON()], components: [row.toJSON()] }).catch(()=>{}); + } + } + + if (!needsApproval) { + const roleId = config[isLoa ? 'loaRole' : 'raRole']; + if (roleId) interaction.member.roles.add(roleId).catch(()=>{}); + await logStatusChange(client, type, 'start', { + targetUser: interaction.user, + startDate, + endDate, + reason, + approverId: null + }); + } + + await interaction.editReply({ + content: localize('staff-management-system', 'success-status-request', { + type, state: needsApproval + ? localize('staff-management-system', 'state-pending') + : localize('staff-management-system', 'state-auto') + }) + }); +} + +async function handleStatusView(client, interaction, type, targetUser) { + const user = targetUser || interaction.user; + const request = await client.models['staff-management-system']['LoaRequest'].findOne({ + where: { userId: user.id, type, status: { [Op.in]: ['APPROVED', 'PENDING'] }, + endDate: { [Op.gt]: new Date() } }, + order: [['createdAt', 'DESC']] + }); + + if (!request) return interaction.editReply({ + content: localize('staff-management-system', 'no-active-status', { + user: user.username, + type + }) + }); + + const embed = new EmbedBuilder() + .setTitle(`${type} Status: ${user.username}`) + .setColor(request.status === 'APPROVED' + ? 'Green' + : 'Yellow' + ) + .addFields( + { + name: localize('staff-management-system', 'label-stat'), + value: request.status, + inline: true }, + { + name: localize('staff-management-system', 'label-end'), + value: formatDate(request.endDate), + inline: true }, + { + name: localize('staff-management-system', 'general-rsn'), + value: request.reason || localize('staff-management-system', 'info-none') + }) + .setThumbnail(user.displayAvatarURL({ dynamic: true })); + applyFooter(client, embed); + await interaction.editReply({ embeds: [embed.toJSON()] }); +} + +async function handleStatusList(client, interaction, type, filter, page = 1) { + const limit = 10; + const offset = (page - 1) * limit; + + let whereClause = { type }; + let title = `${type} List`; + + if (filter === 'active') { + whereClause.status = 'APPROVED'; + whereClause.endDate = { [Op.gt]: new Date() }; + title += localize('staff-management-system', 'filter-active'); + } + else if (filter === 'expired') { + whereClause.endDate = { [Op.lt]: new Date() }; + title += localize('staff-management-system', 'filter-expired'); + } + else { + whereClause.status = { [Op.ne]: 'PENDING' }; + title += localize('staff-management-system', 'filter-history'); + } + + const { count, rows } = await client.models['staff-management-system']['LoaRequest'].findAndCountAll({ + where: whereClause, + limit, + offset, + order: [['endDate', 'DESC']] + }); + if (count === 0) return interaction.editReply({ + content: localize('staff-management-system', 'err-no-recs') + }); + + const totalPages = Math.ceil(count / limit) || 1; + const embed = new EmbedBuilder() + .setTitle(title) + .setColor('Blue') + .setDescription(rows.map(r => `**<@${r.userId}>** ${r.status === 'APPROVED' ? '✅' : (r.status === 'DENIED' ? '❌' : '⏹️')}\nEnds: ${formatDate(r.endDate)}\nReason: ${r.reason}`).join('\n\n')) + .addFields( + { + name: '\u200b', + value: localize('staff-management-system', 'page-count', { page, total: totalPages }) + } + ); + applyFooter(client, embed); + await interaction.editReply({ embeds: [embed.toJSON()] }); +} + +async function handleStatusManage(client, interaction, targetMember, type) { + const config = getConfig(client, 'status'); + const meta = getStatusMeta(type); + if (!config[meta.enableKey]) return interaction.editReply({ + content: localize('staff-management-system', 'err-status-disabled', { type }) + }); + + const generalConfig = getConfig(client, 'configuration'); + if (!checkStaffPermissions(interaction.member, generalConfig, 'supervisor')) { + return interaction.editReply({ + content: localize('staff-management-system', 'err-gen-no-perm') + })}; + + const LoaRequest = client.models['staff-management-system']['LoaRequest']; + const activeRequest = await LoaRequest.findOne({ + where: { + userId: targetMember.user.id, + type, + status: { [Op.in]: ['APPROVED', 'PENDING'] }, + endDate: { [Op.gt]: new Date() } + }, + order: [['createdAt', 'DESC']] + } + ); + const totalCount = await LoaRequest.count({ + where: { userId: targetMember.user.id, type } + }); + + const embed = applyFooter(client, new EmbedBuilder() + .setTitle(localize('staff-management-system', 'manage-status-title', { + label: meta.label, + username: targetMember.user.username + })) + .setThumbnail(targetMember.user.displayAvatarURL({ dynamic: true })) + .setColor(activeRequest + ? meta.color + : 'Grey' + ) + .setDescription(localize('staff-management-system', 'manage-stat-desc', { + status: activeRequest + ? meta.activeText + : localize('staff-management-system', 'no-act-stat', { + label: meta.label + }), + label: meta.label, + count: Math.max(0, totalCount - (activeRequest ? 1 : 0)) + })) + ); + + embed.addFields({ + name: localize('staff-management-system', 'manage-active-details', { label: meta.label }), + value: activeRequest ? `**${localize('staff-management-system', 'general-start')}:** ${formatDate(activeRequest.startDate)}\n**${localize('staff-management-system', 'general-end')}:** ${formatDate(activeRequest.endDate)}\n**${localize('staff-management-system', 'label-stat')}:** ${activeRequest.status}\n**${localize('staff-management-system', 'label-appr-by')}:** ${activeRequest.approverId ? `<@${activeRequest.approverId}>` : localize('staff-management-system', 'label-auto')}\n**${localize('staff-management-system', 'general-rsn')}:** ${activeRequest.reason || localize('staff-management-system', 'info-none')}` : localize('staff-management-system', 'manage-no-active-user', { label: meta.label }) + }); + + const p = meta.actionPrefix; + const rid = activeRequest?.id ?? 'none'; + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(`staff-mgmt_${p}-end_${rid}`) + .setLabel(localize('staff-management-system', 'btn-end-early', { label: meta.label })) + .setEmoji('🚫').setStyle(ButtonStyle.Danger) + .setDisabled(!activeRequest), + new ButtonBuilder() + .setCustomId(`staff-mgmt_${p}-extend_${rid}`) + .setLabel(localize('staff-management-system', 'btn-extend', { label: meta.label })) + .setEmoji('⏳') + .setStyle(ButtonStyle.Primary) + .setDisabled(!activeRequest), + new ButtonBuilder() + .setCustomId(`staff-mgmt_${p}-hist_${targetMember.user.id}_1`) + .setLabel(localize('staff-management-system', 'btn-view-history')) + .setEmoji('📜') + .setStyle(ButtonStyle.Secondary) + .setDisabled(totalCount === 0) + ); + await interaction.editReply({ + embeds: [embed.toJSON()], + components: [row.toJSON()] + }); +} + +async function handleStatusEnd(interaction, type) { + const meta = getStatusMeta(type); + const requestId = interaction.customId.split('_')[2]; + if (requestId === 'none') return interaction.reply({ + content: localize('staff-management-system', 'err-no-active-end', { label: meta.label }), + flags: MessageFlags.Ephemeral + }); + + const modal = new ModalBuilder() + .setCustomId(`staff-mgmt_${meta.actionPrefix}-end-submit_${requestId}`) + .setTitle(localize('staff-management-system', 'modal-end-early-title', { label: meta.label })); + modal.addComponents(new ActionRowBuilder() + .addComponents( + new TextInputBuilder() + .setCustomId('end_reason') + .setLabel(localize('staff-management-system', 'modal-end-early-reason')) + .setStyle(TextInputStyle.Paragraph) + .setRequired(true) + )); + return interaction.showModal(modal); +} + +async function handleStatusEndSubmit(client, interaction, type) { + const meta = getStatusMeta(type); + const request = await client.models['staff-management-system']['LoaRequest'].findByPk(interaction.customId.split('_')[2]); + if (!request || request.status === 'ENDED' || request.status === 'DENIED') return interaction.reply({ + content: localize('staff-management-system', 'err-stat-inact', { label: meta.label }), + flags: MessageFlags.Ephemeral + }); + + const reason = interaction.fields.getTextInputValue('end_reason'); + const member = await interaction.guild.members.fetch(request.userId).catch(() => null); + + if (member && getConfig(client, 'status')[meta.roleKey]) await member.roles.remove(getConfig(client, 'status')[meta.roleKey]).catch(() => {}); + + await request.update({ status: 'ENDED', endDate: new Date() }); + await client.models['staff-management-system']['StaffProfile'].update({ activityStatus: 'ACTIVE' }, { + where: { userId: request.userId } + }); + + if (member) await sendStatusDm(member.user, type, 'ended_early', { + ender: interaction.user.tag, + reason + }); + await logStatusChange(client, type, 'end', { + userId: request.userId, + startDate: request.startDate, + reason: request.reason + }); + + const updatedEmbed = EmbedBuilder.from(interaction.message.embeds[0]) + .setColor('Grey') + .setDescription(localize('staff-management-system', 'status-ended-embed-desc', { + label: meta.label, user: interaction.user.tag, reason + })) + .spliceFields(0, 1, { + name: localize('staff-management-system', 'manage-active-details', { label: meta.label }), + value: localize('staff-management-system', 'manage-no-active-user', { label: meta.label }) + }); + + const p = meta.actionPrefix; + const disabledRow = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId(`${p}-end-done`) + .setLabel(localize('staff-management-system', 'btn-end-early', { label: meta.label })) + .setEmoji('🚫') + .setStyle(ButtonStyle.Danger) + .setDisabled(true), + new ButtonBuilder() + .setCustomId(`${p}-extend-done`) + .setLabel(localize('staff-management-system', 'btn-extend', { label: meta.label })) + .setEmoji('⏳') + .setStyle(ButtonStyle.Primary) + .setDisabled(true), + new ButtonBuilder() + .setCustomId(`staff-mgmt_${p}-hist_${request.userId}_1`) + .setLabel(localize('staff-management-system', 'btn-view-history')) + .setEmoji('📜') + .setStyle(ButtonStyle.Secondary) + ); + return interaction.update({ + embeds: [updatedEmbed.toJSON()], + components: [disabledRow.toJSON()] + }); +} + +async function handleStatusExtend(interaction, type) { + const meta = getStatusMeta(type); + const requestId = interaction.customId.split('_')[2]; + if (requestId === 'none') return interaction.reply({ + content: localize('staff-management-system', 'err-no-active-extend', { label: meta.label }), + flags: MessageFlags.Ephemeral + }); + + const modal = new ModalBuilder() + .setCustomId(`staff-mgmt_${meta.actionPrefix}-extend-submit_${requestId}`) + .setTitle(localize('staff-management-system', 'modal-extend-title', { + label: meta.label + })); + modal.addComponents( + new ActionRowBuilder() + .addComponents( + new TextInputBuilder() + .setCustomId('extend_days') + .setLabel(localize('staff-management-system', 'modal-extend-days')) + .setStyle(TextInputStyle.Short) + .setPlaceholder("7") + .setRequired(true) + ), + new ActionRowBuilder() + .addComponents( + new TextInputBuilder() + .setCustomId('extend_reason') + .setLabel(localize('staff-management-system', 'modal-extend-reason')) + .setStyle(TextInputStyle.Paragraph) + .setRequired(true) + ) + ); + return interaction.showModal(modal); +} + +function scheduleStatusExpiry(client, request) { + const jobName = `staff-mgmt-status-expiry-${request.id}`; + const existingJob = schedule.scheduledJobs[jobName]; + if (existingJob) existingJob.cancel(); + + schedule.scheduleJob(jobName, new Date(request.endDate), async () => { + try { + const req = await client.models['staff-management-system']['LoaRequest'].findByPk(request.id); + if (!req || req.status !== 'APPROVED' || new Date(req.endDate) > new Date()) return; + + await req.update({ status: 'ENDED' }); + await client.models['staff-management-system']['StaffProfile'].update( + { activityStatus: 'ACTIVE' }, + { where: { userId: req.userId } } + ); + + const member = await client.guilds.cache.get(client.guildID)?.members.fetch(req.userId).catch(() => null); + if (member) { + const roleKey = req.type === 'LOA' ? 'loaRole' : 'raRole'; + const roleId = getConfig(client, 'status')[roleKey]; + if (roleId) await member.roles.remove(roleId).catch(() => {}); + await sendStatusDm(member.user, req.type, 'ended'); + } + + await logStatusChange(client, req.type, 'end', { + userId: req.userId, + startDate: req.startDate, + reason: req.reason + }); + } catch (e) { + client.logger.error(localize('staff-management-system', 'log-status-expiry-fail', { + error: e.message + })); + } + }); +} + +async function handleStatusExtendSubmit(client, interaction, type) { + const meta = getStatusMeta(type); + const request = await client.models['staff-management-system']['LoaRequest'].findByPk(interaction.customId.split('_')[2]); + if (!request || request.status === 'ENDED' || request.status === 'DENIED') return interaction.reply({ + content: localize('staff-management-system', 'err-stat-inact', { + label: meta.label + }), + flags: MessageFlags.Ephemeral + }); + + const days = parseInt(interaction.fields.getTextInputValue('extend_days'), 10); + const reason = interaction.fields.getTextInputValue('extend_reason'); + if (isNaN(days) || days <= 0 || days > 180) return interaction.reply({ + content: localize('staff-management-system', 'err-inv-dur'), + flags: MessageFlags.Ephemeral + }); + + const oldEndDate = new Date(request.endDate); + const newEndDate = new Date(oldEndDate.getTime() + days * 24 * 60 * 60 * 1000); + await request.update({ endDate: newEndDate }); + scheduleStatusExpiry(client, request); + + const member = await interaction.guild.members.fetch(request.userId).catch(() => null); + if (member) await sendStatusDm(member.user, type, 'extended', { + extender: interaction.user.tag, + days, + endDate: newEndDate, + reason + }); + await logStatusChange(client, type, 'adjusted', { + userId: request.userId, + executorId: interaction.user.id, + changesText: localize('staff-management-system', 'status-adjusted-log', { + label: meta.label, + newEnd: ``, + oldEnd: ``, + reason + }) + }); + + const updatedEmbed = EmbedBuilder.from(interaction.message.embeds[0]) + .spliceFields(0, 1, { + name: localize('staff-management-system', 'manage-active-details', { label: meta.label }), + value: localize('staff-management-system', 'mod-stat-ext', { + s: formatDate(request.startDate), + e: formatDate(newEndDate), + d: days, + t: request.status, + a: request.approverId + ? `<@${request.approverId}>` + : localize('staff-management-system', 'label-auto'), + r: request.reason || localize('staff-management-system', 'info-none') + }) + }); + return interaction.update({ + embeds: [updatedEmbed.toJSON()], + components: interaction.message.components.map(c => c.toJSON()) + }); +} + +async function generateStatusHistoryResponse(client, targetUser, page = 1, type) { + const meta = getStatusMeta(type); + const limit = 5; + const offset = (page - 1) * limit; + + const { count, rows } = await client.models['staff-management-system']['LoaRequest'].findAndCountAll({ + where: { userId: targetUser.id, type }, + order: [['createdAt', 'DESC']], + limit, + offset + }); + if (count === 0) return { + content: localize('staff-management-system', 'info-no-status-history', { label: meta.label }), + flags: MessageFlags.Ephemeral + }; + + const totalPages = Math.ceil(count / limit) || 1; + const embed = applyFooter(client, new EmbedBuilder() + .setTitle(`${meta.histTitle} - ${targetUser.username}`) + .setThumbnail(targetUser.displayAvatarURL({ dynamic: true })) + .setColor(meta.color) + .setDescription(localize('staff-management-system', 'status-history-desc', { + count: rows.length, + total: count, + label: meta.label + } + )) + ); + + const statusIcons = { + APPROVED: '✅', + DENIED: '❌', + ENDED: '⏹️', + PENDING: '🕐' + }; + rows.forEach((req, index) => embed.addFields({ + name: `${statusIcons[req.status] ?? '❓'} ${meta.label} #${offset + index + 1} - ${req.status}`, + value: `**${localize('staff-management-system', 'general-start')}:** ${formatDate(req.startDate)}\n**${localize('staff-management-system', 'general-end')}:** ${formatDate(req.endDate)}\n**${localize('staff-management-system', 'label-appr-by')}:** ${req.approverId ? `<@${req.approverId}>` : localize('staff-management-system', 'label-auto')}\n**${localize('staff-management-system', 'general-rsn')}:** ${req.reason || localize('staff-management-system', 'info-none')}` })); + embed.addFields({ + name: '\u200b', + value: localize('staff-management-system', 'page-count', { page, total: totalPages }) + }); + + const row = buildPaginationRow( + `staff-mgmt_${meta.actionPrefix}-hist_${targetUser.id}_${page - 1}`, + `${meta.actionPrefix}_hist_page_count`, + `staff-mgmt_${meta.actionPrefix}-hist_${targetUser.id}_${page + 1}`, + page, + totalPages + ); + return { + embeds: [embed.toJSON()], + components: [row.toJSON()] + }; +} + +async function handleStatusHistPage(client, interaction, type) { + const parts = interaction.customId.split('_'); + const targetUser = await client.users.fetch(parts[2]).catch(() => null); + if (!targetUser) return interaction.reply({ + content: localize('staff-management-system', 'err-gen-no-user'), + flags: MessageFlags.Ephemeral + }); + + const payload = await generateStatusHistoryResponse(client, targetUser, parseInt(parts[3], 10), type); + if (payload.content) return interaction.reply({ + ...payload, + flags: MessageFlags.Ephemeral + }); + return interaction.message?.embeds?.[0]?.title?.startsWith(getStatusMeta(type).histTitle) + ? interaction.update(payload) + : interaction.reply({ ...payload, flags: MessageFlags.Ephemeral }); +} module.exports.beforeSubcommand = async function (interaction) { if (!interaction.replied && !interaction.deferred) { @@ -218,4 +973,17 @@ module.exports.config = { ] } ] -}; \ No newline at end of file +}; + +module.exports.sendStatusDm = sendStatusDm; +module.exports.logStatusChange = logStatusChange; +module.exports.handleStatusRequest = handleStatusRequest; +module.exports.handleStatusView = handleStatusView; +module.exports.handleStatusList = handleStatusList; +module.exports.handleStatusManage = handleStatusManage; +module.exports.handleStatusEnd = handleStatusEnd; +module.exports.handleStatusEndSubmit = handleStatusEndSubmit; +module.exports.handleStatusExtend = handleStatusExtend; +module.exports.handleStatusExtendSubmit = handleStatusExtendSubmit; +module.exports.handleStatusHistPage = handleStatusHistPage; +module.exports.scheduleStatusExpiry = scheduleStatusExpiry; \ No newline at end of file diff --git a/modules/staff-management-system/events/botReady.js b/modules/staff-management-system/events/botReady.js index 615cd05a..144688c5 100644 --- a/modules/staff-management-system/events/botReady.js +++ b/modules/staff-management-system/events/botReady.js @@ -1,81 +1,185 @@ const schedule = require('node-schedule'); const { localize } = require('../../../src/functions/localize'); const { Op } = require('sequelize'); -const { scheduleStatusExpiry } = require('../staff-management'); +const { scheduleStatusExpiry } = require('../commands/status.js'); +const suspension_check_job = 'staff-management-checks'; module.exports.run = async (client) => { + const guild = client.guilds.cache.get(client.guildID); try { const LoaRequest = client.models['staff-management-system']['LoaRequest']; const activeRequests = await LoaRequest.findAll({ where: { status: 'APPROVED' } }); - let loaded = 0; for (const req of activeRequests) { scheduleStatusExpiry(client, req); - loaded++; } } catch (e) { - client.logger.error(localize('staff-management-system', 'log-sched-fail', { error: e.message })); + client.logger.error(localize('staff-management-system', 'log-sched-fail', { + error: e.message + })); } - const jobName = 'staff-management-checks'; - const existingJob = schedule.scheduledJobs[jobName]; + if (guild) { + try { + await recoverActiveBreaks(client); + } catch (e) { + client.logger.error(localize('staff-management-system', 'log-err-break-recov', { + error: e.message + })); + } + + try { + await checkExpiredSuspensions(client, guild); + } catch (e) { + client.logger.error(localize('staff-management-system', 'log-err-exp-susp', { + error: e.message + })); + } + } + + const existingJob = schedule.scheduledJobs[suspension_check_job]; if (existingJob) existingJob.cancel(); - const job = schedule.scheduleJob(jobName, '0 * * * *', async function() { + schedule.scheduleJob(suspension_check_job, '0 * * * *', async () => { if (!client.botReadyAt) return; - + const guild = client.guilds.cache.get(client.guildID); if (!guild) return; - await checkExpiredSuspensions(client, guild); + try { + await checkExpiredSuspensions(client, guild); + } catch (e) { + client.logger.error(localize('staff-management-system', 'log-err-exp-susp', { + error: e.message + })); + } }); - if (!client.intervals) client.intervals = []; - client.intervals.push(job); }; +async function recoverActiveBreaks(client) { + const StaffProfile = client.models['staff-management-system']['StaffProfile']; + const Shift = client.models['staff-management-system']['StaffShift']; + const now = new Date(); + + const profilesOnBreak = await StaffProfile.findAll({ + where: { + onDuty: true, + onBreak: true, + breakStartTime: { [Op.not]: null } + } + }); + + if (!profilesOnBreak.length) return; + + const userIds = profilesOnBreak.map(profile => profile.userId); + + const activeShifts = await Shift.findAll({ + where: { + userId: { [Op.in]: userIds }, + endTime: null + } + }); + + const activeShiftMap = new Map(activeShifts.map(shift => [shift.userId, shift])); + + for (const profile of profilesOnBreak) { + const activeShift = activeShiftMap.get(profile.userId); + + if (!activeShift) { + await profile.update({ + onBreak: false, + breakStartTime: null + }); + continue; + } + + const breakStartedAt = new Date(profile.breakStartTime); + if (Number.isNaN(breakStartedAt.getTime()) || breakStartedAt > now) { + await profile.update({ breakStartTime: now }); + continue; + } + + const elapsedBreakMs = now.getTime() - breakStartedAt.getTime(); + + await activeShift.update({ + startTime: new Date(new Date(activeShift.startTime).getTime() + elapsedBreakMs) + }); + + await profile.update({ + breakStartTime: now + }); + } +} + async function checkExpiredSuspensions(client, guild) { const Infraction = client.models['staff-management-system']['Infraction']; const StaffProfile = client.models['staff-management-system']['StaffProfile']; const config = client.configurations['staff-management-system']['infractions']; - const activeSuspensions = await Infraction.findAll({ - where: { type: 'Suspension', active: true } + const now = new Date(); + + const expiredSuspensions = await Infraction.findAll({ + where: { + type: 'Suspension', + active: true, + expiresAt: { + [Op.not]: null, + [Op.lte]: now + } + } }); - for (const susp of activeSuspensions) { - const startDate = new Date(susp.createdAt); - const expireDate = new Date(startDate.getTime() + (susp.durationDays * 24 * 60 * 60 * 1000)); - - if (new Date() >= expireDate) { - const member = await guild.members.fetch(susp.userId).catch(() => null); - const profile = await StaffProfile.findByPk(susp.userId); + for (const susp of expiredSuspensions) { + const member = await guild.members.fetch(susp.userId).catch(() => null); + const profile = await StaffProfile.findByPk(susp.userId); - if (member && profile && profile.suspendedRoles) { + try { + let rolesToRestore = []; + + if (profile?.suspendedRoles) { try { - const rolesToAdd = JSON.parse(profile.suspendedRoles); - if (Array.isArray(rolesToAdd)) { - await member.roles.add(rolesToAdd).catch(e => client.logger.warn(`Failed to restore roles for ${member.user.tag}: ${e.message}`)); - } - - if (config.suspensionRole) { - await member.roles.remove(config.suspensionRole).catch(() => {}); - } - - await susp.update({ active: false }); - await profile.update({ - isSuspended: false, - suspendedRoles: null - }); + const parsed = JSON.parse(profile.suspendedRoles); + if (Array.isArray(parsed)) rolesToRestore = parsed; + } catch (e) { + client.logger.warn( + `[Staff Management] Failed to parse suspendedRoles for ${susp.userId}: ${e.message}` + ); + } + } - client.logger.info(localize('staff-management-system', 'log-susp-end', { tag: member.user.tag })); + if (member) { + if (rolesToRestore.length > 0) { + await member.roles.add(rolesToRestore).catch(e => { + client.logger.warn( + `Failed to restore roles for ${member.user.tag}: ${e.message}` + ); + }); + } - } catch (e) { - client.logger.error(localize('staff-management-system', 'log-susp-err', { error: e.message })); + if (config.suspensionRole) { + await member.roles.remove(config.suspensionRole).catch(() => {}); } - } else { - await susp.update({ active: false }); } + + await susp.update({ active: false }); + + if (profile) { + await profile.update({ + isSuspended: false, + suspendedRoles: null + }); + } + + if (member) { + client.logger.info(localize('staff-management-system', 'log-susp-end', { + tag: member.user.tag + })); + } + } catch (e) { + client.logger.error(localize('staff-management-system', 'log-susp-err', { + error: e.message + })); } } } \ No newline at end of file diff --git a/modules/staff-management-system/events/interactionCreate.js b/modules/staff-management-system/events/interactionCreate.js index 2e6a12a0..fd1a77ef 100644 --- a/modules/staff-management-system/events/interactionCreate.js +++ b/modules/staff-management-system/events/interactionCreate.js @@ -1,30 +1,34 @@ -const { ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags, EmbedBuilder } = require('discord.js'); -const { - generateReviewHistoryResponse, - handleStatusEnd, - scheduleStatusExpiry, - handleStatusEndSubmit, - handleStatusExtend, - handleStatusExtendSubmit, - handleStatusHistPage, - sendStatusDm, +const { + getConfig, + checkStaffPermissions, + applyFooter, + generateReviewHistoryResponse, generatePromotionHistoryResponse, - generateInfractionHistoryResponse, - generateUserPanel, + generateInfractionHistoryResponse, + generateUserPanel, generatePanelInfractions, - generatePanelPromotions, - generatePanelReviews, - generatePanelStatus, - generatePanelActivity, - generatePanelShifts, + generatePanelPromotions, + generatePanelReviews, + generatePanelStatus, + generatePanelActivity, + generatePanelShifts, generatePanelDeletion, - executeDataDeletion, - generatePanelSubpage, - logStatusChange + executeDataDeletion, + generatePanelSubpage } = require('../staff-management'); +const { + handleStatusEnd, + scheduleStatusExpiry, + handleStatusEndSubmit, + handleStatusExtend, + handleStatusExtendSubmit, + handleStatusHistPage, + sendStatusDm, + logStatusChange +} = require('../commands/status.js'); const { localize } = require('../../../src/functions/localize'); const dutyHandlers = require('../commands/duty.js').buttonHandlers; -const configuration = require('../configuration.json'); +const { ActionRowBuilder, ButtonBuilder, ButtonStyle, ComponentType, EmbedBuilder, MessageFlags, ModalBuilder, TextInputBuilder, TextInputStyle } = require('discord.js'); module.exports.run = async (client, interaction) => { if (!client.botReadyAt) return; @@ -83,12 +87,12 @@ module.exports.run = async (client, interaction) => { const type = action.startsWith('loa-') ? 'LOA' : 'RA'; const base = action.replace(/^(loa|ra)-/, ''); - if (base === 'end') return handleStatusEnd(interaction, type); - if (base === 'end-submit') return handleStatusEndSubmit(interaction, type); + if (base === 'end') return handleStatusEnd(client, interaction, type); + if (base === 'end-submit') return handleStatusEndSubmit(client, interaction, type); if (base === 'extend') return handleStatusExtend(interaction, type); - if (base === 'extend-submit') return handleStatusExtendSubmit(interaction, type); - if (base === 'hist') return handleStatusHistPage(interaction, type); - } + if (base === 'extend-submit') return handleStatusExtendSubmit(client, interaction, type); + if (base === 'hist') return handleStatusHistPage(client, interaction, type); + } // ----- Promotion history pagination ----- if (action === 'prom-hist') { @@ -173,16 +177,9 @@ module.exports.run = async (client, interaction) => { // ----- Data deletion modal submission ----- if (interaction.isModalSubmit() && interaction.customId.startsWith('staff-mgmt_del-confirm_')) { - const managementRoles = Array.isArray(configuration.managementRoles) - ? configuration.managementRoles - : []; - const memberRoles = interaction.member && interaction.member.roles && interaction.member.roles.cache - ? interaction.member.roles.cache - : null; - const hasManagementRole = memberRoles - ? managementRoles.some((roleId) => memberRoles.has(roleId)) - : false; - if (!hasManagementRole) { + const configuration = getConfig(client, 'configuration'); + + if (!checkStaffPermissions(interaction.member, configuration, 'management')) { return interaction.reply({ content: localize('staff-management-system', 'del-no-perm'), flags: MessageFlags.Ephemeral @@ -203,10 +200,11 @@ module.exports.run = async (client, interaction) => { } if (selection === 'del_all') { - const embed = new EmbedBuilder() + const embed = applyFooter(client, new EmbedBuilder() .setTitle(localize('staff-management-system', 'del-all-title')) .setDescription(localize('staff-management-system', 'del-all-desc')) - .setColor('DarkRed'); + .setColor('DarkRed') + ); const row = new ActionRowBuilder() .addComponents( @@ -227,17 +225,15 @@ module.exports.run = async (client, interaction) => { }); const reply = await interaction.fetchReply(); - const collector = reply.createMessageComponentCollector({ time: 30000 }); + const collector = reply.createMessageComponentCollector({ + componentType: ComponentType.Button, + time: 30000, + max: 1, + filter: (btnInt) => btnInt.user.id === interaction.user.id + }); collector.on('collect', async (btnInt) => { - const managementRoles = Array.isArray(configuration.managementRoles) ? configuration.managementRoles : []; - const memberRoles = btnInt.member && btnInt.member.roles && btnInt.member.roles.cache - ? btnInt.member.roles.cache - : null; - const hasManagementRole = memberRoles - ? managementRoles.some((roleId) => memberRoles.has(roleId)) - : false; - if (!hasManagementRole) { + if (!checkStaffPermissions(btnInt.member, configuration, 'management')) { return btnInt.reply({ content: localize('staff-management-system', 'del-no-perm'), flags: MessageFlags.Ephemeral @@ -250,10 +246,12 @@ module.exports.run = async (client, interaction) => { embeds: [], components: [] }); - collector.stop('cancelled'); - } else if (btnInt.customId.includes('confirm')) { + return; + } + + if (btnInt.customId.includes('confirm')) { await executeDataDeletion(client, targetId, 'del_all'); - + client.logger.info(localize('staff-management-system', 'log-del-all', { target: targetId, admin: btnInt.user.id @@ -264,26 +262,24 @@ module.exports.run = async (client, interaction) => { const payload = await generateUserPanel(client, targetUser); await interaction.message.edit(payload).catch(()=>{}); } - - await btnInt.update({ - content: localize('staff-management-system', 'succ-del-all'), - embeds: [], - components: [] + + await btnInt.update({ + content: localize('staff-management-system', 'succ-del-all'), + embeds: [], + components: [] }); - collector.stop('confirmed'); } }); - collector.on('end', (reason) => { + collector.on('end', async (_collected, reason) => { if (reason === 'time') { - interaction.editReply({ + await interaction.editReply({ content: localize('staff-management-system', 'err-del-time'), embeds: [], components: [] }).catch(()=>{}); } }); - return; } await executeDataDeletion(client, targetId, selection); diff --git a/modules/staff-management-system/staff-management.js b/modules/staff-management-system/staff-management.js index 49ae01ae..96b31cef 100644 --- a/modules/staff-management-system/staff-management.js +++ b/modules/staff-management-system/staff-management.js @@ -6,7 +6,7 @@ const { ModalBuilder, TextInputBuilder, TextInputStyle, EmbedBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags } = require('discord.js'); const { Op } = require('sequelize'); const schedule = require('node-schedule'); -const { embedTypeV2, formatDate } = require('../../src/functions/helpers'); +const { embedTypeV2, safeSetFooter } = require('../../src/functions/helpers'); const { localize } = require('../../src/functions/localize'); // --- Local helpers --- @@ -32,11 +32,36 @@ const parseDurationToDays = (input) => { }; const applyFooter = (client, embed) => { - embed.setFooter({ text: client.strings.footer, iconURL: client.strings.footerImgUrl }); - if (!client.strings.disableFooterTimestamp) embed.setTimestamp(); + safeSetFooter(embed, client); + if (!(client.strings && client.strings.disableFooterTimestamp)) { + embed.setTimestamp(); + } return embed; }; +function checkStaffPermissions(member, config, level = 'staff') { + if (!member) return false; + if (member.permissions?.has('Administrator')) return true; + + const roleMap = { + staff: [ + ...(config?.staffRoles || []), + ...(config?.supervisorRoles || []), + ...(config?.managementRoles || []) + ], + supervisor: [ + ...(config?.supervisorRoles || []), + ...(config?.managementRoles || []) + ], + management: [ + ...(config?.managementRoles || []) + ] + }; + + const allowedRoles = roleMap[level] || roleMap.staff; + return member.roles?.cache?.some(role => allowedRoles.includes(role.id)) || false; +} + const buildPaginationRow = (backId, countId, nextId, page, totalPages) => { return new ActionRowBuilder().addComponents( new ButtonBuilder() @@ -78,149 +103,6 @@ function formatDuration(seconds) { return parts.join(', ') || localize('staff-management-system', 'time-zero'); } -// ---------- Status DM's and logging ---------- - -async function sendStatusDm(user, type, dmType, data = {}) { - const label = type === 'LOA' - ? 'LoA' - : 'RA'; - const viewCmd = type === 'LOA' - ? '`/loa view`' - : '`/ra view`'; - const endFmt = data.endDate - ? `` - : ''; - - // These messages use the locales key to be easily used later - const messages = { - approved: { - title: 'dm-appr-title', - color: 'Green', - desc: 'dm-appr-desc', - params: { label, approver: data.approver, endFmt, viewCmd } - }, - denied: { - title: 'dm-deny-title', - color: 'Red', - desc: 'dm-deny-desc', - params: { label, denier: data.denier, reason: data.reason } - }, - extended: { - title: 'dm-ext-title', - color: 'Yellow', - desc: 'dm-ext-desc', - params: { label, extender: data.extender, days: data.days, endFmt, reason: data.reason, viewCmd } - }, - ended_early: { - title: 'dm-early-title', - color: 'Red', - desc: 'dm-early-desc', - params: { label, ender: data.ender, reason: data.reason } - }, - ended: { - title: 'dm-end-title', - color: 'Black', - desc: 'dm-end-desc', - params: { label } - } - }; - - const msg = messages[dmType]; - if (!msg) return; - - const embed = new EmbedBuilder() - .setTitle(localize('staff-management-system', msg.title, msg.params)) - .setDescription(localize('staff-management-system', msg.desc, msg.params)) - .setColor(msg.color); - applyFooter(user.client, embed); - - try { - await user.send({ - embeds: [embed.toJSON()] - }); - } catch (e) { - user.client.logger.error( - localize('staff-management-system', 'log-stat-dm-error', { - e: e.message, - u: user.tag - }) - ); -} -} - -async function logStatusChange(client, type, action, data) { - const statusConfig = getConfig(client, 'status'); - if (!statusConfig?.logStatusChanges) return; - - const channelId = getSafeChannelId(statusConfig.statusChangeLogChannel) || getSafeChannelId(getConfig(client, 'configuration')?.generalLogChannel); - if (!channelId) return; - - const guild = client.guilds.cache.get(client.guildID); - if (!guild) return; - const channel = await guild.channels.fetch(channelId).catch(() => null); - if (!channel) return; - - const label = type === 'LOA' - ? 'LoA' - : 'RA'; - const targetUserObj = data.targetUser || await client.users.fetch(data.userId).catch(() => null); - const mention = targetUserObj - ? targetUserObj.toString() - : `<@${data.userId}>`; - const username = targetUserObj - ? targetUserObj.username - : data.userId; - - const embed = new EmbedBuilder() - .setThumbnail(targetUserObj - ?.displayAvatarURL({ dynamic: true }) || null); - - if (action === 'start') { - embed.setTitle(localize('staff-management-system', 'log-start-title', { label, username })) - .setColor('Green') - .setDescription(localize('staff-management-system', 'log-start-desc', - { label, mention, apprText: data.approverId - ? ` ${localize('staff-management-system', 'label-appr-by')}: <@${data.approverId}>.` - : '' - })) - .addFields({ - name: localize('staff-management-system', 'log-info-hdr', { label }), - value: `**${localize('staff-management-system', 'general-start')}:** \n**${localize('staff-management-system', 'general-end')}:** \n**${localize('staff-management-system', 'general-rsn')}:** ${data.reason || localize('staff-management-system', 'none-provided')}` - }); - - } else if (action === 'end') { - embed.setTitle(localize('staff-management-system', 'log-end-title', { label, username })) - .setColor('Red') - .setDescription(localize('staff-management-system', 'log-end-desc', { label, mention })) - .addFields({ - name: localize('staff-management-system', 'log-info-hdr', { label }), - value: `**${localize('staff-management-system', 'general-started')}:** \n**${localize('staff-management-system', 'general-ended')}:** \n**${localize('staff-management-system', 'general-rsn')}:** ${data.reason || localize('staff-management-system', 'none-provided')}` - }); - - } else if (action === 'adjusted') { - embed.setTitle(localize('staff-management-system', 'log-adj-title', { label, username })) - .setColor('Yellow') - .setDescription(localize('staff-management-system', 'log-adj-desc', { label, mention, executor: data.executorId })) - .addFields({ - name: localize('staff-management-system', 'log-changes'), - value: data.changesText - }); - } - - applyFooter(client, embed); - try { - await channel.send({ - embeds: [embed.toJSON()] - }); - } catch (e) { - client.logger.error( - localize('staff-management-system', 'log-status-adj-error', { - e: e.message - }) - ); - } -} - // ---------- Infractions ---------- async function issueInfraction(client, interaction, targetMember, type, reason, expiryInput) { const config = getConfig(client, 'infractions'); @@ -1065,21 +947,35 @@ async function generatePanelStatus(client, targetUser, page = 1) { // Activity checks page async function generatePanelActivity(client, targetUser, page = 1) { const ActivityCheck = client.models['staff-management-system']['ActivityCheck']; - const allChecks = await ActivityCheck.findAll(); - + const cutoff = new Date(); + cutoff.setDate(cutoff.getDate() - 90); + + const recentChecks = await ActivityCheck.findAll({ + where: { + createdAt: { [Op.gte]: cutoff } + }, + order: [['createdAt', 'DESC']] + }); + let userResponses = 0; const historyRows = []; - allChecks.forEach(check => { - const responded = JSON.parse(check.respondedUsers || '[]'); + + for (const check of recentChecks) { + let responded = []; + try { + responded = JSON.parse(check.respondedUsers || '[]'); + } catch (e) { + client.logger.warn(`[Staff Management] ${e.message}`); + continue; + } + if (responded.includes(targetUser.id)) { userResponses++; historyRows.push(check); } - }); - - historyRows.sort((a, b) => b.createdAt - a.createdAt); + } + const count = historyRows.length; - let totalPages = 1; if (count > 3) totalPages = 1 + Math.ceil((count - 3) / 5); const limit = page === 1 @@ -1097,16 +993,24 @@ async function generatePanelActivity(client, targetUser, page = 1) { .setColor('Blue') .setThumbnail(targetUser.displayAvatarURL({ dynamic: true })) ); - + let desc = localize('staff-management-system', 'p-act-desc', { count: userResponses }); - - if (paginatedRows.length === 0) desc += localize('staff-management-system', 'p-no-hist'); - else { - desc += paginatedRows.map(r => `**${localize('staff-management-system', 'label-chk')} **\n**${localize('staff-management-system', 'label-end')}:** \n**${localize('staff-management-system', 'label-chan')}:** <#${r.channelId}>`).join('\n\n'); + + if (paginatedRows.length === 0) { + desc += localize('staff-management-system', 'p-no-hist'); + } else { + desc += paginatedRows.map(r => + `**${localize('staff-management-system', 'label-chk')} **\n` + + `**${localize('staff-management-system', 'label-end')}:** \n` + + `**${localize('staff-management-system', 'label-chan')}:** <#${r.channelId}>` + ).join('\n\n'); } - + embed.setDescription(desc); - embed.addFields({ name: '\u200b', value: localize('staff-management-system', 'page-count', { page, total: totalPages }) }); + embed.addFields({ + name: '\u200b', + value: localize('staff-management-system', 'page-count', { page, total: totalPages }) + }); const menu = ActionRowBuilder.from((await generateUserPanel(client, targetUser)).components[0]); menu.components[0].options.find(opt => opt.data.value === 'activity').data.default = true; @@ -1115,7 +1019,8 @@ async function generatePanelActivity(client, targetUser, page = 1) { `staff-mgmt_panel-act_${targetUser.id}_${page - 1}`, 'panel_act_count', `staff-mgmt_panel-act_${targetUser.id}_${page + 1}`, - page, totalPages + page, + totalPages ); return { @@ -1296,618 +1201,90 @@ async function generatePanelDeletion(client, targetUser) { async function executeDataDeletion(client, targetId, dataType) { const models = client.models['staff-management-system']; - - if (['del_infractions', 'del_all'].includes(dataType)) await models['Infraction'].destroy({ - where: { userId: targetId } - }); - if (['del_promotions', 'del_all'].includes(dataType)) await models['Promotion'].destroy({ - where: { userId: targetId } - }); - if (['del_reviews', 'del_all'].includes(dataType)) await models['StaffReview'].destroy({ - where: { targetId: targetId } - }); - if (['del_shifts', 'del_all'].includes(dataType)) { - await models['StaffShift'].destroy({ - where: { userId: targetId } - }); - await models['StaffProfile'].destroy({ - where: { userId: targetId } + + if (['del_infractions', 'del_all'].includes(dataType)) { + await models.Infraction.destroy({ + where: { userId: targetId } }); } - if (['del_status', 'del_all'].includes(dataType)) await models['LoaRequest'].destroy({ - where: { userId: targetId } - }); - if (['del_activity', 'del_all'].includes(dataType)) { - const allChecks = await models['ActivityCheck'].findAll(); - for (const check of allChecks) { - let responded = JSON.parse(check.respondedUsers || '[]'); - if (responded.includes(targetId)) { - responded = responded.filter(id => id !== targetId); - await check.update({ respondedUsers: JSON.stringify(responded) }); - } - } - } -} -// ----- Status ----- -const getStatusMeta = (type) => ({ - isLoa: type === 'LOA', - label: type === 'LOA' - ? 'LoA' - : 'RA', - enableKey: type === 'LOA' - ? 'enableLoa' - : 'enableRa', - roleKey: type === 'LOA' - ? 'loaRole' - : 'raRole', - maxDaysKey: type === 'LOA' - ? 'loaMaxDays' - : 'raMaxDays', - color: type === 'LOA' - ? 'Green' - : 'Orange', - activeText: localize('staff-management-system', type === 'LOA' - ? 'status-active-loa' - : 'status-active-ra' - ), - histTitle: localize('staff-management-system', type === 'LOA' - ? 'status-hist-loa' - : 'status-hist-ra' - ), - actionPrefix: type === 'LOA' - ? 'loa' - : 'ra' -}); - -async function handleStatusRequest(client, interaction, type, durationInput, reason) { - const config = getConfig(client, 'status'); - const isLoa = type === 'LOA'; - if (!config[isLoa - ? 'enableLoa' - : 'enableRa']) return interaction.editReply({ - content: localize('staff-management-system', 'err-status-disabled', { type }) - } - ); - - const days = parseDurationToDays(durationInput?.trim()); - if (!days || isNaN(days) || days <= 0) return interaction.editReply({ - content: localize('staff-management-system', 'err-invalid-duration') - }); - - const maxDays = (isLoa ? config.loaMaxDays : config.raMaxDays) || (isLoa ? 60 : 30); - if (days > maxDays) return interaction.editReply({ - content: localize('staff-management-system', 'err-duration-max', { max: maxDays }) - }); + if (['del_promotions', 'del_all'].includes(dataType)) { + await models.Promotion.destroy({ + where: { userId: targetId } + }); + } - const LoaRequest = client.models['staff-management-system']['LoaRequest']; - if (await LoaRequest.findOne({ - where: { userId: interaction.user.id, type, status: { [Op.in]: ['PENDING', 'APPROVED'] }, - endDate: { [Op.gt]: new Date() } } - })) { - return interaction.editReply({ - content: localize('staff-management-system', 'err-status-exists', { type }) + if (['del_reviews', 'del_all'].includes(dataType)) { + await models.StaffReview.destroy({ + where: { targetId } }); } - const startDate = new Date(); - const endDate = new Date(startDate.getTime() + days * 24 * 60 * 60 * 1000); - const needsApproval = isLoa - ? config.requireLoaApproval !== false - : config.requireRaApproval !== false; - - const req = await LoaRequest.create({ - userId: interaction.user.id, - type, - reason, - startDate, - endDate, - status: needsApproval - ? 'PENDING' - : 'APPROVED' - }); + if (['del_shifts', 'del_all'].includes(dataType)) { + await models.StaffShift.destroy({ + where: { userId: targetId } + }); - const logChannelId = getSafeChannelId(config.statusLogChannel); - if (logChannelId && needsApproval) { - const channel = await interaction.guild.channels.fetch(logChannelId).catch(() => null); - if (channel) { - const embed = new EmbedBuilder() - .setTitle(localize('staff-management-system', 'status-request-title', { type })) - .setColor('Blue') - .setAuthor({ name: `Request ID: ${req.id}`}) - .addFields( - { name: localize('staff-management-system', 'status-req-user'), - value: interaction.user.toString(), - inline: true - }, - { name: localize('staff-management-system', 'status-req-duration'), - value: `${days} ${localize('staff-management-system', 'label-days')}`, - inline: true - }, - { name: localize('staff-management-system', 'general-rsn'), - value: reason - } - ); - - applyFooter(client, embed); - const row = new ActionRowBuilder() - .addComponents(new ButtonBuilder() - .setCustomId(`staff-mgmt_approve_${req.id}`) - .setLabel(localize('staff-management-system', 'btn-approve')) - .setStyle(ButtonStyle.Success), - new ButtonBuilder() - .setCustomId(`staff-mgmt_deny_${req.id}`) - .setLabel(localize('staff-management-system', 'btn-deny')) - .setStyle(ButtonStyle.Danger)); - channel.send({ embeds: [embed.toJSON()], components: [row.toJSON()] }).catch(()=>{}); + const profile = await models.StaffProfile.findByPk(targetId); + if (profile) { + await profile.update({ + onDuty: false, + onBreak: false, + breakStartTime: null, + lastClockIn: null + }); } } - if (!needsApproval) { - const roleId = config[isLoa ? 'loaRole' : 'raRole']; - if (roleId) interaction.member.roles.add(roleId).catch(()=>{}); - await logStatusChange(client, type, 'start', { - targetUser: interaction.user, - startDate, - endDate, - reason, - approverId: null + if (['del_status', 'del_all'].includes(dataType)) { + await models.LoaRequest.destroy({ + where: { userId: targetId } }); - } - - await interaction.editReply({ - content: localize('staff-management-system', 'success-status-request', { - type, state: needsApproval - ? localize('staff-management-system', 'state-pending') - : localize('staff-management-system', 'state-auto') - }) - }); -} - -async function handleStatusView(client, interaction, type, targetUser) { - const user = targetUser || interaction.user; - const request = await client.models['staff-management-system']['LoaRequest'].findOne({ - where: { userId: user.id, type, status: { [Op.in]: ['APPROVED', 'PENDING'] }, - endDate: { [Op.gt]: new Date() } }, - order: [['createdAt', 'DESC']] - }); - if (!request) return interaction.editReply({ - content: localize('staff-management-system', 'no-active-status', { - user: user.username, - type - }) - }); - - const embed = new EmbedBuilder() - .setTitle(`${type} Status: ${user.username}`) - .setColor(request.status === 'APPROVED' - ? 'Green' - : 'Yellow' - ) - .addFields( - { - name: localize('staff-management-system', 'label-stat'), - value: request.status, - inline: true }, - { - name: localize('staff-management-system', 'label-end'), - value: formatDate(request.endDate), - inline: true }, - { - name: localize('staff-management-system', 'general-rsn'), - value: request.reason || localize('staff-management-system', 'info-none') - }) - .setThumbnail(user.displayAvatarURL({ dynamic: true })); - applyFooter(client, embed); - await interaction.editReply({ embeds: [embed.toJSON()] }); -} - -async function handleStatusList(client, interaction, type, filter, page = 1) { - const limit = 10; - const offset = (page - 1) * limit; - - let whereClause = { type }; - let title = `${type} List`; - - if (filter === 'active') { - whereClause.status = 'APPROVED'; - whereClause.endDate = { [Op.gt]: new Date() }; - title += localize('staff-management-system', 'filter-active'); - } - else if (filter === 'expired') { - whereClause.endDate = { [Op.lt]: new Date() }; - title += localize('staff-management-system', 'filter-expired'); - } - else { - whereClause.status = { [Op.ne]: 'PENDING' }; - title += localize('staff-management-system', 'filter-history'); + const profile = await models.StaffProfile.findByPk(targetId); + if (profile) { + await profile.update({ + activityStatus: null + }); + } } - const { count, rows } = await client.models['staff-management-system']['LoaRequest'].findAndCountAll({ - where: whereClause, - limit, - offset, - order: [['endDate', 'DESC']] - }); - if (count === 0) return interaction.editReply({ - content: localize('staff-management-system', 'err-no-recs') - }); - - const totalPages = Math.ceil(count / limit) || 1; - const embed = new EmbedBuilder() - .setTitle(title) - .setColor('Blue') - .setDescription(rows.map(r => `**<@${r.userId}>** ${r.status === 'APPROVED' ? '✅' : (r.status === 'DENIED' ? '❌' : '⏹️')}\nEnds: ${formatDate(r.endDate)}\nReason: ${r.reason}`).join('\n\n')) - .addFields( - { - name: '\u200b', - value: localize('staff-management-system', 'page-count', { page, total: totalPages }) - } - ); - applyFooter(client, embed); - await interaction.editReply({ embeds: [embed.toJSON()] }); -} - -async function handleStatusManage(client, interaction, targetMember, type) { - const config = getConfig(client, 'status'); - const meta = getStatusMeta(type); - if (!config[meta.enableKey]) return interaction.editReply({ - content: localize('staff-management-system', 'err-status-disabled', { type }) - }); - - const generalConfig = getConfig(client, 'configuration'); - const canManage = interaction.member.roles.cache.some(r => [...(generalConfig.supervisorRoles || []), ...(generalConfig.managementRoles || [])].includes(r.id)) || interaction.member.permissions.has('Administrator'); - if (!canManage) return interaction.editReply({ - content: localize('staff-management-system', 'err-gen-no-perm') - }); - - const LoaRequest = client.models['staff-management-system']['LoaRequest']; - const activeRequest = await LoaRequest.findOne({ - where: { - userId: targetMember.user.id, - type, - status: { [Op.in]: ['APPROVED', 'PENDING'] }, - endDate: { [Op.gt]: new Date() } - }, - order: [['createdAt', 'DESC']] + if (dataType === 'del_all') { + const profile = await models.StaffProfile.findByPk(targetId); + if (profile) { + await profile.update({ + customNickname: null, + customIntro: null + }); } - ); - const totalCount = await LoaRequest.count({ - where: { userId: targetMember.user.id, type } - }); - - const embed = applyFooter(client, new EmbedBuilder() - .setTitle(localize('staff-management-system', 'manage-status-title', { - label: meta.label, - username: targetMember.user.username - })) - .setThumbnail(targetMember.user.displayAvatarURL({ dynamic: true })) - .setColor(activeRequest - ? meta.color - : 'Grey' - ) - .setDescription(localize('staff-management-system', 'manage-stat-desc', { - status: activeRequest - ? meta.activeText - : localize('staff-management-system', 'no-act-stat', { - label: meta.label - }), - label: meta.label, - count: Math.max(0, totalCount - (activeRequest ? 1 : 0)) - })) - ); - - embed.addFields({ - name: localize('staff-management-system', 'manage-active-details', { label: meta.label }), - value: activeRequest ? `**${localize('staff-management-system', 'general-start')}:** ${formatDate(activeRequest.startDate)}\n**${localize('staff-management-system', 'general-end')}:** ${formatDate(activeRequest.endDate)}\n**${localize('staff-management-system', 'label-stat')}:** ${activeRequest.status}\n**${localize('staff-management-system', 'label-appr-by')}:** ${activeRequest.approverId ? `<@${activeRequest.approverId}>` : localize('staff-management-system', 'label-auto')}\n**${localize('staff-management-system', 'general-rsn')}:** ${activeRequest.reason || localize('staff-management-system', 'info-none')}` : localize('staff-management-system', 'manage-no-active-user', { label: meta.label }) - }); - - const p = meta.actionPrefix; - const rid = activeRequest?.id ?? 'none'; - const row = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId(`staff-mgmt_${p}-end_${rid}`) - .setLabel(localize('staff-management-system', 'btn-end-early', { label: meta.label })) - .setEmoji('🚫').setStyle(ButtonStyle.Danger) - .setDisabled(!activeRequest), - new ButtonBuilder() - .setCustomId(`staff-mgmt_${p}-extend_${rid}`) - .setLabel(localize('staff-management-system', 'btn-extend', { label: meta.label })) - .setEmoji('⏳') - .setStyle(ButtonStyle.Primary) - .setDisabled(!activeRequest), - new ButtonBuilder() - .setCustomId(`staff-mgmt_${p}-hist_${targetMember.user.id}_1`) - .setLabel(localize('staff-management-system', 'btn-view-history')) - .setEmoji('📜') - .setStyle(ButtonStyle.Secondary) - .setDisabled(totalCount === 0) - ); - await interaction.editReply({ - embeds: [embed.toJSON()], - components: [row.toJSON()] - }); -} - -async function handleStatusEnd(interaction, type) { - const meta = getStatusMeta(type); - const requestId = interaction.customId.split('_')[2]; - if (requestId === 'none') return interaction.reply({ - content: localize('staff-management-system', 'err-no-active-end', { label: meta.label }), - flags: MessageFlags.Ephemeral - }); - - const modal = new ModalBuilder() - .setCustomId(`staff-mgmt_${meta.actionPrefix}-end-submit_${requestId}`) - .setTitle(localize('staff-management-system', 'modal-end-early-title', { label: meta.label })); - modal.addComponents(new ActionRowBuilder() - .addComponents( - new TextInputBuilder() - .setCustomId('end_reason') - .setLabel(localize('staff-management-system', 'modal-end-early-reason')) - .setStyle(TextInputStyle.Paragraph) - .setRequired(true) - )); - return interaction.showModal(modal); -} - -async function handleStatusEndSubmit(client, interaction, type) { - const meta = getStatusMeta(type); - const request = await client.models['staff-management-system']['LoaRequest'].findByPk(interaction.customId.split('_')[2]); - if (!request || request.status === 'ENDED' || request.status === 'DENIED') return interaction.reply({ - content: localize('staff-management-system', 'err-stat-inact', { label: meta.label }), - flags: MessageFlags.Ephemeral - }); - - const reason = interaction.fields.getTextInputValue('end_reason'); - const member = await interaction.guild.members.fetch(request.userId).catch(() => null); - - if (member && getConfig(client, 'status')[meta.roleKey]) await member.roles.remove(getConfig(client, 'status')[meta.roleKey]).catch(() => {}); - - await request.update({ status: 'ENDED', endDate: new Date() }); - await client.models['staff-management-system']['StaffProfile'].update({ activityStatus: 'ACTIVE' }, { - where: { userId: request.userId } - }); - - if (member) await sendStatusDm(member.user, type, 'ended_early', { - ender: interaction.user.tag, - reason - }); - await logStatusChange(client, type, 'end', { - userId: request.userId, - startDate: request.startDate, - reason: request.reason - }); - - const updatedEmbed = EmbedBuilder.from(interaction.message.embeds[0]) - .setColor('Grey') - .setDescription(localize('staff-management-system', 'status-ended-embed-desc', { - label: meta.label, user: interaction.user.tag, reason - })) - .spliceFields(0, 1, { - name: localize('staff-management-system', 'manage-active-details', { label: meta.label }), - value: localize('staff-management-system', 'manage-no-active-user', { label: meta.label }) - }); - - const p = meta.actionPrefix; - const disabledRow = new ActionRowBuilder() - .addComponents( - new ButtonBuilder() - .setCustomId(`${p}-end-done`) - .setLabel(localize('staff-management-system', 'btn-end-early', { label: meta.label })) - .setEmoji('🚫') - .setStyle(ButtonStyle.Danger) - .setDisabled(true), - new ButtonBuilder() - .setCustomId(`${p}-extend-done`) - .setLabel(localize('staff-management-system', 'btn-extend', { label: meta.label })) - .setEmoji('⏳') - .setStyle(ButtonStyle.Primary) - .setDisabled(true), - new ButtonBuilder() - .setCustomId(`staff-mgmt_${p}-hist_${request.userId}_1`) - .setLabel(localize('staff-management-system', 'btn-view-history')) - .setEmoji('📜') - .setStyle(ButtonStyle.Secondary) - ); - return interaction.update({ - embeds: [updatedEmbed.toJSON()], - components: [disabledRow.toJSON()] - }); -} - -async function handleStatusExtend(interaction, type) { - const meta = getStatusMeta(type); - const requestId = interaction.customId.split('_')[2]; - if (requestId === 'none') return interaction.reply({ - content: localize('staff-management-system', 'err-no-active-extend', { label: meta.label }), - flags: MessageFlags.Ephemeral - }); - - const modal = new ModalBuilder() - .setCustomId(`staff-mgmt_${meta.actionPrefix}-extend-submit_${requestId}`) - .setTitle(localize('staff-management-system', 'modal-extend-title', { - label: meta.label - })); - modal.addComponents( - new ActionRowBuilder() - .addComponents( - new TextInputBuilder() - .setCustomId('extend_days') - .setLabel(localize('staff-management-system', 'modal-extend-days')) - .setStyle(TextInputStyle.Short) - .setPlaceholder("7") - .setRequired(true) - ), - new ActionRowBuilder() - .addComponents( - new TextInputBuilder() - .setCustomId('extend_reason') - .setLabel(localize('staff-management-system', 'modal-extend-reason')) - .setStyle(TextInputStyle.Paragraph) - .setRequired(true) - ) - ); - return interaction.showModal(modal); -} + } -function scheduleStatusExpiry(client, request) { - schedule.scheduleJob(new Date(request.endDate), async () => { - try { - const req = await client.models['staff-management-system']['LoaRequest'].findByPk(request.id); - if (req && req.status === 'APPROVED' && new Date(req.endDate) <= new Date()) { - await req.update({ status: 'ENDED' }); - await client.models['staff-management-system']['StaffProfile'].update({ activityStatus: 'ACTIVE' }, { - where: { userId: req.userId } - }); + if (['del_activity', 'del_all'].includes(dataType)) { + const cutoff = new Date(); + cutoff.setDate(cutoff.getDate() - 365); - const member = await client.guilds.cache.get(client.guildID)?.members.fetch(req.userId).catch(() => null); - if (member) { - const roleKey = req.type === 'LOA' - ? 'loaRole' - : 'raRole'; - if (getConfig(client, 'status')[roleKey]) await member.roles.remove(getConfig(client, 'status')[roleKey]).catch(() => null); - await sendStatusDm(member.user, req.type, 'ended'); - } - await logStatusChange(client, req.type, 'end', { userId: req.userId, startDate: req.startDate, reason: req.reason }); + const checks = await models.ActivityCheck.findAll({ + where: { + createdAt: { [Op.gte]: cutoff } } - } catch (e) {} - }); -} - -async function handleStatusExtendSubmit(client, interaction, type) { - const meta = getStatusMeta(type); - const request = await client.models['staff-management-system']['LoaRequest'].findByPk(interaction.customId.split('_')[2]); - if (!request || request.status === 'ENDED' || request.status === 'DENIED') return interaction.reply({ - content: localize('staff-management-system', 'err-stat-inact', { - label: meta.label - }), - flags: MessageFlags.Ephemeral - }); - - const days = parseInt(interaction.fields.getTextInputValue('extend_days'), 10); - const reason = interaction.fields.getTextInputValue('extend_reason'); - if (isNaN(days) || days <= 0 || days > 180) return interaction.reply({ - content: localize('staff-management-system', 'err-inv-dur'), - flags: MessageFlags.Ephemeral - }); - - const oldEndDate = new Date(request.endDate); - const newEndDate = new Date(oldEndDate.getTime() + days * 24 * 60 * 60 * 1000); - await request.update({ endDate: newEndDate }); - scheduleStatusExpiry(client, request); - - const member = await interaction.guild.members.fetch(request.userId).catch(() => null); - if (member) await sendStatusDm(member.user, type, 'extended', { - extender: interaction.user.tag, - days, - endDate: newEndDate, - reason - }); - await logStatusChange(client, type, 'adjusted', { - userId: request.userId, - executorId: interaction.user.id, - changesText: localize('staff-management-system', 'status-adjusted-log', { - label: meta.label, - newEnd: ``, - oldEnd: ``, - reason - }) - }); - - const updatedEmbed = EmbedBuilder.from(interaction.message.embeds[0]) - .spliceFields(0, 1, { - name: localize('staff-management-system', 'manage-active-details', { label: meta.label }), - value: localize('staff-management-system', 'mod-stat-ext', { - s: formatDate(request.startDate), - e: formatDate(newEndDate), - d: days, - t: request.status, - a: request.approverId - ? `<@${request.approverId}>` - : localize('staff-management-system', 'label-auto'), - r: request.reason || localize('staff-management-system', 'info-none') - }) - }); - return interaction.update({ - embeds: [updatedEmbed.toJSON()], - components: interaction.message.components.map(c => c.toJSON()) - }); -} - -async function generateStatusHistoryResponse(client, targetUser, page = 1, type) { - const meta = getStatusMeta(type); - const limit = 5; - const offset = (page - 1) * limit; - - const { count, rows } = await client.models['staff-management-system']['LoaRequest'].findAndCountAll({ - where: { userId: targetUser.id, type }, - order: [['createdAt', 'DESC']], - limit, - offset - }); - if (count === 0) return { - content: localize('staff-management-system', 'info-no-status-history', { label: meta.label }), - flags: MessageFlags.Ephemeral - }; + }); - const totalPages = Math.ceil(count / limit) || 1; - const embed = applyFooter(client, new EmbedBuilder() - .setTitle(`${meta.histTitle} - ${targetUser.username}`) - .setThumbnail(targetUser.displayAvatarURL({ dynamic: true })) - .setColor(meta.color) - .setDescription(localize('staff-management-system', 'status-history-desc', { - count: rows.length, - total: count, - label: meta.label + for (const check of checks) { + let responded = []; + try { + responded = JSON.parse(check.respondedUsers || '[]'); + } catch { + continue; } - )) - ); - const statusIcons = { - APPROVED: '✅', - DENIED: '❌', - ENDED: '⏹️', - PENDING: '🕐' - }; - rows.forEach((req, index) => embed.addFields({ - name: `${statusIcons[req.status] ?? '❓'} ${meta.label} #${offset + index + 1} - ${req.status}`, - value: `**${localize('staff-management-system', 'general-start')}:** ${formatDate(req.startDate)}\n**${localize('staff-management-system', 'general-end')}:** ${formatDate(req.endDate)}\n**${localize('staff-management-system', 'label-appr-by')}:** ${req.approverId ? `<@${req.approverId}>` : localize('staff-management-system', 'label-auto')}\n**${localize('staff-management-system', 'general-rsn')}:** ${req.reason || localize('staff-management-system', 'info-none')}` })); - embed.addFields({ - name: '\u200b', - value: localize('staff-management-system', 'page-count', { page, total: totalPages }) - }); - - const row = buildPaginationRow( - `staff-mgmt_${meta.actionPrefix}-hist_${targetUser.id}_${page - 1}`, - `${meta.actionPrefix}_hist_page_count`, - `staff-mgmt_${meta.actionPrefix}-hist_${targetUser.id}_${page + 1}`, - page, - totalPages - ); - return { - embeds: [embed.toJSON()], - components: [row.toJSON()] - }; -} - -async function handleStatusHistPage(client, interaction, type) { - const parts = interaction.customId.split('_'); - const targetUser = await client.users.fetch(parts[2]).catch(() => null); - if (!targetUser) return interaction.reply({ - content: localize('staff-management-system', 'err-gen-no-user'), - flags: MessageFlags.Ephemeral - }); - - const payload = await generateStatusHistoryResponse(client, targetUser, parseInt(parts[3], 10), type); - if (payload.content) return interaction.reply({ - ...payload, - flags: MessageFlags.Ephemeral - }); - return interaction.message?.embeds?.[0]?.title?.startsWith(getStatusMeta(type).histTitle) - ? interaction.update(payload) - : interaction.reply({ ...payload, flags: MessageFlags.Ephemeral }); + if (responded.includes(targetId)) { + responded = responded.filter(id => id !== targetId); + await check.update({ + respondedUsers: JSON.stringify(responded) + }); + } + } + } } // ---------- Activity Checks ---------- @@ -2079,7 +1456,11 @@ async function endActivityCheckProcess(client, activeCheck) { const finalMessage = { embeds: [embed.toJSON()] }; if (pingText) finalMessage.content = pingText; - await logChannel.send(finalMessage); + await logChannel.send(finalMessage).catch((e) => { + client.logger.error(localize('staff-management-system', 'log-ac-send-fail', { + error: e.message + })); +}); } function initActivityCheckAutomation(client) { @@ -2141,7 +1522,7 @@ async function submitReview(client, interaction, targetUser, stars, comment) { if (config.onlyAllowStaffReview !== false) { const genCfg = getConfig(client, 'configuration'); - if (!targetMember.roles.cache.some(r => [...(genCfg?.staffRoles || []), ...(genCfg?.supervisorRoles || []), ...(genCfg?.managementRoles || [])].includes(r.id))) { + if (!checkStaffPermissions(targetMember, genCfg, 'staff')) { return interaction.reply({ content: localize('staff-management-system', 'err-staff-rate'), flags: MessageFlags.Ephemeral @@ -2247,9 +1628,9 @@ async function getReviewHistory(client, interaction, targetUser) { } module.exports = { - logStatusChange, getConfig, applyFooter, + checkStaffPermissions, buildPaginationRow, formatDuration, issueInfraction, @@ -2270,21 +1651,10 @@ module.exports = { generatePanelDeletion, executeDataDeletion, generatePanelSubpage, - handleStatusRequest, - handleStatusView, - handleStatusList, - handleStatusManage, - handleStatusEnd, - handleStatusEndSubmit, - handleStatusExtend, - handleStatusExtendSubmit, - handleStatusHistPage, startActivityCheck, initActivityCheckAutomation, endActivityCheckProcess, submitReview, getReviewHistory, generateReviewHistoryResponse, - sendStatusDm, - scheduleStatusExpiry }; \ No newline at end of file From 55d4199f8857f186ca90dffd85b8daaece0f7ad6 Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Sat, 4 Apr 2026 21:26:08 +0200 Subject: [PATCH 12/27] New changes from feedback. --- .../staff-management-system/commands/duty.js | 91 +++++++++--- .../commands/staff-management.js | 29 ++-- .../commands/status.js | 52 ++++--- .../events/botReady.js | 72 ++------- .../events/guildMemberRemove.js | 28 +++- .../events/interactionCreate.js | 137 ++++++++++++------ .../models/ActivityCheckResponse.js | 36 +++++ .../models/StaffShift.js | 2 +- .../staff-management.js | 104 +++++++------ 9 files changed, 337 insertions(+), 214 deletions(-) create mode 100644 modules/staff-management-system/models/ActivityCheckResponse.js diff --git a/modules/staff-management-system/commands/duty.js b/modules/staff-management-system/commands/duty.js index 836bd271..8d3dabbc 100644 --- a/modules/staff-management-system/commands/duty.js +++ b/modules/staff-management-system/commands/duty.js @@ -1,6 +1,6 @@ const { MessageFlags, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, StringSelectMenuBuilder, ModalBuilder, TextInputBuilder, TextInputStyle } = require('discord.js'); const { Op, fn, col, literal } = require('sequelize'); -const { getConfig, applyFooter, formatDuration, buildPaginationRow } = require('../staff-management'); +const { getConfig, applyFooter, formatDuration, buildPaginationRow, checkStaffPermissions } = require('../staff-management'); const { localize } = require('../../../src/functions/localize'); function getLookbackDate(config) { @@ -12,6 +12,25 @@ function getLookbackDate(config) { return date; } +function canUseDutyAdmin(client, member) { + const generalConfig = getConfig(client, 'configuration'); + return checkStaffPermissions(member, generalConfig, 'supervisor'); +} + +function checkDutyAdminPermission(client, interaction) { + if (canUseDutyAdmin(client, interaction.member)) return null; + + const payload = { + content: localize('staff-management-system', 'err-no-perm'), + flags: MessageFlags.Ephemeral + }; + + if (interaction.deferred || interaction.replied) { + return interaction.followUp(payload); + } + return interaction.reply(payload); +} + async function applyBreakElapsedToShift(activeShift, breakStartTime, now = new Date()) { if (!activeShift || !breakStartTime) return; @@ -532,6 +551,7 @@ async function handleDutyEndButton(client, interaction) { const activeShifts = await Shift.findAll({ where: { userId, endTime: null } }); const shiftType = activeShifts.length > 0 ? activeShifts[0].type : 'Staff'; + let discardedForMinimum = false; for (const activeShift of activeShifts) { if (profile.onBreak && profile.breakStartTime) { @@ -545,6 +565,7 @@ async function handleDutyEndButton(client, interaction) { if (config.minShiftDuration && (durationSeconds / 60) < config.minShiftDuration) { await activeShift.destroy(); + discardedForMinimum = true; } else { await activeShift.update({ endTime, duration: durationSeconds }); } @@ -562,7 +583,17 @@ async function handleDutyEndButton(client, interaction) { } const payload = await buildDutyManagePayload(client, userId, interaction.guild, shiftType); - return interaction.editReply(payload); + await interaction.editReply(payload); + + if (discardedForMinimum) { + await interaction.followUp({ + content: localize('staff-management-system', 'err-shift-too-short', { + min: config.minShiftDuration + }), + flags: MessageFlags.Ephemeral + }); + } + return; } async function handleDutyHistPageButton(client, interaction) { @@ -605,6 +636,9 @@ async function handleDutyLbPageButton(client, interaction) { // ----- Admin handler ----- async function handleDutyAdminForceEnd(client, interaction) { + const permCheck = checkDutyAdminPermission(client, interaction); + if (permCheck) return permCheck; + const targetUserId = interaction.customId.split('_')[2]; const config = getConfig(client, 'shifts'); const Profile = client.models['staff-management-system']['StaffProfile']; @@ -647,6 +681,9 @@ async function handleDutyAdminForceEnd(client, interaction) { } async function handleDutyAdminVoidActive(client, interaction) { + const permCheck = checkDutyAdminPermission(client, interaction); + if (permCheck) return permCheck; + const targetUserId = interaction.customId.split('_')[2]; const config = getConfig(client, 'shifts'); const Profile = client.models['staff-management-system']['StaffProfile']; @@ -674,26 +711,36 @@ async function handleDutyAdminVoidActive(client, interaction) { } async function handleDutyAdminVoidAll(client, interaction) { + const permCheck = checkDutyAdminPermission(client, interaction); + if (permCheck) return permCheck; + const targetUserId = interaction.customId.split('_')[2]; + const confirmPhrase = localize('staff-management-system', 'del-conf-phrase'); const modal = new ModalBuilder() - .setCustomId(`duty-mgmt_admin-voidall-submit_${targetUserId}`) - .setTitle(localize('staff-management-system', 'mod-v-all-title')); + .setCustomId(`duty-mgmt_admin-voidall-submit_${targetUserId}`) + .setTitle(localize('staff-management-system', 'mod-v-all-title')); + modal.addComponents( - new ActionRowBuilder() - .addComponents(new TextInputBuilder() - .setCustomId('confirm') - .setLabel(localize('staff-management-system', 'mod-v-all-lbl')) - .setStyle(TextInputStyle.Short) - .setPlaceholder('CONFIRM') - .setRequired(true)) + new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId('confirm') + .setLabel(localize('staff-management-system', 'mod-v-all-lbl')) + .setStyle(TextInputStyle.Short) + .setPlaceholder(confirmPhrase) + .setRequired(true) + ) ); return interaction.showModal(modal); } async function handleDutyAdminVoidAllSubmit(client, interaction) { + const permCheck = checkDutyAdminPermission(client, interaction); + if (permCheck) return permCheck; + const targetUserId = interaction.customId.split('_')[2]; + const confirmPhrase = localize('staff-management-system', 'del-conf-phrase'); - if (interaction.fields.getTextInputValue('confirm') !== 'CONFIRM') { + if (interaction.fields.getTextInputValue('confirm').trim() !== confirmPhrase) { return interaction.reply({ content: localize('staff-management-system', 'err-conf-fail'), flags: MessageFlags.Ephemeral @@ -732,6 +779,9 @@ async function handleDutyAdminVoidAllSubmit(client, interaction) { } async function handleDutyAdminAddTimeButton(client, interaction) { + const permCheck = checkDutyAdminPermission(client, interaction); + if (permCheck) return permCheck; + const targetUserId = interaction.customId.split('_')[2]; const config = getConfig(client, 'shifts'); const dutyTypes = config.dutyTypes && config.dutyTypes.length > 0 @@ -770,11 +820,13 @@ async function handleDutyAdminAddTimeSubmit(client, interaction) { const minutesRaw = interaction.fields.getTextInputValue('minutes'); const shiftType = interaction.fields.getTextInputValue('type'); - const minutes = parseInt(minutesRaw); - if (isNaN(minutes) || minutes <= 0) { - return interaction.reply({ - content: localize('staff-management-system', 'err-inv-min'), - flags: MessageFlags.Ephemeral + const maxMinutes = 10080; + const minutes = parseInt(minutesRaw, 10); + + if (isNaN(minutes) || minutes <= 0 || minutes > maxMinutes) { + return interaction.reply({ + content: localize('staff-management-system', 'err-inv-min'), + flags: MessageFlags.Ephemeral }); } @@ -816,7 +868,10 @@ async function handleDutyAdminAddTimeSubmit(client, interaction) { const targetMember = await interaction.guild.members.fetch(targetUserId).catch(() => null); const payload = await buildDutyAdminPayload(client, targetMember, interaction.member); - return interaction.update(payload); + return interaction.reply({ + ...payload, + flags: MessageFlags.Ephemeral + }); } // ----- Dropdown handler ----- diff --git a/modules/staff-management-system/commands/staff-management.js b/modules/staff-management-system/commands/staff-management.js index 56c4cc08..77f01209 100644 --- a/modules/staff-management-system/commands/staff-management.js +++ b/modules/staff-management-system/commands/staff-management.js @@ -2,6 +2,7 @@ const { MessageFlags, EmbedBuilder, ModalBuilder, TextInputBuilder, TextInputSty const { embedTypeV2 } = require('../../../src/functions/helpers'); const { localize } = require('../../../src/functions/localize'); const { + applyFooter, issueInfraction, getInfractionHistory, issueSuspension, @@ -355,6 +356,7 @@ module.exports.subcommands = { }); const ActivityCheck = i.client.models['staff-management-system']['ActivityCheck']; + const ActivityCheckResponse = i.client.models['staff-management-system']['ActivityCheckResponse']; const activeCheck = await ActivityCheck.findOne({ where: { status: 'ACTIVE' } }); @@ -367,24 +369,27 @@ module.exports.subcommands = { if (Array.isArray(logChannelId)) logChannelId = logChannelId[0]; const channelPing = logChannelId - ? `<#${logChannelId}>` - : localize('staff-management-system', 'lbl-log-chan'); - return i.editReply({ + ? `<#${logChannelId}>` + : localize('staff-management-system', 'lbl-log-chan'); + + return i.editReply({ content: localize('staff-management-system', 'info-ac-none', { c: channelPing }) }); } - const responded = JSON.parse(activeCheck.respondedUsers || '[]'); - const embed = new EmbedBuilder() + const responseCount = await ActivityCheckResponse.count({ + where: { activityCheckId: activeCheck.id } + }); + + const embed = applyFooter(i.client, new EmbedBuilder() .setTitle(localize('staff-management-system', 'ac-live-title')) .setColor('Blue') - .setDescription(`**${localize('staff-management-system', 'general-ends')}:** \n**${localize('staff-management-system', 'general-chan')}:** <#${activeCheck.channelId}>\n**${localize('staff-management-system', 'ac-tot-res')}:** ${responded.length}`) - .setFooter({ - text: `${i.client.strings.footer}`, - iconURL: i.client.strings.footerImgUrl - }); - - if (!i.client.strings.disableFooterTimestamp) embed.setTimestamp(); + .setDescription( + `**${localize('staff-management-system', 'general-ends')}:** \n` + + `**${localize('staff-management-system', 'general-chan')}:** <#${activeCheck.channelId}>\n` + + `**${localize('staff-management-system', 'ac-tot-res')}:** ${responseCount}` + ) + ); await i.editReply({ embeds: [embed] }); diff --git a/modules/staff-management-system/commands/status.js b/modules/staff-management-system/commands/status.js index b9af2a2a..f9e2bfdc 100644 --- a/modules/staff-management-system/commands/status.js +++ b/modules/staff-management-system/commands/status.js @@ -27,8 +27,8 @@ async function sendStatusDm(user, type, dmType, data = {}) { ? 'LoA' : 'RA'; const viewCmd = type === 'LOA' - ? '`/loa view`' - : '`/ra view`'; + ? '`/status loa view`' + : '`/status ra view`'; const endFmt = data.endDate ? `` : ''; @@ -337,10 +337,7 @@ async function handleStatusView(client, interaction, type, targetUser) { await interaction.editReply({ embeds: [embed.toJSON()] }); } -async function handleStatusList(client, interaction, type, filter, page = 1) { - const limit = 10; - const offset = (page - 1) * limit; - +async function handleStatusList(client, interaction, type, filter) { let whereClause = { type }; let title = `${type} List`; @@ -360,25 +357,17 @@ async function handleStatusList(client, interaction, type, filter, page = 1) { const { count, rows } = await client.models['staff-management-system']['LoaRequest'].findAndCountAll({ where: whereClause, - limit, - offset, order: [['endDate', 'DESC']] }); if (count === 0) return interaction.editReply({ content: localize('staff-management-system', 'err-no-recs') }); - const totalPages = Math.ceil(count / limit) || 1; const embed = new EmbedBuilder() .setTitle(title) .setColor('Blue') - .setDescription(rows.map(r => `**<@${r.userId}>** ${r.status === 'APPROVED' ? '✅' : (r.status === 'DENIED' ? '❌' : '⏹️')}\nEnds: ${formatDate(r.endDate)}\nReason: ${r.reason}`).join('\n\n')) - .addFields( - { - name: '\u200b', - value: localize('staff-management-system', 'page-count', { page, total: totalPages }) - } - ); + .setDescription(rows.map(r => `**<@${r.userId}>** ${r.status === 'APPROVED' ? '✅' : (r.status === 'DENIED' ? '❌' : '⏹️')}\nEnds: ${formatDate(r.endDate)}\nReason: ${r.reason}`).join('\n\n')); + applyFooter(client, embed); await interaction.editReply({ embeds: [embed.toJSON()] }); } @@ -487,6 +476,14 @@ async function handleStatusEnd(interaction, type) { } async function handleStatusEndSubmit(client, interaction, type) { + const generalConfig = getConfig(client, 'configuration'); + if (!checkStaffPermissions(interaction.member, generalConfig, 'supervisor')) { + return interaction.reply({ + content: localize('staff-management-system', 'err-gen-no-perm'), + flags: MessageFlags.Ephemeral + }); + } + const meta = getStatusMeta(type); const request = await client.models['staff-management-system']['LoaRequest'].findByPk(interaction.customId.split('_')[2]); if (!request || request.status === 'ENDED' || request.status === 'DENIED') return interaction.reply({ @@ -545,9 +542,10 @@ async function handleStatusEndSubmit(client, interaction, type) { .setEmoji('📜') .setStyle(ButtonStyle.Secondary) ); - return interaction.update({ - embeds: [updatedEmbed.toJSON()], - components: [disabledRow.toJSON()] + return interaction.reply({ + embeds: [updatedEmbed.toJSON()], + components: [disabledRow.toJSON()], + flags: MessageFlags.Ephemeral }); } @@ -624,6 +622,14 @@ function scheduleStatusExpiry(client, request) { } async function handleStatusExtendSubmit(client, interaction, type) { + const generalConfig = getConfig(client, 'configuration'); + if (!checkStaffPermissions(interaction.member, generalConfig, 'supervisor')) { + return interaction.reply({ + content: localize('staff-management-system', 'err-gen-no-perm'), + flags: MessageFlags.Ephemeral + }); + } + const meta = getStatusMeta(type); const request = await client.models['staff-management-system']['LoaRequest'].findByPk(interaction.customId.split('_')[2]); if (!request || request.status === 'ENDED' || request.status === 'DENIED') return interaction.reply({ @@ -643,6 +649,7 @@ async function handleStatusExtendSubmit(client, interaction, type) { const oldEndDate = new Date(request.endDate); const newEndDate = new Date(oldEndDate.getTime() + days * 24 * 60 * 60 * 1000); await request.update({ endDate: newEndDate }); + request.endDate = newEndDate; scheduleStatusExpiry(client, request); const member = await interaction.guild.members.fetch(request.userId).catch(() => null); @@ -677,9 +684,10 @@ async function handleStatusExtendSubmit(client, interaction, type) { r: request.reason || localize('staff-management-system', 'info-none') }) }); - return interaction.update({ - embeds: [updatedEmbed.toJSON()], - components: interaction.message.components.map(c => c.toJSON()) + return interaction.reply({ + embeds: [updatedEmbed.toJSON()], + components: interaction.message.components.map(c => c.toJSON()), + flags: MessageFlags.Ephemeral }); } diff --git a/modules/staff-management-system/events/botReady.js b/modules/staff-management-system/events/botReady.js index 144688c5..eff0a9e7 100644 --- a/modules/staff-management-system/events/botReady.js +++ b/modules/staff-management-system/events/botReady.js @@ -2,6 +2,7 @@ const schedule = require('node-schedule'); const { localize } = require('../../../src/functions/localize'); const { Op } = require('sequelize'); const { scheduleStatusExpiry } = require('../commands/status.js'); +const { initActivityCheckAutomation } = require('../staff-management'); const suspension_check_job = 'staff-management-checks'; module.exports.run = async (client) => { @@ -22,14 +23,6 @@ module.exports.run = async (client) => { } if (guild) { - try { - await recoverActiveBreaks(client); - } catch (e) { - client.logger.error(localize('staff-management-system', 'log-err-break-recov', { - error: e.message - })); - } - try { await checkExpiredSuspensions(client, guild); } catch (e) { @@ -39,6 +32,14 @@ module.exports.run = async (client) => { } } + try { + initActivityCheckAutomation(client); + } catch (e) { + client.logger.error(localize('staff-management-system', 'log-sched-fail', { + error: e.message + })); + } + const existingJob = schedule.scheduledJobs[suspension_check_job]; if (existingJob) existingJob.cancel(); @@ -58,61 +59,6 @@ module.exports.run = async (client) => { }); }; -async function recoverActiveBreaks(client) { - const StaffProfile = client.models['staff-management-system']['StaffProfile']; - const Shift = client.models['staff-management-system']['StaffShift']; - const now = new Date(); - - const profilesOnBreak = await StaffProfile.findAll({ - where: { - onDuty: true, - onBreak: true, - breakStartTime: { [Op.not]: null } - } - }); - - if (!profilesOnBreak.length) return; - - const userIds = profilesOnBreak.map(profile => profile.userId); - - const activeShifts = await Shift.findAll({ - where: { - userId: { [Op.in]: userIds }, - endTime: null - } - }); - - const activeShiftMap = new Map(activeShifts.map(shift => [shift.userId, shift])); - - for (const profile of profilesOnBreak) { - const activeShift = activeShiftMap.get(profile.userId); - - if (!activeShift) { - await profile.update({ - onBreak: false, - breakStartTime: null - }); - continue; - } - - const breakStartedAt = new Date(profile.breakStartTime); - if (Number.isNaN(breakStartedAt.getTime()) || breakStartedAt > now) { - await profile.update({ breakStartTime: now }); - continue; - } - - const elapsedBreakMs = now.getTime() - breakStartedAt.getTime(); - - await activeShift.update({ - startTime: new Date(new Date(activeShift.startTime).getTime() + elapsedBreakMs) - }); - - await profile.update({ - breakStartTime: now - }); - } -} - async function checkExpiredSuspensions(client, guild) { const Infraction = client.models['staff-management-system']['Infraction']; const StaffProfile = client.models['staff-management-system']['StaffProfile']; diff --git a/modules/staff-management-system/events/guildMemberRemove.js b/modules/staff-management-system/events/guildMemberRemove.js index 38e0f6f3..795715d8 100644 --- a/modules/staff-management-system/events/guildMemberRemove.js +++ b/modules/staff-management-system/events/guildMemberRemove.js @@ -8,27 +8,41 @@ module.exports.run = async (client, member) => { const StaffProfile = client.models['staff-management-system']['StaffProfile']; try { - const openShift = await StaffShift.findOne({ + const profile = await StaffProfile.findByPk(member.id); + const openShifts = await StaffShift.findAll({ where: { userId: member.id, endTime: null } }); - if (openShift) { + for (const openShift of openShifts) { const now = new Date(); - const duration = Math.floor((now - openShift.startTime) / 1000); + let effectiveStart = new Date(openShift.startTime); + + if (profile?.onBreak && profile.breakStartTime) { + const breakStartedAt = new Date(profile.breakStartTime); + if (!Number.isNaN(breakStartedAt.getTime()) && breakStartedAt <= now) { + effectiveStart = new Date( + effectiveStart.getTime() + (now.getTime() - breakStartedAt.getTime()) + ); + } + } + + const duration = Math.max(0, Math.floor((now.getTime() - effectiveStart.getTime()) / 1000)); await openShift.update({ endTime: now, - duration: duration + duration }); - - client.logger.info(localize('staff-management-system', 'log-shift-leave', { tag: member.user.tag })); } await StaffProfile.update( - { onDuty: false }, + { + onDuty: false, + onBreak: false, + breakStartTime: null + }, { where: { userId: member.id } } ); diff --git a/modules/staff-management-system/events/interactionCreate.js b/modules/staff-management-system/events/interactionCreate.js index fd1a77ef..3effb220 100644 --- a/modules/staff-management-system/events/interactionCreate.js +++ b/modules/staff-management-system/events/interactionCreate.js @@ -74,9 +74,10 @@ module.exports.run = async (client, interaction) => { flags: MessageFlags.Ephemeral }); - const payload = await generateReviewHistoryResponse(client, targetUser, parseInt(parts[3])); - if (payload.content) return interaction.reply(payload); - return interaction.update(payload); + await interaction.deferUpdate(); + const payload = await generateReviewHistoryResponse(client, targetUser, parseInt(parts[3], 10)); + if (payload.content) return interaction.followUp({ ...payload, flags: MessageFlags.Ephemeral }); + return interaction.editReply(payload); } // ----- LOA/RA handlers ----- @@ -87,7 +88,7 @@ module.exports.run = async (client, interaction) => { const type = action.startsWith('loa-') ? 'LOA' : 'RA'; const base = action.replace(/^(loa|ra)-/, ''); - if (base === 'end') return handleStatusEnd(client, interaction, type); + if (base === 'end') return handleStatusEnd(interaction, type); if (base === 'end-submit') return handleStatusEndSubmit(client, interaction, type); if (base === 'extend') return handleStatusExtend(interaction, type); if (base === 'extend-submit') return handleStatusExtendSubmit(client, interaction, type); @@ -102,9 +103,10 @@ module.exports.run = async (client, interaction) => { flags: MessageFlags.Ephemeral }); + await interaction.deferUpdate(); const payload = await generatePromotionHistoryResponse(client, targetUser, parseInt(parts[3], 10)); - if (payload.content) return interaction.reply(payload); - return interaction.update(payload); + if (payload.content) return interaction.followUp({ ...payload, flags: MessageFlags.Ephemeral }); + return interaction.editReply(payload); } // ----- Infraction history pagination ----- @@ -114,9 +116,11 @@ module.exports.run = async (client, interaction) => { content: localize('staff-management-system', 'err-gen-no-user'), flags: MessageFlags.Ephemeral }); + + await interaction.deferUpdate(); const payload = await generateInfractionHistoryResponse(client, targetUser, parseInt(parts[3], 10)); - if (payload.content) return interaction.reply(payload); - return interaction.update(payload); + if (payload.content) return interaction.followUp({ ...payload, flags: MessageFlags.Ephemeral }); + return interaction.editReply(payload); } // ----- User panel dropdown ----- @@ -280,6 +284,7 @@ module.exports.run = async (client, interaction) => { }).catch(()=>{}); } }); + return; } await executeDataDeletion(client, targetId, selection); @@ -419,38 +424,65 @@ module.exports.run = async (client, interaction) => { // ----- Deny modal submission ----- if (interaction.isModalSubmit() && action === 'loa-deny') { + const configuration = getConfig(client, 'configuration'); + + if (!checkStaffPermissions(interaction.member, configuration, 'supervisor')) { + return interaction.reply({ + content: localize('staff-management-system', 'err-gen-no-perm'), + flags: MessageFlags.Ephemeral + }); + } + const reason = interaction.fields.getTextInputValue('reason'); const request = await LoARequest.findByPk(parts[2]); - - if (request) { - await request.update({ - status: 'DENIED', - approverId: interaction.user.id, - rejectionReason: reason + if (!request) { + return interaction.reply({ + content: localize('staff-management-system', 'err-no-req'), + flags: MessageFlags.Ephemeral }); - const member = await interaction.guild.members.fetch(request.userId).catch(() => null); - if (member) await sendStatusDm(member.user, request.type, 'denied', { - denier: interaction.user.tag, - reason + } + + await request.update({ + status: 'DENIED', + approverId: interaction.user.id, + rejectionReason: reason + }); + + const member = await interaction.guild.members.fetch(request.userId).catch(() => null); + if (member) { + await sendStatusDm(member.user, request.type, 'denied', { + denier: interaction.user.tag, + reason }); + } - const embed = EmbedBuilder + const embed = EmbedBuilder .from(interaction.message.embeds[0]) .setColor('Red') - .addFields({ - name: localize('staff-management-system', 'general-stat'), - value: localize('staff-management-system', 'req-deny-by', { - user: interaction.user.tag - }) - }, { - name: localize('staff-management-system', 'general-rsn'), - value: reason - }); - return interaction.update({ - embeds: [embed.toJSON()], - components: [] - }); - } + .addFields( + { + name: localize('staff-management-system', 'general-stat'), + value: localize('staff-management-system', 'req-deny-by', { + user: interaction.user.tag + }) + }, + { + name: localize('staff-management-system', 'general-rsn'), + value: reason + } + ); + + await interaction.message.edit({ + embeds: [embed.toJSON()], + components: [] + }).catch(() => {}); + + return interaction.reply({ + content: localize('staff-management-system', 'req-deny-by', { + user: interaction.user.tag + }), + flags: MessageFlags.Ephemeral + }); } // ----- Profile edit submission ----- @@ -473,6 +505,8 @@ module.exports.run = async (client, interaction) => { // ----- Activity checks button ----- if (action === 'ac-respond') { const ActivityCheck = client.models['staff-management-system']['ActivityCheck']; + const ActivityCheckResponse = client.models['staff-management-system']['ActivityCheckResponse']; + const activeCheck = await ActivityCheck.findOne({ where: { status: 'ACTIVE', @@ -492,19 +526,36 @@ module.exports.run = async (client, interaction) => { flags: MessageFlags.Ephemeral }); - let responded = JSON.parse(activeCheck.respondedUsers || '[]'); - if (responded.includes(interaction.user.id)) return interaction.reply({ - content: localize('staff-management-system', 'info-ac-alr-conf'), - flags: MessageFlags.Ephemeral + const existingResponse = await ActivityCheckResponse.findOne({ + where: { + activityCheckId: activeCheck.id, + userId: interaction.user.id + } }); - responded.push(interaction.user.id); - await activeCheck.update({ - respondedUsers: JSON.stringify(responded) + if (existingResponse) return interaction.reply({ + content: localize('staff-management-system', 'info-ac-alr-conf'), + flags: MessageFlags.Ephemeral }); - return interaction.reply({ - content: localize('staff-management-system', 'succ-ac-log'), - flags: MessageFlags.Ephemeral + + try { + await ActivityCheckResponse.create({ + activityCheckId: activeCheck.id, + userId: interaction.user.id + }); + } catch (e) { + if (e.name === 'SequelizeUniqueConstraintError') { + return interaction.reply({ + content: localize('staff-management-system', 'info-ac-alr-conf'), + flags: MessageFlags.Ephemeral + }); + } + throw e; + } + + return interaction.reply({ + content: localize('staff-management-system', 'succ-ac-log'), + flags: MessageFlags.Ephemeral }); } diff --git a/modules/staff-management-system/models/ActivityCheckResponse.js b/modules/staff-management-system/models/ActivityCheckResponse.js new file mode 100644 index 00000000..3a3a1f30 --- /dev/null +++ b/modules/staff-management-system/models/ActivityCheckResponse.js @@ -0,0 +1,36 @@ +const { DataTypes, Model } = require('sequelize'); + +module.exports = class StaffManagementActivityCheckResponse extends Model { + static init(sequelize) { + return super.init({ + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + activityCheckId: { + type: DataTypes.INTEGER, + allowNull: false + }, + userId: { + type: DataTypes.STRING, + allowNull: false + } + }, { + tableName: 'staff_management_activity_check_responses', + timestamps: true, + sequelize, + indexes: [ + { + unique: true, + fields: ['activityCheckId', 'userId'] + } + ] + }); + } +}; + +module.exports.config = { + name: 'ActivityCheckResponse', + module: 'staff-management-system' +}; \ No newline at end of file diff --git a/modules/staff-management-system/models/StaffShift.js b/modules/staff-management-system/models/StaffShift.js index bc5789bc..a9b50316 100644 --- a/modules/staff-management-system/models/StaffShift.js +++ b/modules/staff-management-system/models/StaffShift.js @@ -21,7 +21,7 @@ module.exports = class StaffManagementShift extends Model { }, type: { type: DataTypes.STRING, - defaultValue: "General" + defaultValue: "Staff" } }, { tableName: 'staff_management_shifts', diff --git a/modules/staff-management-system/staff-management.js b/modules/staff-management-system/staff-management.js index 96b31cef..a4724a4b 100644 --- a/modules/staff-management-system/staff-management.js +++ b/modules/staff-management-system/staff-management.js @@ -230,18 +230,23 @@ async function issueSuspension(client, interaction, targetMember, durationInput, const expiresAt = new Date(Date.now() + durationDays * 24 * 60 * 60 * 1000); const durationString = `${durationDays} ${localize('staff-management-system', 'label-days')}`; + let rolesToRemove = []; const hierarchyRole = interaction.guild.roles.cache.get(config.suspensionHierarchyRole); if (hierarchyRole) { - const rolesToRemove = targetMember.roles.cache.filter(r => r.position >= hierarchyRole.position && r.id !== interaction.guild.id && !r.managed).map(r => r.id); + rolesToRemove = targetMember.roles.cache + .filter(r => r.position >= hierarchyRole.position && r.id !== interaction.guild.id && !r.managed) + .map(r => r.id); + if (rolesToRemove.length) { await targetMember.roles.remove(rolesToRemove).catch(() => {}); - await client.models['staff-management-system']['StaffProfile'].upsert({ - userId: targetMember.id, - isSuspended: true, - suspendedRoles: JSON.stringify(rolesToRemove) - }); } } + + await client.models['staff-management-system']['StaffProfile'].upsert({ + userId: targetMember.id, + isSuspended: true, + suspendedRoles: JSON.stringify(rolesToRemove) + }); if (config.suspensionRole) await targetMember.roles.add(config.suspensionRole).catch(() => {}); const record = await client.models['staff-management-system']['Infraction'].create({ @@ -947,6 +952,8 @@ async function generatePanelStatus(client, targetUser, page = 1) { // Activity checks page async function generatePanelActivity(client, targetUser, page = 1) { const ActivityCheck = client.models['staff-management-system']['ActivityCheck']; + const ActivityCheckResponse = client.models['staff-management-system']['ActivityCheckResponse']; + const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - 90); @@ -957,24 +964,37 @@ async function generatePanelActivity(client, targetUser, page = 1) { order: [['createdAt', 'DESC']] }); - let userResponses = 0; - const historyRows = []; + if (recentChecks.length === 0) { + const embed = applyFooter(client, new EmbedBuilder() + .setTitle(localize('staff-management-system', 'p-act-title', { + username: targetUser.username + })) + .setColor('Blue') + .setThumbnail(targetUser.displayAvatarURL({ dynamic: true })) + .setDescription(localize('staff-management-system', 'p-act-desc', { count: 0 }) + localize('staff-management-system', 'p-no-hist')) + ); - for (const check of recentChecks) { - let responded = []; - try { - responded = JSON.parse(check.respondedUsers || '[]'); - } catch (e) { - client.logger.warn(`[Staff Management] ${e.message}`); - continue; - } + const menu = ActionRowBuilder.from((await generateUserPanel(client, targetUser)).components[0]); + menu.components[0].options.find(opt => opt.data.value === 'activity').data.default = true; - if (responded.includes(targetUser.id)) { - userResponses++; - historyRows.push(check); - } + return { + embeds: [embed.toJSON()], + components: [menu.toJSON()] + }; } + const checkIds = recentChecks.map(check => check.id); + const responses = await ActivityCheckResponse.findAll({ + where: { + activityCheckId: { [Op.in]: checkIds }, + userId: targetUser.id + }, + attributes: ['activityCheckId'] + }); + + const respondedCheckIds = new Set(responses.map(response => response.activityCheckId)); + const historyRows = recentChecks.filter(check => respondedCheckIds.has(check.id)); + const count = historyRows.length; let totalPages = 1; if (count > 3) totalPages = 1 + Math.ceil((count - 3) / 5); @@ -994,7 +1014,7 @@ async function generatePanelActivity(client, targetUser, page = 1) { .setThumbnail(targetUser.displayAvatarURL({ dynamic: true })) ); - let desc = localize('staff-management-system', 'p-act-desc', { count: userResponses }); + let desc = localize('staff-management-system', 'p-act-desc', { count }); if (paginatedRows.length === 0) { desc += localize('staff-management-system', 'p-no-hist'); @@ -1254,36 +1274,17 @@ async function executeDataDeletion(client, targetId, dataType) { if (profile) { await profile.update({ customNickname: null, - customIntro: null + customIntro: null, + isSuspended: false, + suspendedRoles: null }); } } if (['del_activity', 'del_all'].includes(dataType)) { - const cutoff = new Date(); - cutoff.setDate(cutoff.getDate() - 365); - - const checks = await models.ActivityCheck.findAll({ - where: { - createdAt: { [Op.gte]: cutoff } - } + await models.ActivityCheckResponse.destroy({ + where: { userId: targetId } }); - - for (const check of checks) { - let responded = []; - try { - responded = JSON.parse(check.respondedUsers || '[]'); - } catch { - continue; - } - - if (responded.includes(targetId)) { - responded = responded.filter(id => id !== targetId); - await check.update({ - respondedUsers: JSON.stringify(responded) - }); - } - } } } @@ -1356,7 +1357,6 @@ async function startActivityCheck(client, interactionOrChannel, isAutomated = fa channelId: targetChannel.id, endTime, targetRoles: JSON.stringify(rolesToCheck), - respondedUsers: '[]', status: 'ACTIVE' }); schedule.scheduleJob(endTime, async () => { @@ -1395,14 +1395,20 @@ async function endActivityCheckProcess(client, activeCheck) { if (!logChannel) return; const targetRoles = JSON.parse(activeCheck.targetRoles || '[]'); - const respondedUsers = JSON.parse(activeCheck.respondedUsers || '[]'); + const ActivityCheckResponse = client.models['staff-management-system']['ActivityCheckResponse']; + const responses = await ActivityCheckResponse.findAll({ + where: { activityCheckId: activeCheck.id }, + attributes: ['userId'] + }); + + const respondedUserIds = new Set(responses.map(response => response.userId)); const expectedMembers = guild.members.cache.filter(m => !m.user.bot && m.roles.cache.some(r => targetRoles.includes(r.id))); const [responded, exceptions, failed] = [[], [], []]; const profiles = await client.models['staff-management-system']['StaffProfile'].findAll(); expectedMembers.forEach(member => { - if (respondedUsers.includes(member.id)) return responded.push(member); + if (respondedUserIds.has(member.id)) return responded.push(member); let isException = false; const prof = profiles.find(p => p.userId === member.id); @@ -1629,6 +1635,8 @@ async function getReviewHistory(client, interaction, targetUser) { module.exports = { getConfig, + getSafeChannelId, + parseDurationToDays, applyFooter, checkStaffPermissions, buildPaginationRow, From bc627412a0023dc5f53690fbad899773f675ab8e Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Sun, 5 Apr 2026 09:17:46 +0200 Subject: [PATCH 13/27] New changes from feedback, and removed many unused locales keys --- locales/en.json | 46 ++----------------- .../staff-management-system/commands/duty.js | 27 ++++++++++- .../events/interactionCreate.js | 2 +- 3 files changed, 32 insertions(+), 43 deletions(-) diff --git a/locales/en.json b/locales/en.json index deb020c1..2645901f 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1129,7 +1129,6 @@ "req-deny-by": "❌ Denied by %user", "general-stat": "Status", "err-ac-alr-end": "❌ This activity check has already ended.", - "err-ac-notreq": "❌ You are not required to respond to this.", "info-ac-alr-conf": "ℹ️ You already confirmed your activity!", "succ-ac-log": "✅ Activity logged successfully!", "err-internal": "❌ An internal error occurred.", @@ -1289,58 +1288,21 @@ "rev-desc": "**Average:** %avg ⭐ (%count reviews)", "label-hist": "History", "info-ac-none": "There are no active activity checks. Please check recent results in %c.", - "log-sched-loa": "[Staff Management] Successfully scheduled %count active LoA/RA expirations.", "log-sched-fail": "[Staff Management] Failed to init expiry schedules: %error", "log-susp-end": "[Staff Management] Automatically ended suspension for %tag", "log-susp-err": "[Staff Management] Error expiring suspension: %error", - "log-shift-leave": "[Staff Management] Auto-ended shift for user %tag (User left guild).", "log-leave-err": "[Staff Management] Error handling member leave: %error", "log-del-all": "[Staff Management] Data deletion (ALL) executed for user %target by admin %admin.", "log-del-type": "[Staff Management] Data deletion (%type) executed for user %target by admin %admin.", "log-int-error": "[Staff Management] Interaction Error: %error", "log-void-all": "[Staff management] All shift data for the user with ID %target has been deleted by admin %admin.", "log-add-time": "[Staff Management] %admin added %min mins of %type duty time to %target.", - "log-panel-shift-err": "[Staff Management] User panel error: %error", - "log-ac-auto": "[Staff Management] Automated activity check is being initiated.", "log-stat-dm-error": "[Staff Management] Failed to send status DM to %u: %e", "log-status-adj-error": "[Staff Management] Logging status adjustment failed: %e", "log-promo-msg-error": "[Staff Management] Failed to send promotion announcement: %e", - "label-user": "User", - "label-dur": "Duration", - "btn-appr": "Approve", - "stat-pend": "Pending Approval", - "stat-auto": "Auto-Approved", - "filter-act": " (Active)", - "filter-exp": " (Expired)", - "filter-hist": " (History)", - "manage-stat-title": "Manage %l - %u", - "manage-stat-f1": "📋 Active %l Details", - "no-act-user": "This staff member does not have an active %l.", - "btn-ext": "Extend %l", - "err-no-act-end": "❌ No active %l to end.", - "modal-end-early": "End %l Early", - "label-reason-end": "Reason for ending early", - "err-stat-not-act": "❌ This %l is no longer active.", - "mod-stat-end-desc": "**Status:** ⚫ %l ended early by %u\n**Reason:** %r", - "err-no-act-ext": "❌ No active %l to extend.", - "modal-ext": "Extend %l", - "label-add-days": "Additional days (e.g. 3, 7, 14)", - "label-ext-reason": "Reason for extension", - "log-adj-text": "1. **%l extended:** it now ends at %n. Old end date: %o.\n2. **Reason:** %r", - "label-ext-by": "extended by %dd", - "info-no-stat-hist": "ℹ️ This staff member has no %l history.", - "stat-hist-desc": "Showing %r of %c %l records.", - "err-no-user": "❌ Could not find that user.", - "btn-conf-act": "Confirm Activity", - "duty-hi-line": "Start: | End: ", - "err-gen": "❌ Error: %e", "lbl-log-chan": "the configured log channel", "ac-live-title": "Live Activity Check Status", - "lbl-ends": "Ends", - "del-conf-phr": "I understand that this will delete the specified data for this user and it cannot be undone.", - "err-ac-ended": "❌ This activity check has already ended.", "err-ac-not-req": "❌ You are not required to respond to this activity check.", - "info-ac-alr": "ℹ️ You have already confirmed your activity!", "cmd-desc-status": "Manage Leave of Absence (LoA) and Reduced Activity (RA).", "cmd-desc-loa": "Manage Leave of Absence (LoA).", "cmd-desc-loa-request": "Request a Leave of Absence.", @@ -1366,7 +1328,6 @@ "cmd-desc-duty-manage": "Manage your duty status.", "cmd-desc-duty-manage-type": "The duty type", "cmd-desc-duty-active": "View all staff currently on duty.", - "cmd-desc-duty-history": "View your duty history.", "cmd-desc-duty-lb": "View the duty time leaderboard.", "cmd-desc-duty-lb-type": "The duty type for the leaderboard.", "cmd-desc-duty-time": "View your total duty time.", @@ -1413,7 +1374,10 @@ "cmd-desc-submit-stars": "The star rating for the review.", "cmd-desc-submit-comment": "Your review comment.", "del-no-perm": "You do not have sufficient permissions to perform data deletion.", - "log-err-break-recov": "Break recovery failed: %error", - "log-err-exp-susp": "Suspension check failed: %error" + "log-err-exp-susp": "Suspension check failed: %error", + "duty-admin-target-left": "The action was completed, but the user is no longer in the server.", + "err-shift-too-short": "Your shift was not counted because it was shorter than the minimum required duration of %min% minute(s).", + "log-status-expiry-fail": "[Staff Management] Failed to process automatic status expiry: %error%", + "none-provided": "No reason provided." } } diff --git a/modules/staff-management-system/commands/duty.js b/modules/staff-management-system/commands/duty.js index 8d3dabbc..7d7b2cc4 100644 --- a/modules/staff-management-system/commands/duty.js +++ b/modules/staff-management-system/commands/duty.js @@ -676,6 +676,14 @@ async function handleDutyAdminForceEnd(client, interaction) { } const targetMember = await interaction.guild.members.fetch(targetUserId).catch(() => null); + if (!targetMember) { + return interaction.editReply({ + content: localize('staff-management-system', 'duty-admin-target-left'), + embeds: [], + components: [] + }); + } + const payload = await buildDutyAdminPayload(client, targetMember, interaction.member); return interaction.editReply(payload); } @@ -706,6 +714,14 @@ async function handleDutyAdminVoidActive(client, interaction) { } const targetMember = await interaction.guild.members.fetch(targetUserId).catch(() => null); + if (!targetMember) { + return interaction.editReply({ + content: localize('staff-management-system', 'duty-admin-target-left'), + embeds: [], + components: [] + }); + } + const payload = await buildDutyAdminPayload(client, targetMember, interaction.member); return interaction.editReply(payload); } @@ -816,6 +832,9 @@ async function handleDutyAdminAddTimeButton(client, interaction) { } async function handleDutyAdminAddTimeSubmit(client, interaction) { + const permCheck = checkDutyAdminPermission(client, interaction); + if (permCheck) return permCheck; + const targetUserId = interaction.customId.split('_')[2]; const minutesRaw = interaction.fields.getTextInputValue('minutes'); const shiftType = interaction.fields.getTextInputValue('type'); @@ -866,8 +885,14 @@ async function handleDutyAdminAddTimeSubmit(client, interaction) { })); const targetMember = await interaction.guild.members.fetch(targetUserId).catch(() => null); + if (!targetMember) { + return interaction.reply({ + content: localize('staff-management-system', 'duty-admin-target-left'), + flags: MessageFlags.Ephemeral + }); + } + const payload = await buildDutyAdminPayload(client, targetMember, interaction.member); - return interaction.reply({ ...payload, flags: MessageFlags.Ephemeral diff --git a/modules/staff-management-system/events/interactionCreate.js b/modules/staff-management-system/events/interactionCreate.js index 3effb220..72d9a462 100644 --- a/modules/staff-management-system/events/interactionCreate.js +++ b/modules/staff-management-system/events/interactionCreate.js @@ -522,7 +522,7 @@ module.exports.run = async (client, interaction) => { const targetRoles = JSON.parse(activeCheck.targetRoles || '[]'); const hasRole = targetRoles.length === 0 || interaction.member.roles.cache.some(r => targetRoles.includes(r.id)); if (!hasRole) return interaction.reply({ - content: localize('staff-management-system', 'err-ac-notreq'), + content: localize('staff-management-system', 'err-not-req'), flags: MessageFlags.Ephemeral }); From 217dc78de2d76e6cf440bf962e29171041716752 Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Wed, 8 Apr 2026 21:37:08 +0200 Subject: [PATCH 14/27] Some changes --- locales/en.json | 6 +-- .../commands/staff-management.js | 2 +- .../configs/infractions.json | 39 +++++++++---------- .../configs/profiles.json | 31 ++++++++------- .../configs/promotions.json | 2 + .../configs/reviews.json | 14 +++++-- .../configs/status.json | 6 ++- 7 files changed, 56 insertions(+), 44 deletions(-) diff --git a/locales/en.json b/locales/en.json index 2645901f..47ecc067 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1280,9 +1280,9 @@ "ac-f-fail": "❌ Failed (%count)", "ac-f-exc": "🛡️ Exceptions (%count)", "log-ac-send-fail": "Failed to send activity check results message: %error", - "err-not-mem": "❌ Not a member.", - "err-self-rate": "❌ Cannot rate yourself.", - "err-staff-rate": "❌ Can only rate staff.", + "err-not-mem": "❌ That is not a member.", + "err-self-rate": "❌ You cannot rate yourself.", + "err-staff-rate": "❌ You can only rate staff.", "succ-review": "✅ Rated %tag %stars stars.", "rev-title": "Reviews: %username", "rev-desc": "**Average:** %avg ⭐ (%count reviews)", diff --git a/modules/staff-management-system/commands/staff-management.js b/modules/staff-management-system/commands/staff-management.js index 77f01209..647824cb 100644 --- a/modules/staff-management-system/commands/staff-management.js +++ b/modules/staff-management-system/commands/staff-management.js @@ -144,7 +144,7 @@ async function handleProfileView(client, interaction, targetUser) { let msgOpts = await embedTypeV2(embedTemplate, placeholders); - if (!msgOpts || (!msgOpts.content && (!msgOpts.embeds || msgOpts.embeds.length === 0))) { + if (!msgOpts) { return interaction.editReply({ content: localize('staff-management-system', 'err-prof-empty') }); diff --git a/modules/staff-management-system/configs/infractions.json b/modules/staff-management-system/configs/infractions.json index 385198be..0ce6384b 100644 --- a/modules/staff-management-system/configs/infractions.json +++ b/modules/staff-management-system/configs/infractions.json @@ -117,10 +117,10 @@ "name": "suspensionMessage", "category": "suspensions", "humanName": { - "en": "Suspension Announcement Embed" + "en": "Suspension Announcement Message" }, "description": { - "en": "The embed sent to the log channel when a staff member is suspended." + "en": "The message sent to the log channel when a staff member is suspended." }, "type": "string", "allowEmbed": true, @@ -185,6 +185,7 @@ ], "default": { "en": { + "_schema": "v3", "content": "%user%", "embeds": [ { @@ -223,10 +224,10 @@ "name": "infractionMessage", "category": "messages", "humanName": { - "en": "Infraction Announcement Embed" + "en": "Infraction Announcement Message" }, "description": { - "en": "The embed sent to the log channel for regular infractions." + "en": "The message sent to the log channel for regular infractions." }, "type": "string", "allowEmbed": true, @@ -257,22 +258,15 @@ } }, { - "name": "issuerPfp", - "description": { - "en": "Avatar of the issuer" - }, - "isImage": true - }, - { - "name": "duration", + "name": "type", "description": { - "en": "Duration of suspension" + "en": "Type of infraction (e.g., Warning, Strike)" } }, { "name": "endDate", "description": { - "en": "Timestamp of when the suspension ends" + "en": "Timestamp of when this infraction expires" } }, { @@ -290,6 +284,7 @@ ], "default": { "en": { + "_schema": "v3", "content": "%user%", "embeds": [ { @@ -297,7 +292,7 @@ "name": "Signed, %issuerName% • Case #%caseId%", "iconURL": "%issuerPfp%" }, - "title": "⚠️ New %type%", + "title": "⚠️ New infraction", "description": "**Staff Member:** %user%\n**Action Taken:** %type%\n**Expires:** %endDate%\n**Reason:** %reason%", "color": "#e67e22", "thumbnailURL": "%userPfp%" @@ -310,7 +305,7 @@ "name": "dmInfractedUser", "category": "messages", "humanName": { - "en": "DM User on Infraction?" + "en": "DM User on infraction?" }, "description": { "en": "If enabled, the bot will DM the staff member when they receive an infraction or suspension." @@ -324,7 +319,7 @@ "name": "infractionDmMessage", "category": "messages", "humanName": { - "en": "Infraction DM Embed" + "en": "Infraction DM Message" }, "description": { "en": "The message sent directly to the staff member." @@ -372,13 +367,14 @@ ], "default": { "en": { + "_schema": "v3", "embeds": [ { "author": { "name": "Signed, %issuerName% • Case #%caseId%" }, - "title": "⚠️ Staff Notice: %type%", - "description": "You have received a formal **%type%** from the management team.\n\n**Reason:** %reason%\n**Expires:** %endDate%", + "title": "⚠️ You have been infracted", + "description": "**Type:** %type%\n**Reason:** %reason%\n**Expires:** %endDate%", "color": "#e67e22" } ] @@ -389,7 +385,7 @@ "name": "suspensionDmMessage", "category": "messages", "humanName": { - "en": "Suspension DM Embed" + "en": "Suspension DM Message1" }, "description": { "en": "The message sent directly to the staff member when suspended." @@ -437,13 +433,14 @@ ], "default": { "en": { + "_schema": "v3", "embeds": [ { "author": { "name": "Signed, %issuerName% • Case #%caseId%" }, "title": "⛔ Staff Suspension", - "description": "Your staff privileges have been temporarily suspended.\n\n**Duration:** %duration%\n**Returns:** %endDate%\n**Reason:** %reason%\n\nDuring this time, your roles have been removed and you are expected to step away from all staff duties.", + "description": "You have been temporarily suspended.\n\n**Duration:** %duration%\n**Returns:** %endDate%\n**Reason:** %reason%", "color": "#ed4245" } ] diff --git a/modules/staff-management-system/configs/profiles.json b/modules/staff-management-system/configs/profiles.json index bbbaacfd..3aceb777 100644 --- a/modules/staff-management-system/configs/profiles.json +++ b/modules/staff-management-system/configs/profiles.json @@ -121,20 +121,25 @@ ], "default": { "en": { - "title": "Staff Profile: %nickname%", - "description": "%intro%", - "color": "#2b2d31", - "thumbnail": "%pfp%", - "fields": [ + "_schema": "v3", + "embeds": [ { - "name": "Status", - "value": "%status%", - "inline": true - }, - { - "name": "Average Rating", - "value": "%rating%", - "inline": true + "title": "Staff Profile: %nickname%", + "description": "%intro%", + "color": "#2b2d31", + "thumbnailURL": "%pfp%", + "fields": [ + { + "name": "Status", + "value": "%status%", + "inline": true + }, + { + "name": "Average Rating", + "value": "%rating%", + "inline": true + } + ] } ] } diff --git a/modules/staff-management-system/configs/promotions.json b/modules/staff-management-system/configs/promotions.json index ff4e2337..279925e4 100644 --- a/modules/staff-management-system/configs/promotions.json +++ b/modules/staff-management-system/configs/promotions.json @@ -133,6 +133,7 @@ ], "default": { "en": { + "_schema": "v3", "content": "%user%", "embeds": [ { @@ -227,6 +228,7 @@ ], "default": { "en": { + "_schema": "v3", "content": "%user%", "embeds": [ { diff --git a/modules/staff-management-system/configs/reviews.json b/modules/staff-management-system/configs/reviews.json index b8685ebc..fdbb7dac 100644 --- a/modules/staff-management-system/configs/reviews.json +++ b/modules/staff-management-system/configs/reviews.json @@ -136,10 +136,16 @@ ], "default": { "en": { - "title": "🌟 New Staff Rating", - "description": "**Staff:** %target%\n**Rated by:** %author%\n\n**Rating:** %stars% (%rating%/5)\n**Comment:**\n%comment%", - "color": "#f1c40f", - "thumbnail": "%staff-profile-picture%" + "_schema": "v3", + "content": "%user%", + "embeds": [ + { + "title": "🌟 New Staff Rating", + "description": "**Staff:** %target%\n**Rated by:** %author%\n\n**Rating:** %stars% (%rating%/5)\n**Comment:**\n%comment%", + "color": "#f1c40f", + "thumbnailURL": "%staff-profile-picture%" + } + ] } } } diff --git a/modules/staff-management-system/configs/status.json b/modules/staff-management-system/configs/status.json index 599cac5b..fcc58e2b 100644 --- a/modules/staff-management-system/configs/status.json +++ b/modules/staff-management-system/configs/status.json @@ -72,7 +72,8 @@ "type": "integer", "default": { "en": 60 - } + }, + "minValue": 1 }, { "name": "requireLoaApproval", @@ -130,7 +131,8 @@ "type": "integer", "default": { "en": 30 - } + }, + "minValue": 1 }, { "name": "requireRaApproval", From e85a4b90a8393b7bbc46e5e209adc66afcab0bcd Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Fri, 10 Apr 2026 18:21:58 +0200 Subject: [PATCH 15/27] Made changes according to feedbacks, bugs and more. Also includes some improved locales --- locales/en.json | 52 +- .../staff-management-system/commands/duty.js | 408 +++++++++++-- .../commands/staff-management.js | 552 ++++++++++-------- .../commands/status.js | 54 +- .../configs/activity-checks.json | 6 +- .../configs/configuration.json | 4 + .../configs/infractions.json | 61 +- .../configs/profiles.json | 10 +- .../configs/promotions.json | 50 +- .../configs/reviews.json | 36 +- .../configs/shifts.json | 44 +- .../configs/status.json | 58 +- .../models/StaffShift.js | 5 + .../staff-management.js | 229 +++++--- 14 files changed, 1065 insertions(+), 504 deletions(-) diff --git a/locales/en.json b/locales/en.json index 47ecc067..8ccd02b1 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1281,7 +1281,7 @@ "ac-f-exc": "🛡️ Exceptions (%count)", "log-ac-send-fail": "Failed to send activity check results message: %error", "err-not-mem": "❌ That is not a member.", - "err-self-rate": "❌ You cannot rate yourself.", + "err-self-rate": "A good detective never investigates themselves. Neither do you.", "err-staff-rate": "❌ You can only rate staff.", "succ-review": "✅ Rated %tag %stars stars.", "rev-title": "Reviews: %username", @@ -1350,13 +1350,15 @@ "cmd-desc-history": "View a user's history.", "cmd-desc-history-user": "The user whose history you want to view.", "cmd-desc-void": "Void an infraction case.", - "cmd-desc-void-case-id": "The case ID of the infraction to void.", + "cmd-desc-void-case-ref": "The case ID or message link of the infraction to void.", "cmd-desc-promotion": "Manage staff promotions.", "cmd-desc-promote": "Promote a staff member to a new rank.", "cmd-desc-promote-user": "The user to promote.", "cmd-desc-promote-rank": "The rank to promote the user to.", "cmd-desc-promote-reason": "The reason for the promotion.", "cmd-desc-promote-channel": "The channel to announce the promotion in.", + "cmd-desc-prom-history": "View the promotion history of a staff member.", + "cmd-desc-prom-history-user": "The user whose promotion history you want to view.", "cmd-desc-ac": "Manage activity checks.", "cmd-desc-ac-start": "Start a new activity check.", "cmd-desc-ac-start-channel": "The channel where the activity check will be posted.", @@ -1369,15 +1371,47 @@ "cmd-desc-profile-wipe": "Wipe a staff member's profile data.", "cmd-desc-profile-wipe-user": "The user whose profile will be wiped.", "cmd-desc-review": "Manage staff reviews.", - "cmd-desc-submit": "Submit a review for a staff member.", - "cmd-desc-submit-user": "The user you are reviewing.", - "cmd-desc-submit-stars": "The star rating for the review.", - "cmd-desc-submit-comment": "Your review comment.", + "cmd-desc-review-submit": "Submit a review for a staff member.", + "cmd-desc-review-submit-user": "The user you are reviewing.", + "cmd-desc-review-submit-stars": "The star rating for the review.", + "cmd-desc-review-submit-comment": "Your review comment.", + "cmd-desc-review-history": "View the review history of a staff member.", + "cmd-desc-review-history-user": "The user whose review history you want to view.", "del-no-perm": "You do not have sufficient permissions to perform data deletion.", "log-err-exp-susp": "Suspension check failed: %error", "duty-admin-target-left": "The action was completed, but the user is no longer in the server.", - "err-shift-too-short": "Your shift was not counted because it was shorter than the minimum required duration of %min% minute(s).", - "log-status-expiry-fail": "[Staff Management] Failed to process automatic status expiry: %error%", - "none-provided": "No reason provided." + "err-shift-too-short": "Your shift was not counted because it was shorter than the minimum required duration of %min minute(s).", + "log-status-expiry-fail": "[Staff Management] Failed to process automatic status expiry: %error", + "none-provided": "No reason provided.", + "log-infract-dm-fail": "[Staff Management] Failed to send infraction DM to %user: %error", + "log-susp-dm-fail": "[Staff Management] Failed to send suspension DM to %user: %error", + "log-promo-dm-fail": "[Staff Management] Failed to send promotion DM to %user: %error", + "duty-started-title": "⏲️ Shift Started", + "duty-break-title": "⏸️ On Break", + "duty-ended-title": "↩️ Off-Duty", + "duty-shift-overview": "Shift Overview", + "duty-shift-report-title": "Shift Report", + "duty-shift-information": "Shift Information", + "label-started": "Started", + "label-ended": "Ended", + "label-elapsed-time": "Elapsed Time", + "label-shift-type": "Shift Type", + "log-duty-dm-fail": "[Staff Management] Failed to send shift report DM to %user: %error", + "label-breaks": "Breaks", + "log-duty-start-title": "%username went on-duty", + "log-duty-start-desc": "%mention has started a duty shift.", + "log-duty-break-title": "%username went on break", + "log-duty-break-desc": "%mention is now on break.", + "log-duty-resume-title": "%username resumed duty", + "log-duty-resume-desc": "%mention is back on duty.", + "log-duty-end-title": "%username went off-duty", + "log-duty-end-desc": "%mention has ended their duty shift.", + "log-duty-void-title": "%username's active shift was voided", + "log-duty-void-desc": "%mention's active shift was voided by %executor.", + "log-duty-info-hdr": "Information", + "label-ended-by": "Ended by", + "log-duty-log-fail": "[Staff Management] Failed to log duty change (%action): %error", + "err-self-infract": "That's not in the code... well, it's more of a guideline anyway. Still no.\n-# You cannot infract yourself", + "err-self-promo": "You can't promote yourself through a black hole of audacity and expect it to work." } } diff --git a/modules/staff-management-system/commands/duty.js b/modules/staff-management-system/commands/duty.js index 7d7b2cc4..77fedaf3 100644 --- a/modules/staff-management-system/commands/duty.js +++ b/modules/staff-management-system/commands/duty.js @@ -1,6 +1,6 @@ const { MessageFlags, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, StringSelectMenuBuilder, ModalBuilder, TextInputBuilder, TextInputStyle } = require('discord.js'); const { Op, fn, col, literal } = require('sequelize'); -const { getConfig, applyFooter, formatDuration, buildPaginationRow, checkStaffPermissions } = require('../staff-management'); +const { getConfig, applyFooter, getSafeChannelId, formatDuration, buildPaginationRow, checkStaffPermissions } = require('../staff-management'); const { localize } = require('../../../src/functions/localize'); function getLookbackDate(config) { @@ -65,7 +65,144 @@ function getQuotaForMember(member, config) { return bestQuota; } -async function buildDutyManagePayload(client, userId, guild, shiftType) { +async function sendShiftEndDm(client, member, shift) { + if (!member || !shift) return; + + const embed = applyFooter(client, new EmbedBuilder() + .setTitle(localize('staff-management-system', 'duty-shift-report-title')) + .setThumbnail(member.user.displayAvatarURL({ dynamic: true })) + .addFields( + { + name: localize('staff-management-system', 'duty-shift-information'), + value: + `>>> **${localize('staff-management-system', 'label-shift-type')}:** ${shift.type || 'Staff'}\n` + + `**${localize('staff-management-system', 'general-start')}:** \n` + + `**${localize('staff-management-system', 'general-end')}:** \n` + + `**${localize('staff-management-system', 'label-breaks')}:** ${shift.breakCount || 0}` + }, + { + name: localize('staff-management-system', 'label-elapsed-time'), + value: `> ${formatDuration(parseInt(shift.duration) || 0)}` + } + ) + ); + + try { + await member.user.send({ embeds: [embed.toJSON()] }); + } catch (e) { + client.logger.warn(localize('staff-management-system', 'log-duty-dm-fail', { + user: member.user.tag, + error: e.message + })); + } +} + +async function logShiftChange(client, action, data) { + const shiftsConfig = getConfig(client, 'shifts'); + if (!shiftsConfig?.logShiftChanges) return; + const channelId = + getSafeChannelId(shiftsConfig.logShiftChangesChannel) || + getSafeChannelId(getConfig(client, 'configuration')?.generalLogChannel); + if (!channelId) return; + + const guild = client.guilds.cache.get(client.guildID); + if (!guild) return; + const channel = await guild.channels.fetch(channelId).catch(() => null); + if (!channel) return; + + const targetUserObj = data.targetUser || await client.users.fetch(data.userId).catch(() => null); + const mention = targetUserObj ? targetUserObj.toString() : `<@${data.userId}>`; + const username = targetUserObj ? targetUserObj.username : data.userId; + + const embed = new EmbedBuilder() + .setThumbnail(targetUserObj?.displayAvatarURL({ dynamic: true }) || null); + + if (action === 'start') { + embed + .setTitle(localize('staff-management-system', 'log-duty-start-title', { username })) + .setColor('Green') + .setDescription(localize('staff-management-system', 'log-duty-start-desc', { mention })) + .addFields({ + name: localize('staff-management-system', 'log-duty-info-hdr'), + value: + `**${localize('staff-management-system', 'general-start')}:** \n` + + `**${localize('staff-management-system', 'label-shift-type')}:** ${data.shiftType || 'Staff'}` + }); + } else if (action === 'break') { + embed + .setTitle(localize('staff-management-system', 'log-duty-break-title', { username })) + .setColor('Yellow') + .setDescription(localize('staff-management-system', 'log-duty-break-desc', { mention })) + .addFields({ + name: localize('staff-management-system', 'log-duty-info-hdr'), + value: + `**${localize('staff-management-system', 'general-start')}:** \n` + + `**${localize('staff-management-system', 'label-shift-type')}:** ${data.shiftType || 'Staff'}\n` + + `**${localize('staff-management-system', 'label-breaks')}:** ${data.breakCount || 0}\n` + + `**${localize('staff-management-system', 'label-elapsed-time')}:** ${formatDuration(data.elapsedSeconds || 0)}` + }); + } else if (action === 'resume') { + embed + .setTitle(localize('staff-management-system', 'log-duty-resume-title', { username })) + .setColor('Green') + .setDescription(localize('staff-management-system', 'log-duty-resume-desc', { mention })) + .addFields({ + name: localize('staff-management-system', 'log-duty-info-hdr'), + value: + `**${localize('staff-management-system', 'general-start')}:** \n` + + `**${localize('staff-management-system', 'label-shift-type')}:** ${data.shiftType || 'Staff'}\n` + + `**${localize('staff-management-system', 'label-breaks')}:** ${data.breakCount || 0}\n` + + `**${localize('staff-management-system', 'label-elapsed-time')}:** ${formatDuration(data.elapsedSeconds || 0)}` + }); + } else if (action === 'end') { + embed + .setTitle(localize('staff-management-system', 'log-duty-end-title', { username })) + .setColor('Red') + .setDescription(localize('staff-management-system', 'log-duty-end-desc', { mention })) + .addFields({ + name: localize('staff-management-system', 'log-duty-info-hdr'), + value: + `**${localize('staff-management-system', 'general-start')}:** \n` + + `**${localize('staff-management-system', 'general-end')}:** \n` + + `**${localize('staff-management-system', 'label-shift-type')}:** ${data.shiftType || 'Staff'}\n` + + `**${localize('staff-management-system', 'label-breaks')}:** ${data.breakCount || 0}\n` + + `**${localize('staff-management-system', 'label-elapsed-time')}:** ${formatDuration(data.durationSeconds || 0)}` + + (data.executorId + ? `\n**${localize('staff-management-system', 'label-ended-by')}:** <@${data.executorId}>` + : '') + }); + } else if (action === 'void') { + embed + .setTitle(localize('staff-management-system', 'log-duty-void-title', { username })) + .setColor('DarkRed') + .setDescription(localize('staff-management-system', 'log-duty-void-desc', { + mention, + executor: `<@${data.executorId}>` + })) + .addFields({ + name: localize('staff-management-system', 'log-duty-info-hdr'), + value: + `**${localize('staff-management-system', 'general-start')}:** \n` + + `**${localize('staff-management-system', 'label-shift-type')}:** ${data.shiftType || 'Staff'}\n` + + `**${localize('staff-management-system', 'label-breaks')}:** ${data.breakCount || 0}` + }); + } else { + return; + } + + applyFooter(client, embed); + + try { + await channel.send({ embeds: [embed.toJSON()] }); + } catch (e) { + client.logger.error(localize('staff-management-system', 'log-duty-log-fail', { + action, + error: e.message + })); + } +} + +async function buildDutyManagePayload(client, userId, shiftType, endedShift = null) { const Profile = client.models['staff-management-system']['StaffProfile']; const Shift = client.models['staff-management-system']['StaffShift']; @@ -75,10 +212,14 @@ async function buildDutyManagePayload(client, userId, guild, shiftType) { const onDuty = profile?.onDuty || false; const onBreak = profile?.onBreak || false; - let statusText, statusColor; - if (onDuty && onBreak) { statusText = localize('staff-management-system', 'stat-brk'); statusColor = 'Yellow'; } - else if (onDuty) { statusText = localize('staff-management-system', 'stat-on'); statusColor = 'Green'; } - else { statusText = localize('staff-management-system', 'stat-off'); statusColor = 'Red'; } + let statusColor; + if (onDuty && onBreak) { + statusColor = 'Yellow'; + } else if (onDuty) { + statusColor = 'Green'; + } else { + statusColor = 'Red'; + } const completedShifts = await Shift.findAll({ where: { @@ -91,44 +232,88 @@ async function buildDutyManagePayload(client, userId, guild, shiftType) { const totalShifts = completedShifts.length; const totalSeconds = completedShifts.reduce((sum, s) => sum + (parseInt(s.duration) || 0), 0); const avgSeconds = totalShifts > 0 - ? Math.floor(totalSeconds / totalShifts) - : 0; + ? Math.floor(totalSeconds / totalShifts) + : 0; + + const activeShift = onDuty + ? await Shift.findOne({ + where: { userId, endTime: null }, + order: [['startTime', 'DESC']] + }) + : null; + + let titleKey = 'duty-panel-title'; + if (onDuty && onBreak) titleKey = 'duty-break-title'; + else if (onDuty) titleKey = 'duty-started-title'; + else if (endedShift) titleKey = 'duty-ended-title'; const embed = applyFooter(client, new EmbedBuilder() - .setTitle(localize('staff-management-system', 'duty-panel-title', { type: shiftType })) + .setTitle(localize('staff-management-system', titleKey, { type: shiftType })) .setColor(statusColor) .setThumbnail(user?.displayAvatarURL({ dynamic: true }) || null) - .setDescription(`**${user?.username || userId}**\n${statusText}`) - .addFields( - { - name: localize('staff-management-system', 'duty-stats'), - value: localize('staff-management-system', 'duty-stat-desc', - { - duration: formatDuration(totalSeconds), - count: totalShifts, - average: formatDuration(avgSeconds) - } - ) - } - ) ); + if (onDuty && activeShift) { + let elapsedSeconds; + if (onBreak && profile?.breakStartTime) { + elapsedSeconds = Math.max( + 0, + Math.floor( + (new Date(profile.breakStartTime).getTime() - new Date(activeShift.startTime).getTime()) / 1000 + ) + ); + } else { + elapsedSeconds = Math.max( + 0, + Math.floor((Date.now() - new Date(activeShift.startTime).getTime()) / 1000) + ); + } + + embed.addFields({ + name: localize('staff-management-system', 'duty-shift-overview'), + value: + `>>> **${localize('staff-management-system', 'label-started')}:** \n` + + `**${localize('staff-management-system', 'label-breaks')}:** ${activeShift.breakCount || 0}\n` + + `**${localize('staff-management-system', 'label-elapsed-time')}:** ${formatDuration(elapsedSeconds)}` + }); + } else if (endedShift) { + embed.addFields({ + name: localize('staff-management-system', 'duty-shift-overview'), + value: + `>>> **${localize('staff-management-system', 'label-started')}:** \n` + + `**${localize('staff-management-system', 'label-breaks')}:** ${endedShift.breakCount || 0}\n` + + `**${localize('staff-management-system', 'label-ended')}:** ` + }); + } else { + embed.addFields({ + name: localize('staff-management-system', 'duty-stats'), + value: localize('staff-management-system', 'duty-stat-desc', { + duration: formatDuration(totalSeconds), + count: totalShifts, + average: formatDuration(avgSeconds) + }) + }); + } + const row = new ActionRowBuilder().addComponents( new ButtonBuilder() - .setCustomId(`duty-mgmt_start_${userId}_${shiftType}`) - .setLabel(localize('staff-management-system', 'btn-duty-on')) - .setStyle(ButtonStyle.Success) - .setDisabled(onDuty), + .setCustomId(`duty-mgmt_start_${userId}_${shiftType}`) + .setLabel(localize('staff-management-system', 'btn-duty-on')) + .setStyle(ButtonStyle.Success) + .setDisabled(onDuty), new ButtonBuilder() - .setCustomId(`duty-mgmt_break_${userId}`) - .setLabel(onBreak ? localize('staff-management-system', 'btn-duty-res') : localize('staff-management-system', 'btn-duty-brk')) - .setStyle(ButtonStyle.Secondary) - .setDisabled(!onDuty), + .setCustomId(`duty-mgmt_break_${userId}`) + .setLabel(onBreak + ? localize('staff-management-system', 'btn-duty-res') + : localize('staff-management-system', 'btn-duty-brk') + ) + .setStyle(ButtonStyle.Secondary) + .setDisabled(!onDuty), new ButtonBuilder() - .setCustomId(`duty-mgmt_end_${userId}`) - .setLabel(localize('staff-management-system', 'btn-duty-off')) - .setStyle(ButtonStyle.Danger) - .setDisabled(!onDuty) + .setCustomId(`duty-mgmt_end_${userId}`) + .setLabel(localize('staff-management-system', 'btn-duty-off')) + .setStyle(ButtonStyle.Danger) + .setDisabled(!onDuty) ); return { @@ -403,7 +588,7 @@ async function buildDutyAdminPayload(client, targetMember, requestingMember) { })) .setColor(statusColor) .setThumbnail(targetUser.displayAvatarURL({ dynamic: true })) - .setDescription(`**${targetUser.username}**\n${statusText}`) + .setDescription(`**${statusText}**`) .addFields( { name: localize('staff-management-system', 'duty-stats'), @@ -472,16 +657,17 @@ async function handleDutyStartButton(client, interaction) { flags: MessageFlags.Ephemeral }); - await Shift.create({ - userId, - startTime: new Date(), - type: shiftType + const startTime = new Date(); + await Shift.create({ + userId, + startTime, + type: shiftType }); - await Profile.upsert({ - userId, - onDuty: true, - onBreak: false, - lastClockIn: new Date() + await Profile.upsert({ + userId, + onDuty: true, + onBreak: false, + lastClockIn: startTime }); if (config.onDutyRole) { @@ -489,7 +675,14 @@ async function handleDutyStartButton(client, interaction) { if (member) await member.roles.add(config.onDutyRole).catch(() => {}); } - const payload = await buildDutyManagePayload(client, userId, interaction.guild, shiftType); + await logShiftChange(client, 'start', { + userId, + targetUser: interaction.user, + shiftType, + startTime + }); + + const payload = await buildDutyManagePayload(client, userId, shiftType); return interaction.editReply(payload); } @@ -515,20 +708,58 @@ async function handleDutyBreakButton(client, interaction) { const shiftType = activeShift?.type || 'Staff'; const nowOnBreak = !profile.onBreak; + let breakCount = activeShift?.breakCount || 0; + if (nowOnBreak && activeShift) { + breakCount += 1; + await activeShift.update({ + breakCount + }); + } if (!nowOnBreak && profile.breakStartTime && activeShift) { await applyBreakElapsedToShift(activeShift, profile.breakStartTime); } + const elapsedSeconds = activeShift + ? Math.max( + 0, + Math.floor( + ((nowOnBreak ? new Date() : new Date(profile.breakStartTime || Date.now())).getTime() - + new Date(activeShift.startTime).getTime()) / 1000 + ) + ) + : 0; + + const breakStartTime = nowOnBreak ? new Date() : null; await Profile.update({ onBreak: nowOnBreak, - breakStartTime: nowOnBreak - ? new Date() - : null + breakStartTime }, { - where: { userId } + where: { userId } }); - const payload = await buildDutyManagePayload(client, userId, interaction.guild, shiftType); + if (activeShift) { + if (nowOnBreak) { + await logShiftChange(client, 'break', { + userId, + targetUser: interaction.user, + shiftType, + startTime: activeShift.startTime, + breakCount: activeShift.breakCount || 0, + elapsedSeconds + }); + } else { + await logShiftChange(client, 'resume', { + userId, + targetUser: interaction.user, + shiftType, + startTime: activeShift.startTime, + breakCount: activeShift.breakCount || 0, + elapsedSeconds + }); + } + } + + const payload = await buildDutyManagePayload(client, userId, shiftType); return interaction.editReply(payload); } @@ -552,6 +783,7 @@ async function handleDutyEndButton(client, interaction) { const activeShifts = await Shift.findAll({ where: { userId, endTime: null } }); const shiftType = activeShifts.length > 0 ? activeShifts[0].type : 'Staff'; let discardedForMinimum = false; + let endedShiftForDisplay = null; for (const activeShift of activeShifts) { if (profile.onBreak && profile.breakStartTime) { @@ -567,22 +799,43 @@ async function handleDutyEndButton(client, interaction) { await activeShift.destroy(); discardedForMinimum = true; } else { - await activeShift.update({ endTime, duration: durationSeconds }); + await activeShift.update({ + endTime, + duration: durationSeconds + }); + endedShiftForDisplay = activeShift; } } await Profile.update({ onDuty: false, onBreak: false, - breakStartTime: null }, { - where: { userId } - }); - if (config.onDutyRole) { - const member = await interaction.guild.members.fetch(userId).catch(() => null); - if (member) await member.roles.remove(config.onDutyRole).catch(() => {}); + breakStartTime: null + }, { + where: { userId } + }); + + const member = await interaction.guild.members.fetch(userId).catch(() => null); + if (config.onDutyRole && member) { + await member.roles.remove(config.onDutyRole).catch(() => {}); + } + if (member && endedShiftForDisplay) { + await sendShiftEndDm(client, member, endedShiftForDisplay); } - const payload = await buildDutyManagePayload(client, userId, interaction.guild, shiftType); + if (endedShiftForDisplay) { + await logShiftChange(client, 'end', { + userId, + targetUser: interaction.user, + shiftType: endedShiftForDisplay.type || shiftType, + startTime: endedShiftForDisplay.startTime, + endTime: endedShiftForDisplay.endTime, + breakCount: endedShiftForDisplay.breakCount || 0, + durationSeconds: parseInt(endedShiftForDisplay.duration) || 0 + }); +} + + const payload = await buildDutyManagePayload(client, userId, shiftType, endedShiftForDisplay); await interaction.editReply(payload); if (discardedForMinimum) { @@ -644,6 +897,7 @@ async function handleDutyAdminForceEnd(client, interaction) { const Profile = client.models['staff-management-system']['StaffProfile']; const Shift = client.models['staff-management-system']['StaffShift']; const profile = await Profile.findByPk(targetUserId); + let endedShiftForDisplay = null; const activeShifts = await Shift.findAll({ where: { userId: targetUserId, endTime: null } @@ -662,6 +916,7 @@ async function handleDutyAdminForceEnd(client, interaction) { endTime, duration: durationSeconds }); + endedShiftForDisplay = activeShift; } await Profile.update({ @@ -675,6 +930,18 @@ async function handleDutyAdminForceEnd(client, interaction) { if (member) await member.roles.remove(config.onDutyRole).catch(() => {}); } + if (endedShiftForDisplay) { + await logShiftChange(client, 'end', { + userId: targetUserId, + shiftType: endedShiftForDisplay.type || 'Staff', + startTime: endedShiftForDisplay.startTime, + endTime: endedShiftForDisplay.endTime, + breakCount: endedShiftForDisplay.breakCount || 0, + durationSeconds: parseInt(endedShiftForDisplay.duration) || 0, + executorId: interaction.user.id + }); + } + const targetMember = await interaction.guild.members.fetch(targetUserId).catch(() => null); if (!targetMember) { return interaction.editReply({ @@ -697,9 +964,13 @@ async function handleDutyAdminVoidActive(client, interaction) { const Profile = client.models['staff-management-system']['StaffProfile']; const Shift = client.models['staff-management-system']['StaffShift']; - const activeShifts = await Shift.findAll({ - where: { userId: targetUserId, endTime: null } + const activeShifts = await Shift.findAll({ + where: { userId: targetUserId, endTime: null }, + order: [['startTime', 'DESC']] }); + const shiftForLog = activeShifts.length > 0 + ? activeShifts[0] + : null; for (const activeShift of activeShifts) await activeShift.destroy(); await Profile.update({ @@ -713,6 +984,16 @@ async function handleDutyAdminVoidActive(client, interaction) { if (member) await member.roles.remove(config.onDutyRole).catch(() => {}); } + if (shiftForLog) { + await logShiftChange(client, 'void', { + userId: targetUserId, + shiftType: shiftForLog.type || 'Staff', + startTime: shiftForLog.startTime, + breakCount: shiftForLog.breakCount || 0, + executorId: interaction.user.id + }); +} + const targetMember = await interaction.guild.members.fetch(targetUserId).catch(() => null); if (!targetMember) { return interaction.editReply({ @@ -902,7 +1183,7 @@ async function handleDutyAdminAddTimeSubmit(client, interaction) { // ----- Dropdown handler ----- async function handleDutyDropdown(client, interaction, action, selectedType) { if (action === 'manage') { - const payload = await buildDutyManagePayload(client, interaction.user.id, interaction.guild, selectedType); + const payload = await buildDutyManagePayload(client, interaction.user.id, selectedType); return interaction.editReply({ content: '', ...payload }); } if (action === 'leaderboard') { @@ -953,7 +1234,7 @@ async function handleCommonDutyCommand(i, action) { } if (action === 'manage') { - const payload = await buildDutyManagePayload(i.client, i.user.id, i.guild, shiftType); + const payload = await buildDutyManagePayload(i.client, i.user.id, shiftType); await i.editReply(payload); } else if (action === 'leaderboard') { const payload = await buildLeaderboardPayload(i.client, 1, shiftType); @@ -1168,6 +1449,9 @@ module.exports.config = { usage: '/duty', type: 'slash', defaultPermission: false, + disabled: function (client) { + return !client.configurations['staff-management-system']['shifts']?.enableShifts; + }, options: [ { type: 'SUB_COMMAND', diff --git a/modules/staff-management-system/commands/staff-management.js b/modules/staff-management-system/commands/staff-management.js index 647824cb..7b633014 100644 --- a/modules/staff-management-system/commands/staff-management.js +++ b/modules/staff-management-system/commands/staff-management.js @@ -124,13 +124,13 @@ async function handleProfileView(client, interaction, targetUser) { const nicknameText = profile.customNickname || user.username; const placeholders = { - '%user%': user.toString(), + '%user-mention%': user.toString(), '%username%': user.username, '%nickname%': nicknameText, '%intro%': introText, '%status%': statusLines.join('\n'), '%rating%': ratingDisplay, - '%pfp%': user.displayAvatarURL({ + '%avatar%': user.displayAvatarURL({ dynamic: true, format: 'png', size: 1024 @@ -325,7 +325,7 @@ module.exports.subcommands = { await getInfractionHistory(i.client, i, user); }, 'void': async (i) => { - const caseId = i.options.getInteger('case_id'); + const caseId = i.options.getString('reference'); await voidInfraction(i.client, i, caseId); } }, @@ -452,8 +452,16 @@ module.exports.config = { usage: '/staff-management', type: 'slash', defaultPermission: false, - options: [ - { + options: function (client) { + const array = []; + + const infractionsConfig = client.configurations['staff-management-system']['infractions'] || {}; + const promotionsConfig = client.configurations['staff-management-system']['promotions'] || {}; + const activityChecksConfig = client.configurations['staff-management-system']['activity-checks'] || {}; + const profilesConfig = client.configurations['staff-management-system']['profiles'] || {}; + const reviewsConfig = client.configurations['staff-management-system']['reviews'] || {}; + + array.push({ type: 'SUB_COMMAND', name: 'panel', description: localize('staff-management-system', 'cmd-desc-panel'), @@ -465,258 +473,286 @@ module.exports.config = { required: true } ] - }, - { - type: 'SUB_COMMAND_GROUP', - name: 'infraction', - description: localize('staff-management-system', 'cmd-desc-infractions'), - options: [ - { - type: 'SUB_COMMAND', - name: 'issue', - description: localize('staff-management-system', 'cmd-desc-issue'), - options: [ - { - type: 'USER', - name: 'user', - description: localize('staff-management-system', 'cmd-desc-issue-user'), - required: true - }, - { - type: 'STRING', - name: 'type', - description: localize('staff-management-system', 'cmd-desc-issue-type'), - required: true, - autocomplete: true - }, - { - type: 'STRING', - name: 'reason', - description: localize('staff-management-system', 'cmd-desc-issue-reason'), - required: true - }, - { - type: 'STRING', - name: 'expiry', - description: localize('staff-management-system', 'cmd-desc-issue-expiry'), - required: false - } - ] - }, - { - type: 'SUB_COMMAND', - name: 'suspend', - description: localize('staff-management-system', 'cmd-desc-suspend'), - options: [ - { - type: 'USER', - name: 'user', - description: localize('staff-management-system', 'cmd-desc-suspend-user'), - required: true - }, - { - type: 'STRING', - name: 'duration', - description: localize('staff-management-system', 'cmd-desc-suspend-duration'), - required: true - }, - { - type: 'STRING', - name: 'reason', - description: localize('staff-management-system', 'cmd-desc-suspend-reason'), - required: true - } - ] - }, - { - type: 'SUB_COMMAND', - name: 'history', - description: localize('staff-management-system', 'cmd-desc-history'), - options: [ - { - type: 'USER', - name: 'user', - description: localize('staff-management-system', 'cmd-desc-history-user'), - required: true - } - ] - }, - { - type: 'SUB_COMMAND', - name: 'void', - description: localize('staff-management-system', 'cmd-desc-void'), - options: [ - { - type: 'INTEGER', - name: 'case_id', - description: localize('staff-management-system', 'cmd-desc-void-case-id'), - required: true - } - ] - } - ] - }, - { - type: 'SUB_COMMAND_GROUP', - name: 'promotion', - description: localize('staff-management-system', 'cmd-desc-promotion'), - options: [ - { - type: 'SUB_COMMAND', - name: 'promote', - description: localize('staff-management-system', 'cmd-desc-promote'), - options: [ - { - type: 'USER', - name: 'user', - description: localize('staff-management-system', 'cmd-desc-promote-user'), - required: true - }, - { - type: 'ROLE', - name: 'rank', - description: localize('staff-management-system', 'cmd-desc-promote-rank'), - required: true - }, - { - type: 'STRING', - name: 'reason', - description: localize('staff-management-system', 'cmd-desc-promote-reason'), - required: false - }, - { - type: 'CHANNEL', - name: 'channel', - description: localize('staff-management-system', 'cmd-desc-promote-channel'), - required: false, - channelTypes: [0, 5] - } - ] - }, - { - type: 'SUB_COMMAND', - name: 'history', - description: localize('staff-management-system', 'cmd-desc-history'), - options: [{ - type: 'USER', - name: 'user', - description: localize('staff-management-system', 'cmd-desc-history-user'), - required: true - }] - } - ] - }, - { - type: 'SUB_COMMAND_GROUP', - name: 'activity-check', - description: localize('staff-management-system', 'cmd-desc-ac'), - options: [ - { - type: 'SUB_COMMAND', - name: 'start', - description: localize('staff-management-system', 'cmd-desc-ac-start'), - options: [ - { - type: 'CHANNEL', - name: 'channel', - description: localize('staff-management-system', 'cmd-desc-ac-start-channel'), - required: false, - channelTypes: [0] - } - ] - }, - { - type: 'SUB_COMMAND', - name: 'view', - description: localize('staff-management-system', 'cmd-desc-ac-view') - }, - { - type: 'SUB_COMMAND', - name: 'end', - description: localize('staff-management-system', 'cmd-desc-ac-end') - } - ] - }, - { - type: 'SUB_COMMAND_GROUP', - name: 'profile', - description: localize('staff-management-system', 'cmd-desc-profile'), - options: [ - { - type: 'SUB_COMMAND', - name: 'view', - description: localize('staff-management-system', 'cmd-desc-profile-view'), - options: [{ - type: 'USER', - name: 'user', - description: localize('staff-management-system', 'cmd-desc-profile-view-user'), - required: false - }] - }, - { - type: 'SUB_COMMAND', - name: 'edit', - description: localize('staff-management-system', 'cmd-desc-profile-edit') - }, - { - type: 'SUB_COMMAND', - name: 'wipe', - description: localize('staff-management-system', 'cmd-desc-profile-wipe'), - options: [ - { - type: 'USER', - name: 'user', - description: localize('staff-management-system', 'cmd-desc-profile-wipe-user'), - required: true - } - ] - } - ] - }, - { - type: 'SUB_COMMAND_GROUP', - name: 'review', - description: localize('staff-management-system', 'cmd-desc-review'), - options: [ - { - type: 'SUB_COMMAND', - name: 'submit', - description: localize('staff-management-system', 'cmd-desc-submit'), - options: [ - { - type: 'USER', - name: 'user', - description: localize('staff-management-system', 'cmd-desc-submit-user'), - required: true - }, - { - type: 'INTEGER', - name: 'stars', - description: localize('staff-management-system', 'cmd-desc-submit-stars'), - required: true, - minValue: 1, - maxValue: 5 - }, - { - type: 'STRING', - name: 'comment', - description: localize('staff-management-system', 'cmd-desc-submit-comment'), - required: true - } - ] - }, - { - type: 'SUB_COMMAND', - name: 'history', - description: localize('staff-management-system', 'cmd-desc-history'), - options: [{ - type: 'USER', - name: 'user', - description: localize('staff-management-system', 'cmd-desc-history-user'), - required: false - }] - } - ] + }); + + if (infractionsConfig.enableInfractions) { + array.push({ + type: 'SUB_COMMAND_GROUP', + name: 'infraction', + description: localize('staff-management-system', 'cmd-desc-infractions'), + options: [ + { + type: 'SUB_COMMAND', + name: 'issue', + description: localize('staff-management-system', 'cmd-desc-issue'), + options: [ + { + type: 'USER', + name: 'user', + description: localize('staff-management-system', 'cmd-desc-issue-user'), + required: true + }, + { + type: 'STRING', + name: 'type', + description: localize('staff-management-system', 'cmd-desc-issue-type'), + required: true, + autocomplete: true + }, + { + type: 'STRING', + name: 'reason', + description: localize('staff-management-system', 'cmd-desc-issue-reason'), + required: true + }, + { + type: 'STRING', + name: 'expiry', + description: localize('staff-management-system', 'cmd-desc-issue-expiry'), + required: false + } + ] + }, + ...(infractionsConfig.enableSuspensions ? [{ + type: 'SUB_COMMAND', + name: 'suspend', + description: localize('staff-management-system', 'cmd-desc-suspend'), + options: [ + { + type: 'USER', + name: 'user', + description: localize('staff-management-system', 'cmd-desc-suspend-user'), + required: true + }, + { + type: 'STRING', + name: 'duration', + description: localize('staff-management-system', 'cmd-desc-suspend-duration'), + required: true + }, + { + type: 'STRING', + name: 'reason', + description: localize('staff-management-system', 'cmd-desc-suspend-reason'), + required: true + } + ] + }] : []), + { + type: 'SUB_COMMAND', + name: 'history', + description: localize('staff-management-system', 'cmd-desc-history'), + options: [ + { + type: 'USER', + name: 'user', + description: localize('staff-management-system', 'cmd-desc-history-user'), + required: true + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'void', + description: localize('staff-management-system', 'cmd-desc-void'), + options: [ + { + type: 'STRING', + name: 'reference', + description: localize('staff-management-system', 'cmd-desc-void-case-ref'), + required: true + } + ] + } + ] + }); } - ] + + if (promotionsConfig.enablePromotions) { + array.push({ + type: 'SUB_COMMAND_GROUP', + name: 'promotion', + description: localize('staff-management-system', 'cmd-desc-promotion'), + options: [ + { + type: 'SUB_COMMAND', + name: 'promote', + description: localize('staff-management-system', 'cmd-desc-promote'), + options: [ + { + type: 'USER', + name: 'user', + description: localize('staff-management-system', 'cmd-desc-promote-user'), + required: true + }, + { + type: 'ROLE', + name: 'rank', + description: localize('staff-management-system', 'cmd-desc-promote-rank'), + required: true + }, + { + type: 'STRING', + name: 'reason', + description: localize('staff-management-system', 'cmd-desc-promote-reason'), + required: true + }, + { + type: 'CHANNEL', + name: 'channel', + description: localize('staff-management-system', 'cmd-desc-promote-channel'), + required: false, + channelTypes: [0, 5] + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'history', + description: localize('staff-management-system', 'cmd-desc-prom-history'), + options: [ + { + type: 'USER', + name: 'user', + description: localize('staff-management-system', 'cmd-desc-prom-history-user'), + required: true + } + ] + } + ] + }); + } + + if (activityChecksConfig.enableActivityChecks) { + array.push({ + type: 'SUB_COMMAND_GROUP', + name: 'activity-check', + description: localize('staff-management-system', 'cmd-desc-ac'), + options: [ + { + type: 'SUB_COMMAND', + name: 'start', + description: localize('staff-management-system', 'cmd-desc-ac-start'), + options: [ + { + type: 'CHANNEL', + name: 'channel', + description: localize('staff-management-system', 'cmd-desc-ac-start-channel'), + required: false, + channelTypes: [0] + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'view', + description: localize('staff-management-system', 'cmd-desc-ac-view') + }, + { + type: 'SUB_COMMAND', + name: 'end', + description: localize('staff-management-system', 'cmd-desc-ac-end') + } + ] + }); + } + + if (profilesConfig.enableProfiles) { + array.push({ + type: 'SUB_COMMAND_GROUP', + name: 'profile', + description: localize('staff-management-system', 'cmd-desc-profile'), + options: [ + { + type: 'SUB_COMMAND', + name: 'view', + description: localize('staff-management-system', 'cmd-desc-profile-view'), + options: [ + { + type: 'USER', + name: 'user', + description: localize('staff-management-system', 'cmd-desc-profile-view-user'), + required: false + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'edit', + description: localize('staff-management-system', 'cmd-desc-profile-edit') + }, + { + type: 'SUB_COMMAND', + name: 'wipe', + description: localize('staff-management-system', 'cmd-desc-profile-wipe'), + options: [ + { + type: 'USER', + name: 'user', + description: localize('staff-management-system', 'cmd-desc-profile-wipe-user'), + required: true + } + ] + } + ] + }); + } + + if (reviewsConfig.enableReviews) { + array.push({ + type: 'SUB_COMMAND_GROUP', + name: 'review', + description: localize('staff-management-system', 'cmd-desc-review'), + options: [ + { + type: 'SUB_COMMAND', + name: 'submit', + description: localize('staff-management-system', 'cmd-desc-review-submit'), + options: [ + { + type: 'USER', + name: 'user', + description: localize('staff-management-system', 'cmd-desc-review-submit-user'), + required: true + }, + { + type: 'INTEGER', + name: 'stars', + description: localize('staff-management-system', 'cmd-desc-review-submit-stars'), + required: true, + choices: [ + { name: '1 ⭐', value: 1 }, + { name: '2 ⭐⭐', value: 2 }, + { name: '3 ⭐⭐⭐', value: 3 }, + { name: '4 ⭐⭐⭐⭐', value: 4 }, + { name: '5 ⭐⭐⭐⭐⭐', value: 5 } + ] + }, + { + type: 'STRING', + name: 'comment', + description: localize('staff-management-system', 'cmd-desc-review-submit-comment'), + required: true + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'history', + description: localize('staff-management-system', 'cmd-desc-review-history'), + options: [ + { + type: 'USER', + name: 'user', + description: localize('staff-management-system', 'cmd-desc-review-history-user'), + required: false + } + ] + } + ] + }); + } + + return array; + } }; \ No newline at end of file diff --git a/modules/staff-management-system/commands/status.js b/modules/staff-management-system/commands/status.js index f9e2bfdc..9962e8c1 100644 --- a/modules/staff-management-system/commands/status.js +++ b/modules/staff-management-system/commands/status.js @@ -87,7 +87,13 @@ async function sendStatusDm(user, type, dmType, data = {}) { u: user.tag }) ); -} +}} + +function isStatusTypeEnabled(config, type) { + if (!config?.enableStatusSystem) return false; + return type === 'LOA' + ? !!config.enableLoa + : !!config.enableRa; } async function logStatusChange(client, type, action, data) { @@ -197,9 +203,8 @@ const getStatusMeta = (type) => ({ async function handleStatusRequest(client, interaction, type, durationInput, reason) { const config = getConfig(client, 'status'); const isLoa = type === 'LOA'; - if (!config[isLoa - ? 'enableLoa' - : 'enableRa']) return interaction.editReply({ + if (!isStatusTypeEnabled(config, type)) + return interaction.editReply({ content: localize('staff-management-system', 'err-status-disabled', { type }) } ); @@ -375,8 +380,9 @@ async function handleStatusList(client, interaction, type, filter) { async function handleStatusManage(client, interaction, targetMember, type) { const config = getConfig(client, 'status'); const meta = getStatusMeta(type); - if (!config[meta.enableKey]) return interaction.editReply({ - content: localize('staff-management-system', 'err-status-disabled', { type }) + if (!isStatusTypeEnabled(config, type)) + return interaction.editReply({ + content: localize('staff-management-system', 'err-status-disabled', { type }) }); const generalConfig = getConfig(client, 'configuration'); @@ -826,12 +832,22 @@ module.exports.config = { usage: '/status', type: 'slash', defaultPermission: false, - options: [ - { - type: 'SUB_COMMAND_GROUP', - name: 'loa', - description: localize('staff-management-system', 'cmd-desc-loa'), - options: [ + disabled: function (client) { + return !client.configurations['staff-management-system']['status']?.enableStatusSystem; + }, + + options: function (client) { + const config = getConfig(client, 'status'); + const array = []; + + if (!config?.enableStatusSystem) return array; + + if (config.enableLoa) { + array.push({ + type: 'SUB_COMMAND_GROUP', + name: 'loa', + description: localize('staff-management-system', 'cmd-desc-loa'), + options: [ { type: 'SUB_COMMAND', name: 'request', @@ -901,9 +917,11 @@ module.exports.config = { } ] } - ] - }, - { + ]}); + } + + if (config.enableRa) { + array.push({ type: 'SUB_COMMAND_GROUP', name: 'ra', description: localize('staff-management-system', 'cmd-desc-ra'), @@ -978,9 +996,11 @@ module.exports.config = { } ] } - ] + ]}); } - ] + + return array; + } }; module.exports.sendStatusDm = sendStatusDm; diff --git a/modules/staff-management-system/configs/activity-checks.json b/modules/staff-management-system/configs/activity-checks.json index 5e18b796..16813d19 100644 --- a/modules/staff-management-system/configs/activity-checks.json +++ b/modules/staff-management-system/configs/activity-checks.json @@ -128,7 +128,8 @@ }, "type": "channelID", "channelTypes": [ - "GUILD_TEXT" + "GUILD_TEXT", + "GUILD_NEWS" ], "default": { "en": "" @@ -278,7 +279,8 @@ "en": "" }, "channelTypes": [ - "GUILD_TEXT" + "GUILD_TEXT", + "GUILD_NEWS" ], "allowNull": true }, diff --git a/modules/staff-management-system/configs/configuration.json b/modules/staff-management-system/configs/configuration.json index 600aeff7..a29c0df5 100644 --- a/modules/staff-management-system/configs/configuration.json +++ b/modules/staff-management-system/configs/configuration.json @@ -78,6 +78,10 @@ "en": "The default channel where logs happen such as status changes/request and more. This can be overridden in some features." }, "type": "channelID", + "channelTypes": [ + "GUILD_TEXT", + "GUILD_NEWS" + ], "default": { "en": "" } diff --git a/modules/staff-management-system/configs/infractions.json b/modules/staff-management-system/configs/infractions.json index 0ce6384b..05a43418 100644 --- a/modules/staff-management-system/configs/infractions.json +++ b/modules/staff-management-system/configs/infractions.json @@ -133,26 +133,26 @@ } }, { - "name": "userPfp", + "name": "user-avatar", "description": { "en": "Avatar of the staff member" }, "isImage": true }, { - "name": "issuerMention", + "name": "issuer-mention", "description": { "en": "Mention of the manager issuing it" } }, { - "name": "issuerName", + "name": "issuer-name", "description": { "en": "Name of the issuer" } }, { - "name": "issuerPfp", + "name": "issuer-avatar", "description": { "en": "Avatar of the issuer" }, @@ -165,7 +165,7 @@ } }, { - "name": "endDate", + "name": "end-date", "description": { "en": "Timestamp of when the suspension ends" } @@ -177,7 +177,7 @@ } }, { - "name": "caseId", + "name": "case-id", "description": { "en": "Database Case ID" } @@ -190,13 +190,13 @@ "embeds": [ { "author": { - "name": "Signed, %issuerName% • Case #%caseId%", - "iconURL": "%issuerPfp%" + "name": "Signed, %issuer-name% • Case #%case-id%", + "iconURL": "%issuer-avatar%" }, "title": "⛔ Staff Suspension", - "description": "**Staff Member:** %user%\n**Duration:** %duration%\n**Ends:** %endDate%\n**Reason:** %reason%", + "description": "**Staff Member:** %user%\n**Duration:** %duration%\n**Ends:** %end-date%\n**Reason:** %reason%", "color": "#ed4245", - "thumbnailURL": "%userPfp%" + "thumbnailURL": "%user-avatar%" } ] } @@ -239,24 +239,31 @@ } }, { - "name": "userPfp", + "name": "user-avatar", "description": { "en": "Avatar of the staff member" }, "isImage": true }, { - "name": "issuerMention", + "name": "issuer-mention", "description": { "en": "Mention of the manager issuing it" } }, { - "name": "issuerName", + "name": "issuer-name", "description": { "en": "Name of the issuer" } }, + { + "name": "issuer-avatar", + "description": { + "en": "Avatar of the issuer" + }, + "isImage": true + }, { "name": "type", "description": { @@ -289,13 +296,13 @@ "embeds": [ { "author": { - "name": "Signed, %issuerName% • Case #%caseId%", - "iconURL": "%issuerPfp%" + "name": "Signed, %issuer-name% • Case #%case-id%", + "iconURL": "%issuer-avatar%" }, "title": "⚠️ New infraction", - "description": "**Staff Member:** %user%\n**Action Taken:** %type%\n**Expires:** %endDate%\n**Reason:** %reason%", + "description": "**Staff Member:** %user%\n**Action Taken:** %type%\n**Expires:** %end-date%\n**Reason:** %reason%", "color": "#e67e22", - "thumbnailURL": "%userPfp%" + "thumbnailURL": "%user-avatar%" } ] } @@ -335,7 +342,7 @@ } }, { - "name": "issuerName", + "name": "issuer-name", "description": { "en": "Name of the issuer" } @@ -347,7 +354,7 @@ } }, { - "name": "endDate", + "name": "end-date", "description": { "en": "Timestamp of when this infraction expires" } @@ -359,7 +366,7 @@ } }, { - "name": "caseId", + "name": "case-id", "description": { "en": "Database Case ID" } @@ -371,10 +378,10 @@ "embeds": [ { "author": { - "name": "Signed, %issuerName% • Case #%caseId%" + "name": "Signed, %issuer-name% • Case #%case-id%" }, "title": "⚠️ You have been infracted", - "description": "**Type:** %type%\n**Reason:** %reason%\n**Expires:** %endDate%", + "description": "**Type:** %type%\n**Reason:** %reason%\n**Expires:** %end-date%", "color": "#e67e22" } ] @@ -401,7 +408,7 @@ } }, { - "name": "issuerName", + "name": "issuer-name", "description": { "en": "Name of the issuer" } @@ -413,7 +420,7 @@ } }, { - "name": "endDate", + "name": "end-date", "description": { "en": "Timestamp of when this infraction expires" } @@ -425,7 +432,7 @@ } }, { - "name": "caseId", + "name": "case-id", "description": { "en": "Database Case ID" } @@ -437,10 +444,10 @@ "embeds": [ { "author": { - "name": "Signed, %issuerName% • Case #%caseId%" + "name": "Signed, %issuer-name% • Case #%case-id%" }, "title": "⛔ Staff Suspension", - "description": "You have been temporarily suspended.\n\n**Duration:** %duration%\n**Returns:** %endDate%\n**Reason:** %reason%", + "description": "You have been temporarily suspended.\n\n**Duration:** %duration%\n**Returns:** %end-date%\n**Reason:** %reason%", "color": "#ed4245" } ] diff --git a/modules/staff-management-system/configs/profiles.json b/modules/staff-management-system/configs/profiles.json index 3aceb777..5865ccd6 100644 --- a/modules/staff-management-system/configs/profiles.json +++ b/modules/staff-management-system/configs/profiles.json @@ -76,7 +76,7 @@ "allowEmbed": true, "params": [ { - "name": "user", + "name": "user-mention", "description": { "en": "The user's mention." } @@ -90,7 +90,7 @@ { "name": "nickname", "description": { - "en": "The user's custom profile nickname (or default username if not set)." + "en": "The user's custom profile nickname (uses default username if not set)." } }, { @@ -102,7 +102,7 @@ { "name": "status", "description": { - "en": "The user's current status (On Duty, Off Duty, LoA, etc.)." + "en": "The user's current status (LoA, RA, etc.)." } }, { @@ -112,7 +112,7 @@ } }, { - "name": "pfp", + "name": "avatar", "description": { "en": "The user's avatar URL." }, @@ -127,7 +127,7 @@ "title": "Staff Profile: %nickname%", "description": "%intro%", "color": "#2b2d31", - "thumbnailURL": "%pfp%", + "thumbnailURL": "%avatar%", "fields": [ { "name": "Status", diff --git a/modules/staff-management-system/configs/promotions.json b/modules/staff-management-system/configs/promotions.json index 279925e4..745b9da8 100644 --- a/modules/staff-management-system/configs/promotions.json +++ b/modules/staff-management-system/configs/promotions.json @@ -64,7 +64,7 @@ "type": "channelID", "channelTypes": [ "GUILD_TEXT", - "GUILD_ANNOUNCEMENT" + "GUILD_NEWS" ], "default": { "en": "" @@ -83,31 +83,31 @@ "allowEmbed": true, "params": [ { - "name": "user", + "name": "user-mention", "description": { "en": "Pings the promoted user." } }, { - "name": "newRoleName", + "name": "new-role-name", "description": { "en": "The plain text name of the new role." } }, { - "name": "newRoleMention", + "name": "new-role-mention", "description": { "en": "The pingable mention of the new role." } }, { - "name": "promoterMention", + "name": "promoter-mention", "description": { "en": "Pings the staff member who issued the promotion." } }, { - "name": "promoterName", + "name": "promoter-name", "description": { "en": "The username of the staff member who issued the promotion." } @@ -119,13 +119,13 @@ } }, { - "name": "userPfp", + "name": "user-avatar", "description": { "en": "The avatar URL of the promoted user." } }, { - "name": "promoterPfp", + "name": "promoter-avatar", "description": { "en": "The avatar URL of the promoter." } @@ -134,17 +134,17 @@ "default": { "en": { "_schema": "v3", - "content": "%user%", + "content": "%user-mention%", "embeds": [ { "author": { - "name": "Signed, %promoterName%", - "imageURL": "%promoterPfp%" + "name": "Signed, %promoter-name%", + "imageURL": "%promoter-avatar%" }, "title": "🎉 New promotion!", - "description": "Congratulations, you have been promoted to **%newRoleName%**!\n\n**Promoted to:** %newRoleMention%\n**On behalf of:** %promoterMention%\n**Reason:** %reason%", + "description": "Congratulations, you have been promoted to **%new-role-name%**!\n\n**Promoted to:** %new-role-mention%\n**On behalf of:** %promoter-mention%\n**Reason:** %reason%", "color": "#f1c40f", - "thumbnailURL": "%userPfp%" + "thumbnailURL": "%user-avatar%" } ] } @@ -178,31 +178,31 @@ "dependsOn": "dmPromotedUser", "params": [ { - "name": "user", + "name": "user-mention", "description": { "en": "Pings the promoted user." } }, { - "name": "newRoleName", + "name": "new-role-name", "description": { "en": "The plain text name of the new role." } }, { - "name": "newRoleMention", + "name": "new-role-mention", "description": { "en": "The pingable mention of the new role." } }, { - "name": "promoterMention", + "name": "promoter-mention", "description": { "en": "Pings the staff member who issued the promotion." } }, { - "name": "promoterName", + "name": "promoter-name", "description": { "en": "The username of the staff member who issued the promotion." } @@ -214,13 +214,13 @@ } }, { - "name": "userPfp", + "name": "user-avatar", "description": { "en": "The avatar URL of the promoted user." } }, { - "name": "promoterPfp", + "name": "promoter-avatar", "description": { "en": "The avatar URL of the promoter." } @@ -229,17 +229,17 @@ "default": { "en": { "_schema": "v3", - "content": "%user%", + "content": "%user-mention%", "embeds": [ { "author": { - "name": "Signed, %promoterName%", - "imageURL": "%promoterPfp%" + "name": "Signed, %promoter-name%", + "imageURL": "%promoter-avatar%" }, "title": "🎉 New promotion!", - "description": "Congratulations, you have been promoted to **%newRoleName%**!\n\n**Promoted to:** %newRoleMention%\n**On behalf of:** %promoterMention%\n**Reason:** %reason%", + "description": "Congratulations, you have been promoted to **%new-role-name%**!\n\n**Promoted to:** %new-role-mention%\n**On behalf of:** %promoter-mention%\n**Reason:** %reason%", "color": "#f1c40f", - "thumbnailURL": "%userPfp%" + "thumbnailURL": "%user-avatar%" } ] } diff --git a/modules/staff-management-system/configs/reviews.json b/modules/staff-management-system/configs/reviews.json index fdbb7dac..cb1d3eda 100644 --- a/modules/staff-management-system/configs/reviews.json +++ b/modules/staff-management-system/configs/reviews.json @@ -1,7 +1,7 @@ { "filename": "reviews.json", "humanName": { - "en": "Staff Ratings" + "en": "Staff Reviews" }, "description": { "en": "Configure the staff rating system and feedback channels." @@ -47,6 +47,10 @@ "en": "Channel where new reviews are posted." }, "type": "channelID", + "channelTypes": [ + "GUILD_TEXT", + "GUILD_NEWS" + ], "default": { "en": "" } @@ -92,58 +96,60 @@ "allowEmbed": true, "params": [ { - "name": "target", + "name": "staff-mention", "description": { - "en": "The staff member" + "en": "Mention of the staff member" } }, { - "name": "author", + "name": "reviewer-mention", "description": { - "en": "The reviewer" + "en": "Mention of the reviewer" } }, { "name": "stars", "description": { - "en": "Star emoji string (⭐⭐⭐⭐⭐)" + "en": "Amount of stars rated in emoji's (⭐⭐⭐⭐⭐)" } }, { "name": "rating", "description": { - "en": "Number (1-5)" + "en": "Amount of stars rated in text (1-5)" } }, { "name": "comment", "description": { - "en": "The review text" + "en": "The review's text" } }, { - "name": "staff-profile-picture", + "name": "staff-avatar", "description": { "en": "The staff member's profile picture (URL)" - } + }, + "isImage": true }, { - "name": "reviewer-profile-picture", + "name": "reviewer-avatar", "description": { "en": "The reviewer's profile picture (URL)" - } + }, + "isImage": true } ], "default": { "en": { "_schema": "v3", - "content": "%user%", + "content": "%staff%", "embeds": [ { "title": "🌟 New Staff Rating", - "description": "**Staff:** %target%\n**Rated by:** %author%\n\n**Rating:** %stars% (%rating%/5)\n**Comment:**\n%comment%", + "description": "**Staff:** %staff-mention%\n**Rated by:** %reviewer-mention%\n\n**Rating:** %stars% (%rating%/5)\n**Comment:**\n%comment%", "color": "#f1c40f", - "thumbnailURL": "%staff-profile-picture%" + "thumbnailURL": "%staff-avatar%" } ] } diff --git a/modules/staff-management-system/configs/shifts.json b/modules/staff-management-system/configs/shifts.json index ee7b250c..614b5c9a 100644 --- a/modules/staff-management-system/configs/shifts.json +++ b/modules/staff-management-system/configs/shifts.json @@ -27,6 +27,13 @@ "displayName": { "en": "Quotas" } + }, + { + "id": "logging", + "icon": "fas fa-message-lines", + "displayName": { + "en": "Logging" + } } ], "content": [ @@ -87,7 +94,8 @@ "type": "integer", "default": { "en": 0 - } + }, + "minValue": 0 }, { "name": "enableLeaderboard", @@ -174,6 +182,40 @@ "en": {} }, "dependsOn": "enableQuotas" + }, + { + "name": "logShiftChanges", + "category": "logging", + "humanName": { + "en": "Log Shift Changes" + }, + "description":{ + "en": "When enabled, shift changes (such as going on-duty, on break, or off-duty) will be logged in a custom channel." + }, + "type": "boolean", + "default": { + "en": true + } + }, + { + "name": "logShiftChangesChannel", + "category": "logging", + "humanName": { + "en": "Channel for shift change logs" + }, + "description": { + "en": "The channel where shift changes will be logged. You can set this empty to use the general log channel." + }, + "type": "channelID", + "channelTypes": [ + "GUILD_TEXT", + "GUILD_NEWS" + ], + "default": { + "en": "" + }, + "allowNull": true, + "dependsOn": "logShiftChanges" } ] } \ No newline at end of file diff --git a/modules/staff-management-system/configs/status.json b/modules/staff-management-system/configs/status.json index fcc58e2b..e59d216c 100644 --- a/modules/staff-management-system/configs/status.json +++ b/modules/staff-management-system/configs/status.json @@ -7,6 +7,13 @@ "en": "Configure Leave of Absence (LoA) and Reduced Activity (RA) settings." }, "categories": [ + { + "id": "base", + "icon": "fas fa-gears", + "displayName": { + "en": "Base Settings" + } + }, { "id": "loa", "icon": "fas fa-door-open", @@ -30,6 +37,21 @@ } ], "content": [ + { + "name": "enableStatusSystem", + "category": "base", + "humanName": { + "en": "Enable Status System" + }, + "description": { + "en": "Enabling this unlocks the Leave of Absence (LoA) and Reduced Activity (RA) system, allowing staff to request these statuses and have them tracked." + }, + "type": "boolean", + "default": { + "en": false + }, + "elementToggle": true + }, { "name": "enableLoa", "category": "loa", @@ -41,9 +63,8 @@ }, "type": "boolean", "default": { - "en": false - }, - "elementToggle": true + "en": true + } }, { "name": "loaRole", @@ -58,7 +79,8 @@ "allowNull": true, "default": { "en": "" - } + }, + "dependsOn": "enableLoa" }, { "name": "loaMaxDays", @@ -73,7 +95,8 @@ "default": { "en": 60 }, - "minValue": 1 + "minValue": 1, + "dependsOn": "enableLoa" }, { "name": "requireLoaApproval", @@ -87,7 +110,8 @@ "type": "boolean", "default": { "en": true - } + }, + "dependsOn": "enableLoa" }, { "name": "enableRa", @@ -100,9 +124,8 @@ }, "type": "boolean", "default": { - "en": false - }, - "elementToggle": true + "en": true + } }, { "name": "raRole", @@ -117,7 +140,8 @@ "allowNull": true, "default": { "en": "" - } + }, + "dependsOn": "enableRa" }, { "name": "raMaxDays", @@ -132,7 +156,8 @@ "default": { "en": 30 }, - "minValue": 1 + "minValue": 1, + "dependsOn": "enableRa" }, { "name": "requireRaApproval", @@ -146,7 +171,8 @@ "type": "boolean", "default": { "en": true - } + }, + "dependsOn": "enableRa" }, { "name": "statusLogChannel", @@ -159,6 +185,10 @@ }, "type": "channelID", "allowNull": true, + "channelTypes": [ + "GUILD_TEXT", + "GUILD_NEWS" + ], "default": { "en": "" } @@ -188,6 +218,10 @@ }, "type": "channelID", "allowNull": true, + "channelTypes": [ + "GUILD_TEXT", + "GUILD_NEWS" + ], "default": { "en": "" }, diff --git a/modules/staff-management-system/models/StaffShift.js b/modules/staff-management-system/models/StaffShift.js index a9b50316..9be88163 100644 --- a/modules/staff-management-system/models/StaffShift.js +++ b/modules/staff-management-system/models/StaffShift.js @@ -22,6 +22,11 @@ module.exports = class StaffManagementShift extends Model { type: { type: DataTypes.STRING, defaultValue: "Staff" + }, + breakCount: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0 } }, { tableName: 'staff_management_shifts', diff --git a/modules/staff-management-system/staff-management.js b/modules/staff-management-system/staff-management.js index a4724a4b..17479e7f 100644 --- a/modules/staff-management-system/staff-management.js +++ b/modules/staff-management-system/staff-management.js @@ -110,6 +110,14 @@ async function issueInfraction(client, interaction, targetMember, type, reason, content: localize('staff-management-system', 'err-feat-disabled', { feature: 'Infractions' }), flags: MessageFlags.Ephemeral }); + + if (targetMember.id === interaction.user.id) { + return interaction.reply({ + content: localize('staff-management-system', 'err-self-infract'), + flags: MessageFlags.Ephemeral + }); + } + if (type.toLowerCase() === 'suspension') { return interaction.reply({ content: localize('staff-management-system', 'err-use-susp'), @@ -136,14 +144,14 @@ async function issueInfraction(client, interaction, targetMember, type, reason, const placeholders = { '%user%': targetMember.user.toString(), - '%userPfp%': targetMember.user.displayAvatarURL({ dynamic: true, format: 'png', size: 1024 }) || '', - '%issuerMention%': interaction.user.toString(), - '%issuerName%': interaction.user.username, - '%issuerPfp%': interaction.user.displayAvatarURL({ dynamic: true, format: 'png', size: 1024 }) || '', + '%user-avatar%': targetMember.user.displayAvatarURL({ dynamic: true, format: 'png', size: 1024 }) || '', + '%issuer-mention%': interaction.user.toString(), + '%issuer-name%': interaction.user.username, + '%issuer-avatar%': interaction.user.displayAvatarURL({ dynamic: true, format: 'png', size: 1024 }) || '', '%type%': type, '%reason%': reason, - '%caseId%': record.caseId.toString(), - '%endDate%': expiresAt + '%case-id%': record.caseId.toString(), + '%end-date%': expiresAt ? `` : localize('staff-management-system', 'label-never') }; @@ -176,21 +184,30 @@ async function issueInfraction(client, interaction, targetMember, type, reason, } } - if (config.dmInfractedUser) { + if (config.dmInfractedUser && config.infractionDmMessage) { let dmTemplate = config.infractionDmMessage; - if (typeof dmTemplate === 'string') { - try { dmTemplate = JSON.parse(dmTemplate); } - catch (e) {} - } - else if (typeof dmTemplate === 'object') { - dmTemplate = JSON.parse(JSON.stringify(dmTemplate)); + if (typeof dmTemplate === 'string') { + try { + dmTemplate = JSON.parse(dmTemplate); + } catch (e) {} + } else if (typeof dmTemplate === 'object') { + dmTemplate = JSON.parse(JSON.stringify(dmTemplate)); } - + if (dmTemplate && dmTemplate.embeds && !dmTemplate._schema) dmTemplate._schema = 'v3'; - let dmOpts = await embedTypeV2(dmTemplate, placeholders); + const dmOpts = await embedTypeV2(dmTemplate, placeholders); if (dmOpts?.content?.trim() === '') delete dmOpts.content; - if (dmOpts && (dmOpts.content || dmOpts.embeds?.length > 0)) - await targetMember.send(dmOpts).catch(()=>{}); + + if (dmOpts) { + try { + await targetMember.user.send(dmOpts); + } catch (e) { + client.logger.warn(localize('staff-management-system', 'log-infract-dm-fail', { + user: targetMember.user.tag, + error: e.message + })); + } + } } await interaction.reply({ @@ -220,6 +237,13 @@ async function issueSuspension(client, interaction, targetMember, durationInput, flags: MessageFlags.Ephemeral }); + if (targetMember.id === interaction.user.id) { + return interaction.reply({ + content: localize('staff-management-system', 'err-self-infract'), + flags: MessageFlags.Ephemeral + }); + } + const durationDays = parseDurationToDays(durationInput); if (!durationDays) return interaction.reply({ @@ -259,14 +283,14 @@ async function issueSuspension(client, interaction, targetMember, durationInput, const placeholders = { '%user%': targetMember.user.toString(), - '%userPfp%': targetMember.user.displayAvatarURL({ dynamic: true, format: 'png', size: 1024 }) || '', - '%issuerMention%': interaction.user.toString(), - '%issuerName%': interaction.user.username, - '%issuerPfp%': interaction.user.displayAvatarURL({ dynamic: true, format: 'png', size: 1024 }) || '', + '%user-avatar%': targetMember.user.displayAvatarURL({ dynamic: true, format: 'png', size: 1024 }) || '', + '%issuer-mention%': interaction.user.toString(), + '%issuer-name%': interaction.user.username, + '%issuer-avatar%': interaction.user.displayAvatarURL({ dynamic: true, format: 'png', size: 1024 }) || '', '%duration%': durationString, '%reason%': reason, - '%caseId%': record.caseId.toString(), - '%endDate%': `` + '%case-id%': record.caseId.toString(), + '%end-date%': `` }; const channelId = getSafeChannelId(config.infractionLogChannel); @@ -299,7 +323,7 @@ async function issueSuspension(client, interaction, targetMember, durationInput, } } - if (config.dmInfractedUser) { + if (config.dmInfractedUser && config.suspensionDmMessage) { let dmTemplate = config.suspensionDmMessage; if (typeof dmTemplate === 'string') { try { @@ -310,11 +334,21 @@ async function issueSuspension(client, interaction, targetMember, durationInput, else if (typeof dmTemplate === 'object') { dmTemplate = JSON.parse(JSON.stringify(dmTemplate)); } - + if (dmTemplate && dmTemplate.embeds && !dmTemplate._schema) dmTemplate._schema = 'v3'; - let dmOpts = await embedTypeV2(dmTemplate, placeholders); + const dmOpts = await embedTypeV2(dmTemplate, placeholders); if (dmOpts?.content?.trim() === '') delete dmOpts.content; - if (dmOpts && (dmOpts.content || dmOpts.embeds?.length > 0)) await targetMember.send(dmOpts).catch(()=>{}); + + if (dmOpts) { + try { + await targetMember.user.send(dmOpts); + } catch (e) { + client.logger.warn(localize('staff-management-system', 'log-susp-dm-fail', { + user: targetMember.user.tag, + error: e.message + })); + } + } } await interaction.reply({ @@ -327,8 +361,35 @@ async function issueSuspension(client, interaction, targetMember, durationInput, }); } +async function resolveInfractionReference(client, reference) { + const Infraction = client.models['staff-management-system']['Infraction']; + const value = reference?.trim(); + + if (!value) return null; + + if (/^\d+$/.test(value)) { + return await Infraction.findByPk(parseInt(value, 10)); + } + + try { + const parsed = new URL(value); + const validHosts = ['discord.com', 'canary.discord.com', 'ptb.discord.com']; + + if (!validHosts.includes(parsed.hostname)) return null; + + const parts = parsed.pathname.split('/').filter(Boolean); + if (parts.length !== 4 || parts[0] !== 'channels') return null; + + return await Infraction.findOne({ + where: { messageUrl: value } + }); + } catch (e) { + return null; + } +} + // ----- Infractions voiding ----- -async function voidInfraction(client, interaction, caseId) { +async function voidInfraction(client, interaction, reference) { const config = getConfig(client, 'infractions'); if (!config?.enableInfractions) return interaction.reply({ content: localize('staff-management-system', 'err-feat-disabled', { @@ -344,15 +405,19 @@ async function voidInfraction(client, interaction, caseId) { flags: MessageFlags.Ephemeral }); - const record = await client.models['staff-management-system']['Infraction'].findByPk(caseId); - if (!record) return interaction.reply({ - content: localize('staff-management-system', 'err-no-case', { caseId }), - flags: MessageFlags.Ephemeral - }); - if (!record.active) return interaction.reply({ - content: localize('staff-management-system', 'err-case-inact', { caseId }), - flags: MessageFlags.Ephemeral - }); + const record = await resolveInfractionReference(client, reference); + if (!record) { + return interaction.reply({ + content: localize('staff-management-system', 'err-no-case-ref', { reference }), + flags: MessageFlags.Ephemeral + }); + } + if (!record.active) { + return interaction.reply({ + content: localize('staff-management-system', 'err-case-inact', { caseId: record.caseId }), + flags: MessageFlags.Ephemeral + }); + } await record.update({ active: false }); @@ -371,14 +436,14 @@ async function voidInfraction(client, interaction, caseId) { await profile.update({ isSuspended: false, suspendedRoles: '[]' }); } catch (e) { return interaction.reply({ - content: localize('staff-management-system', 'succ-void-fail', { caseId }), + content: localize('staff-management-system', 'succ-void-fail', { caseId: record.caseId }), flags: MessageFlags.Ephemeral }); } } } await interaction.reply({ - content: localize('staff-management-system', 'succ-void', { caseId }), + content: localize('staff-management-system', 'succ-void', { caseId: record.caseId }), flags: MessageFlags.Ephemeral }); } @@ -456,6 +521,13 @@ async function promoteUser(client, interaction, targetMember, newRole, reason) { flags: MessageFlags.Ephemeral }); + if (targetMember.id === interaction.user.id) { + return interaction.reply({ + content: localize('staff-management-system', 'err-self-promo'), + flags: MessageFlags.Ephemeral + }); + } + const finalReason = reason && reason.trim() !== '' ? reason : localize('staff-management-system', 'none-provided'); @@ -486,14 +558,14 @@ async function promoteUser(client, interaction, targetMember, newRole, reason) { }); const placeholders = { - '%user%': targetMember.user.toString(), - '%newRoleName%': newRole.name, - '%newRoleMention%': newRole.toString(), - '%promoterMention%': interaction.user.toString(), - '%promoterName%': interaction.user.username, + '%user-mention%': targetMember.user.toString(), + '%new-role-name%': newRole.name, + '%new-role-mention%': newRole.toString(), + '%promoter-mention%': interaction.user.toString(), + '%promoter-name%': interaction.user.username, '%reason%': finalReason, - '%userPfp%': targetMember.user.displayAvatarURL({ dynamic: true, format: 'png', size: 1024 }) || '', - '%promoterPfp%': interaction.user.displayAvatarURL({ dynamic: true, format: 'png', size: 1024 }) || '' + '%user-avatar%': targetMember.user.displayAvatarURL({ dynamic: true, format: 'png', size: 1024 }) || '', + '%promoter-avatar%': interaction.user.displayAvatarURL({ dynamic: true, format: 'png', size: 1024 }) || '' }; const targetChannelId = channelOverride @@ -538,24 +610,31 @@ async function promoteUser(client, interaction, targetMember, newRole, reason) { } if (config.dmPromotedUser && config.promotionDmMessage) { - try { - let dmTemplate = config.promotionDmMessage; - if (typeof dmTemplate === 'string') { - try { - dmTemplate = JSON.parse(dmTemplate); - } catch (e) {} } - else if (typeof dmTemplate === 'object') { - dmTemplate = JSON.parse(JSON.stringify(dmTemplate)); - } + let dmTemplate = config.promotionDmMessage; + if (typeof dmTemplate === 'string') { + try { + dmTemplate = JSON.parse(dmTemplate); + } + catch (e) {} + } + else if (typeof dmTemplate === 'object') { + dmTemplate = JSON.parse(JSON.stringify(dmTemplate)); + } - if (dmTemplate && dmTemplate.embeds && !dmTemplate._schema) dmTemplate._schema = 'v3'; - let dmOpts = await embedTypeV2(dmTemplate, placeholders); - if (dmOpts?.content?.trim() === '') delete dmOpts.content; - - if (dmOpts && (dmOpts.content || (dmOpts.embeds && dmOpts.embeds.length > 0))) { - await targetMember.send(dmOpts).catch(()=>{}); + if (dmTemplate && dmTemplate.embeds && !dmTemplate._schema) dmTemplate._schema = 'v3'; + const dmOpts = await embedTypeV2(dmTemplate, placeholders); + if (dmOpts?.content?.trim() === '') delete dmOpts.content; + + if (dmOpts) { + try { + await targetMember.user.send(dmOpts); + } catch (e) { + client.logger.warn(localize('staff-management-system', 'log-promo-dm-fail', { + user: targetMember.user.tag, + error: e.message + })); } - } catch (e) {} + } } await interaction.reply({ @@ -1527,11 +1606,19 @@ async function submitReview(client, interaction, targetUser, stars, comment) { }); if (config.onlyAllowStaffReview !== false) { - const genCfg = getConfig(client, 'configuration'); - if (!checkStaffPermissions(targetMember, genCfg, 'staff')) { - return interaction.reply({ - content: localize('staff-management-system', 'err-staff-rate'), - flags: MessageFlags.Ephemeral + const generalConfig = getConfig(client, 'configuration') || {}; + const staffRoles = Array.isArray(generalConfig.staffRoles) + ? generalConfig.staffRoles + : (generalConfig.staffRoles ? [generalConfig.staffRoles] : []); + + const hasStaffRole = staffRoles.length > 0 && targetMember.roles.cache.some(role => + staffRoles.includes(role.id) + ); + + if (!hasStaffRole) { + return interaction.reply({ + content: localize('staff-management-system', 'err-staff-rate'), + flags: MessageFlags.Ephemeral }); } } @@ -1548,13 +1635,13 @@ async function submitReview(client, interaction, targetUser, stars, comment) { const channel = interaction.guild.channels.cache.get(channelId); if (channel) { let msgOpts = await embedTypeV2(config.ratingMessage, { - '%target%': targetUser.toString(), - '%author%': interaction.user.toString(), + '%staff-mention%': targetUser.toString(), + '%reviewer-mention%': interaction.user.toString(), '%stars%': "⭐".repeat(stars), '%rating%': stars.toString(), '%comment%': comment, - '%staff-profile-picture%': targetUser.displayAvatarURL({ dynamic: true }), - '%reviewer-profile-picture%': interaction.user.displayAvatarURL({ dynamic: true }) + '%staff-avatar%': targetUser.displayAvatarURL({ dynamic: true }), + '%reviewer-avatar%': interaction.user.displayAvatarURL({ dynamic: true }) }); if (msgOpts?.content?.trim() === '') delete msgOpts.content; const sentMessage = await channel.send(msgOpts).catch(()=>{}); From 971cdce7f1541bdcb016e8d2bc3e7df8b7fbb6ba Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Sat, 11 Apr 2026 09:04:56 +0200 Subject: [PATCH 16/27] Updated according to feedbacks --- .../staff-management-system/commands/duty.js | 6 +- .../commands/status.js | 41 +++++++++---- .../configs/infractions.json | 1 - .../events/interactionCreate.js | 12 +++- .../staff-management.js | 60 +++++++------------ 5 files changed, 64 insertions(+), 56 deletions(-) diff --git a/modules/staff-management-system/commands/duty.js b/modules/staff-management-system/commands/duty.js index 77fedaf3..be878195 100644 --- a/modules/staff-management-system/commands/duty.js +++ b/modules/staff-management-system/commands/duty.js @@ -642,7 +642,7 @@ async function handleDutyStartButton(client, interaction) { const userId = parts[2]; const shiftType = parts[3] || 'Staff'; - if (interaction.user.id !== userId) return interaction.followUp({ + if (interaction.user.id !== userId) return interaction.editReply({ content: localize('staff-management-system', 'err-not-yours'), flags: MessageFlags.Ephemeral }); @@ -688,7 +688,7 @@ async function handleDutyStartButton(client, interaction) { async function handleDutyBreakButton(client, interaction) { const userId = interaction.customId.split('_')[2]; - if (interaction.user.id !== userId) return interaction.followUp({ + if (interaction.user.id !== userId) return interaction.editReply({ content: localize('staff-management-system', 'err-not-yours'), flags: MessageFlags.Ephemeral }); @@ -765,7 +765,7 @@ async function handleDutyBreakButton(client, interaction) { async function handleDutyEndButton(client, interaction) { const userId = interaction.customId.split('_')[2]; - if (interaction.user.id !== userId) return interaction.followUp({ + if (interaction.user.id !== userId) return interaction.editReply({ content: localize('staff-management-system', 'err-not-yours'), flags: MessageFlags.Ephemeral }); diff --git a/modules/staff-management-system/commands/status.js b/modules/staff-management-system/commands/status.js index 9962e8c1..dfcb4ee1 100644 --- a/modules/staff-management-system/commands/status.js +++ b/modules/staff-management-system/commands/status.js @@ -142,7 +142,7 @@ async function logStatusChange(client, type, action, data) { .setDescription(localize('staff-management-system', 'log-end-desc', { label, mention })) .addFields({ name: localize('staff-management-system', 'log-info-hdr', { label }), - value: `**${localize('staff-management-system', 'general-started')}:** \n**${localize('staff-management-system', 'general-ended')}:** \n**${localize('staff-management-system', 'general-rsn')}:** ${data.reason || localize('staff-management-system', 'none-provided')}` + value: `**${localize('staff-management-system', 'general-started')}:** \n**${localize('staff-management-system', 'general-ended')}:** \n**${localize('staff-management-system','general-req-reason')}:** ${data.reqReason}\n**${localize('staff-management-system', 'general-rsn')}:** ${data.reason || localize('staff-management-system', 'none-provided')}` }); } else if (action === 'adjusted') { @@ -343,35 +343,51 @@ async function handleStatusView(client, interaction, type, targetUser) { } async function handleStatusList(client, interaction, type, filter) { + const LoaRequest = client.models['staff-management-system']['LoaRequest']; + const now = new Date(); + const cutoff = new Date(); + cutoff.setDate(cutoff.getDate() - 60); + let whereClause = { type }; let title = `${type} List`; if (filter === 'active') { whereClause.status = 'APPROVED'; - whereClause.endDate = { [Op.gt]: new Date() }; + whereClause.endDate = { [Op.gt]: now }; title += localize('staff-management-system', 'filter-active'); } else if (filter === 'expired') { - whereClause.endDate = { [Op.lt]: new Date() }; + whereClause.status = { [Op.in]: ['APPROVED', 'ENDED'] }; + whereClause.endDate = { [Op.between]: [cutoff, now] }; title += localize('staff-management-system', 'filter-expired'); } else { - whereClause.status = { [Op.ne]: 'PENDING' }; + whereClause.status = { [Op.in]: ['APPROVED', 'ENDED'] }; + whereClause.endDate = { [Op.between]: [cutoff, now] }; title += localize('staff-management-system', 'filter-history'); } - const { count, rows } = await client.models['staff-management-system']['LoaRequest'].findAndCountAll({ - where: whereClause, - order: [['endDate', 'DESC']] - }); - if (count === 0) return interaction.editReply({ - content: localize('staff-management-system', 'err-no-recs') + const rows = await LoaRequest.findAll({ + where: whereClause, + order: [['endDate', 'DESC']], + limit: 25 }); + if (rows.length === 0) { + return interaction.editReply({ + content: localize('staff-management-system', 'err-no-recs') + }); + } const embed = new EmbedBuilder() .setTitle(title) .setColor('Blue') - .setDescription(rows.map(r => `**<@${r.userId}>** ${r.status === 'APPROVED' ? '✅' : (r.status === 'DENIED' ? '❌' : '⏹️')}\nEnds: ${formatDate(r.endDate)}\nReason: ${r.reason}`).join('\n\n')); + .setDescription( + rows.map(r => + `**<@${r.userId}>** ${r.status === 'APPROVED' ? '✅' : '⏹️'}\n` + + `${localize('staff-management-system', 'label-end')}: ${formatDate(r.endDate)}\n` + + `${localize('staff-management-system', 'general-rsn')}: ${r.reason || localize('staff-management-system', 'info-none')}` + ).join('\n\n') + ); applyFooter(client, embed); await interaction.editReply({ embeds: [embed.toJSON()] }); @@ -514,7 +530,8 @@ async function handleStatusEndSubmit(client, interaction, type) { await logStatusChange(client, type, 'end', { userId: request.userId, startDate: request.startDate, - reason: request.reason + reason: reason, + reqReason: request.reason }); const updatedEmbed = EmbedBuilder.from(interaction.message.embeds[0]) diff --git a/modules/staff-management-system/configs/infractions.json b/modules/staff-management-system/configs/infractions.json index 05a43418..fa71c960 100644 --- a/modules/staff-management-system/configs/infractions.json +++ b/modules/staff-management-system/configs/infractions.json @@ -76,7 +76,6 @@ "en": "Suspensions temporarily strip a staff member of their roles." }, "type": "boolean", - "elementToggle": true, "default": { "en": true } diff --git a/modules/staff-management-system/events/interactionCreate.js b/modules/staff-management-system/events/interactionCreate.js index 72d9a462..5524f2c7 100644 --- a/modules/staff-management-system/events/interactionCreate.js +++ b/modules/staff-management-system/events/interactionCreate.js @@ -126,6 +126,7 @@ module.exports.run = async (client, interaction) => { // ----- User panel dropdown ----- if (interaction.customId.startsWith('staff-mgmt_panel-menu_')) { const targetId = interaction.customId.split('_')[2]; + await interaction.deferUpdate(); const targetUser = await client.users.fetch(targetId).catch(() => null); if (!targetUser) return interaction.reply({ content: localize('staff-management-system', 'err-gen-no-user'), @@ -143,7 +144,7 @@ module.exports.run = async (client, interaction) => { else if (selection === 'shifts') payload = await generatePanelShifts(client, targetUser); else if (selection === 'deletion') payload = await generatePanelDeletion(client, targetUser); - return interaction.update(payload); + return interaction.editReply(payload); } // ----- User panel deletion dropdown ----- @@ -376,6 +377,7 @@ module.exports.run = async (client, interaction) => { } if (action === 'approve') { + await interaction.deferUpdate(); await request.update({ status: 'APPROVED', approverId: interaction.user.id @@ -415,7 +417,7 @@ module.exports.run = async (client, interaction) => { user: interaction.user.tag }) }); - return interaction.update({ + return interaction.editReply({ embeds: [embed.toJSON()], components: [] }); @@ -441,6 +443,12 @@ module.exports.run = async (client, interaction) => { flags: MessageFlags.Ephemeral }); } + if (request.status !== 'PENDING') { + return interaction.reply({ + content: localize('staff-management-system', 'err-req-hndl', { status: request.status }), + flags: MessageFlags.Ephemeral + }); + } await request.update({ status: 'DENIED', diff --git a/modules/staff-management-system/staff-management.js b/modules/staff-management-system/staff-management.js index 17479e7f..147442bb 100644 --- a/modules/staff-management-system/staff-management.js +++ b/modules/staff-management-system/staff-management.js @@ -398,8 +398,7 @@ async function voidInfraction(client, interaction, reference) { flags: MessageFlags.Ephemeral }); - const generalConfig = getConfig(client, 'configuration'); - const canManage = interaction.member.roles.cache.some(r => [...(generalConfig.supervisorRoles || []), ...(generalConfig.managementRoles || [])].includes(r.id)) || interaction.member.permissions.has('Administrator'); + const canManage = checkStaffPermissions(member, config, 'supervisor') if (!canManage) return interaction.reply({ content: localize('staff-management-system', 'err-gen-no-perm'), flags: MessageFlags.Ephemeral @@ -419,8 +418,6 @@ async function voidInfraction(client, interaction, reference) { }); } - await record.update({ active: false }); - if (record.type.toLowerCase() === 'suspension') { const Profile = client.models['staff-management-system']['StaffProfile']; const profile = await Profile.findOne({ @@ -442,6 +439,7 @@ async function voidInfraction(client, interaction, reference) { } } } + await record.update({ active: false }); await interaction.reply({ content: localize('staff-management-system', 'succ-void', { caseId: record.caseId }), flags: MessageFlags.Ephemeral @@ -1319,45 +1317,28 @@ async function executeDataDeletion(client, targetId, dataType) { }); } + const profileUpdates = {}; if (['del_shifts', 'del_all'].includes(dataType)) { - await models.StaffShift.destroy({ - where: { userId: targetId } - }); - - const profile = await models.StaffProfile.findByPk(targetId); - if (profile) { - await profile.update({ - onDuty: false, - onBreak: false, - breakStartTime: null, - lastClockIn: null - }); - } + profileUpdates.onDuty = false; + profileUpdates.onBreak = false; + profileUpdates.breakStartTime = null; + profileUpdates.lastClockIn = null; } if (['del_status', 'del_all'].includes(dataType)) { - await models.LoaRequest.destroy({ - where: { userId: targetId } - }); - - const profile = await models.StaffProfile.findByPk(targetId); - if (profile) { - await profile.update({ - activityStatus: null - }); - } + profileUpdates.activityStatus = null; } if (dataType === 'del_all') { + profileUpdates.customNickname = null; + profileUpdates.customIntro = null; + profileUpdates.isSuspended = false; + profileUpdates.suspendedRoles = null; + } + + if (Object.keys(profileUpdates).length > 0) { const profile = await models.StaffProfile.findByPk(targetId); - if (profile) { - await profile.update({ - customNickname: null, - customIntro: null, - isSuspended: false, - suspendedRoles: null - }); - } + if (profile) await profile.update(profileUpdates); } if (['del_activity', 'del_all'].includes(dataType)) { @@ -1482,9 +1463,12 @@ async function endActivityCheckProcess(client, activeCheck) { const respondedUserIds = new Set(responses.map(response => response.userId)); - const expectedMembers = guild.members.cache.filter(m => !m.user.bot && m.roles.cache.some(r => targetRoles.includes(r.id))); - const [responded, exceptions, failed] = [[], [], []]; - const profiles = await client.models['staff-management-system']['StaffProfile'].findAll(); + const expectedIds = [...expectedMembers.keys()]; + const profiles = await StaffProfile.findAll({ + where: { + userId: { [Op.in]: expectedIds } + } + }); expectedMembers.forEach(member => { if (respondedUserIds.has(member.id)) return responded.push(member); From e33fd701a81a7966e85b06b57bb056842b87769a Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Sun, 12 Apr 2026 08:44:16 +0200 Subject: [PATCH 17/27] Changed some lines of code according to feedback and self-testing --- modules/staff-management-system/staff-management.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/staff-management-system/staff-management.js b/modules/staff-management-system/staff-management.js index 147442bb..79edaa01 100644 --- a/modules/staff-management-system/staff-management.js +++ b/modules/staff-management-system/staff-management.js @@ -398,7 +398,7 @@ async function voidInfraction(client, interaction, reference) { flags: MessageFlags.Ephemeral }); - const canManage = checkStaffPermissions(member, config, 'supervisor') + const canManage = checkStaffPermissions(interaction.member, getConfig(client, 'configuration'), 'supervisor'); if (!canManage) return interaction.reply({ content: localize('staff-management-system', 'err-gen-no-perm'), flags: MessageFlags.Ephemeral @@ -1462,7 +1462,9 @@ async function endActivityCheckProcess(client, activeCheck) { }); const respondedUserIds = new Set(responses.map(response => response.userId)); - + const StaffProfile = client.models['staff-management-system']['StaffProfile']; + const expectedMembers = guild.members.cache.filter(m => !m.user.bot && m.roles.cache.some(r => targetRoles.includes(r.id))); + const [responded, exceptions, failed] = [[], [], []]; const expectedIds = [...expectedMembers.keys()]; const profiles = await StaffProfile.findAll({ where: { From 6d272e93f7e3d472b3a4f561967666cfea004a7a Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Tue, 14 Apr 2026 17:02:12 +0200 Subject: [PATCH 18/27] Some minor changes --- locales/en.json | 5 +++-- .../commands/status.js | 5 +++-- .../events/interactionCreate.js | 18 +++++++-------- .../staff-management.js | 22 +++++++++++++++---- 4 files changed, 32 insertions(+), 18 deletions(-) diff --git a/locales/en.json b/locales/en.json index 8ccd02b1..84432667 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1161,7 +1161,7 @@ "succ-infract": "✅ Issued **%type** (Case #%caseId) to %user.", "label-days": "days", "succ-susp": "✅ Issued Suspension (Case #%caseId) to %user for %duration.", - "err-no-case": "❌ Case #%caseId does not exist.", + "err-no-case-ref": "❌ No case found for %reference.", "err-case-inact": "⚠️ Case #%caseId is inactive.", "succ-void-fail": "✅ Case #%caseId voided, role restore failed.", "succ-void": "✅ Voided Case #%caseId.", @@ -1412,6 +1412,7 @@ "label-ended-by": "Ended by", "log-duty-log-fail": "[Staff Management] Failed to log duty change (%action): %error", "err-self-infract": "That's not in the code... well, it's more of a guideline anyway. Still no.\n-# You cannot infract yourself", - "err-self-promo": "You can't promote yourself through a black hole of audacity and expect it to work." + "err-self-promo": "You can't promote yourself through a black hole of audacity and expect it to work.", + "general-req-reason": "Reason for request" } } diff --git a/modules/staff-management-system/commands/status.js b/modules/staff-management-system/commands/status.js index dfcb4ee1..b04861f6 100644 --- a/modules/staff-management-system/commands/status.js +++ b/modules/staff-management-system/commands/status.js @@ -634,7 +634,8 @@ function scheduleStatusExpiry(client, request) { await logStatusChange(client, req.type, 'end', { userId: req.userId, startDate: req.startDate, - reason: req.reason + reason: reason, + reqReason: req.reason }); } catch (e) { client.logger.error(localize('staff-management-system', 'log-status-expiry-fail', { @@ -655,7 +656,7 @@ async function handleStatusExtendSubmit(client, interaction, type) { const meta = getStatusMeta(type); const request = await client.models['staff-management-system']['LoaRequest'].findByPk(interaction.customId.split('_')[2]); - if (!request || request.status === 'ENDED' || request.status === 'DENIED') return interaction.reply({ + if (!request || request.status === 'EN flags: MessageFlags.Ephemeral DED' || request.status === 'DENIED') return interaction.reply({ content: localize('staff-management-system', 'err-stat-inact', { label: meta.label }), diff --git a/modules/staff-management-system/events/interactionCreate.js b/modules/staff-management-system/events/interactionCreate.js index 5524f2c7..a04096cc 100644 --- a/modules/staff-management-system/events/interactionCreate.js +++ b/modules/staff-management-system/events/interactionCreate.js @@ -182,12 +182,12 @@ module.exports.run = async (client, interaction) => { // ----- Data deletion modal submission ----- if (interaction.isModalSubmit() && interaction.customId.startsWith('staff-mgmt_del-confirm_')) { + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); const configuration = getConfig(client, 'configuration'); if (!checkStaffPermissions(interaction.member, configuration, 'management')) { - return interaction.reply({ - content: localize('staff-management-system', 'del-no-perm'), - flags: MessageFlags.Ephemeral + return interaction.editReply({ + content: localize('staff-management-system', 'del-no-perm') }); } @@ -198,9 +198,8 @@ module.exports.run = async (client, interaction) => { const confirmPhrase = localize('staff-management-system', 'del-conf-phrase'); if (interaction.fields.getTextInputValue('confirm').trim() !== confirmPhrase) { - return interaction.reply({ - content: localize('staff-management-system', 'err-conf-fail'), - flags: MessageFlags.Ephemeral + return interaction.editReply({ + content: localize('staff-management-system', 'err-conf-fail') }); } @@ -223,10 +222,9 @@ module.exports.run = async (client, interaction) => { .setStyle(ButtonStyle.Secondary) ); - await interaction.reply({ + await interaction.editReply({ embeds: [embed.toJSON()], - components: [row.toJSON()], - flags: MessageFlags.Ephemeral + components: [row.toJSON()] }); const reply = await interaction.fetchReply(); @@ -530,7 +528,7 @@ module.exports.run = async (client, interaction) => { const targetRoles = JSON.parse(activeCheck.targetRoles || '[]'); const hasRole = targetRoles.length === 0 || interaction.member.roles.cache.some(r => targetRoles.includes(r.id)); if (!hasRole) return interaction.reply({ - content: localize('staff-management-system', 'err-not-req'), + content: localize('staff-management-system', 'err-ac-not-req'), flags: MessageFlags.Ephemeral }); diff --git a/modules/staff-management-system/staff-management.js b/modules/staff-management-system/staff-management.js index 79edaa01..77d35963 100644 --- a/modules/staff-management-system/staff-management.js +++ b/modules/staff-management-system/staff-management.js @@ -1534,6 +1534,16 @@ async function endActivityCheckProcess(client, activeCheck) { }); } +function getIsoWeekNumber(date = new Date()) { + const tmp = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); + const day = tmp.getUTCDay() || 7; + + tmp.setUTCDate(tmp.getUTCDate() + 4 - day); + + const yearStart = new Date(Date.UTC(tmp.getUTCFullYear(), 0, 1)); + return Math.ceil((((tmp - yearStart) / 86400000) + 1) / 7); +} + function initActivityCheckAutomation(client) { const config = getConfig(client, 'activity-checks'); if (!config?.enableActivityChecks || !config?.automatedChecks) return; @@ -1559,10 +1569,14 @@ function initActivityCheckAutomation(client) { } if (!cronString) return; - let toggleWeek = false; - schedule.scheduleJob('automated-activity-check', cronString, async () => { - if (config.automatedCheckInterval === 'Biweekly' && (toggleWeek = !toggleWeek, !toggleWeek)) return; - + const jobName = 'automated-activity-check'; + const existingJob = schedule.scheduledJobs[jobName]; + if (existingJob) existingJob.cancel(); + schedule.scheduleJob(jobName, cronString, async () => { + if (config.automatedCheckInterval === 'Biweekly' && getIsoWeekNumber(new Date()) % 2 !== 0) { + return; + } + const channel = client.guilds.cache.get(client.guildID)?.channels.cache.get(getSafeChannelId(config.sendingChannel)); if (channel) { client.logger.info(`[Activity Checks] Starting automated check.`); From 9f7c41d3ef9ac2c2afe3e1a08ff852ebe2641adf Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Tue, 14 Apr 2026 18:09:43 +0200 Subject: [PATCH 19/27] Some parameters fixes and adjustments --- .../configs/activity-checks.json | 2 +- .../staff-management-system/configs/infractions.json | 12 +++++++++--- modules/staff-management-system/staff-management.js | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/modules/staff-management-system/configs/activity-checks.json b/modules/staff-management-system/configs/activity-checks.json index 16813d19..8fbab120 100644 --- a/modules/staff-management-system/configs/activity-checks.json +++ b/modules/staff-management-system/configs/activity-checks.json @@ -97,7 +97,7 @@ "allowEmbed": true, "params": [ { - "name": "endtime", + "name": "end-time", "description": { "en": "The Discord timestamp when the check ends." } diff --git a/modules/staff-management-system/configs/infractions.json b/modules/staff-management-system/configs/infractions.json index fa71c960..971285db 100644 --- a/modules/staff-management-system/configs/infractions.json +++ b/modules/staff-management-system/configs/infractions.json @@ -160,7 +160,7 @@ { "name": "duration", "description": { - "en": "Duration of suspension" + "en": "Duration of the suspension" } }, { @@ -270,7 +270,7 @@ } }, { - "name": "endDate", + "name": "end-date", "description": { "en": "Timestamp of when this infraction expires" } @@ -282,7 +282,7 @@ } }, { - "name": "caseId", + "name": "case-id", "description": { "en": "Database Case ID" } @@ -418,6 +418,12 @@ "en": "Type of infraction (e.g., Warning, Strike)" } }, + { + "name": "duration", + "description": { + "en": "Duration of the suspension" + } + }, { "name": "end-date", "description": { diff --git a/modules/staff-management-system/staff-management.js b/modules/staff-management-system/staff-management.js index 77d35963..04f6b504 100644 --- a/modules/staff-management-system/staff-management.js +++ b/modules/staff-management-system/staff-management.js @@ -1386,7 +1386,7 @@ async function startActivityCheck(client, interactionOrChannel, isAutomated = fa ? JSON.parse(config.checkMessage) : config.checkMessage; let msgOpts = await embedTypeV2(embedTemplate, { - '%endtime%': ``, + '%end-time%': ``, '%duration%': durationHours.toString() }); From d88af1e1c7d499216208ff8f09f7c7fd41db621d Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Tue, 14 Apr 2026 19:37:43 +0200 Subject: [PATCH 20/27] Quick config image fix --- .../staff-management-system/configs/promotions.json | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/modules/staff-management-system/configs/promotions.json b/modules/staff-management-system/configs/promotions.json index 745b9da8..20fb6a05 100644 --- a/modules/staff-management-system/configs/promotions.json +++ b/modules/staff-management-system/configs/promotions.json @@ -122,13 +122,15 @@ "name": "user-avatar", "description": { "en": "The avatar URL of the promoted user." - } + }, + "isImage": true }, { "name": "promoter-avatar", "description": { "en": "The avatar URL of the promoter." - } + }, + "isImage": true } ], "default": { @@ -217,13 +219,15 @@ "name": "user-avatar", "description": { "en": "The avatar URL of the promoted user." - } + }, + "isImage": true }, { "name": "promoter-avatar", "description": { "en": "The avatar URL of the promoter." - } + }, + "isImage": true } ], "default": { From d14c9bc5c8a4a48aa4e6cf3e1794b257a1e35de4 Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Tue, 14 Apr 2026 22:19:17 +0200 Subject: [PATCH 21/27] Added deferReply for multiple functions to avoid Discord timeouts in replies --- .../staff-management.js | 139 ++++++++---------- 1 file changed, 60 insertions(+), 79 deletions(-) diff --git a/modules/staff-management-system/staff-management.js b/modules/staff-management-system/staff-management.js index 04f6b504..6fd5e7dc 100644 --- a/modules/staff-management-system/staff-management.js +++ b/modules/staff-management-system/staff-management.js @@ -105,32 +105,29 @@ function formatDuration(seconds) { // ---------- Infractions ---------- async function issueInfraction(client, interaction, targetMember, type, reason, expiryInput) { + await interaction.deferReply({ ephemeral: true }); const config = getConfig(client, 'infractions'); - if (!config?.enableInfractions) return interaction.reply({ - content: localize('staff-management-system', 'err-feat-disabled', { feature: 'Infractions' }), - flags: MessageFlags.Ephemeral + if (!config?.enableInfractions) return interaction.editReply({ + content: localize('staff-management-system', 'err-feat-disabled', { feature: 'Infractions' }) }); if (targetMember.id === interaction.user.id) { - return interaction.reply({ - content: localize('staff-management-system', 'err-self-infract'), - flags: MessageFlags.Ephemeral + return interaction.editReply({ + content: localize('staff-management-system', 'err-self-infract') }); } if (type.toLowerCase() === 'suspension') { - return interaction.reply({ - content: localize('staff-management-system', 'err-use-susp'), - flags: MessageFlags.Ephemeral + return interaction.editReply({ + content: localize('staff-management-system', 'err-use-susp') }); } let expiresAt = null; if (expiryInput) { const days = parseDurationToDays(expiryInput); - if (!days) return interaction.reply({ - content: localize('staff-management-system', 'err-inv-dur'), - flags: MessageFlags.Ephemeral + if (!days) return interaction.editReply({ + content: localize('staff-management-system', 'err-inv-dur') }); expiresAt = new Date(Date.now() + days * 24 * 60 * 60 * 1000); } @@ -210,45 +207,43 @@ async function issueInfraction(client, interaction, targetMember, type, reason, } } - await interaction.reply({ + await interaction.editReply({ content: localize('staff-management-system', 'succ-infract', { - type, caseId: record.caseId, user: targetMember.user.tag - }), - flags: MessageFlags.Ephemeral + type, + caseId: record.caseId, + user: targetMember.user.tag + }) }); } // ---------- Suspensions ---------- async function issueSuspension(client, interaction, targetMember, durationInput, reason) { + await interaction.deferReply({ ephemeral: true }); const config = getConfig(client, 'infractions'); if (!config?.enableInfractions) - return interaction.reply({ + return interaction.editReply({ content: localize('staff-management-system', 'err-feat-disabled', { feature: 'Infractions' - }), - flags: MessageFlags.Ephemeral + }) }); if (!config?.enableSuspensions) - return interaction.reply({ + return interaction.editReply({ content: localize('staff-management-system', 'err-feat-disabled', { feature: 'Suspensions' - }), - flags: MessageFlags.Ephemeral + }) }); if (targetMember.id === interaction.user.id) { - return interaction.reply({ - content: localize('staff-management-system', 'err-self-infract'), - flags: MessageFlags.Ephemeral + return interaction.editReply({ + content: localize('staff-management-system', 'err-self-infract') }); } const durationDays = parseDurationToDays(durationInput); if (!durationDays) - return interaction.reply({ - content: localize('staff-management-system', 'err-inv-dur'), - flags: MessageFlags.Ephemeral + return interaction.editReply({ + content: localize('staff-management-system', 'err-inv-dur') }); const expiresAt = new Date(Date.now() + durationDays * 24 * 60 * 60 * 1000); @@ -351,13 +346,12 @@ async function issueSuspension(client, interaction, targetMember, durationInput, } } - await interaction.reply({ + await interaction.editReply({ content: localize('staff-management-system', 'succ-susp', { caseId: record.caseId, user: targetMember.user.tag, duration: durationString - }), - flags: MessageFlags.Ephemeral + }) }); } @@ -390,31 +384,28 @@ async function resolveInfractionReference(client, reference) { // ----- Infractions voiding ----- async function voidInfraction(client, interaction, reference) { + await interaction.deferReply({ ephemeral: true }); const config = getConfig(client, 'infractions'); - if (!config?.enableInfractions) return interaction.reply({ + if (!config?.enableInfractions) return interaction.editReply({ content: localize('staff-management-system', 'err-feat-disabled', { feature: 'Infractions' - }), - flags: MessageFlags.Ephemeral + }) }); const canManage = checkStaffPermissions(interaction.member, getConfig(client, 'configuration'), 'supervisor'); - if (!canManage) return interaction.reply({ - content: localize('staff-management-system', 'err-gen-no-perm'), - flags: MessageFlags.Ephemeral + if (!canManage) return interaction.editReply({ + content: localize('staff-management-system', 'err-gen-no-perm') }); const record = await resolveInfractionReference(client, reference); if (!record) { - return interaction.reply({ - content: localize('staff-management-system', 'err-no-case-ref', { reference }), - flags: MessageFlags.Ephemeral + return interaction.editReply({ + content: localize('staff-management-system', 'err-no-case-ref', { reference }) }); } if (!record.active) { - return interaction.reply({ - content: localize('staff-management-system', 'err-case-inact', { caseId: record.caseId }), - flags: MessageFlags.Ephemeral + return interaction.editReply({ + content: localize('staff-management-system', 'err-case-inact', { caseId: record.caseId }) }); } @@ -432,17 +423,15 @@ async function voidInfraction(client, interaction, reference) { if (config.suspensionRole) await member.roles.remove(config.suspensionRole); await profile.update({ isSuspended: false, suspendedRoles: '[]' }); } catch (e) { - return interaction.reply({ - content: localize('staff-management-system', 'succ-void-fail', { caseId: record.caseId }), - flags: MessageFlags.Ephemeral + return interaction.editReply({ + content: localize('staff-management-system', 'succ-void-fail', { caseId: record.caseId }) }); } } } await record.update({ active: false }); - await interaction.reply({ - content: localize('staff-management-system', 'succ-void', { caseId: record.caseId }), - flags: MessageFlags.Ephemeral + await interaction.editReply({ + content: localize('staff-management-system', 'succ-void', { caseId: record.caseId }) }); } @@ -513,16 +502,15 @@ async function getInfractionHistory(client, interaction, targetUser) { // ---------- Promotions ---------- async function promoteUser(client, interaction, targetMember, newRole, reason) { + await interaction.deferReply({ ephemeral: true }); const config = getConfig(client, 'promotions'); - if (!config?.enablePromotions) return interaction.reply({ - content: localize('staff-management-system', 'err-feat-disabled', { feature: 'Promotions' }), - flags: MessageFlags.Ephemeral + if (!config?.enablePromotions) return interaction.editReply({ + content: localize('staff-management-system', 'err-feat-disabled', { feature: 'Promotions' }) }); if (targetMember.id === interaction.user.id) { - return interaction.reply({ - content: localize('staff-management-system', 'err-self-promo'), - flags: MessageFlags.Ephemeral + return interaction.editReply({ + content: localize('staff-management-system', 'err-self-promo') }); } @@ -533,18 +521,16 @@ async function promoteUser(client, interaction, targetMember, newRole, reason) { if (config.autoAddRole) { if (interaction.guild.members.me.roles.highest.position <= newRole.position) { - return interaction.reply({ - content: localize('staff-management-system', 'err-role-hier'), - flags: MessageFlags.Ephemeral + return interaction.editReply({ + content: localize('staff-management-system', 'err-role-hier') }); } try { await targetMember.roles.add(newRole); } catch (e) { - return interaction.reply({ - content: localize('staff-management-system', 'err-add-role', { e: e.message }), - flags: MessageFlags.Ephemeral + return interaction.editReply({ + content: localize('staff-management-system', 'err-add-role', { e: e.message }) }); } } @@ -635,12 +621,11 @@ async function promoteUser(client, interaction, targetMember, newRole, reason) { } } - await interaction.reply({ + await interaction.editReply({ content: localize('staff-management-system', 'succ-promo', { user: targetMember.user.tag, role: newRole.name - }), - flags: MessageFlags.Ephemeral + }) }); } @@ -1587,22 +1572,20 @@ function initActivityCheckAutomation(client) { // ---------- Reviews ---------- async function submitReview(client, interaction, targetUser, stars, comment) { + await interaction.deferReply({ ephemeral: true }); const config = getConfig(client, 'reviews'); - if (!config?.enableReviews) return interaction.reply({ + if (!config?.enableReviews) return interaction.editReply({ content: localize('staff-management-system', 'err-feat-disabled', { feature: 'Reviews' - }), - flags: MessageFlags.Ephemeral + }) }); const targetMember = await interaction.guild.members.fetch(targetUser.id).catch(() => null); - if (!targetMember) return interaction.reply({ - content: localize('staff-management-system', 'err-not-mem'), - flags: MessageFlags.Ephemeral + if (!targetMember) return interaction.editReply({ + content: localize('staff-management-system', 'err-not-mem') }); - if (!config.allowSelfRating && targetUser.id === interaction.user.id) return interaction.reply({ - content: localize('staff-management-system', 'err-self-rate'), - flags: MessageFlags.Ephemeral + if (!config.allowSelfRating && targetUser.id === interaction.user.id) return interaction.editReply({ + content: localize('staff-management-system', 'err-self-rate') }); if (config.onlyAllowStaffReview !== false) { @@ -1616,9 +1599,8 @@ async function submitReview(client, interaction, targetUser, stars, comment) { ); if (!hasStaffRole) { - return interaction.reply({ - content: localize('staff-management-system', 'err-staff-rate'), - flags: MessageFlags.Ephemeral + return interaction.editReply({ + content: localize('staff-management-system', 'err-staff-rate') }); } } @@ -1648,12 +1630,11 @@ async function submitReview(client, interaction, targetUser, stars, comment) { if (sentMessage) await review.update({ messageUrl: sentMessage.url }); } } - await interaction.reply({ + await interaction.editReply({ content: localize('staff-management-system', 'succ-review', { tag: targetUser.tag, stars - }), - flags: MessageFlags.Ephemeral + }) }); } From 3f0d0c50857a6761495c495023c348720d19fecf Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Tue, 14 Apr 2026 22:30:52 +0200 Subject: [PATCH 22/27] Added deferReply to 3 more functions --- .../staff-management.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/modules/staff-management-system/staff-management.js b/modules/staff-management-system/staff-management.js index 6fd5e7dc..5c91c881 100644 --- a/modules/staff-management-system/staff-management.js +++ b/modules/staff-management-system/staff-management.js @@ -492,11 +492,11 @@ async function generateInfractionHistoryResponse(client, targetUser, page = 1) { // ----- Gets infraction history ----- async function getInfractionHistory(client, interaction, targetUser) { + await interaction.deferReply({ ephemeral: true }); const response = await generateInfractionHistoryResponse(client, targetUser, 1); if (response.content && response.content.startsWith('ℹ️')) return interaction.reply(response); - await interaction.reply({ - ...response, - flags: MessageFlags.Ephemeral + await interaction.editReply({ + ...response }); } @@ -677,9 +677,13 @@ async function generatePromotionHistoryResponse(client, targetUser, page = 1) { } async function getPromotionHistory(client, interaction, targetUser) { + await interaction.deferReply({ ephemeral: true }); const response = await generatePromotionHistoryResponse(client, targetUser, 1); - if (response.content && response.content.startsWith('ℹ️')) return interaction.reply(response); - await interaction.reply({ ...response, flags: MessageFlags.Ephemeral }); + if (response.content && response.content.startsWith('ℹ️')) return interaction.editReply(response); + + await interaction.editReply({ + ...response + }); } // ---------- User Panel ---------- @@ -1693,11 +1697,12 @@ async function generateReviewHistoryResponse(client, targetUser, page = 1) { } async function getReviewHistory(client, interaction, targetUser) { + await interaction.deferReply({ ephemeral: true }); const response = await generateReviewHistoryResponse(client, targetUser, 1); if (response.content && response.content.startsWith('❌')) return interaction.reply(response); + await interaction.reply({ - ...response, - flags: MessageFlags.Ephemeral + ...response }); } From 6aa556cf882f8a2c4455372d2b5cb40f8a848482 Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Fri, 17 Apr 2026 17:52:08 +0200 Subject: [PATCH 23/27] Applied fixes --- locales/en.json | 3 ++- .../staff-management-system/commands/status.js | 16 +++++++++------- .../events/interactionCreate.js | 14 +++++++------- .../staff-management-system/staff-management.js | 6 +++--- 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/locales/en.json b/locales/en.json index 84432667..f0fec168 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1413,6 +1413,7 @@ "log-duty-log-fail": "[Staff Management] Failed to log duty change (%action): %error", "err-self-infract": "That's not in the code... well, it's more of a guideline anyway. Still no.\n-# You cannot infract yourself", "err-self-promo": "You can't promote yourself through a black hole of audacity and expect it to work.", - "general-req-reason": "Reason for request" + "general-req-reason": "Reason for request", + "status-expired-auto": "Ended automatically because the status expired." } } diff --git a/modules/staff-management-system/commands/status.js b/modules/staff-management-system/commands/status.js index b04861f6..6ca28798 100644 --- a/modules/staff-management-system/commands/status.js +++ b/modules/staff-management-system/commands/status.js @@ -634,7 +634,7 @@ function scheduleStatusExpiry(client, request) { await logStatusChange(client, req.type, 'end', { userId: req.userId, startDate: req.startDate, - reason: reason, + reason: localize('staff-management-system', 'status-expired-auto'), reqReason: req.reason }); } catch (e) { @@ -656,12 +656,14 @@ async function handleStatusExtendSubmit(client, interaction, type) { const meta = getStatusMeta(type); const request = await client.models['staff-management-system']['LoaRequest'].findByPk(interaction.customId.split('_')[2]); - if (!request || request.status === 'EN flags: MessageFlags.Ephemeral DED' || request.status === 'DENIED') return interaction.reply({ - content: localize('staff-management-system', 'err-stat-inact', { - label: meta.label - }), - flags: MessageFlags.Ephemeral - }); + if (!request || request.status === 'ENDED' || request.status === 'DENIED') { + return interaction.reply({ + content: localize('staff-management-system', 'err-stat-inact', { + label: meta.label + }), + flags: MessageFlags.Ephemeral + }); + } const days = parseInt(interaction.fields.getTextInputValue('extend_days'), 10); const reason = interaction.fields.getTextInputValue('extend_reason'); diff --git a/modules/staff-management-system/events/interactionCreate.js b/modules/staff-management-system/events/interactionCreate.js index a04096cc..5aa48d37 100644 --- a/modules/staff-management-system/events/interactionCreate.js +++ b/modules/staff-management-system/events/interactionCreate.js @@ -68,13 +68,13 @@ module.exports.run = async (client, interaction) => { // ----- Review history pagination ----- if (action === 'rev-page') { + await interaction.deferUpdate(); const targetUser = await client.users.fetch(parts[2]).catch(() => null); - if (!targetUser) return interaction.reply({ + if (!targetUser) return interaction.followUp({ content: localize('staff-management-system', 'err-gen-no-user'), flags: MessageFlags.Ephemeral }); - await interaction.deferUpdate(); const payload = await generateReviewHistoryResponse(client, targetUser, parseInt(parts[3], 10)); if (payload.content) return interaction.followUp({ ...payload, flags: MessageFlags.Ephemeral }); return interaction.editReply(payload); @@ -97,13 +97,13 @@ module.exports.run = async (client, interaction) => { // ----- Promotion history pagination ----- if (action === 'prom-hist') { + await interaction.deferUpdate(); const targetUser = await client.users.fetch(parts[2]).catch(() => null); - if (!targetUser) return interaction.reply({ + if (!targetUser) return interaction.followUp({ content: localize('staff-management-system', 'err-gen-no-user'), flags: MessageFlags.Ephemeral }); - await interaction.deferUpdate(); const payload = await generatePromotionHistoryResponse(client, targetUser, parseInt(parts[3], 10)); if (payload.content) return interaction.followUp({ ...payload, flags: MessageFlags.Ephemeral }); return interaction.editReply(payload); @@ -111,13 +111,13 @@ module.exports.run = async (client, interaction) => { // ----- Infraction history pagination ----- if (action === 'inf-hist') { + await interaction.deferUpdate(); const targetUser = await client.users.fetch(parts[2]).catch(() => null); - if (!targetUser) return interaction.reply({ + if (!targetUser) return interaction.followUp({ content: localize('staff-management-system', 'err-gen-no-user'), flags: MessageFlags.Ephemeral }); - await interaction.deferUpdate(); const payload = await generateInfractionHistoryResponse(client, targetUser, parseInt(parts[3], 10)); if (payload.content) return interaction.followUp({ ...payload, flags: MessageFlags.Ephemeral }); return interaction.editReply(payload); @@ -128,7 +128,7 @@ module.exports.run = async (client, interaction) => { const targetId = interaction.customId.split('_')[2]; await interaction.deferUpdate(); const targetUser = await client.users.fetch(targetId).catch(() => null); - if (!targetUser) return interaction.reply({ + if (!targetUser) return interaction.followUp({ content: localize('staff-management-system', 'err-gen-no-user'), flags: MessageFlags.Ephemeral }); diff --git a/modules/staff-management-system/staff-management.js b/modules/staff-management-system/staff-management.js index 5c91c881..64fa6644 100644 --- a/modules/staff-management-system/staff-management.js +++ b/modules/staff-management-system/staff-management.js @@ -494,7 +494,7 @@ async function generateInfractionHistoryResponse(client, targetUser, page = 1) { async function getInfractionHistory(client, interaction, targetUser) { await interaction.deferReply({ ephemeral: true }); const response = await generateInfractionHistoryResponse(client, targetUser, 1); - if (response.content && response.content.startsWith('ℹ️')) return interaction.reply(response); + if (response.content && response.content.startsWith('ℹ️')) return interaction.editReply(response); await interaction.editReply({ ...response }); @@ -1699,9 +1699,9 @@ async function generateReviewHistoryResponse(client, targetUser, page = 1) { async function getReviewHistory(client, interaction, targetUser) { await interaction.deferReply({ ephemeral: true }); const response = await generateReviewHistoryResponse(client, targetUser, 1); - if (response.content && response.content.startsWith('❌')) return interaction.reply(response); + if (response.content && response.content.startsWith('❌')) return interaction.editReply(response); - await interaction.reply({ + await interaction.editReply({ ...response }); } From 1e2252211b93e66e52da52a80826e1a1e3f7dd49 Mon Sep 17 00:00:00 2001 From: Kevinking500 Date: Fri, 17 Apr 2026 20:13:02 +0200 Subject: [PATCH 24/27] Quick fix regarding defer reply --- modules/staff-management-system/events/interactionCreate.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/staff-management-system/events/interactionCreate.js b/modules/staff-management-system/events/interactionCreate.js index 5aa48d37..9aa2ed04 100644 --- a/modules/staff-management-system/events/interactionCreate.js +++ b/modules/staff-management-system/events/interactionCreate.js @@ -298,7 +298,7 @@ module.exports.run = async (client, interaction) => { await interaction.message.edit(payload).catch(()=>{}); } - return interaction.reply({ + return interaction.editReply({ content: localize('staff-management-system', 'succ-del-tgt'), flags: MessageFlags.Ephemeral }); From ee3d7154e94c8bc6ccb18ffba627fadf0e5a65fd Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 24 Apr 2026 12:27:01 +0200 Subject: [PATCH 25/27] synced to closed source version --- .github/workflows/verify-configs.yml | 16 + LICENSE | 4 +- README.md | 522 +- config-generator/config.json | 177 +- config-generator/strings.json | 213 +- config-localizations/convert-configs.js | 253 + config-localizations/en.json | 4907 +++++++++++++++++ config-localizations/generate-files.js | 322 ++ config-localizations/getLocale.js | 449 ++ developer-docs/README.md | 42 + developer-docs/commands.md | 184 + developer-docs/config-localization.md | 274 + developer-docs/configuration.md | 567 ++ developer-docs/database-models.md | 101 + developer-docs/events.md | 88 + developer-docs/localization.md | 64 + developer-docs/migration.md | 351 ++ developer-docs/writing-a-module.md | 173 + generate-config.js | 4 +- locales/en.json | 2854 +++++----- main.js | 130 +- .../admin-tools/always-temporary-roles.json | 32 + modules/admin-tools/commands/admin.js | 1 + modules/admin-tools/config.json | 10 +- .../admin-tools/events/guildMemberUpdate.js | 49 + modules/admin-tools/module.json | 16 +- modules/admin-tools/role-bans.json | 33 + modules/admin-tools/temporaryRoles.js | 2 +- modules/afk-system/config.json | 105 +- modules/afk-system/module.json | 12 +- modules/anti-ghostping/config.json | 67 +- .../anti-ghostping/events/messageCreate.js | 2 +- .../anti-ghostping/events/messageDelete.js | 2 + modules/anti-ghostping/module.json | 12 +- modules/auto-delete/channels.json | 52 +- modules/auto-delete/module.json | 13 +- modules/auto-delete/voice-channels.json | 38 +- modules/auto-messager/cronjob.json | 72 +- modules/auto-messager/daily.json | 87 +- modules/auto-messager/hourly.json | 72 +- modules/auto-messager/module.json | 13 +- modules/auto-publisher/config.json | 64 +- .../auto-publisher/events/messageCreate.js | 5 +- modules/auto-publisher/module.json | 13 +- modules/auto-thread/config.json | 50 +- modules/auto-thread/events/messageCreate.js | 13 +- modules/auto-thread/module.json | 13 +- modules/betterstatus/commands/status.js | 84 + modules/betterstatus/config.json | 171 +- modules/betterstatus/events/botReady.js | 2 +- modules/betterstatus/module.json | 13 +- modules/channel-stats/channels.json | 145 +- modules/channel-stats/events/botReady.js | 19 +- modules/channel-stats/module.json | 13 +- modules/color-me/commands/color-me.js | 267 +- modules/color-me/configs/config.json | 80 +- modules/color-me/configs/strings.json | 137 +- modules/color-me/module.json | 12 +- modules/connect-four/commands/connect-four.js | 7 +- modules/connect-four/module.json | 13 +- modules/counter/config.json | 279 +- modules/counter/events/messageCreate.js | 29 +- modules/counter/milestones.json | 74 +- modules/counter/module.json | 13 +- modules/duel/commands/duel.js | 12 +- modules/duel/module.json | 12 +- modules/economy-system/commands/shop.js | 17 +- modules/economy-system/configs/config.json | 334 +- modules/economy-system/configs/strings.json | 565 +- modules/economy-system/economy-system.js | 30 +- .../events/interactionCreate.js | 1 + modules/economy-system/module.json | 12 +- modules/fun/config.json | 344 +- modules/fun/module.json | 13 +- modules/guess-the-number/config.json | 152 - modules/guess-the-number/configs/channel.json | 66 +- modules/guess-the-number/configs/config.json | 126 +- .../events/interactionCreate.js | 30 + .../guess-the-number/events/messageCreate.js | 26 + modules/guess-the-number/guessTheNumber.js | 24 +- modules/guess-the-number/models/User.js | 32 + modules/guess-the-number/module.json | 12 +- modules/info-commands/commands/info.js | 35 +- modules/info-commands/module.json | 10 +- modules/info-commands/strings.json | 309 +- modules/levels/commands/leaderboard.js | 5 +- modules/levels/commands/manage-levels.js | 25 +- modules/levels/commands/profile.js | 11 +- modules/levels/configs/config.json | 472 +- .../configs/random-levelup-messages.json | 69 +- .../configs/special-levelup-messages.json | 68 +- modules/levels/configs/strings.json | 283 +- modules/levels/events/messageCreate.js | 33 +- modules/levels/events/voiceStateUpdate.js | 83 +- modules/levels/leaderboardChannel.js | 6 +- modules/levels/module.json | 12 +- modules/massrole/configs/config.json | 31 +- modules/massrole/configs/strings.json | 40 +- modules/massrole/module.json | 12 +- modules/moderation/commands/moderate.js | 4 +- modules/moderation/configs/antiGrief.json | 114 +- modules/moderation/configs/antiJoinRaid.json | 109 +- modules/moderation/configs/antiSpam.json | 200 +- modules/moderation/configs/config.json | 427 +- modules/moderation/configs/joinGate.json | 138 +- modules/moderation/configs/lockdown.json | 178 +- modules/moderation/configs/strings.json | 369 +- modules/moderation/configs/verification.json | 299 +- modules/moderation/events/botReady.js | 15 +- modules/moderation/events/guildMemberAdd.js | 242 +- .../moderation/events/guildMemberUpdate.js | 3 +- .../moderation/events/interactionCreate.js | 372 +- modules/moderation/events/messageCreate.js | 44 +- modules/moderation/lockdown.js | 59 +- .../moderation/models/VerificationRequest.js | 46 + modules/moderation/moderationActions.js | 30 +- modules/moderation/module.json | 16 +- modules/nicknames/configs/config.json | 24 +- modules/nicknames/configs/strings.json | 52 +- modules/nicknames/module.json | 13 +- modules/ping-on-vc-join/actual-config.json | 52 +- modules/ping-on-vc-join/config.json | 153 +- .../events/voiceStateUpdate.js | 56 +- modules/ping-on-vc-join/module.json | 12 +- .../commands/ping-protection.js | 334 +- .../configs/configuration.json | 197 +- .../ping-protection/configs/moderation.json | 126 +- modules/ping-protection/configs/storage.json | 92 +- .../events/autoModerationActionExecution.js | 11 +- modules/ping-protection/events/botReady.js | 5 +- .../ping-protection/events/guildMemberAdd.js | 2 +- .../events/guildMemberRemove.js | 5 +- .../events/interactionCreate.js | 74 +- .../ping-protection/events/messageCreate.js | 110 +- modules/ping-protection/models/LeaverData.js | 5 +- .../ping-protection/models/ModerationLog.js | 9 +- modules/ping-protection/models/PingHistory.js | 5 +- modules/ping-protection/module.json | 13 +- modules/ping-protection/ping-protection.js | 368 +- modules/polls/configs/config.json | 51 +- modules/polls/configs/strings.json | 52 +- modules/polls/module.json | 13 +- modules/quiz/commands/quiz.js | 6 +- modules/quiz/configs/config.json | 128 +- modules/quiz/configs/quizList.json | 66 +- modules/quiz/configs/strings.json | 61 +- modules/quiz/module.json | 13 +- modules/quiz/quizUtil.js | 6 +- modules/reminders/config.json | 56 +- modules/reminders/events/interactionCreate.js | 46 + modules/reminders/module.json | 13 +- modules/reminders/reminders.js | 40 + .../commands/rock-paper-scissors.js | 6 +- modules/rock-paper-scissors/module.json | 13 +- .../staff-management-system/commands/duty.js | 648 +-- .../commands/staff-management.js | 291 +- .../commands/{status.js => staff-status.js} | 825 +-- .../configs/activity-checks.json | 224 +- .../configs/configuration.json | 64 +- .../configs/infractions.json | 514 +- .../configs/profiles.json | 128 +- .../configs/promotions.json | 204 +- .../configs/reviews.json | 150 +- .../configs/shifts.json | 158 +- .../configs/status.json | 168 +- .../events/botReady.js | 2 +- .../events/interactionCreate.js | 229 +- modules/staff-management-system/module.json | 13 +- .../staff-management.js | 993 ++-- modules/starboard/configs/config.json | 349 +- modules/starboard/handleStarboard.js | 17 +- modules/starboard/module.json | 12 +- modules/status-roles/configs/config.json | 68 +- modules/status-roles/events/presenceUpdate.js | 1 + modules/status-roles/module.json | 13 +- .../configs/sticky-messages.json | 86 +- .../sticky-messages/events/messageCreate.js | 4 +- modules/sticky-messages/module.json | 13 +- .../suggestions/commands/manage-suggestion.js | 1 + modules/suggestions/config.json | 411 +- modules/suggestions/module.json | 13 +- modules/team-list/config.json | 124 +- modules/team-list/events/botReady.js | 63 +- modules/team-list/models/TeamListMessage.js | 28 + modules/team-list/module.json | 13 +- modules/temp-channels/channel-settings.js | 130 +- .../temp-channels/commands/temp-channel.js | 3 +- modules/temp-channels/config.json | 548 +- modules/temp-channels/events/botReady.js | 80 +- .../temp-channels/events/interactionCreate.js | 167 +- .../temp-channels/events/voiceStateUpdate.js | 225 +- modules/temp-channels/locales.json | 29 + .../temp-channels/models/SettingsMessage.js | 25 + modules/temp-channels/models/TempChannel.js | 7 +- modules/temp-channels/module.json | 13 +- modules/tic-tak-toe/commands/tic-tac-toe.js | 7 +- modules/tic-tak-toe/module.json | 13 +- modules/tickets/config.json | 270 +- modules/tickets/events/interactionCreate.js | 39 +- modules/tickets/module.json | 12 +- .../twitch-notifications/configs/config.json | 45 +- .../configs/streamers.json | 128 +- .../twitch-notifications/events/botReady.js | 11 +- modules/twitch-notifications/module.json | 13 +- modules/uno/commands/uno.js | 12 +- modules/uno/module.json | 12 +- modules/welcomer/configs/channels.json | 215 +- modules/welcomer/configs/config.json | 227 +- modules/welcomer/configs/random-messages.json | 118 +- modules/welcomer/events/guildMemberAdd.js | 2 +- modules/welcomer/events/guildMemberRemove.js | 4 +- modules/welcomer/module.json | 13 +- package.json | 3 +- scripts/verify-config-defaults.js | 340 ++ src/cli.js | 1 + src/commands/help.js | 106 +- src/commands/reload.js | 7 +- src/discordjs-fix.js | 76 +- src/events/botReady.js | 2 - src/events/guildDelete.js | 14 + src/events/interactionCreate.js | 11 +- src/functions/configuration.js | 76 +- src/functions/helpers.js | 232 +- src/functions/localize.js | 3 +- src/global-params.json | 58 + 225 files changed, 17909 insertions(+), 13381 deletions(-) create mode 100644 .github/workflows/verify-configs.yml create mode 100644 config-localizations/convert-configs.js create mode 100644 config-localizations/en.json create mode 100644 config-localizations/generate-files.js create mode 100644 config-localizations/getLocale.js create mode 100644 developer-docs/README.md create mode 100644 developer-docs/commands.md create mode 100644 developer-docs/config-localization.md create mode 100644 developer-docs/configuration.md create mode 100644 developer-docs/database-models.md create mode 100644 developer-docs/events.md create mode 100644 developer-docs/localization.md create mode 100644 developer-docs/migration.md create mode 100644 developer-docs/writing-a-module.md create mode 100644 modules/admin-tools/always-temporary-roles.json create mode 100644 modules/admin-tools/events/guildMemberUpdate.js create mode 100644 modules/admin-tools/role-bans.json create mode 100644 modules/betterstatus/commands/status.js delete mode 100644 modules/guess-the-number/config.json create mode 100644 modules/guess-the-number/models/User.js create mode 100644 modules/moderation/models/VerificationRequest.js create mode 100644 modules/reminders/events/interactionCreate.js rename modules/staff-management-system/commands/{status.js => staff-status.js} (65%) create mode 100644 modules/team-list/models/TeamListMessage.js create mode 100644 modules/temp-channels/locales.json create mode 100644 modules/temp-channels/models/SettingsMessage.js create mode 100644 scripts/verify-config-defaults.js create mode 100644 src/events/guildDelete.js create mode 100644 src/global-params.json diff --git a/.github/workflows/verify-configs.yml b/.github/workflows/verify-configs.yml new file mode 100644 index 00000000..cb85537c --- /dev/null +++ b/.github/workflows/verify-configs.yml @@ -0,0 +1,16 @@ +name: Verify configs + +on: + push: + branches: [ main ] + pull_request: + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + - run: node scripts/verify-config-defaults.js \ No newline at end of file diff --git a/LICENSE b/LICENSE index 55ca2671..18202d7a 100644 --- a/LICENSE +++ b/LICENSE @@ -4,12 +4,12 @@ License text copyright (c) 2020 MariaDB Corporation Ab, All Rights Reserved. Parameters Licensor: ScootKit UG (haftungsbeschränkt) -Licensed Work: ScootKit/CustomDCBot. The Licensed Work is (c) 2025 ScootKit UG (haftungsbeschränkt) +Licensed Work: ScootKit/CustomDCBot. The Licensed Work is (c) 2026 ScootKit UG (haftungsbeschränkt) Additional Use Grant: You may make production use of the Licensed Work, provided such use does not include offering the Licensed Work to third parties on a hosted or embedded basis which is competitive with ScootKit UG (haftungsbeschränkt)'s products. -Change Date: Six years from the date the Licensed Work is published. +Change Date: Eight years from the date the Licensed Work is published. Change License: MIT License For information about alternative licensing arrangements for the Licensed Work, diff --git a/README.md b/README.md index d8928ed4..a83de3d8 100644 --- a/README.md +++ b/README.md @@ -1,358 +1,214 @@ # Custom-Bot v3 -Create your own discord bot - Fully customizable and with a lot of features. This bot is for advanced JS-Users, you -should only use it if you have some experience with Javascript, discord.js and JSON files. +Create your own Discord bot - fully customizable and modular. This bot is for advanced JS users with experience in +JavaScript, discord.js, and JSON configuration. ---- +## Get your own Custom-Bot for free -## Get your own Custom-Bot completely free and with a modern webinterface and a lot more features! - -Go check it out on our [website](https://scnx.xyz) or get started in the [dashboard](https://scnx.app). -In addition to the here -available features we offer: +Go check it out on our [website](https://scnx.xyz) (the [dashboard](https://scnx.app) and bot are fully translated). In +addition to the features available here, we offer: * Free hosting -* Custom-Commands -* Easy-to-use Embed-Editor -* Self-Roles +* Custom commands +* Easy-to-use embed editor and configuration editor * Send and edit messages in specific channels -* Easy-to-use Configuration-Editor -* Human-Readable Issue Reporting - never look at logs again -* and a modern dashboard -* and *a lot* more - for free - -[Get started now](https://scnx.xyz) - it's free - forever! +* Human-readable issue reporting +* Modern dashboard +* and a lot more -## Applicable [license](LICENSE) terms if you use this bot +[Get started now](https://scnx.xyz) - it's free, forever! -We really love open-source. It does not make sense financially to publish this Source-Code publicly (as our business -model is to host these bots on [SCNX](https://scnx.xyz), but we still do it. -While this project does not fit the [definition of Open Source](https://opensource.org/osd-annotated) -set forward by the Open Source Initiative, -we are committed to allowing you as much freedom as possible. -Please read the [license](LICENSE) and follow it. +## License -Here's a summary: +Please read the [license](LICENSE) before using this bot. -* You may use the bot on your server and change the source code (as long as you follow the license). -* You have to retain a link to the [LICENSE](LICENSE) and this repository in your bot, most likely in your `/help` - command. -* All changes you make to this codebase are subject to these license terms, you cannot remove the link to the license, - even if you change large parts of the bot. -* You may not create a competitor to [SCNX](https://scnx.xyz) or other ScootKit products using this source code. -* You may not use the "ScootKit" brand name or any other trademarks outside of the LICENSE notice. +In short: -Please read the full [license](LICENSE), as the terms laid out there apply. This is not legal advice. +* **Disclose source** - your source code must be made available when using this bot +* **State changes** - every change to the source code must be documented and published -Failure to abide by these terms might result in deactivation of your bot from Discord or legal action being taken -(but we'll act in good faith and usually try to solve the issue before doing anything drastic). +Please read the full [license](LICENSE). This is not legal advice. For information on how this aligns with the +closed-source SCNX version, see [this issue](https://github.com/SCNetwork/CustomDCBot/issues/13). ## Support development -As mentioned above, our business model is to host these bots for servers - it does not really make sense to publish our -product here - but we do it anyway - but we need your support! Feel free to [contribute](.github/CONTRIBUTING.md) or -becoming a [GitHub Sponsor](https://github.com/sponsors/ScootKit/). Thank you so much <3 - -## Need help? - -Are you stuck? Please do not ask on our Discord (unless you are using our hosted version), instead ask in -the [discussions-tab](https://github.com/ScootKit/CustomDCBot/discussions). - -## Need something even more custom? - -We are happy to give you a quote for individual requirements. Please email `sales@sc-network.net` with your -requirements. +Our business model is hosting these bots for servers. Feel free +to [contribute](.github/CONTRIBUTING.md), [donate on Patreon](https://patreon.com/scnetwork), or +on [any other platform](https://github.com/SCNetwork/CustomDCBot?sponsor=1). -### Table of contents - -[Installation](#installation)\ -[Features](#features)\ -[Configuration](#configuration)\ -[Modules](#modules)\ -[Add your own module (or API)](#add-your-own-modules) - -### Installation +## Installation 1. Clone this repo 2. Run `npm ci` 3. Run `npm run generate-config` -4. Replace your token in the `config/config.json` file. +4. Replace your token in `config/config.json` 5. Start the bot with `npm start` -6. The bot is now generating a `modules.json` and a `strings.json` file inside your `config` directory. You - can [change](#configuration) them. - -When reading thought the code, you may encounter code "tracking" / "issue reporting" parts of the bot. -This part is only enabled in the SCNX-Version and only used to allow users to see (configuration) issues of their bot -and to allow our team to detect bugs more easily (users can opt-out of that if they want to; we use the sentry-sdk for -that, but don't actually send any data to them, instead to our glitchtip instance - the open-source-version does neither -of that). -This open-source-version won't contact SCNX, SC Network and won't share any information with us, don't worry. You -can verify this by looking at the source code, which you should do before executing any code from the internet. - -### Features - -* Everything is split in different [modules](#modules) - you can enable, configure and disable it how you want -* Highly configurable - The goal with this bot is that you can change *everything* -* Add your own modules -* Easy configuration - Every config field has a description in an example file - -### Configuration - -You can find all the configuration-files inside your `config` folder. Every **enabled module** will have their own -folder with config-files inside them. **These files are generated automatically**. Every module has slightly different -configuration options. Every module has example files. Inside these files are more information about every configuration -option. -Some config values also support [embeds](https://discordjs.guide/popular-topics/embeds.html). This is the case -if `allowEmbed` is true.\ -You either input a string (normal Discord message), or an embed object with the following values: - -* `title`: Title of the embed -* `message`: Message outside the embed (optional) -* `description`: Description of the embed (optional) -* `color`: Color of the embed, must be - a [ColorResolvable](https://old.discordjs.dev/#/docs/discord.js/13.16.0/typedef/ColorResolvable) (optional) -* `url`: URL of the embed (optional) -* `image`: Image of the embed, should be an url (optional) -* `thumbnail`: Thumbnail-Image of the embed, should be an url (optional) -* `author` (optional): - * `name`: Name of the author - * `img`: Image of the author, should be an url -* `fields`: Fields of the embed, must be an array - of [EmbedFieldData](https://old.discordjs.dev/#/docs/discord.js/13.16.0/typedef/EmbedFieldData) (optional) -* `footer`: Footer value (optional, default: global footer value) -* `footerImgUrl`: URL to image of the footer (optional, default: global footer value) - -The footer of the embed is global and is defined in your global `strings.json` file. The timestamp is set automatically -to the current time. +6. The bot generates `modules.json` and `strings.json` in your `config` directory - see [Configuration](#configuration) + for details + +When reading the code, you may encounter tracking/issue-reporting sections. These are only active in the SCNX version +and are used for bug detection and user-facing diagnostics (users can opt out; we use Sentry SDK with our own Glitchtip +instance). The open-source version does not contact SCNX or share any data. + +## Features + +* **Modular architecture** - enable, configure, and disable each module independently +* **Highly configurable** - every message, role, channel, and behavior can be customized +* **Custom modules** - add your own modules with commands, events, and database models +* **Auto-generated configs** - every config field has a description and default value ### Modules -The bot is split in modules. Each module can register their own commands, events and even database models, so they can -do basically anything. Every module can register "example-config-files" witch are files with information about the -config file, so the bot can automatically check configs and do all the boring stuff for you. - -### Add your own modules - -As per the [License](LICENSE) you *have* to make *every* of your modules publicly available under the same license. -Please read the license for more information. - -**Before you make a module**: -Please create an issue with your suggestion and claim that you are working on it so nobody is working on the same -thing (;\ -Also please read the [Rules for modules](#rules-for-modules).\ -**Submit a module**: Simply create a pull request, and we will check your module and merge it then (; - -#### Rules for modules - -Every module should - -* Use Slash-Commands wherever possible -* Should provide a file with exported functions which other modules can use to manipulate data or perform actions in - your module (eg: an economy module should provide a file with exported functions like `User.addToBalance()`) -* Answer with ephemeral messages wherever it makes sense -* Create as few commands as possible (we have a limit to 100 commands in total), so please try to - use [Sub-Commands](https://discord.com/developers/docs/interactions/application-commands#subcommands-and-subcommand-groups) - wherever possible (eg: instead of having /ban, /kick, /mute etc, have a /moderate command with sub-commands) -* Use the newest features of the discord api and discord.js (buttons, selects, etc) if possible -* Process and Store only needed user information and data -* Support localization (you don't need to translate everything, you only need to support translations, read - more [here](#Localization) -* protect sensitive slash-commands with the proper [`defaultMemberPermissions`](#interaction-command) settings -* must comply with our [end-user documentation requirements](https://docs.scnx.xyz/oss/create-module-docs) -* follow our [terms of service](https://sc-net.work/tos), [Discord's Terms of Service](https://discord.com/tos) and - the [Discord Developer Terms of Service](https://discord.com/developers/docs/legal). A module should not allow users - to bypass or break the mentioned documents. This includes but is not limited to Nitro-Only-Features. - -#### Localization - -We'd like to offer SCNX and this bot in as many languages as possible. Because of this, we highly encourage you to use -translationable systems in your module. - -* Localizations of not-user-editable strings: Use `localize(key, string, replace = {})` from `src/functions/localize.js` - to localize strings. Translations of these strings happen - on [Weblate](https://localize.sc-network.net/projects/custombot/locales/) - * `key`: Key of the string (usually your module name, check out any files in `locales` to get an idea how this - works) - * `string`: Name of the string - * `replace` (optional, object): Will replace `%` in the source string by `` -* Localizations of configuration-files and user-editable strings: All localizable configuration fields are an object - with values keyed based on language codes. - Example: `{"description": {"de": "Beschreibung des Feldes", "en": "Description of the field"}`. Each field needs to - have at least an English value, as every other language will default back to English. - -#### module.json - -Every module has to contain a `module.json` file with the following content: - -* `name` of the module. Should be the same as the name of your dictionary. -* `humanReadableName`: [Localized](#localization) name of the module, shown to users -* `author` - * `name`: Name of the author - * `link`: Link to the author - * `scnxOrgID`: [SCNX](https://scnx.xyz)-Organisation-ID of the developer (allows you to accept donations in the - dashboard and will show up to users in the dashboard) -* `openSourceURL`: URL to the Source-Code of the module licensed under an Open-Source-License (will show - donation-banners in the SCNX Dashboard (if orgID is set) and qualifies (qualified) developers for financial support - from the Open-Source-Pool of SCNX) -* `description`: [Localized](#localization) short description of the module -* `cli` (optional): [CLI-File](#cli-files) of your module -* `commands-dir` (optional): Directory inside your module folder where all - the [interaction-command-files](#interaction-command) are in -* `on-load-event` (optional): File with exported `onLoad` function in it. Gets executed when your commands got loaded - successfully; at this point the Client is not logged in yet, so you can't communicate with Discord (yet). -* `events-dir` (optional): Directory inside your module folder where all the [event-files](#events) are in -* `models-dir` (optional): Directory inside your module folder where all the models-files are in -* `config-example-files` (optional, seriously leave this out when you don't have config files): Array - of [config-files](#example-config-file) inside your module directory. -* `tags` (optional): Array of tags. -* `fa-icon`: Used for matching of icons in our dashboard. We will fill this out for you, please do not set this field. - -#### Interaction-Command - -Note: Interaction-Commands get loaded after the configuration got checked.\ -An interaction-command ("slash command") file has to export the following things: - -* `run` (function; provided arguments: `interaction`): - * Without subcommands: Function that gets triggered if the interactions is being used - * With subcommands: Optional function that gets triggered after the subcommand functions (if specified) got executed -* `beforeSubcommand` (optional, only if subcommands exit): Function which gets executed before the function in - subcommands gets executed -* `autoComplete` (only required if any of your options use `autocomplete`): Object of functions, sorted by - subcommandgroup, subcommand and option name -* `subcommands` (only required if subcommands exist): Object of functions, sorted by subcommandgroup and subcommand -* `help` -* `config` (both for !help and slash-commands) - * `name`: Name of the command (should be the same name as the file name) - * `description`: Description of the command - * `restricted`: Can this command only be run one of the bot operators (e.g. config reloading, change status or ..., - boolean) - * `defaultMemberPermissions`: This will determine which users can use your commands by default - leave `null` (or `undefined`) to allow usage by @everyone, otherwise, use [PermissionsResolvable](https://old.discordjs.dev/#/docs/discord.js/main/typedef/PermissionResolvable). - * `options`: - * [ApplicationCommandOptionData](https://old.discordjs.dev/#/docs/discord.js/13.16.0/typedef/ApplicationCommandData) - OR - * Async function - returning [ApplicationCommandOptionData](https://old.discordjs.dev/#/docs/discord.js/13.16.0/typedef/ApplicationCommandData) ( - gets called with `client` as argument) - -#### Message-Command - -Starting V3, message-commands are no longer supported. Please use [Interaction-Commands](#interaction-command) -instead. Read more in [CHANGELOG.md](CHANGELOG.md). - -#### Events - -An event file should export the following things: - -* `run`: Function that gets triggered if the event gets executed (provided arguments: `client` (discord.js Client) and - all the arguments that gets past by discord.js for this event) -* `allowPartial` (optional, default: `false`): Boolean determining whether the `run` function should be called if the event - has [partial structures](https://discordjs.guide/popular-topics/partials.html#enabling-partials). When enabling, - please make sure you handle partial data correctly. - -#### CLI-Files - -A CLI-File should export the following things: - -* `commands`: Array of the following objects: - * `command`: Command which should be entered in the CLI - * `description`: Description of the command - * `run`: Function which should be executed when the command gets executed. The function gets executed with an object - of following structure as argument: - * `input`: The whole input - * `args`: Array of arguments (split by spaces) - * `client`: [Client](https://old.discordjs.dev/#/docs/discord.js/13.16.0/class/Client) - * `cliCommands`: Array of all CLICommands - -Note: We might allow users to execute CLI-Commands via the Dashboard in the future. This is not supported right now. - -#### Config-Elements - -Certain configuration may contain an array of multiple objects with different values - these are called " -Config-Elements". - -To add a new Config-Element to your configuration -use `node add-config-element-object.js `. - -#### Example config-file - -An example config file should include the following things: - -* `filename`: Name of the generated config file -* `humanname`: [Localized](#localization) name of the file, shown to users -* `description`: [Localized](#localization) description of the file, shown to users -* `configElements` (boolean, default: `false`): If enabled the configuration-file will be an array of an object of the - content-fields -* `elementLimits` (optional, if configElements = `true`): Configuration to limit the amount of configuration elements - that guilds with a specific plan -* `commandsWarnings`: This field is used to indicate, that users need to manually set up the permissions for commands in - their discord-server-settings - * `normal`: Array of commands which that can be configured without any limitation in the discord-server-settings - * `special`: Array of commands that need special configuration in addition to editing the permissions in the - server-settings - * `name`: Name of the command - * `info`: Key by language; Information about the command; used to explain users what exactly they should do -* `content`: Array of content fields: - * `field_name`: Name of the config field - * `default`: [Localized](#localization) default value of this field - * `type`: Can be `channelID`, `userID`, `imgURL`, `select`, `timezone` (treated as string, please check validity - before using), `roleID` - , `boolean`, `integer`, `array`, `emoji`, `keyed` (codename for an JS-Object) - or `string` - * `description`: [Localized](#localization) description of this field - * `humanname`: [Localized](#localization) name of this field show to users - * `allowEmbed` (if type === `array, keyed or string`): Allow the usage of an [embed](#configuration) (Note: Please - use the build-in function in `src/functions/helpers.js`) - * `content` (if type === `array`): Type (see `type` above) of every value - * `content` (if type === `channelID`): Array of - supported [ChannelType](https://old.discordjs.dev/#/docs/discord.js/13.16.0/typedef/ChannelType)s ( - default: `['GUILD_TEXT', 'GUILD_VOICE', 'GUILD_CATEGORY', 'GUILD_NEWS', 'GUILD_STAGE_VOICE']`). To improve user - experience, we recommend adding information about supported types into `description`. The bot will verify that the - channel is inside the bot's guild. - * `content` (if type === `select`): Array of the possible options - * `content` (if type === `keyed`): - * `key`: Type (see `type` above) of the index of every value - * `value`: Type as string (see `type` above) of the value of every value - * `params`: (if type === `string`, array, optional) Possible parameters - * `name`: Name of the parameter (e.g. `%mention%`) - * `description`: [Localized](#localization) Description of the parameter (e.g. `Mention of the user`) - * `isImage`: If true, users will be able to set this parameter as Image, Author-Icon, Footer-Icon or Thumbnail - of an embed (only if `allowEmbed` is enabled) - * `allowNull` (default: `false`, optional): If the value of this field can be empty - * `disableKeyEdits` (if type === `keyed`): If enabled the user can not edit the keys of the object - * `elementToggle` (if type === `boolean`): If this option gets turned off, other fields of the config-element / file - will not be rendered in the dashboard - * `dependsOn` (a name of any (other) boolean-field): If the referenced boolean field (the value of this option - should be equal to the `field.field_name` of a boolean field) is turned off, the field will be not be rendered in - the dashboard - * `links` (optional): Array of links displayed below the field description in the SCNX Dashboard - * `label`: [Localized](#localization) label of the link displayed to the user - * `url`: URL the user will be redirected to on click - -#### `botReady`-Event and Config-Reload - -If you plan to use the [ready](https://old.discordjs.dev/#/docs/discord.js/13.16.0/class/Client?scrollTo=e-ready) event of -discord.js to run some action when the client is ready, and you need to load some configuration-files you should use -the `botReady`-event instead. Please remember that this event gets re-emitted on configuration reloading. If you set -callbacks that get executed later or similar please remember to remove them on `configReload`. If you set intervals, -please push the return value to `client.intervals` to get them removed on `configReload` or do it manually. - -#### Helper-Functions - -The bot includes a lot of functions to make your live easier. Check out the file `src/functions/helpers.js`. - -### Support for developers - -As we earn some money with hosting your modules for users, we have decided to give you some (remember, we need to pay -for hosting) of this money. Here are the main ways to earn some pocket-cash with developing for SCNX: - -* [Open-Source-Developer-Pool](https://faq.scnx.app/open-source-developer-pool/): We give you a monthly amount for each - paying server using your module -* [Bounties](https://faq.scnx.app/open-source-developer-pool/#bounties): We give you a small amount of money for merged - pull-requests and contributions - We support a lot of payout-methods, learn more [here](https://faq.scnx.app/scnx-referrals-faq/#payout-methods). - -© Simon Csaba, 2020-2023 - -ScootKit is a trademark, registered in Germany. - -We ♥ you - yes you. \ No newline at end of file +The bot ships with 30+ modules including: + +| Module | Description | +|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------| +| **Moderation** | Auto-mod (bad words, invite blocking with smart resolution, scam links), lockdown with configurable notification channels, warnings, quarantine | +| **Levels** | XP system with role rewards, leaderboard, multipliers per role/channel, custom formulas | +| **Birthdays** | Birthday tracking with admin management (`/manage-birthday`), lock/unlock, auto-announcements | +| **Tickets** | Multi-category ticket system with transcripts | +| **Giveaways** | Giveaway creation and management | +| **Activity Streaks** | Daily/weekly/monthly streak tracking with nickname display, milestone roles, leaderboard, hide option, staff-managed or automatic mode | +| **Guess the Number** | Number guessing game with leaderboard and player statistics | +| **Welcome/Leave** | Customizable welcome and leave messages | +| **Logging** | Audit log forwarding to Discord channels | +| **Auto-React** | Automatic reactions per channel, role, or user | +| **Temp Channels** | Temporary voice channels | +| **RSS Notifications** | RSS feed monitoring with notifications | +| **Status Roles** | Roles based on user presence/status | +| **Applications** | Application/form system with approval workflow | +| **Economy** | Virtual currency with shop system | +| **And more** | Team list, team goals, polls, partner list, invite tracking, starboard, live messages, etc. | + +## Configuration + +All configuration files live in your `config` folder. Each enabled module gets its own subfolder with config files. +These files are auto-generated with defaults and descriptions. + +For embed-capable fields (`allowEmbed: true`), the value can be a plain string or an embed object with: `title`, +`message`, `description`, `color`, `url`, `image`, `thumbnail`, `author`, `fields`, `footer`, `footerImgUrl`. The footer +and timestamp are controlled globally via `strings.json`. + +For full details on writing config files, see [developer-docs/configuration.md](developer-docs/configuration.md). + +## Developer Documentation + +Detailed guides for module developers: + +| Document | Description | +|--------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------| +| [Configuration](developer-docs/configuration.md) | How to write `config.json` files - field types, categories, conditional fields, parameters, config elements | +| [Migrations](developer-docs/migration.md) | How to write safe database migrations - the `DatabaseSchemeVersion` pattern, shutdown protection, multi-version migrations | +| [Config Localization](developer-docs/config-localization.md) | How config translations work - external localization files, what gets localized, extraction script | + +## Creating modules + +As per the [license](LICENSE), you **must** make every module publicly available under the same license. + +Before building a module, create an issue with your suggestion so nobody duplicates work. Submit modules via pull +request. + +### Module structure + +``` +modules/your-module/ + module.json # Module metadata (required) + configs/ + config.json # Configuration schema + commands/ + your-command.js # Slash commands + events/ + botReady.js # Event handlers + messageCreate.js + models/ + YourModel.js # Sequelize models +``` + +### module.json + +```json +{ + "name": "your-module", + "humanReadableName": { + "en": "Your Module", + "de": "Dein Modul" + }, + "description": { + "en": "Short description", + "de": "Kurze Beschreibung" + }, + "author": { + "name": "Your Name", + "link": "https://your-site.com" + }, + "commands-dir": "/commands", + "events-dir": "/events", + "models-dir": "/models", + "config-example-files": [ + "configs/config.json" + ] +} +``` + +Optional fields: `cli`, `on-load-event`, `tags`, `openSourceURL`, `fa-icon` (set by us - browse and request icons +at https://scnx.app/developers/icons). + +### Commands + +Export `run`, `config`, and optionally `subcommands`, `beforeSubcommand`, `autoComplete`: + +```js +module.exports.run = async function (interaction) { /* ... */ +}; + +module.exports.config = { + name: 'your-command', + description: localize('your-module', 'command-description'), + defaultMemberPermissions: null, // null = everyone, ['Administrator'] = admin only + options: [] // or async function(client) { return [...]; } +}; +``` + +Use subcommands over separate commands - there's a 100-command limit. Use +`disabled: function(client) { return !condition; }` to conditionally hide commands. + +### Events + +Export a `run` function: + +```js +module.exports.run = async function (client, ...args) { /* ... */ +}; +``` + +Use `botReady` instead of discord.js `ready` when you need configs loaded. Remember that `botReady` re-fires on config +reload - clean up intervals by pushing to `client.intervals` or `client.jobs`. + +### Models + +Use Sequelize models with the standard pattern. See [developer-docs/migration.md](developer-docs/migration.md) for +adding fields to existing models. + +### Rules for modules + +* Use slash commands with subcommands wherever possible +* Reply with ephemeral messages where it makes sense +* Export functions for cross-module interaction +* Use the newest Discord API features (buttons, selects, modals) +* Process and store only needed user data +* Support localization (see below) +* Follow the [SCNX ToS](https://scootk.it/scnx-tos), [Discord ToS](https://discord.com/tos), + and [Discord Developer ToS](https://discord.com/developers/docs/legal) + +### Localization + +Use `localize(module, key, replacements)` from `src/functions/localize.js` for non-user-editable strings. Translations +happen on [Weblate](https://localize.sc-network.net/projects/custombot/locales/). + +For user-editable strings (config fields), provide values in multiple languages using the `{ "en": "...", "de": "..." }` +pattern - the bot and dashboard select the correct one automatically. + +### Helper functions + +Check `src/functions/helpers.js` for utilities: `embedType()`, `formatDiscordUserName()`, `parseEmbedColor()`, +`formatDate()`, `truncate()`, and more. \ No newline at end of file diff --git a/config-generator/config.json b/config-generator/config.json index 8ef98cf5..bfc6f280 100644 --- a/config-generator/config.json +++ b/config-generator/config.json @@ -1,51 +1,54 @@ { - "description": { - "en": "Configure the basic features of the bot here", - "de": "Generelle Konfiguration deines Bots" - }, - "humanName": { - "en": "Configuration", - "de": "Konfiguration" - }, + "description": "Configure the basic features of the bot here", + "humanName": "Configuration", "filename": "config.json", "content": [ { "name": "token", "humanName": {}, - "default": { - "en": "yourtokengoeshere" - }, - "description": { - "en": "Replace this with your token" - }, + "default": "yourtokengoeshere", + "description": "Replace this with your token", "hidden": true, "type": "string" }, + { + "name": "dmAbuseButton", + "humanName": {}, + "default": false, + "description": "Used to allow mass dm reporting", + "hidden": true, + "type": "boolean" + }, + { + "name": "scnxToken", + "humanName": {}, + "default": "yourtokengoeshere", + "description": "Replace this with your token", + "hidden": true, + "type": "string" + }, + { + "name": "scnxHostOverwirde", + "humanName": {}, + "default": null, + "description": "Replace this with your token", + "hidden": true, + "type": "string", + "allowNull": true + }, { "name": "prefix", - "humanName": { - "en": "Prefix of your bot", - "de": "Prefix deines Botes" - }, - "default": { - "en": "!" - }, - "description": { - "en": "Set the prefix of your bot here", - "de": "Dein eigener Prefix - Wir empfehlen ihn einfach bei ! zu belassen." - }, + "humanName": "Prefix of your bot", + "default": "!", + "description": "Set the prefix of your bot here", "hidden": true, "type": "string" }, { "name": "botOperators", "humanName": {}, - "default": { - "en": [] - }, - "description": { - "en": "Bot operators can reload the configuration and perform system relevant actions with this bot. Please only add users you really trust (and yourself of course)" - }, + "default": [], + "description": "Bot operators can reload the configuration and perform system relevant actions with this bot. Please only add users you really trust (and yourself of course)", "hidden": true, "type": "array", "content": "string" @@ -53,57 +56,30 @@ { "name": "guildID", "humanName": {}, - "default": { - "en": "489786377261678592" - }, - "description": { - "en": "Replace this the id of the guild the bot should work in." - }, + "default": "489786377261678592", + "description": "Replace this the id of the guild the bot should work in.", "hidden": true, "type": "guildID" }, { "name": "disableStatus", - "humanName": { - "en": "Disable Bot-Status", - "de": "Bot-Status deaktivieren" - }, - "default": { - "en": false - }, - "description": { - "en": "If enabled, the bot won't have a status in discord", - "de": "Wenn aktiviert wird der Bot keinen Status in Discord haben" - }, + "humanName": "Disable Bot-Status", + "default": false, + "description": "If enabled, the bot won't have a status in discord", "type": "boolean" }, { "name": "user_presence", - "humanName": { - "en": "Bot-Status" - }, - "default": { - "en": "Change this in your Bot-Configuration on scnx.app: https://scootk.it/change-status", - "de": "Ändere das in deiner Bot-Konfiguration auf scnx.app: https://scootk.it/change-status" - }, - "description": { - "en": "This will show up in Discord as \"Playing \"", - "de": "Das wird in Discord als \"Spielt \" angezeigt" - }, + "humanName": "Bot-Status", + "default": "Change this in your Bot-Configuration on scnx.app: https://scootk.it/change-status", + "description": "This will show up in Discord as \"Playing \"", "type": "string" }, { "name": "logLevel", - "humanName": { - "en": "Logging-Level" - }, - "default": { - "en": "debug" - }, - "description": { - "en": "Log-Level of the bot. Leave it as it is, if you don't know what this means", - "de": "Log-Level des Bots. Belasse es wie es ist, wenn du nicht weißt, was das bedeutet." - }, + "humanName": "Logging-Level", + "default": "debug", + "description": "Log-Level of the bot. Leave it as it is, if you don't know what this means", "hidden": true, "type": "select", "content": [ @@ -117,63 +93,38 @@ }, { "name": "logChannelID", - "humanName": { - "en": "Log-Channel", - "de": "Log-Kanal" - }, - "default": { - "en": "" - }, - "description": { - "en": "Default log-channel for most modules and used to log relevant information", - "de": "Standard Log-Kanal für viele Module. Wird außerdem genutzt, um wesentliche Bot-Informationen zu senden" - }, + "humanName": "Log-Channel", + "default": "", + "description": "Default log-channel for most modules and used to log relevant information", "type": "channelID", "allowNull": true }, { "name": "timezone", - "humanName": { - "en": "Timezone", - "de": "Zeitzone" - }, - "default": { - "en": "Europe/Berlin" - }, - "description": { - "en": "Timezone the bot runs in", - "de": "Zeitzone in der der Bot laufen soll" - }, + "humanName": "Timezone", + "default": "Europe/Berlin", + "description": "Timezone the bot runs in", "type": "timezone" }, { "name": "disableEveryoneProtection", - "humanName": { - "en": "Allow @everyone / @here pings", - "de": "@everyone und @here Pings erlauben" - }, - "default": { - "en": false - }, - "description": { - "en": "Allows @everyone and @here pings for messages configurable in the dashboard", - "de": "Erlaubt @everyone und @here pings in im Dashboard anpassbaren Nachrichten" - }, + "humanName": "Allow @everyone / @here pings", + "default": false, + "description": "Allows @everyone and @here pings for messages configurable in the dashboard", + "type": "boolean" + }, + { + "name": "disableFileArchival", + "humanName": "Disable attachment archival", + "default": false, + "description": "When archival is enabled (recommended), the bot uploads Discord attachments (images, videos, files) to your scnx CDN so the starboard, moderation evidence, deleted-message logs and anti-nuke sticker restores keep working after Discord's signed URLs expire (usually within hours). Disabling this stops all uploads to scnx — no data leaves the bot for archival — but those features will silently break once Discord's short-lived URLs expire. Archived files count against your guild's file-storage quota (view current usage: https://scnx.app/glink?page=images). Free-plan guilds may run out of space and need to upgrade or delete files to keep archival working.", "type": "boolean" }, { "name": "syncCommandGlobally", - "humanName": { - "en": "Sync module commands as global commands", - "de": "Speichere Modul-Befehle als Globale-Befehle" - }, - "default": { - "en": false - }, - "description": { - "en": "If enabled, module-commands will be synced to discord as global commands. They will show up on other servers, but won't work. Syncing can take up to 2 hours, so changes may not be reflected immediately.", - "de": "Wenn aktiviert, werden Befehle von Modulen als Globale-Befehle zu Discord gesendet. Sie werden auf anderen Servern angezeigt, werden aber nicht funktionieren. Synchronisierung kann bis zu 2 Stunden dauern." - }, + "humanName": "Sync module commands as global commands", + "default": false, + "description": "If enabled, module-commands will be synced to discord as global commands. They will show up on other servers, but won't work. Syncing can take up to 2 hours, so changes may not be reflected immediately.", "type": "boolean" } ] diff --git a/config-generator/strings.json b/config-generator/strings.json index 5fbdd4af..9d63e777 100644 --- a/config-generator/strings.json +++ b/config-generator/strings.json @@ -1,203 +1,104 @@ { - "description": { - "en": "Configure strings & messages of your bot here", - "de": "Passe die Nachrichten deines Botes an" - }, - "humanName": { - "en": "Messages", - "de": "Nachrichten" - }, + "description": "Configure strings & messages of your bot here", + "humanName": "Messages", "filename": "strings.json", "content": [ { "name": "addAtToUsernames", - "humanName": { - "en": "Add @ to usernames", - "de": "@ zu Nutzernamen hinzufügen" - }, - "default": { - "en": false - }, - "description": { - "en": "If enabled, every username will be prefixed by an \"@\". Example: \"scderox\" -> \"@scderox\"", - "de": "Wenn aktiviert, wird vor jedem Nutzername ein \"@\" eingefügt. Beispiel: \"scderox\" -> \"@scderox\"" - }, + "humanName": "Add @ to usernames", + "default": false, + "description": "If enabled, every username will be prefixed by an \"@\". Example: \"scderox\" -> \"@scderox\"", "type": "boolean" }, { "name": "footer", - "humanName": { - "en": "Embed-Footer" - }, - "default": { - "en": "Powered by scnx.xyz ⚡" - }, - "description": { - "en": "Footer of every embed", - "de": "Footer jedes Embeds" - }, + "humanName": "Embed-Footer", + "default": "Powered by scnx.xyz ⚡", + "description": "Footer of every embed", "type": "string", "pro": true }, { "name": "footerImgUrl", - "humanName": { - "en": "Embed-Footer-Image-URL", - "de": "Embed-Footer-Bild-URL" - }, - "default": { - "en": "https://scnx.xyz/favicon.png" - }, + "humanName": "Embed-Footer-Image-URL", + "default": "https://scnx.xyz/favicon.png", "allowNull": true, - "description": { - "en": "Footer-Image of every embed", - "de": "Footer-Bild von jedem Embed" - }, + "description": "Footer-Image of every embed", "type": "imgURL", "pro": true }, { "name": "need_args", - "humanName": { - "en": "More arguments are needed", - "de": "Mehr Argumente werden benötigt" - }, - "default": { - "en": "This command needs more arguments - you passed %count%, but you need to provide at least %neededCount%.", - "de": "This command needs more arguments - you passed %count%, but you need to provide at least %neededCount%." - }, - "description": { - "en": "This message gets sent if there are not enough arguments specified", - "de": "Diese Nachricht wird versendet, wenn eine oder mehrere Argumente für einen Befehl fehlen" - }, + "humanName": "More arguments are needed", + "default": "This command needs more arguments - you passed %count%, but you need to provide at least %neededCount%.", + "description": "This message gets sent if there are not enough arguments specified", "type": "string", "allowEmbed": true, "params": [ { "name": "count", - "description": { - "en": "Count of arguments provided", - "de": "Anzahl von angegebenen Parameter" - } + "description": "Count of arguments provided" }, { "name": "neededCount", - "description": { - "en": "Count of arguments needed", - "de": "Anzahl von benötigten Argumenten" - } + "description": "Count of arguments needed" } ] }, { "name": "updated_roles", - "humanName": { - "en": "Roles updated", - "de": "Rollen erfolgreich geupdated" - }, - "default": { - "en": "✅ Updated roles according to your settings", - "de": "✅ Rollen-Änderungen übernommen" - }, - "description": { - "en": "This message gets sent after a user selects self-roles on a self-role-element.", - "de": "Diese Nachricht wird gesendet, wenn ein Nutzer eine Self-Rolle auswählt." - }, + "humanName": "Roles updated", + "default": "✅ Updated roles according to your settings", + "description": "This message gets sent after a user selects self-roles on a self-role-element.", "type": "string", "allowEmbed": true }, { "name": "added_role", - "humanName": { - "en": "Role added", - "de": "Rolle erfolgreich hinzugefügt" - }, - "default": { - "en": "✅ Role %role% successfully added", - "de": "✅ Rolle %role% erfolgreich hinzugefügt" - }, - "description": { - "en": "This message gets sent when a user adds a role to themselves.", - "de": "Diese Nachricht wird gesendet, wenn ein Nutzer sich selbst eine Self-Rolle hinzufügt." - }, + "humanName": "Role added", + "default": "✅ Role %role% successfully added", + "description": "This message gets sent when a user adds a role to themselves.", "type": "string", "allowEmbed": true, "params": [ { "name": "role", - "description": { - "en": "Name of the role", - "de": "Name der Rolle" - } + "description": "Name of the role" } ] }, { "name": "removed_role", - "humanName": { - "en": "Role removed", - "de": "Rolle erfolgreich entfernt" - }, - "default": { - "en": "✅ Role %role% successfully removed", - "de": "✅ Rolle %role% erfolgreich entfernt" - }, - "description": { - "en": "This message gets sent when a user removes a role from themselves.", - "de": "Diese Nachricht wird gesendet, wenn ein Nutzer sich selbst eine Self-Rolle entfernt." - }, + "humanName": "Role removed", + "default": "✅ Role %role% successfully removed", + "description": "This message gets sent when a user removes a role from themselves.", "type": "string", "allowEmbed": true, "params": [ { "name": "role", - "description": { - "en": "Name of the role", - "de": "Name der Rolle" - } + "description": "Name of the role" } ] }, { "name": "not_enough_permissions", - "humanName": { - "en": "Not enough permissions", - "de": "Nicht genügend Rechte" - }, - "default": { - "en": "Seems like you don't have enough permissions.", - "de": "Scheint als hättest du nicht genügend Rechte." - }, - "description": { - "en": "This message gets sent if an user don't hase enough permissions", - "de": "Diese Nachricht wird versendet, wenn der Nutzer nicht genügen Rechte hat" - }, + "humanName": "Not enough permissions", + "default": "Seems like you don't have enough permissions.", + "description": "This message gets sent if an user don't hase enough permissions", "type": "string", "allowEmbed": true }, { "name": "helpembed", - "humanName": { - "en": "Help-Message", - "de": "Hilfe-Nachricht" - }, + "humanName": "Help-Message", "default": { - "en": { - "title": "Help", - "description": "You can find every command here", - "module_translation": "%name% by %author%: %description%", - "build_in": "Build-In-Commands" - }, - "de": { - "title": "Help", - "description": "Alle Commands findest du hier", - "module_translation": "%name% by %author%: %description%", - "build_in": "Build-In-Commands" - } - }, - "description": { - "en": "Strings for help command" + "title": "Help", + "description": "You can find every command here", + "module_translation": "%name% by %author%: %description%", + "build_in": "Build-In-Commands" }, + "description": "Strings for help command", "type": "keyed", "content": { "key": "string", @@ -207,48 +108,24 @@ }, { "name": "disableHelpEmbedStats", - "humanName": { - "en": "Disable Stats in Help-Embed", - "de": "Deaktiviere Stats im Hilfe-Embed" - }, - "default": { - "en": false - }, - "description": { - "en": "If enabled, the stats-field in the Help-Embed will get hidden", - "de": "Wenn aktiviert, wird der Stats-Bereich im Help-Embed verborgen" - }, + "humanName": "Disable Stats in Help-Embed", + "default": false, + "description": "If enabled, the stats-field in the Help-Embed will get hidden", "type": "boolean", "pro": true }, { "name": "disableFooterTimestamp", - "humanName": { - "en": "Disable default Timestamp in footer", - "de": "Standard-Timestamp im Footer deaktivieren" - }, - "default": { - "en": false - }, - "description": { - "en": "If enabled, the current time will not be displayed in the embed footer", - "de": "Wenn aktiviert, wird die Aktuelle Uhrzeit nicht im Footer angezeigt" - }, + "humanName": "Disable default Timestamp in footer", + "default": false, + "description": "If enabled, the current time will not be displayed in the embed footer", "type": "boolean" }, { "name": "putBotInfoOnLastSite", - "humanName": { - "en": "Hides the Bot-Info in the Help-Embed", - "de": "Verbergt die Bot-Info Sektion im Hilfe-Embed" - }, - "default": { - "en": false - }, - "description": { - "en": "If enabled, the Bot-Info-Section of the Help-Embed will be hidden.", - "de": "Wenn aktiviert, wird der Bot-Info-Bereich im Help-Embed verborgen." - }, + "humanName": "Hides the Bot-Info in the Help-Embed", + "default": false, + "description": "If enabled, the Bot-Info-Section of the Help-Embed will be hidden.", "type": "boolean", "pro": true } diff --git a/config-localizations/convert-configs.js b/config-localizations/convert-configs.js new file mode 100644 index 00000000..f6669345 --- /dev/null +++ b/config-localizations/convert-configs.js @@ -0,0 +1,253 @@ +/** + * Converts all config JSON files from inline localization format to English-only format. + * + * Reads module.json config-example-files to discover ALL config files per module. + * + * Before: { "description": { "en": "Configure here", "de": "Konfigurieren" } } + * After: { "description": "Configure here" } + * + * For default values, the {en: value} wrapper is removed for ALL types: + * { "default": { "en": false } } → { "default": false } + * { "default": { "en": "Hello" } } → { "default": "Hello" } + * + * Usage: node config-localizations/convert-configs.js [--dry-run] + */ + +const fs = require('fs'); +const path = require('path'); + +const ROOT = path.resolve(__dirname, '..'); +const DRY_RUN = process.argv.includes('--dry-run'); + +let filesModified = 0; +let fieldsConverted = 0; + +/** + * Check if a value is a localized object ({en: ..., de: ...}). + */ +function isLocalizedObject(value) { + if (value === null || value === undefined) return false; + if (typeof value !== 'object' || Array.isArray(value)) return false; + if (!('en' in value)) return false; + const keys = Object.keys(value); + return keys.length > 0 && keys.every(k => /^[a-z]{2,3}$/.test(k)); +} + +/** + * Unwrap a localized object to its English value. + */ +function unwrap(value) { + if (isLocalizedObject(value)) { + fieldsConverted++; + return value.en; + } + return value; +} + +/** + * Recursively unwrap all localized objects within a nested structure. + */ +function recursiveUnwrap(obj) { + if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) return; + for (const key of Object.keys(obj)) { + if (isLocalizedObject(obj[key])) { + obj[key] = unwrap(obj[key]); + } else if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) { + recursiveUnwrap(obj[key]); + } + } +} + +/** + * Process a single config file, converting all localized objects to English-only. + */ +function convertConfig(configData) { + // Top-level localized properties + for (const key of ['description', 'humanName', 'warningBanner', 'informationBanner']) { + if (isLocalizedObject(configData[key])) { + configData[key] = unwrap(configData[key]); + } + } + + // informationBanner may have nested localized objects (e.g. button.text) + if (configData.informationBanner && typeof configData.informationBanner === 'object' && !isLocalizedObject(configData.informationBanner)) { + recursiveUnwrap(configData.informationBanner); + } + + // configElementName: {en: {one: ..., more: ...}, de: {...}} → {one: ..., more: ...} + if (isLocalizedObject(configData.configElementName)) { + configData.configElementName = unwrap(configData.configElementName); + } + + // commandsWarnings.special[].info + if (configData.commandsWarnings && Array.isArray(configData.commandsWarnings.special)) { + for (const warning of configData.commandsWarnings.special) { + if (isLocalizedObject(warning.info)) { + warning.info = unwrap(warning.info); + } + } + } + + // categories[].displayName + if (Array.isArray(configData.categories)) { + for (const cat of configData.categories) { + if (isLocalizedObject(cat.displayName)) { + cat.displayName = unwrap(cat.displayName); + } + } + } + + // content fields + if (Array.isArray(configData.content)) { + for (const field of configData.content) { + convertField(field); + } + } + + return configData; +} + +/** + * Convert a single content field. + */ +function convertField(field) { + // humanName, description — always localized + for (const key of ['humanName', 'description']) { + if (isLocalizedObject(field[key])) { + field[key] = unwrap(field[key]); + } + } + + // default — unwrap {en: value} for ALL types + if (isLocalizedObject(field.default)) { + field.default = unwrap(field.default); + } + + // params[].description + if (Array.isArray(field.params)) { + for (const param of field.params) { + if (isLocalizedObject(param.description)) { + param.description = unwrap(param.description); + } + } + } + + // select content[].displayName (when content is array of objects) + if (Array.isArray(field.content) && field.content.length > 0 && typeof field.content[0] === 'object' && field.content[0] !== null) { + for (const option of field.content) { + if (option && isLocalizedObject(option.displayName)) { + option.displayName = unwrap(option.displayName); + } + } + } + + // links[].label + if (Array.isArray(field.links)) { + for (const link of field.links) { + if (isLocalizedObject(link.label)) { + link.label = unwrap(link.label); + } + } + } +} + +/** + * Process a config file at the given path. + */ +function processFile(filePath) { + let raw; + try { + raw = fs.readFileSync(filePath, 'utf-8'); + } catch (e) { + console.warn(` Skipping ${filePath}: ${e.message}`); + return; + } + + let configData; + try { + configData = JSON.parse(raw); + } catch (e) { + console.warn(` Skipping ${filePath}: invalid JSON`); + return; + } + + // Skip non-config files + if (Array.isArray(configData) && !configData.content) return; + if (!configData.content && !configData.description && !configData.humanName) return; + + const beforeCount = fieldsConverted; + convertConfig(configData); + const changed = fieldsConverted - beforeCount; + + if (changed > 0) { + const output = JSON.stringify(configData, null, 2); + if (DRY_RUN) { + console.log(` [DRY RUN] Would modify ${filePath} (${changed} fields)`); + } else { + fs.writeFileSync(filePath, output); + console.log(` Modified ${filePath} (${changed} fields)`); + } + filesModified++; + } +} + +// Process config-generator files +console.log('Converting config-generator/...'); +const coreDir = path.join(ROOT, 'config-generator'); +if (fs.existsSync(coreDir)) { + for (const file of fs.readdirSync(coreDir).sort()) { + if (!file.endsWith('.json')) continue; + processFile(path.join(coreDir, file)); + } +} + +// Process module config files using module.json +console.log('Converting modules/...'); +const modulesDir = path.join(ROOT, 'modules'); +for (const moduleName of fs.readdirSync(modulesDir).sort()) { + const moduleDir = path.join(modulesDir, moduleName); + if (!fs.statSync(moduleDir).isDirectory()) continue; + + const moduleJsonPath = path.join(moduleDir, 'module.json'); + if (!fs.existsSync(moduleJsonPath)) continue; + + let moduleJson; + try { + moduleJson = JSON.parse(fs.readFileSync(moduleJsonPath, 'utf-8')); + } catch (e) { + console.warn(` Skipping ${moduleName}: invalid module.json`); + continue; + } + + // Convert module.json humanReadableName, description, legalDisclaimer + let mjChanged = false; + for (const key of ['humanReadableName', 'description', 'legalDisclaimer']) { + if (isLocalizedObject(moduleJson[key])) { + moduleJson[key] = unwrap(moduleJson[key]); + mjChanged = true; + } + } + if (mjChanged) { + if (DRY_RUN) { + console.log(` [DRY RUN] Would modify ${moduleName}/module.json`); + } else { + fs.writeFileSync(moduleJsonPath, JSON.stringify(moduleJson, null, 2) + '\n'); + console.log(` Modified ${moduleName}/module.json`); + } + filesModified++; + } + + // Convert config files + const configFiles = moduleJson['config-example-files'] || []; + for (const configFile of configFiles) { + const filePath = path.join(moduleDir, configFile); + if (!fs.existsSync(filePath)) { + console.warn(` Warning: ${moduleName}/${configFile} listed in module.json but not found`); + continue; + } + processFile(filePath); + } +} + +console.log(`\n${DRY_RUN ? '[DRY RUN] ' : ''}Done! ${filesModified} files modified, ${fieldsConverted} fields converted.`); +if (DRY_RUN) console.log('Run without --dry-run to apply changes.'); diff --git a/config-localizations/en.json b/config-localizations/en.json new file mode 100644 index 00000000..67abb50a --- /dev/null +++ b/config-localizations/en.json @@ -0,0 +1,4907 @@ +{ + "_core": { + "config": { + "description": "Configure the basic features of the bot here", + "humanName": "Configuration", + "content": { + "token": { + "description": "Replace this with your token", + "default": "yourtokengoeshere" + }, + "dmAbuseButton": { + "description": "Used to allow mass dm reporting" + }, + "scnxToken": { + "description": "Replace this with your token", + "default": "yourtokengoeshere" + }, + "scnxHostOverwirde": { + "description": "Replace this with your token" + }, + "prefix": { + "humanName": "Prefix of your bot", + "description": "Set the prefix of your bot here", + "default": "!" + }, + "botOperators": { + "description": "Bot operators can reload the configuration and perform system relevant actions with this bot. Please only add users you really trust (and yourself of course)" + }, + "guildID": { + "description": "Replace this the id of the guild the bot should work in." + }, + "disableStatus": { + "humanName": "Disable Bot-Status", + "description": "If enabled, the bot won't have a status in discord" + }, + "user_presence": { + "humanName": "Bot-Status", + "description": "This will show up in Discord as \"Playing \"", + "default": "Change this in your Bot-Configuration on scnx.app: https://scootk.it/change-status" + }, + "logLevel": { + "humanName": "Logging-Level", + "description": "Log-Level of the bot. Leave it as it is, if you don't know what this means" + }, + "logChannelID": { + "humanName": "Log-Channel", + "description": "Default log-channel for most modules and used to log relevant information" + }, + "timezone": { + "humanName": "Timezone", + "description": "Timezone the bot runs in" + }, + "disableEveryoneProtection": { + "humanName": "Allow @everyone / @here pings", + "description": "Allows @everyone and @here pings for messages configurable in the dashboard" + }, + "syncCommandGlobally": { + "humanName": "Sync module commands as global commands", + "description": "If enabled, module-commands will be synced to discord as global commands. They will show up on other servers, but won't work. Syncing can take up to 2 hours, so changes may not be reflected immediately." + } + } + }, + "strings": { + "description": "Configure strings & messages of your bot here", + "humanName": "Messages", + "content": { + "addAtToUsernames": { + "humanName": "Add @ to usernames", + "description": "If enabled, every username will be prefixed by an \"@\". Example: \"scderox\" -> \"@scderox\"" + }, + "footer": { + "humanName": "Embed-Footer", + "description": "Footer of every embed", + "default": "Powered by scnx.xyz ⚡" + }, + "footerImgUrl": { + "humanName": "Embed-Footer-Image-URL", + "description": "Footer-Image of every embed", + "default": "https://scnx.xyz/favicon.png" + }, + "need_args": { + "humanName": "More arguments are needed", + "description": "This message gets sent if there are not enough arguments specified", + "default": "This command needs more arguments - you passed %count%, but you need to provide at least %neededCount%.", + "params": { + "count": { + "description": "Count of arguments provided" + }, + "neededCount": { + "description": "Count of arguments needed" + } + } + }, + "updated_roles": { + "humanName": "Roles updated", + "description": "This message gets sent after a user selects self-roles on a self-role-element.", + "default": "✅ Updated roles according to your settings" + }, + "added_role": { + "humanName": "Role added", + "description": "This message gets sent when a user adds a role to themselves.", + "default": "✅ Role %role% successfully added", + "params": { + "role": { + "description": "Name of the role" + } + } + }, + "removed_role": { + "humanName": "Role removed", + "description": "This message gets sent when a user removes a role from themselves.", + "default": "✅ Role %role% successfully removed", + "params": { + "role": { + "description": "Name of the role" + } + } + }, + "not_enough_permissions": { + "humanName": "Not enough permissions", + "description": "This message gets sent if an user don't hase enough permissions", + "default": "Seems like you don't have enough permissions." + }, + "helpembed": { + "humanName": "Help-Message", + "description": "Strings for help command" + }, + "disableHelpEmbedStats": { + "humanName": "Disable Stats in Help-Embed", + "description": "If enabled, the stats-field in the Help-Embed will get hidden" + }, + "disableFooterTimestamp": { + "humanName": "Disable default Timestamp in footer", + "description": "If enabled, the current time will not be displayed in the embed footer" + }, + "putBotInfoOnLastSite": { + "humanName": "Hides the Bot-Info in the Help-Embed", + "description": "If enabled, the Bot-Info-Section of the Help-Embed will be hidden." + } + } + } + }, + "admin-tools": { + "_module": { + "humanReadableName": "Admin-Tools", + "description": "Simple tools for admins - move channels and roles via commands, assign temporary roles, configure role bans or copy an emoji from another server to your server." + }, + "config": { + "description": "Configure the behaviour of the module here", + "humanName": "Configuration" + }, + "always-temporary-roles": { + "description": "Configure roles that are always temporary. When a user receives one of these roles (by any means), the role will automatically be removed after the configured duration.", + "humanName": "Always-Temporary Roles", + "configElementName": { + "one": "Always-Temporary Role", + "more": "Always-Temporary Roles" + }, + "content": { + "roleID": { + "humanName": "Role", + "description": "The role that should always be temporary. When a user receives this role, it will be automatically removed after the configured duration." + }, + "duration": { + "humanName": "Duration", + "description": "How long the role should last before being automatically removed. Examples: 1h, 12h, 1d, 7d, 30m", + "default": "24h", + "links": { + "https://scootk.it/custombot-durations": { + "label": "Duration format" + } + } + } + } + }, + "role-bans": { + "description": "Configure roles that automatically ban users when assigned. When a user receives one of these roles, they will be immediately banned from the server. Users with the \"Manage Roles\" permission are exempt.", + "humanName": "Role Bans", + "configElementName": { + "one": "Role Ban", + "more": "Role Bans" + }, + "content": { + "roleID": { + "humanName": "Role", + "description": "When a user receives this role, they will be immediately banned from the server. Users with the \"Manage Roles\" permission are exempt." + }, + "reason": { + "humanName": "Ban Reason", + "description": "The reason shown in the audit log when a user is banned for receiving this role.", + "default": "Received a banned role" + }, + "deleteMessageDays": { + "humanName": "Delete Message Days", + "description": "Number of days of messages to delete when banning the user (0-7)." + } + } + } + }, + "afk-system": { + "_module": { + "humanReadableName": "AFK-System", + "description": "Allow users to set their AFK-Status and notify other users if they try to reach them" + }, + "config": { + "description": "Configure the behaviour of the module here", + "humanName": "Configuration", + "content": { + "sessionEndedSuccessfully": { + "humanName": "AFK-Session ended successfully", + "description": "This message gets send if a user ended their AFK-session successfully.", + "default": "✅ Your AFK status has been removed. Welcome back!" + }, + "sessionStartedSuccessfully": { + "humanName": "AFK-Session started successfully", + "description": "This message gets send if a user started their session successfully.", + "default": "✅ Your status has been updated to AFK. If another member mentions you while your AFK, we're going to notify them about your status." + }, + "afkUserWithReason": { + "humanName": "User is AFK with reason", + "description": "This message gets send if a pinged user is currently AFK with a previously specified reason.", + "default": "ℹ %user% is currently AFK and specified the following reason: \"%reason%\".", + "params": { + "reason": { + "description": "Reason for their absence" + }, + "user": { + "description": "Mention of the user who is AFK" + } + } + }, + "afkUserWithoutReason": { + "humanName": "User is AFK without reason", + "description": "This message gets send if a pinged user is currently AFK without a previously specified reason.", + "default": "ℹ %user% is currently AFK.", + "params": { + "user": { + "description": "Mention of the user who is AFK" + } + } + }, + "autoEndMessage": { + "humanName": "AFK Session ended automatically", + "description": "This message gets send if a user who is AFK and hasn't disabled auto-ending their sessions posts a message on the server.", + "default": "Welcome back 👋!\nYou are no longer AFK because you wrote a message. You can start a new session with `/afk start` and disable `auto-end` if you don't want your sessions to be ended automatically.", + "params": { + "user": { + "description": "Mention of the user who was AFK" + } + } + } + } + } + }, + "anti-ghostping": { + "_module": { + "humanReadableName": "Anti-Ghostping", + "description": "This module detects ghost-pings and sends a message if one occurs" + }, + "config": { + "description": "Configure the behaviour of the module here", + "humanName": "Configuration", + "content": { + "awaitBotMessages": { + "humanName": "Wait for Bot-Messages", + "description": "If enabled, the bot will wait ~2 Seconds to make sure no bot like NQN deleted the messages and answered afterwards" + }, + "ignoredChannels": { + "humanName": "Ignored Channels", + "description": "If a ghost ping gets send in one of these configured channels, the bot will not run anti-ghost-ping" + }, + "youJustGotGhostPinged": { + "humanName": "Ghostping-Message", + "description": "This message gets send if a member pings another user and deletes the message afterwards", + "default": "%mentions%,\nYou just got ghost-pinged by %authorMention% with the following message: \"%msgContent%\"", + "params": { + "mentions": { + "description": "Mentions of every user that got pinged in the original message" + }, + "authorMention": { + "description": "Mention of the original message-author." + }, + "msgContent": { + "description": "Content of the original message" + } + } + } + } + } + }, + "auto-delete": { + "_module": { + "humanReadableName": "Auto-Message-Delete", + "description": "This module allows you to delete messages from a channel after a specified timeout to keep your channel clean" + }, + "channels": { + "description": "Set up channels to delete text-messages from", + "humanName": "Text-Channels", + "content": { + "channelID": { + "humanName": "Channel", + "description": "The Channel you want messages to be deleted from." + }, + "timeout": { + "humanName": "Timeout", + "description": "Timeout (in minutes) after which the messages in a channel will be deleted." + }, + "keepMessageCount": { + "humanName": "Amount of messages to keep", + "description": "Set up a number here to always have x messages in your channel left (newest messages are kept). The number has to below 50." + } + } + }, + "voice-channels": { + "description": "Set up voice-channels to delete messages from", + "humanName": "Voice-Channels", + "content": { + "channelID": { + "humanName": "Voice-Channel", + "description": "The Voice-Channel you want the auto-deleter to clear if there are no channel members left." + }, + "timeout": { + "humanName": "Timeout", + "description": "Timeout (in minutes) after which the messages in a Voice-Channel are deleted after the last member left the channel. Entering '0' will result in an instant deletion." + } + } + } + }, + "auto-messager": { + "_module": { + "humanReadableName": "Automatic Messages", + "description": "You can - with this module - send automatic messages" + }, + "hourly": { + "description": "You can send messages on an hourly basic here - this can be once or 24 times a day", + "humanName": "Hourly basic", + "configElementName": { + "one": "Automatic message", + "more": "Automatic messages" + }, + "content": { + "channelID": { + "humanName": "Channel", + "description": "ID of the channel in which the message should be send" + }, + "message": { + "humanName": "Message", + "description": "Message that should be send", + "default": "" + }, + "limitHoursTo": { + "humanName": "Limit hours to", + "description": "If one or more values are set, the message will only get send when the current hour is included in this field" + } + } + }, + "daily": { + "description": "You can send on a daily basic here - this can be once a week or month", + "humanName": "Daily Basic", + "configElementName": { + "one": "Automatic message", + "more": "Automatic messages" + }, + "content": { + "channelID": { + "humanName": "Channel", + "description": "ID of the channel in which the message should be send" + }, + "message": { + "humanName": "Message", + "description": "Message that should be send", + "default": "" + }, + "limitWeekDaysTo": { + "humanName": "Limit Week-Days to", + "description": "If one or more values are set, the message will only get send when the current week-day is included in this field" + }, + "limitDaysTo": { + "humanName": "Limit days to", + "description": "If one or more values are set, the message will only get send when the current day (of the month) is included in this field" + } + } + }, + "cronjob": { + "description": "Advanced users can unleash the full potential of automatic message with cronejobs", + "humanName": "Cronjob (advanced)", + "configElementName": { + "one": "Automatic message", + "more": "Automatic messages" + }, + "content": { + "channelID": { + "humanName": "Channel", + "description": "ID of the channel in which the message should be send" + }, + "message": { + "humanName": "Message", + "description": "Message that should be send", + "default": "" + }, + "expression": { + "humanName": "Expression", + "description": "The message gets scheduled for this expression", + "default": "1 6 1-31 * *" + } + } + } + }, + "auto-publisher": { + "_module": { + "humanReadableName": "Automatic Publishing", + "description": "Publishes messages in announcement channels" + }, + "config": { + "description": "Configure the behaviour of the module here", + "humanName": "Configuration", + "content": { + "mode": { + "humanName": "Message-Publishing-Mode", + "description": "Modus in which this module should operate" + }, + "blacklist": { + "humanName": "Blacklist", + "description": "Channel to be ignored (only if Message-Publishing-Mode = \"blacklist\")" + }, + "whitelist": { + "humanName": "Whitelist", + "description": "Channel in which messages should get published (only if Message-Publishing-Mode = \"whitelist\")" + }, + "ignoreBots": { + "humanName": "Ignore bots?", + "description": "Should bots get ignored when they post a message" + } + } + } + }, + "auto-thread": { + "_module": { + "humanReadableName": "Automatic Thread-Creation", + "description": "Automatically creates a thread under each message that gets posted in a selected channel" + }, + "config": { + "description": "Configure the behaviour of the module here", + "humanName": "Configuration", + "content": { + "channels": { + "humanName": "Channels", + "description": "Here you can add channels in which the bot should create a thread under every message" + }, + "threadName": { + "humanName": "Thread Name", + "description": "Name of every thread", + "default": "Comments" + }, + "threadArchiveDuration": { + "humanName": "Archive Duration", + "description": "Inactivity after which a thread is automatically archived (in minutes, some values are limited by guild boost level; select \"max\" for the longest possible duration)" + } + } + } + }, + "betterstatus": { + "_module": { + "humanReadableName": "Betterstatus", + "description": "Give you more features to make your status even better - change it when someone joins, change it every x seconds and more!" + }, + "config": { + "description": "Configure the bot status, activity type and interval settings here", + "humanName": "Configuration", + "content": { + "enableStatusCommand": { + "humanName": "Enable /status command?", + "description": "If enabled, administrators can change the bot status using the /status slash command" + }, + "enableInterval": { + "humanName": "Enable interval?", + "description": "If enabled the bot will change its status every x seconds" + }, + "intervalStatuses": { + "humanName": "Interval-Statuses", + "description": "Statuses from which the bot should randomly choose one", + "params": { + "onlineMemberCount": { + "description": "Count of online members on your guild (will not work if presence intent not enabled)" + }, + "memberCount": { + "description": "Count of members on your guild" + }, + "randomMemberTag": { + "description": "Tag of one random member on your guild" + }, + "randomOnlineMemberTag": { + "description": "Tag of one random member who is online on your guild" + }, + "channelCount": { + "description": "Count of channels on your guild" + }, + "roleCount": { + "description": "Count of roles on your guild" + } + } + }, + "activityType": { + "humanName": "Activity-Type", + "description": "Type of the user activity" + }, + "botStatus": { + "humanName": "Bot-Status", + "description": "Status of your bot" + }, + "interval": { + "humanName": "Status-Interval", + "description": "The interval in seconds (at least 10 seconds)" + }, + "changeOnUserJoin": { + "humanName": "Change status on user join?", + "description": "If the status should be changed if someone joins your guild" + }, + "userJoinStatus": { + "humanName": "User-Join-Status", + "description": "Status that will be set if a user joins", + "default": "Welcome %tag%!", + "params": { + "tag": { + "description": "Tag of the new user" + }, + "username": { + "description": "Username of the new user" + }, + "memberCount": { + "description": "New member count of your guild" + } + } + }, + "streamingLink": { + "humanName": "Streaming Link", + "description": "Will be shown, if the activity-typ is streaming and your link is supported by Discord", + "default": "" + } + } + } + }, + "channel-stats": { + "_module": { + "humanReadableName": "Channel-Stats", + "description": "Create channels containing stats about your server - updated automatically." + }, + "channels": { + "description": "Configure voice channels that display live server statistics", + "humanName": "Configuration", + "configElementName": { + "one": "Statistics-Channel", + "more": "Statistics-Channels" + }, + "content": { + "channelID": { + "humanName": "Channel", + "description": "ID of the voice channel" + }, + "channelName": { + "humanName": "Channel-Name", + "description": "Name of Channel", + "default": "", + "params": { + "userCount": { + "description": "Total count of users on your server" + }, + "memberCount": { + "description": "Total count of members (not bots) on your server" + }, + "onlineUserCount": { + "description": "Total count of online (dnd or online status) users on your server" + }, + "channelCount": { + "description": "Total count of channels on your server" + }, + "roleCount": { + "description": "Total count of roles on your server" + }, + "botCount": { + "description": "Count of Bots on your server" + }, + "dndCount": { + "description": "Count of members (not bots) with DND as status" + }, + "onlineMemberCount": { + "description": "Count of members (not bots) with online (and only online) as status" + }, + "awayCount": { + "description": "Count of members (not bots) with away status" + }, + "offlineCount": { + "description": "Count of members (not bots) with offline status" + }, + "guildBoosts": { + "description": "Show how often this guild was boosted" + }, + "boostLevel": { + "description": "Shows the current boost-level of this guild" + }, + "boosterCount": { + "description": "Count of boosters on this guild" + }, + "emojiCount": { + "description": "Count of emojis on this guild" + }, + "currentTime": { + "description": "Current time and date" + }, + "userWithRoleCount-": { + "description": "Count of members with a specific role (replace \"\" with an actual role-id)" + }, + "onlineUserWithRoleCount-": { + "description": "Count of members with a specific role who are online (replace \"\" with an actual role-id)" + } + } + }, + "updateInterval": { + "humanName": "Update-Interval", + "description": "You can set an interval here in which the bot should update the channels. Must be higher than seven; in minutes." + } + } + } + }, + "color-me": { + "_module": { + "humanReadableName": "Color me", + "description": "Simple module to reward users who have boosted your server with a custom role!" + }, + "config": { + "description": "Configure the function of the module here", + "humanName": "Configuration", + "content": { + "recreateRole": { + "humanName": "Recreate roles", + "description": "Should the role be created again if the user boosts again?" + }, + "listRoles": { + "humanName": "Separate roles in member-list", + "description": "Should the role be listed separately in the member-list?" + }, + "removeOnUnboost": { + "humanName": "Remove role on unboost", + "description": "Should the role be deleted automatically, if the user stops boosting your server? (disable, if also non-boosters should be able to use this command)" + }, + "updateCooldown": { + "humanName": "Role update cooldown", + "description": "The amount of time a user needs to wait util they can edit their role again (in hours)" + }, + "rolePosition": { + "humanName": "Role position", + "description": "The role, beneath which the custom-roles should be created" + } + } + }, + "strings": { + "description": "Edit the messages and strings of the module here", + "humanName": "Messages", + "content": { + "created": { + "humanName": "Role created", + "description": "This messages gets send when a booster sucessfully created their custom role", + "default": "Your role was created successfully." + }, + "createdNoIcon": { + "humanName": "Role created without icon", + "description": "This message gets send when a booster successfully created their custom role, but the guild has not enough boosts to use role icons", + "default": "Your role was created successfully, but your role icon was not used, as this requires the guild to be boost level 2 or higher." + }, + "updated": { + "humanName": "Role updated", + "description": "This messages gets send when a booster sucessfully updates their custom role", + "default": "Your role was updated successfully." + }, + "updatedNoIcon": { + "humanName": "Role updated without icon", + "description": "This messages gets send when a booster sucessfully updates their custom role, but the guild has not enough boosts to use role icons", + "default": "Your role was updated successfully, but your role icon was not used, as this requires the guild to be boost level 2 or higher." + }, + "removed": { + "humanName": "Role removed", + "description": "This messages gets send when a booster deleted their custom role", + "default": "Your role was removed successfully." + }, + "roleLimit": { + "humanName": "Role-limit reached", + "description": "This messages gets send when a booster-role couldn't be created", + "default": "Your role couldn't be created. This could be, because this server has reached the maximum of roles set by Discord. Ask the staff to delete an unnecessary role to make space for your role or try again later." + }, + "cooldown": { + "humanName": "Cooldown", + "description": "This messages gets send when a booster-role couldn't be edited, since the user is on cooldown", + "default": "Your role couldn't be edited, since you have to wait until %cooldown% for the cooldown to expire.", + "params": { + "cooldown": { + "description": "Timestamp the cooldown expires at" + } + } + }, + "invalidColor": { + "humanName": "Invalid Color", + "description": "This messages gets send when the user provides a wrong color code", + "default": "The color you provided is not a valid HEX-Code." + } + } + } + }, + "connect-four": { + "_module": { + "humanReadableName": "Connect Four", + "description": "Let your users play Connect Four against each other!" + } + }, + "counter": { + "_module": { + "humanReadableName": "Count-Game", + "description": "Allow your users to count together" + }, + "config": { + "description": "Configure counting channels, rules and moderation settings here", + "humanName": "Configuration", + "content": { + "channels": { + "humanName": "Channels", + "description": "Channels in which users can participate in the counting game" + }, + "channelDescription": { + "humanName": "Channel-Description", + "description": "Text which should be set after someone counted (leave blank to disable)", + "default": "Next number %x%", + "params": { + "x": { + "description": "Next number users should count" + } + } + }, + "success-reaction": { + "humanName": "Success-Reaction", + "description": "Reaction which the bot should give when someone counts successfully", + "default": "✅" + }, + "restartOnWrongCount": { + "humanName": "Restart game, if user miscounts", + "description": "If enabled, the game will restarts if a user sends a number that is not in order" + }, + "restartOnWrongCountMessage": { + "humanName": "Message when game gets restarted", + "description": "This message will be sent when the game gets restarted due to a miscount.", + "default": "Due to the incompetence of %mention%, the game had to restart - the next number is **%i%**.", + "params": { + "mention": { + "description": "Mention of the users" + }, + "i": { + "description": "Next number" + } + } + }, + "onlyOneMessagePerUser": { + "humanName": "Only one continuous message per user", + "description": "If enabled, users can not count more than one number continuously" + }, + "protectAgainstDeletion": { + "humanName": "Protect against users deleting the last counting message?", + "description": "If enabled, the bot will send a message when the last correct counting message gets deleted so that other counters can't be fooled into counting an already counted number again." + }, + "protectionMessage": { + "humanName": "Deletion protection message", + "description": "Message that gets send if a user deletes the last correct counting message.", + "default": "It seems like %mention% deleted their last message - the last counted number is **%number%**.", + "params": { + "mention": { + "description": "Mention of the user who's message got removed" + }, + "number": { + "description": "Last counted number in this the channel" + } + } + }, + "removeReactions": { + "humanName": "Remove reactions after 5 seconds?", + "description": "If enabled, the reactions the bot gives will be removed after 5 seconds. This will free up space in the counting channel" + }, + "wrong-input-message": { + "humanName": "Message on wrong input", + "description": "Message that gets send if a user provides an invalid input", + "default": "⚠️ %err%", + "params": { + "err": { + "description": "Description of what they did wrong" + } + } + }, + "strikeAmount": { + "humanName": "Amount of wrong messages to trigger action", + "description": "This is the amount of wrong messages a user has to send to trigger action. Once this amount is reached, the bot will either, depending on your configuration, give a role or disable the SEND_MESSAGES permission for a user (set to 0 to disable)" + }, + "giveRoleInsteadOfPermissionRemoval": { + "humanName": "Give role on action, instead of removing permission", + "description": "If enabled, a role will be given to the user (once their reach the configured action amount of wrong messages) instead of the removal of the \"Send Messages\"-permission in the counter channel" + }, + "strikeRole": { + "humanName": "Role given when amount is being reached", + "description": "This role will be given to users when they reach the configured amount of wrong messages" + }, + "strikeMessage": { + "humanName": "Message when user gets actioned", + "description": "This message will be sent when a user reach the configured amount of wrong messages and gets actioned", + "default": "%mention%, I had to restrict your access to this channel because you repeatedly used it improperly.", + "params": { + "mention": { + "description": "Mention of the users" + } + } + }, + "allowCharactersInMessage": { + "humanName": "Allow text characters in messages?", + "description": "If enabled, users may write additional content into their messages instead of forcing them to just write a number. Messages without a number will still lead to an error." + }, + "allowMaths": { + "humanName": "Allow users to use maths in their messages?", + "description": "If enabled, users can use maths in messages, as long as the result of their formula is the correct next number." + }, + "enableEasterEggs": { + "humanName": "Enable number easter eggs?", + "description": "If enabled, the bot will react with special emojis on certain numbers (e.g. 42, 67, 69, 100, 420)" + } + } + }, + "milestones": { + "description": "Reward your users, when they reach certain goals", + "humanName": "Milestones", + "configElementName": { + "one": "Milestone", + "more": "Milestones" + }, + "content": { + "userMessageCount": { + "humanName": "Message count", + "description": "Count of valid counter-messages the users has to achieve this goal" + }, + "giveRoles": { + "humanName": "Roles", + "description": "These roles are given to the user if they achieve this goal (optional)" + }, + "sendMessage": { + "humanName": "Message", + "description": "This message gets send when they achieve this goal", + "default": "Congrats %mention% for counting %milestone% times!", + "params": { + "mention": { + "description": "Mention the user who achieved the milestone" + }, + "milestone": { + "description": "The milestone (the number of message) that was reached" + } + } + } + } + } + }, + "duel": { + "_module": { + "humanReadableName": "Duel", + "description": "Let users play the game \"Duel\" on your discord" + } + }, + "economy-system": { + "_module": { + "humanReadableName": "Economy", + "description": "A simple economy-system, containing a shop system, message-drops and commands to earn money" + }, + "config": { + "description": "Configure here, how the module should behave", + "humanName": "Configuration", + "content": { + "admins": { + "humanName": "Administrators", + "description": "Users who can perform admin only actions e.g. manage the balance of users (Bot Operators always have this permission)" + }, + "allowCheats": { + "humanName": "Allow Cheats", + "description": "Allow admins to edit the balance of users (for a fair system not recommended!)" + }, + "selfBalance": { + "humanName": "Allow Self-Balance Editing", + "description": "Allow admins to edit their own balance (for a fair system not recommended! DON'T DO THIS!!!!!)" + }, + "shopManagers": { + "humanName": "shop-managers", + "description": "The Ids of the shop managers (Bot Operators have this permission always)" + }, + "startMoney": { + "humanName": "Start Money", + "description": "The amount of money that is given to a new user" + }, + "currencyName": { + "humanName": "currency name", + "description": "The name of the currency", + "default": "" + }, + "currencySymbol": { + "humanName": "Symbol of the currency", + "description": "The symbol of the currency", + "default": "💰" + }, + "maxWorkMoney": { + "humanName": "max work money", + "description": "The highest amount of money you can get for working" + }, + "minWorkMoney": { + "humanName": "min work money", + "description": "The lowest amount of money you can get for working" + }, + "workCooldown": { + "humanName": "work cooldown", + "description": "The amount of time a user needs to wait util they can use the work command again (in minutes)" + }, + "maxCrimeMoney": { + "humanName": "max crime money", + "description": "The highest amount of money you can get for crime" + }, + "minCrimeMoney": { + "humanName": "min crime money", + "description": "The lowest amount of money you can get for crime" + }, + "crimeCooldown": { + "humanName": "crime cooldown", + "description": "The amount of time a user needs to wait util they can use the crime command again (in minutes)" + }, + "maxRobAmount": { + "humanName": "max rob amount", + "description": "The highest amount of money that a user can rob" + }, + "robPercent": { + "humanName": "rob percent", + "description": "The amount that can get robed in percent" + }, + "robCooldown": { + "humanName": "rob cooldown", + "description": "The amount of time a user needs to wait util they can use the rob command again (in minutes)" + }, + "leaderboardChannel": { + "humanName": "leaderboard-channel", + "description": "The channel for the leaderboard. On this leaderboard everyone can see who has the most money." + }, + "shopChannel": { + "humanName": "shop channel", + "description": "The id of the channel for the shop-Message. This message shows the items of the shop" + }, + "msgDropsIgnoredChannels": { + "humanName": "message-drops ignored channels", + "description": "List of Channels where Users can't get message-drops" + }, + "messageDrops": { + "humanName": "Message Drop Chance", + "description": "Chance to get money for a message (Chance: 1/ This value). Set to 0 to disable message drops" + }, + "messageDropsMax": { + "humanName": "Max Message Drop Amount", + "description": "The max amount of money in a message Drop" + }, + "messageDropsMin": { + "humanName": "Min Message Drop Amount", + "description": "The min amount of money in a message Drop" + }, + "dailyReward": { + "humanName": "Daily Reward Amount", + "description": "The daily reward" + }, + "weeklyReward": { + "humanName": "Weekly Reward Amount", + "description": "The weekly reward" + }, + "publicCommandReplies": { + "humanName": "Public Command-Replies", + "description": "Should the Command-replies be displayed for everyone?" + } + } + }, + "strings": { + "description": "Configure messages of this module here", + "humanName": "Messages", + "content": { + "notFound": { + "humanName": "not found message", + "description": "The message that is send if the item wasn't found", + "default": "This item could not be found" + }, + "notEnoughMoney": { + "humanName": "not enough money", + "description": "The message that is send if the user haven't enough money to buy an item", + "default": "You haven't enough money to buy this Item" + }, + "shopMsg": { + "humanName": "shop message", + "description": "Message for the shop. The Items gets added at the end", + "default": { + "title": "Shop", + "description": "%shopItems%" + }, + "params": { + "shopItems": { + "description": "All items of the shop (format specified below)" + } + } + }, + "itemString": { + "humanName": "item string", + "description": "String for the items for the shop message", + "default": "**%id%** %itemName%, **price**: %price%, **sellcount**: %sellcount%", + "params": { + "id": { + "description": "Id of the item" + }, + "itemName": { + "description": "Name of the item" + }, + "price": { + "description": "Price of the item" + }, + "sellcount": { + "description": "Count of the sales of the item" + } + } + }, + "cooldown": { + "humanName": "cooldown", + "description": "This message gets send when a user is currently in cooldown", + "default": "Please wait before using this command again" + }, + "workSuccess": { + "humanName": "Work Success Messages", + "description": "Array of messages from which one random gets send when a user works successfully", + "params": { + "earned": { + "description": "Money that the user had earned" + } + } + }, + "crimeSuccess": { + "humanName": "Crime Success Messages", + "description": "Array of messages from which one random gets send when a user commits a crime successfully", + "params": { + "earned": { + "description": "Money that the user had earned" + } + } + }, + "crimeFail": { + "humanName": "Crime Fail Messages", + "description": "Array of messages from which one random gets send when a user fails to do some crime", + "params": { + "loose": { + "description": "Money that the user looses" + } + } + }, + "robSuccess": { + "humanName": "Rob Success Message", + "description": "This message gets send when a user robs another user successfully", + "default": "You robed %user% earned **%earned%**", + "params": { + "earned": { + "description": "Money that the user had earned" + }, + "user": { + "description": "The user that gets robed by you" + } + } + }, + "leaderboardEmbed": { + "humanName": "Leaderboard Embed", + "description": "Configure the leaderboard embed here" + }, + "dailyReward": { + "humanName": "Daily Reward Message", + "description": "Message that gets send after the user has claimed the daily reward", + "default": "You earned **%earned%** by collecting your daily reward", + "params": { + "earned": { + "description": "Money that the user had earned" + } + } + }, + "weeklyReward": { + "humanName": "Weekly Reward Message", + "description": "Message that gets send after the user has claimed the weekly reward", + "default": "You earned **%earned%** by collecting your weekly reward", + "params": { + "earned": { + "description": "Money that the user had earned" + } + } + }, + "balanceReply": { + "humanName": "Balance Reply", + "description": "Reply for the balance command", + "default": { + "title": "Balance of %user%", + "fields": [ + { + "name": "Balance:", + "value": "%balance%" + }, + { + "name": "Bank:", + "value": "%bank%" + }, + { + "name": "Total:", + "value": "%total%" + } + ] + }, + "params": { + "balance": { + "description": "Current balance of the user" + }, + "bank": { + "description": "Current value that the user has on the bank" + }, + "total": { + "description": "Total balance of the user" + }, + "user": { + "description": "Username and discriminator of the User" + } + } + }, + "userNotFound": { + "humanName": "User Not Found", + "description": "The message that gets sent when the bot can't find a user", + "default": "I can't find the user **%user%**", + "params": { + "user": { + "description": "User that can't been found" + } + } + }, + "buyMsg": { + "humanName": "Purchase Message", + "description": "Message that gets send when a user buys something in the shop", + "default": "You got the item **%item%**", + "params": { + "item": { + "description": "Name of the item" + } + } + }, + "itemCreate": { + "humanName": "Item Created Message", + "description": "Message that gets send when a new shop item gets created", + "default": "Successfully created the item %name% with the id %id%. It costs %price% and you get the role %role%", + "params": { + "name": { + "description": "Name of the created item" + }, + "id": { + "description": "Id of the created item" + }, + "price": { + "description": "Price of the created item" + }, + "role": { + "description": "Role that everyone gets who buys the item" + } + } + }, + "itemDelete": { + "humanName": "Item Deleted Message", + "description": "Message that gets send when a new shop item gets deleted", + "default": "Successfully deleted the item %name%.", + "params": { + "name": { + "description": "Name of the deleted item" + }, + "id": { + "description": "Id of the deleted item" + } + } + }, + "itemEdit": { + "humanName": "Item Edited Message", + "description": "Message that gets sent when a shop item gets edited", + "default": "Successfully edited the item %name%. Check it out using `/shop list`", + "params": { + "name": { + "description": "Name of the edited item" + }, + "id": { + "description": "Id of the edited item" + } + } + }, + "depositMsg": { + "humanName": "deposit message", + "description": "The reply when a user deposits money to the bank", + "default": "Successfully deposited **%amount%** to your bank", + "params": { + "amount": { + "description": "Amount deposited" + } + } + }, + "withdrawMsg": { + "humanName": "withdraw message", + "description": "The reply when a user withdraws money from the bank", + "default": "Successfully withdrew **%amount%** from your bank", + "params": { + "amount": { + "description": "Amount withdrawn" + } + } + }, + "msgDropMsg": { + "humanName": "message drop message", + "description": "The message that gets sent on a message-drop", + "params": { + "earned": { + "description": "Money earned from the drop" + } + } + }, + "NaN": { + "humanName": "not a number", + "description": "Message that gets send if the bot needs a number but gets something different", + "default": "**%input%** isn't a number", + "params": { + "input": { + "description": "The invalid input" + } + } + }, + "msgDropAlreadyEnabled": { + "humanName": "message-drop already enabled", + "description": "Message that gets send if a User trys to enable the Message-Drop message, but it's already enabled", + "default": "The Mesage-Drop message is already enabled!" + }, + "msgDropEnabled": { + "humanName": "message-drop enabled", + "description": "Message that gets send when a User enables the Message-Drop message", + "default": "Successfully enabled the Message-Drop message" + }, + "msgDropAlreadyDisabled": { + "humanName": "message-drop already disabled", + "description": "Message that gets send if a User trys to disable the Message-Drop message, but it's already disabled", + "default": "The Mesage-Drop message is already disabled!" + }, + "msgDropDisabled": { + "humanName": "message-drop disabled", + "description": "Message that gets send when a User disables the Message-Drop message", + "default": "Successfully disabled the Message-Drop message" + }, + "rebuyItem": { + "humanName": "rebuy message", + "description": "The message that is send when the user trys to buy an Item that he already own", + "default": "You already own this Item" + }, + "multipleMatches": { + "humanName": "multiple matches", + "description": "The message that gets send when multiple items match the query", + "default": "Multiple items match the query" + }, + "noMatches": { + "humanName": "no matches", + "description": "The message that gets send when the item can't be found", + "default": "The item with the id %id%/ the name %name% doesn't exists", + "params": { + "id": { + "description": "The specified ID" + }, + "name": { + "description": "The specified name" + } + } + }, + "itemDuplicate": { + "humanName": "item duplicate", + "description": "The message that gets send when an item with the specified id or name already exists", + "default": "There's already an item with the id %id% or the name %name%", + "params": { + "id": { + "description": "The specified ID" + }, + "name": { + "description": "The specified name" + } + } + } + } + } + }, + "fun": { + "_module": { + "humanReadableName": "Fun-Commands", + "description": "Some random fun commands like /hug or /random" + }, + "config": { + "description": "Customize the messages and images for fun commands here", + "humanName": "Configuration", + "content": { + "ikeaMessage": { + "humanName": "IKEA Message", + "description": "Message that gets send when someone uses /random ikea-name", + "default": "Here's a ikea-product-name: %name%", + "params": { + "name": { + "description": "Randomly generated name of an ikea product (probably not real)" + } + } + }, + "randomNumberMessage": { + "humanName": "Random numer message", + "description": "Message that gets send when someone uses /random number", + "default": "Here your random number between %min% and %max%: %number%", + "params": { + "min": { + "description": "Minimal value" + }, + "max": { + "description": "Maximal value" + }, + "number": { + "description": "Generated number" + } + } + }, + "diceRollMessage": { + "humanName": "Dice Roll message", + "description": "Message that gets send when someone uses /random dice", + "default": "🎲 %number%", + "params": { + "number": { + "description": "Generated number" + } + } + }, + "coinFlipMessage": { + "humanName": "Coin toss message", + "description": "Message that gets send when someone uses /random coinfilp", + "default": "🪙 %site%", + "params": { + "site": { + "description": "Site on which the coin landed" + } + } + }, + "hugMessage": { + "humanName": "Hug message", + "description": "Message that gets send when someone uses /hug", + "default": "<@%authorID%> hugs <@%userID%>", + "params": { + "authorID": { + "description": "ID of the user who ran this command" + }, + "userID": { + "description": "ID of the user that gets hugged" + } + } + }, + "hugImages": { + "humanName": "Hug images", + "description": "Images that one will be randomly selected from when someone uses /hug." + }, + "kissMessage": { + "humanName": "Kiss message", + "description": "Message that gets send when someone uses /kiss", + "default": "<@%authorID%> kissed <@%userID%>", + "params": { + "authorID": { + "description": "ID of the user who ran this command" + }, + "userID": { + "description": "ID of the user that gets kissed" + } + } + }, + "kissImages": { + "humanName": "Kiss images", + "description": "Images that one will be randomly selected from when someone uses /kiss." + }, + "slapMessage": { + "humanName": "Slap message", + "description": "Message that gets send when someone uses /slap", + "default": "<@%authorID%> slapped <@%userID%>", + "params": { + "authorID": { + "description": "ID of the user who ran this command" + }, + "userID": { + "description": "ID of the user that gets slapped" + } + } + }, + "slapImages": { + "humanName": "Slap images", + "description": "Images that one will be randomly selected from when someone uses /slap." + }, + "patMessage": { + "humanName": "Pat message", + "description": "Message that gets send when someone uses /pat", + "default": "<@%authorID%> patted <@%userID%>", + "params": { + "authorID": { + "description": "ID of the user who ran this command" + }, + "userID": { + "description": "ID of the user that gets patted" + } + } + }, + "patImages": { + "humanName": "Pat images", + "description": "Images that one will be randomly selected from when someone uses /pat." + }, + "8ballMessage": { + "humanName": "8ball Message", + "description": "Message that gets send when someone uses /random 8ball", + "default": "The oracle has spoken... %answer%", + "params": { + "answer": { + "description": "Answer to the question" + } + } + }, + "8BallMessages": { + "humanName": "8ball responses", + "description": "Possible answers for /random 8ball" + } + } + } + }, + "guess-the-number": { + "_module": { + "humanReadableName": "Guess the number", + "description": "Select a number and let your users guess" + }, + "config": { + "description": "Adjust messages and permissions here", + "humanName": "Configuration", + "commandsWarnings": { + "/guess-the-number": { + "info": "You need to first set the permissions in your server settings for this command and after that add them under \"adminRoles\" here." + } + }, + "content": { + "adminRoles": { + "humanName": "Admin-Roles", + "description": "Every role that can manage game sessions." + }, + "startMessage": { + "humanName": "Start-Message", + "description": "Message that gets send when a new round gets started", + "default": { + "title": "Guess the Number - Game started", + "description": "Guess a number between %min% and %max%. Good luck!" + }, + "params": { + "min": { + "description": "Minimal value to guess" + }, + "max": { + "description": "Maximal value to guess" + } + } + }, + "endMessage": { + "humanName": "End-Message", + "description": "Message that gets send when a round ends", + "default": { + "title": "Guess the Number - Game ended", + "description": "Good game everyone!\nThe winner is %winner%.\nThe number was **%number%**.\nThere were around **%guessCount% guesses** in total." + }, + "params": { + "min": { + "description": "Minimal value to guess" + }, + "max": { + "description": "Maximal value to guess" + }, + "winner": { + "description": "@-mention of the winner" + }, + "guessCount": { + "description": "Count of guesses in this game session" + }, + "number": { + "description": "Winning number" + } + } + }, + "higherLowerReactions": { + "humanName": "React with Lower / Higher reactions", + "description": "If enabled, the bot will react with ⬇ (if the guess is higher than the correct number) or with ⬆ (if the guess is lower than the correct number) on wrong guesses. If disabled, the bot will just react with ❌ on wrong guesses." + }, + "enableLeaderboard": { + "humanName": "Enable leaderboard?", + "description": "If enabled, a leaderboard button is shown on new game messages and user statistics (wins, guesses) are tracked." + } + } + }, + "channel": { + "description": "Enable the Gamechannel mode to automatically re-start games", + "humanName": "Gamechannel Mode", + "content": { + "enabled": { + "humanName": "Enable Gamechannel mode?", + "description": "If enabled, you can configure a game channel, in which a new guess the number game will be started if a number got guessed correctly. You still will be able to manually start games in other channels. Everyone, including admins, can guess in game channels." + }, + "channel": { + "humanName": "Gamechannel", + "description": "In this channel, games will be automatically started if a game ends or no game is currently running" + }, + "minInt": { + "humanName": "Minimum number", + "description": "A number between this and the highest number will be selected at random when a game starts." + }, + "maxInt": { + "humanName": "Highest number", + "description": "A number between this and the minimum number will be selected at random when a game starts." + } + } + } + }, + "info-commands": { + "_module": { + "humanReadableName": "Info-Commands", + "description": "Adds info-commands with information about specific parts of your server" + }, + "strings": { + "description": "Edit the messages and strings of the module here", + "humanName": "Messages", + "content": { + "serverinfo": { + "humanName": "Server Info", + "description": "You can change the parts of the serverinfo-command here" + }, + "userinfo": { + "humanName": "User Info", + "description": "You can change the parts of the userinfo-command here" + }, + "channelInfo": { + "humanName": "Channel Info", + "description": "You can change the parts of the channelinfo-command here" + }, + "roleInfo": { + "humanName": "Role Info", + "description": "You can change the parts of the roleinfo-command here" + }, + "user_not_found": { + "humanName": "User Not Found", + "description": "Message that gets send if the user provided an invalid userid", + "default": "I could not find this user - try using an ID or a mention" + }, + "channel_not_found": { + "humanName": "Channel Not Found", + "description": "Message that gets send if the user provided an invalid userid", + "default": "I could not find this channel - try using an ID or a mention" + }, + "role_not_found": { + "humanName": "Role Not Found", + "description": "Message that gets send if the user provided an invalid roleid", + "default": "I could not find this role - try using an ID or a mention" + }, + "avatarMsg": { + "humanName": "Avatar Message", + "description": "Message that gets send if the user requested an avatar", + "default": "Here is the avatar: (Please reminder that the image may be protected under copyright-law)", + "params": { + "avatarUrl": { + "description": "URL to the avatar" + }, + "tag": { + "description": "Tag of the requested user" + } + } + } + } + } + }, + "levels": { + "_module": { + "humanReadableName": "Level-System", + "description": "Easy to use levelsystem with a lot of customization!" + }, + "config": { + "description": "Configure the function of the module here", + "humanName": "Configuration", + "categories": { + "general": { + "displayName": "General Settings" + }, + "xp": { + "displayName": "XP Settings" + }, + "leaderboard": { + "displayName": "Leaderboard" + }, + "roles": { + "displayName": "Level Roles" + }, + "messages": { + "displayName": "Level-up Messages" + } + }, + "content": { + "min-xp": { + "humanName": "XP given at least for messages", + "description": "How much XP the user gets at least for each message" + }, + "max-xp": { + "humanName": "XP given at most for messages", + "description": "How much XP the user gets at most for each messages" + }, + "voiceXPPerMinute": { + "humanName": "XP given per Voice Minute", + "description": "How many XP will be given to users per minute when they are in a voice channel with other members. No XP will be given if they are alone in their channel or are muted or deafened. Numbers will be rounded and XP will be given every 15 minutes or when the user leaves the channel." + }, + "cooldown": { + "humanName": "Cooldown", + "description": "In ms. How much cooldown there is between each XP getting" + }, + "curveType": { + "humanName": "Type of the leveling curve", + "description": "Type of the leveling curve. The exponential curve is recommended, as archiving new levels gets harder the higher your level is. Leveling is always the same if you use the linear curve.", + "selectOptions": { + "EXPONENTIAL": { + "displayName": "Easy Linear" + }, + "LINEAR": { + "displayName": "Default Linear" + }, + "EXPONENTIATION": { + "displayName": "Exponentiation (softer start, harder leveling after level 14)" + }, + "CUSTOM": { + "displayName": "Custom formula (dangerous!)" + } + }, + "links": { + "https://scootk.it/level-calculator": { + "label": "Calculate how much XP is needed to level up" + } + } + }, + "customLevelCurve": { + "humanName": "Custom Level Formula (if enabled)", + "description": "Your custom leveling formula. Use the x variable (and no other variables). The result of the formula should be the required XP to reach level x (your variable). Example: \"x*750+((x-1)*500)\" (our default level curve)", + "default": "", + "links": { + "https://scootk.it/level-calculator": { + "label": "Calculate how much XP is needed to level up" + } + } + }, + "levelUpMessagesConditions": { + "humanName": "Which Level-Up-Messages should get sent?", + "description": "This settings changes in which cases a level up message should be sent. With the setting \"all\", level up messages will be sent at every level up. With the setting \"only-role-rewards\", level up messages will only be sent if the new level has a role reward. With the \"none\" setting, no level up messages will be sent." + }, + "level_up_channel_id": { + "humanName": "Level-Up-Channel", + "description": "Channel in which Level-Up-Messages should get send. (Leave empty to disable)" + }, + "sortLeaderboardBy": { + "humanName": "Leaderboard-Sort-Category", + "description": "How the leaderboard should be sorted" + }, + "blacklisted_channels": { + "humanName": "Blacklisted Channels", + "description": "Blacklisted-Channels in which users can not earn XP" + }, + "blacklistedRoles": { + "humanName": "Blacklisted roles", + "description": "These roles won't receive XP when writing messages" + }, + "reward_roles": { + "humanName": "Level Reward roles", + "description": "Level at which users should get roles. Parameter 1: Level, Parameter 2: Role-ID" + }, + "multiplication_roles": { + "humanName": "XP Multiplication Roles", + "description": "Allows you to configure roles that have a higher multiplication factor than normal (default value is 1). If a user has more than one of the configured roles, the multiplication factors get multiplied together before multiplying the result with the amount of XP the user receives for their message." + }, + "multiplication_channels": { + "humanName": "XP Multiplication Channels", + "description": "Allows you to configure channels that have a higher multiplication factor than normal (default value is 1). Messages sent in these channels will have their XP value multiplied by the multiplier configured here." + }, + "onlyTopLevelRole": { + "humanName": "Only keep highest Level-Role", + "description": "If enabled, all previous level roles a user had will get removed, when they advance to a new level." + }, + "reset-on-leave": { + "humanName": "Rest Level on leave", + "description": "If enabled, all levels and the XP of a user will be deleted, when they leave your server." + }, + "randomMessages": { + "humanName": "Random messages", + "description": "If enabled the module will randomly select a messages from random-levelup-messages and ignore the one set in strings" + }, + "leaderboard-channel": { + "humanName": "Live Leaderboard-Channel", + "description": "If set, the bot will send a messages in this channel with the current leaderboard and edit it every five minutes" + }, + "leaderboard-channel-max-amount": { + "humanName": "Maximum amount of users displayed in live leaderboard Channel", + "description": "This is the maximum amount of users displayed in the Live Leaderboard channel. /leaderboard will still show the full leaderboard." + }, + "maximumLevelEnabled": { + "humanName": "Enable maximum level?", + "description": "If enabled, users can only level until they reach the configured maximum level. After that, they can't level up and can't earn XP. Can be enabled retroactively." + }, + "maximumLevel": { + "humanName": "Maximum level", + "description": "Once a user reaches this level, they neither earn more XP nor level up anymore." + }, + "startFromZero": { + "humanName": "Start with Level 0?", + "description": "If enabled, the initial level of users will be displayed as zero. This doesn't affect leveling, this is a cosmetic setting and can be applied retroactively." + }, + "useTags": { + "humanName": "Use User's Tags instead of their Mention in the Leaderboard-Channel-Embed", + "description": "If enabled, the bot will use the tag of users in the Leaderboard-Channel-Embed instead of their mention." + }, + "allowCheats": { + "humanName": "Cheats", + "description": "If enabled admins can change the XP of other users (not recommended (please leave it off if you want to have a fair levelling system!!!))" + } + } + }, + "strings": { + "description": "Edit the messages and strings of the module here", + "humanName": "Messages", + "categories": { + "leaderboard": { + "displayName": "Leaderboard Messages" + }, + "general": { + "displayName": "General Messages" + } + }, + "content": { + "user_not_found": { + "humanName": "User not found", + "description": "This messages gets send if someone checks a profile of a user when the user never send a message", + "default": "⚠️ We do not have any records of this user" + }, + "embed": { + "humanName": "Profile Embed", + "description": "Embed which gets send if !profile gets executed" + }, + "leaderboardEmbed": { + "humanName": "Leaderboard Embed", + "description": "This embed gets send if !leaderboard (!lb) gets executed" + }, + "level_up_message": { + "humanName": "Level Up Message", + "description": "This messages gets send if a user levels up (gets overwritten if randomMessages is enabled)", + "default": "Level Up! Your new level is **%newLevel%**!", + "params": { + "mention": { + "description": "Mention of the user" + }, + "avatarURL": { + "description": "Avatar of the user" + }, + "username": { + "description": "Username of the user" + }, + "tag": { + "description": "Tag of the user" + }, + "newLevel": { + "description": "New level of the user" + } + } + }, + "level_up_message_with_reward": { + "humanName": "Level Up Message with Reward", + "description": "This messages gets send if a user levels up and gets a role (gets overwritten if randomMessages is enabled)", + "default": "Level Up! Your new level is **%newLevel%**! You received %role%.", + "params": { + "mention": { + "description": "Mention of the user" + }, + "avatarURL": { + "description": "Avatar of the user" + }, + "username": { + "description": "Username of the user" + }, + "tag": { + "description": "Tag of the user" + }, + "newLevel": { + "description": "New level of the user" + }, + "role": { + "description": "Mention of the role (No ping)" + } + } + }, + "liveLeaderBoardEmbed": { + "humanName": "Live Leaderboard", + "description": "Embed which gets send to the leaderboard-channel and gets updated" + }, + "leaderboard-button-answer": { + "humanName": "Leaderboard Button Response", + "description": "This messages gets send if a user clicks on the button below the live-leaderboard", + "default": "Hi, %name%, you are currently on **level %level%** with **%userXP%**/%nextLevelXP% **XP**. Learn more with `/profile`.", + "params": { + "name": { + "description": "Username of the user" + }, + "level": { + "description": "Level of the user" + }, + "userXP": { + "description": "XP of the user" + }, + "nextLevelXP": { + "description": "XP of the next level" + } + } + } + } + }, + "random-levelup-messages": { + "description": "If enabled, the bot will randomly select a message from here", + "humanName": "Random-Level-Up-Messages", + "content": { + "type": { + "humanName": "Message Type", + "description": "Type of this message" + }, + "message": { + "humanName": "Messages", + "description": "Messages which should be send", + "default": "", + "params": { + "mention": { + "description": "Mention of the user" + }, + "avatarURL": { + "description": "Avatar of the user" + }, + "username": { + "description": "Username of the user" + }, + "tag": { + "description": "Tag of the user" + }, + "newLevel": { + "description": "New level of the user" + }, + "role": { + "description": "Mention of the role (No ping, only if type = with-reward)" + } + } + } + } + }, + "special-levelup-messages": { + "description": "If enabled, the bot will randomly select a message from here", + "humanName": "Selected messages", + "content": { + "level": { + "humanName": "Level", + "description": "Level at which this messages should get send" + }, + "message": { + "humanName": "Message", + "description": "Messages which should be send", + "default": "", + "params": { + "mention": { + "description": "Mention of the user" + }, + "avatarURL": { + "description": "Avatar of the user" + }, + "username": { + "description": "Username of the user" + }, + "tag": { + "description": "Tag of the user" + }, + "newLevel": { + "description": "New level of the user" + }, + "role": { + "description": "Mention of the role (No ping, only if level has reward)" + } + } + } + } + } + }, + "massrole": { + "_module": { + "humanReadableName": "Massrole", + "description": "Simple module to manage the roles of many members at once!" + }, + "config": { + "description": "Configure the function of the module here", + "humanName": "Configuration", + "commandsWarnings": { + "/massrole": { + "info": "You need to first set the permissions in your server settings for this command and after that add them under \"adminRoles\" here." + } + }, + "content": { + "adminRoles": { + "humanName": "Admin Roles", + "description": "Every role that can use the massrole command" + } + } + }, + "strings": { + "description": "Edit the messages and strings of the module here", + "humanName": "Messages", + "content": { + "done": { + "humanName": "Action executed", + "description": "This messages gets send when a action was executed successfully", + "default": "The action was executed successfully." + }, + "notDone": { + "humanName": "Action not executed", + "description": "This messages gets send when a action was not executed successfully", + "default": "The Action couldn't be executed because the bot has not enough permissions." + } + } + } + }, + "moderation": { + "_module": { + "humanReadableName": "Moderation & Security", + "description": "Advanced security- and moderation-system with tons of features" + }, + "config": { + "description": "You can set up permissions and features of this module here", + "humanName": "Configuration", + "commandsWarnings": { + "/moderate": { + "info": "Each moderator needs to be able to execute the /moderate command, so set your permissions in your server-settings accordingly. Additionally, moderator need to be entered into their level below." + } + }, + "categories": { + "general": { + "displayName": "General Settings" + }, + "roles": { + "displayName": "Roles & Permissions" + }, + "reports": { + "displayName": "Reports" + }, + "automod": { + "displayName": "Auto-Moderation" + }, + "actions": { + "displayName": "Actions & Punishments" + }, + "nicknames": { + "displayName": "Nickname Management" + } + }, + "content": { + "logchannel-id": { + "humanName": "Log-Channel", + "description": "Moderative actions will get logged in this channel" + }, + "quarantine-role-id": { + "humanName": "Quarantine-Role", + "description": "When a user gets quarantined, all of their roles will get removed and this quarantine-role wil get assigned" + }, + "report-channel-id": { + "humanName": "Report-Channel", + "description": "Channel in which user-reports should get send. (optional, default: Log-Channel)" + }, + "remove-all-roles-on-quarantine": { + "humanName": "Remove all roles on quarantine", + "description": "If enabled all roles from a user get removed if they get quarantined (they get saved an can be restored with /unquarantine)" + }, + "moderator-roles_level1": { + "humanName": "Moderator-Level 1", + "description": "Moderator roles that can perform the following actions: Warn" + }, + "moderator-roles_level2": { + "humanName": "Moderator-Level 2", + "description": "Moderator roles that can perform the following actions: Warn, Mute, Unmute, Lock, Unlock, Channelmute, Remove-Channel-Mute" + }, + "moderator-roles_level3": { + "humanName": "Moderator-Level 3", + "description": "Moderator roles that can perform the following actions: Warn, Mute, Unmute, Kick, Clear" + }, + "moderator-roles_level4": { + "humanName": "Moderator-Level 4", + "description": "Moderator roles that can perform the following actions: Warn, Mute, Unmute, Kick, Clear, Ban, Unban" + }, + "roles-to-ping-on-report": { + "humanName": "Roles to ping on reports", + "description": "Roles that should get pinged in the log-channel when a user reports someone" + }, + "require_reason": { + "humanName": "Force moderators to set a reason", + "description": "Should moderators be required to set a reason?" + }, + "require_proof": { + "humanName": "Force moderators to upload proof", + "description": "Should moderators be required to upload proof for their actions?" + }, + "action_on_invite": { + "humanName": "Action on invite", + "description": "What should the bot do if someone posts an invite link?" + }, + "allowed_invite_guild_ids": { + "humanName": "Allowed invite guild IDs", + "description": "Guild IDs whose invites should be allowed (in addition to this server's invites which are always allowed)." + }, + "action_on_scam_link": { + "humanName": "Action on Scam-Link", + "description": "What should the bot do if someone posts an suspicious or confirmed scam link?" + }, + "scam_link_level": { + "humanName": "Level of Scam-Link-Detection", + "description": "Select the Level of Scam-Link-Filter. \"confirmed\" only contains verified Scam-Domains, while \"suspicious\" may contain not-harmful domains." + }, + "whitelisted_channels_for_invite_blocking": { + "humanName": "Whitelisted channels for invite-ban", + "description": "Channels or categories where invite blocking is disabled" + }, + "whitelisted_roles_for_invite_blocking": { + "humanName": "Whitelisted roles for invite-ban", + "description": "ID of Roles which are allowed to bypass invite blocking" + }, + "blacklisted_words": { + "humanName": "Blacklisted words", + "description": "Words that are blacklisted" + }, + "action_on_posting_blacklisted_word": { + "humanName": "Action on blacklisted Word", + "description": "What should the bot do if someone posts a blacklisted word?" + }, + "defaultMuteDuration": { + "humanName": "Default Mute-Duration", + "description": "Default mute duration when none was configured. Will also be used for automod features (e.g. when someone posts a blacklisted word). Maximum value of 28 days.", + "default": "14d" + }, + "changeNicknames": { + "humanName": "Change nicknames on Mute- / Quarantine", + "description": "If enabled, the user will get renamed when they get muted or quarantined" + }, + "changeNicknameOnMute": { + "humanName": "New nickname on mute", + "description": "The nickname in which the user should be renamed when they get muted", + "default": "%nickname%", + "params": { + "nickname": { + "description": "Original nickname of the user" + } + } + }, + "changeNicknameOnQuarantine": { + "humanName": "Nickname during quarantine", + "description": "The nickname in which the user should be renamed when they get quarantined", + "default": "%nickname%", + "params": { + "nickname": { + "description": "Original nickname of the user" + } + } + }, + "automod": { + "humanName": "Automod", + "description": "You can define here what should happen (options: mute, kick, ban, quarantine) when someone gets x warns. Specify duration by writing : after the action." + }, + "warnsExpire": { + "humanName": "Should warns be deleted automatically?", + "description": "If enabled, warns will be deleted automatically after a certain period of time. Warns expired this way will completely disappear and can not be viewed again after they expired." + }, + "warnExpiration": { + "humanName": "Time after which warns will be automatically removed", + "description": "Warns will be automatically deleted after this value after it's creation. Please note that this action will delete existing warns if they expired. Enter an english value, such as \"1y\" (= 1 year), \"3 Months\" (= 3 Months) oder \"2w\" (= 2 Weeks).", + "default": "3 months" + } + } + }, + "joinGate": { + "description": "This system can prevent suspicious accounts from getting access to your server", + "humanName": "Join-Gate-Configuration", + "categories": { + "general": { + "displayName": "General Settings" + }, + "roles": { + "displayName": "Roles" + } + }, + "content": { + "enabled": { + "humanName": "Enabled?", + "description": "Enable or disable the join gate" + }, + "allUsers": { + "humanName": "Filter all users", + "description": "If enabled all users action against all new users will be taken" + }, + "action": { + "humanName": "Action", + "description": "Select the action here that should get performed if the join gate gets triggered" + }, + "roleID": { + "humanName": "Role", + "description": "Only if action = give-role. Role that gets given to users who fail the join gate" + }, + "removeOtherRoles": { + "humanName": "Remove other roles", + "description": "Only if action = give-role. If enabled other roles that have been give to the user get removed after a short interval (and the giving of the role from \"roleID\" will be delayed)" + }, + "minAccountAge": { + "humanName": "Minimum account age", + "description": "Age of the account of a new user that is required to be set to pass the join gate (in days)" + }, + "requireProfilePicture": { + "humanName": "Require profile picture", + "description": "If enabled users are required to have a profile picture set to pass the join gate" + }, + "ignoreBots": { + "humanName": "Ignore bots", + "description": "If enabled bots are allowed to pass the join gate without any restrictions" + } + } + }, + "strings": { + "description": "Set up which messages your bot should send", + "humanName": "Messages", + "categories": { + "actions": { + "displayName": "Action Messages" + }, + "errors": { + "displayName": "Error Messages" + } + }, + "content": { + "no_permissions": { + "humanName": "No Permissions", + "description": "Message that gets send if the user doesn't has the required role and/or has not the required mod-level", + "default": "You can not do that. You need at least moderator level %required_level% to do this", + "params": { + "required_level": { + "description": "Required mod-level to do this." + } + } + }, + "user_not_found": { + "humanName": "User Not Found", + "description": "Message that gets send if the user provided an invalid userid", + "default": "I could not find this user - try using an ID or a mention" + }, + "missing_reason": { + "humanName": "Missing Reason", + "description": "Message that gets send if the user does not provide a reason and 'require reason' is activated", + "default": "Please specify an reason" + }, + "this_is_a_mod": { + "humanName": "Target Is a Moderator", + "description": "Message that gets send if the user tries to mute another moderator", + "default": "You can not perform this action on your college." + }, + "submitted-report-message": { + "humanName": "Report Submitted", + "description": "Message that gets send, if someone reports somebody.", + "default": "Thanks for reporting %user%. I notified our server team and transmitted them an [encrypted snapshot](<%mURL%>) of the current messages in this channel, so they can see what really happened. Please make sure that our bots and staff can message you, so we can ask you follow-up-questions, if needed.", + "params": { + "user": { + "description": "Tag of the user they reported" + }, + "mURL": { + "description": "URL to the message log" + } + } + }, + "mute_message": { + "humanName": "Mute Message", + "description": "Message that gets send to a user when they got muted", + "default": "You got muted for **%reason%** by %user%!", + "params": { + "user": { + "description": "Tag of the moderator" + }, + "reason": { + "description": "Reason of the mute" + } + } + }, + "channel_mute": { + "humanName": "Channel Mute Message", + "description": "Message that gets send to a user when they got muted", + "default": "You got channel-muted from %channel% for **%reason%** by %user%!", + "params": { + "user": { + "description": "Tag of the moderator" + }, + "reason": { + "description": "Reason of the mute" + }, + "channel": { + "description": "Channel from which the user got muted" + } + } + }, + "remove-channel_mute": { + "humanName": "Channel Unmute Message", + "description": "Message that gets send to a user when they got muted", + "default": "Your channel-mute from %channel% got removed because of **%reason%** by %user%!", + "params": { + "user": { + "description": "Tag of the moderator" + }, + "reason": { + "description": "Reason of the mute" + }, + "channel": { + "description": "Channel from which the user got unmuted" + } + } + }, + "tmpmute_message": { + "humanName": "Temporary Mute Message", + "description": "Message that gets send to a user when they got temporarily muted", + "default": "You got temporarily muted for **%reason%** by %user%! This action is going to expire on %date%.", + "params": { + "user": { + "description": "Tag of the moderator" + }, + "reason": { + "description": "Reason of the mute" + }, + "date": { + "description": "Timestamp when this action expires" + } + } + }, + "quarantine_message": { + "humanName": "Quarantine Message", + "description": "Message that gets send to a user when they get quarantined", + "default": "You got quarantined for **%reason%** by %user%!", + "params": { + "user": { + "description": "Tag of the moderator" + }, + "reason": { + "description": "Reason of the mute" + } + } + }, + "tmpquarantine_message": { + "humanName": "Temporary Quarantine Message", + "description": "Message that gets send to a user when they get quarantined", + "default": "You got quarantined temporarily for **%reason%** by %user%! This action is going to expire on %date%", + "params": { + "user": { + "description": "Tag of the moderator" + }, + "reason": { + "description": "Reason of the mute" + }, + "date": { + "description": "Date when the quarantine is going to be removed automatically" + } + } + }, + "unquarantine_message": { + "humanName": "Unquarantine Message", + "description": "Message that gets send to a user when they get unquarantined", + "default": "You got unquarantined for **%reason%** by %user%!", + "params": { + "user": { + "description": "Tag of the moderator" + }, + "reason": { + "description": "Reason of the mute" + } + } + }, + "unmute_message": { + "humanName": "Unmute Message", + "description": "Message that gets send to a user when they got unmuted", + "default": "You got unmuted for **%reason%** by %user%!", + "params": { + "user": { + "description": "Tag of the moderator" + }, + "reason": { + "description": "Reason of the unmute" + } + } + }, + "kick_message": { + "humanName": "Kick Message", + "description": "Message that gets send to a user when they got kicked", + "default": "You got kicked for **%reason%** by %user%!", + "params": { + "user": { + "description": "Tag of the moderator" + }, + "reason": { + "description": "Reason of the kick" + } + } + }, + "ban_message": { + "humanName": "Ban Message", + "description": "Message that gets send to a user when they got banned", + "default": "You got banned for **%reason%** by %user%!", + "params": { + "user": { + "description": "Tag of the moderator" + }, + "reason": { + "description": "Reason of the ban" + } + } + }, + "tmpban_message": { + "humanName": "Temporary Ban Message", + "description": "Message that gets send to a user when they got banned temporarily", + "default": "You got temporarily banned for **%reason%** by %user%! This action is going to expire on %date%", + "params": { + "user": { + "description": "Tag of the moderator" + }, + "reason": { + "description": "Reason of the ban" + }, + "date": { + "description": "Date on which the ban expires" + } + } + }, + "warn_message": { + "humanName": "Warn Message", + "description": "Message that gets send to a user when they got warned", + "default": "You got warned for **%reason%** by %user%!", + "params": { + "user": { + "description": "Tag of the moderator" + }, + "reason": { + "description": "Reason of the warn" + } + } + }, + "lock_channel_message": { + "humanName": "Channel Lock Message", + "description": "Message that gets send in a channel if it gets locked", + "default": "This channel got locked because %reason% by %user%", + "params": { + "user": { + "description": "Tag of the moderator" + }, + "reason": { + "description": "Reason of the lock" + } + } + }, + "unlock_channel_message": { + "humanName": "Channel Unlock Message", + "description": "Message that gets send in a channel if it gets unlocked", + "default": "This channel got unlocked by %user%", + "params": { + "user": { + "description": "Tag of the moderator" + } + } + } + } + }, + "antiSpam": { + "description": "You can configure here, how your bot should react to spam", + "humanName": "Anti-Spam-Configuration", + "categories": { + "settings": { + "displayName": "Detection Settings" + }, + "actions": { + "displayName": "Actions" + }, + "exemptions": { + "displayName": "Exemptions" + } + }, + "content": { + "enabled": { + "humanName": "Enabled?", + "description": "Enable or disable the anti spam system" + }, + "timeframe": { + "humanName": "Timeframe (in seconds)", + "description": "Timeframe in seconds after which message objects get deleted (and can not longer be used to detect spam)" + }, + "maxMessagesInTimeframe": { + "humanName": "Maximal count of messages in timeframe", + "description": "Count of messages that are allowed to be sent in the selected timeframe" + }, + "maxDuplicatedMessagesInTimeframe": { + "humanName": "Maximal count of duplicated messages in timeframe", + "description": "Count of identical messages that are allowed to be sent in the selected timeframe" + }, + "maxPingsInTimeframe": { + "humanName": "Maximal count of pings in timeframe", + "description": "Count of pings (also counts replies) that are allowed to be sent in the selected timeframe" + }, + "maxMassPings": { + "humanName": "Maximal count of mass-pings in timeframe", + "description": "Count of mass pings (= @everyone, @here and roles) that are allowed to be sent in the selected timeframe" + }, + "action": { + "humanName": "Action", + "description": "Select what should happen if someone spams" + }, + "sendChatMessage": { + "humanName": "Send Chat-Message", + "description": "If enabled the bot will send a chat message if it has to take action agains a bot" + }, + "message": { + "humanName": "Message", + "description": "This will get send in the channel the spam is occurring in when anti-spam gets triggered", + "default": "Anti-Spam: I took action against <@%userid%> because of **%reason%**", + "params": { + "userid": { + "description": "ID of the user" + }, + "reason": { + "description": "Reason of the action" + } + } + }, + "ignoredChannels": { + "humanName": "Whitelisted Channels", + "description": "You can set channels that get ignored here" + }, + "ignoredRoles": { + "humanName": "Whitelisted roles", + "description": "You can set roles that get ignored here" + } + } + }, + "antiGrief": { + "description": "This system can prevent moderation-tool-abuse by staff-members", + "humanName": "Anti-Grief-Configuration", + "warningBanner": "This feature is currently limited to actions run by the moderation-module. If you've given your moderators native discord-permissions, they can bypass this. We plan to support native actions (+ channel-deletes and other griefing actions) in future.", + "informationBanner": "This feature can automatically quarantine moderators that abuse their permissions (banning / warning / kicking more people than you set up). For this to work, place your bot above all other roles and make sure that the quarantine-role is right below it. This ensures that moderators / admins can not just give permissions to the quarantine-role or remove permissions from the bot.", + "categories": { + "settings": { + "displayName": "Detection Settings" + }, + "actions": { + "displayName": "Actions" + } + }, + "content": { + "enabled": { + "humanName": "Enabled?", + "description": "Enables or disables the anti-join-grief-system" + }, + "timeframe": { + "humanName": "Timeframe (in hours)", + "description": "Timeframe in hours in which the limits can not be overstepped" + }, + "max_warn": { + "humanName": "Maximal amount of warns in the timeframe", + "description": "Maximal amount of warns a moderator can give in the timeframe until they get quarantined" + }, + "max_mute": { + "humanName": "Maximal amount of mutes in the timeframe", + "description": "Maximal amount of mutes a moderator can give in the timeframe until they get quarantined" + }, + "max_kick": { + "humanName": "Maximal amount of kicks in the timeframe", + "description": "Maximal amount of kicks a moderator can give in the timeframe until they get quarantined" + }, + "max_ban": { + "humanName": "Maximal amount of bans in the timeframe", + "description": "Maximal amount of bans a moderator can give in the timeframe until they get quarantined" + } + } + }, + "antiJoinRaid": { + "description": "This system can prevent spammers from raiding your server", + "humanName": "Anti-Join-Raid-Configuration", + "categories": { + "settings": { + "displayName": "Detection Settings" + }, + "actions": { + "displayName": "Actions" + } + }, + "content": { + "enabled": { + "humanName": "Enabled?", + "description": "Enables or disables the anti-join-raid-system" + }, + "timeframe": { + "humanName": "Timeframe (in minutes)", + "description": "Timeframe in which join actions should be recorded (in minutes)" + }, + "maxJoinsInTimeframe": { + "humanName": "Maximal count of new users", + "description": "Count of joins that are allowed to happen in the selected timeframe" + }, + "action": { + "humanName": "Action", + "description": "Select the action here that should get performed if the anti-join-system gets triggered" + }, + "roleID": { + "humanName": "Role", + "description": "Only if action = give-role. Role that gets given to users who trigger the antiJoinRaid-System" + }, + "removeOtherRoles": { + "humanName": "Remove other roles", + "description": "Only if action = give-role. If enabled other roles that have been give to the user get removed after a short interval (and the giving of the role from \"roleID\" will be delayed)" + } + } + }, + "verification": { + "description": "Require accounts to verify that they are not a robot before accessing your server", + "humanName": "Verification-Configuration", + "categories": { + "general": { + "displayName": "General Settings" + }, + "messages": { + "displayName": "Messages" + }, + "roles": { + "displayName": "Roles" + } + }, + "content": { + "enabled": { + "humanName": "Enabled?", + "description": "If checked, verification on your server will be enabled" + }, + "verification-needed-role": { + "humanName": "Role for users with pending verification", + "description": "Role, which members should be given before they verify themselves" + }, + "verification-passed-role": { + "humanName": "Role for users that passed verification", + "description": "Role, which members should be given after they got verified successfully" + }, + "verification-log": { + "humanName": "Verification Log Channel", + "description": "Channel where all verification-actions should get logged" + }, + "type": { + "humanName": "Type of verification", + "description": "How should new members verify themselves on your server?", + "selectOptions": { + "captcha": { + "displayName": "Image Captcha: distorted image, solved in-channel" + }, + "captcha-dm": { + "displayName": "Image Captcha (DM): legacy, sent via direct message" + }, + "word": { + "displayName": "Word challenge: retype a displayed word" + }, + "math": { + "displayName": "Math challenge: solve an arithmetic problem" + }, + "manual": { + "displayName": "Manual: a moderator approves each new member" + }, + "button": { + "displayName": "Button click: one click, no challenge" + } + } + }, + "captchaLevel": { + "humanName": "Challenge difficulty", + "description": "Difficulty of the verification challenge. Applies to Image Captcha, Image Captcha (DM), Word and Math. Not used for Manual or Button.", + "selectOptions": { + "easy": { + "displayName": "Easy: short words / small numbers" + }, + "medium": { + "displayName": "Medium (default)" + }, + "hard": { + "displayName": "Hard: longer words / larger numbers & multiplication" + } + } + }, + "actionOnFail": { + "humanName": "Action on failure of verification", + "description": "What should happen if someone fails the verification?" + }, + "verification-channel": { + "humanName": "Verification Channel", + "description": "Channel where users can verify themselves by clicking the Verify Me button. For the legacy DM type, this serves as a fallback channel for users with DMs disabled." + }, + "maxRetries": { + "humanName": "Maximum verification attempts", + "description": "How many attempts a user gets before the failure action is applied. Applies to Image Captcha, Image Captcha (DM), Word and Math types." + }, + "retryCooldown": { + "humanName": "Cooldown between retries", + "description": "How long a user must wait between verification attempts (e.g. 5m, 10m, 1h).", + "default": "5m" + }, + "actionOnFailDuration": { + "humanName": "Punishment duration", + "description": "Duration for mute or quarantine punishment when a user exhausts all verification attempts (e.g. 1h, 1d). Only applies when action on fail is mute or quarantine.", + "default": "1h" + }, + "cooldown-message": { + "humanName": "Cooldown message", + "description": "Shown when a user needs to wait before verifying again.", + "default": "⏳ Please wait %t% before trying again.", + "params": { + "t": { + "description": "Discord timestamp showing when the user can try again" + } + } + }, + "captcha-message": { + "humanName": "Captcha-Message", + "description": "This message gets sent to users who need to complete a captcha", + "default": "Welcome! Please verify that you are a human. You have two minutes to complete this." + }, + "manual-verification-message": { + "humanName": "Manual-Verification-Message", + "description": "This message gets sent to users who need to get verified manually.", + "default": "Welcome! A human will be verifying your account shortly. I will update you if I have any news." + }, + "captcha-failed-message": { + "humanName": "Captcha failed-Message", + "description": "This message gets sent when a user fails the verification", + "default": "It seems like you failed the verification. This is bad, I will have to take moderative actions against you - sorry fellow bot." + }, + "captcha-succeeded-message": { + "humanName": "Captcha completed-Message", + "description": "This message gets sent to users when they complete the verification", + "default": "Thanks! We have verified that you are indeed not a bot, so I granted you access to the whole server! Have fun <3" + }, + "verify-channel-first-message": { + "humanName": "Verification-Channel-Info-Message", + "description": "This message is the introduction message in the verify-channel.", + "default": "Welcome! Please verify yourself by clicking the button below. This step is required to access this server." + } + } + }, + "lockdown": { + "description": "Configure the server-wide lockdown system. This is separate from per-channel lock/unlock commands.", + "humanName": "Lockdown Configuration", + "categories": { + "general": { + "displayName": "General Settings" + }, + "messages": { + "displayName": "Messages" + }, + "automation": { + "displayName": "Automation" + } + }, + "content": { + "enabled": { + "humanName": "Enable lockdown system?", + "description": "Enables the /moderate lockdown command and automatic lockdown triggers" + }, + "logChannel": { + "humanName": "Lockdown log channel", + "description": "Channel where detailed lockdown log entries are posted. Falls back to the moderation log channel if not set." + }, + "sendMessageInAffectedChannels": { + "humanName": "Send message in affected channels?", + "description": "If enabled, the lockdown/lift message will be sent in every affected channel" + }, + "lockdownMessageChannels": { + "humanName": "Channels for lockdown messages", + "description": "If set, lockdown/lift messages will only be sent in these channels instead of all affected channels. Leave empty to send in all affected channels." + }, + "lockdownMessage": { + "humanName": "Lockdown activation message", + "description": "Message sent in affected channels when lockdown is activated", + "default": "🔒 **Server Lockdown** - This server is currently in lockdown mode. Reason: %reason%", + "params": { + "reason": { + "description": "Reason for the lockdown" + }, + "user": { + "description": "User who activated the lockdown (or 'System' for automatic)" + } + } + }, + "liftMessage": { + "humanName": "Lockdown lifted message", + "description": "Message sent in affected channels when lockdown is lifted", + "default": "🔓 **Lockdown Lifted** - The server lockdown has been lifted. You can chat again.", + "params": { + "user": { + "description": "User who lifted the lockdown" + } + } + }, + "autoLiftAfter": { + "humanName": "Auto-lift lockdown after (minutes, 0 = manual only)", + "description": "Automatically lift the lockdown after this many minutes. Set to 0 to require manual lifting." + }, + "autoTriggerOnJoinRaid": { + "humanName": "Auto-lockdown on join raid?", + "description": "Automatically activate lockdown when the anti-join-raid system is triggered" + }, + "autoTriggerOnJoinGate": { + "humanName": "Auto-lockdown on join-gate violations?", + "description": "Automatically activate lockdown when the join-gate system is triggered. Thresholds are configured in the Join-Gate configuration." + }, + "autoTriggerOnSpam": { + "humanName": "Auto-lockdown on spam detection?", + "description": "Automatically activate lockdown when the anti-spam system is triggered. Thresholds are configured in the Anti-Spam configuration." + } + } + } + }, + "nicknames": { + "_module": { + "humanReadableName": "Role-Nicknames", + "description": "Simple module to edit user nicknames based on roles!" + }, + "config": { + "description": "Configure the function of the module here", + "humanName": "Configuration", + "content": { + "forceDisplayname": { + "humanName": "Force display name", + "description": "Use display names of users instead of custom nicknames." + } + } + }, + "strings": { + "description": "Set a prefixes and/or suffixes for roles.", + "humanName": "Roles", + "content": { + "roleID": { + "humanName": "Role", + "description": "The role you want to set a prefix/suffix for." + }, + "prefix": { + "humanName": "Prefix", + "description": "The Prefix to be set.", + "default": "" + }, + "suffix": { + "humanName": "Suffix", + "description": "The Suffix to be set.", + "default": "" + } + } + } + }, + "ping-on-vc-join": { + "_module": { + "humanReadableName": "Voice-Channel Actions", + "description": "Sends messages when someone joins a voicechat and assign roles to users in Voice-Channels" + }, + "config": { + "description": "Configure messages that should get send when a user joins a Voice-Channel", + "humanName": "Message on Voice Join", + "categories": { + "general": { + "displayName": "General Settings" + }, + "cooldown": { + "displayName": "Cooldown" + }, + "messages": { + "displayName": "Messages" + } + }, + "content": { + "channels": { + "humanName": "Channels", + "description": "Channel-ID in which this messages should get triggered" + }, + "message": { + "humanName": "Message", + "description": "Here you can set the message that should be send if someone joins a selected voicechat", + "default": "The user %tag% joined the voicechat %vc%", + "params": { + "tag": { + "description": "Tag of the user" + }, + "vc": { + "description": "Name of the voicechat" + }, + "mention": { + "description": "Mention of the user" + } + } + }, + "notify_channel_id": { + "humanName": "Notification-Channel", + "description": "Channel where the message should be send" + }, + "cooldownEnabled": { + "humanName": "Enable Cooldown?", + "description": "When enabled, messages will only be sent once per channel within the cooldown period" + }, + "cooldownMinutes": { + "humanName": "Cooldown Duration (Minutes)", + "description": "Duration in minutes to wait before sending another message for the same channel" + }, + "send_pn_to_member": { + "humanName": "Join-DM", + "description": "Should the bot send a PN to the member?" + }, + "pn_message": { + "humanName": "Join-DM-Message", + "description": "This message is sent to the user when they join a voice chat (if \"Join DM\" is enabled).", + "default": "Hi, I saw you joined the voice chat %vc%. Nice (;", + "params": { + "vc": { + "description": "Name of the voicechat" + } + } + } + } + }, + "actual-config": { + "description": "Configure messages that should get send when a user joins a Voice-Channel", + "humanName": "Configuration", + "categories": { + "roles": { + "displayName": "Voice Roles" + } + }, + "content": { + "assignRoleToUsersInVoiceChannels": { + "humanName": "Assign roles to members connected to voice channels?", + "description": "If enabled, users will receive a role when they join a voice channel. This role will be removed when they leave the voice channel (switching voice channels does not trigger a role removal)." + }, + "voiceRoles": { + "humanName": "Roles for users that are connected to voice channels", + "description": "Users that are currently connected to a voice channel will be assigned these roles." + } + } + } + }, + "ping-protection": { + "_module": { + "humanReadableName": "Ping-Protection", + "description": "Powerful and highly customizable ping-protection module to protect members/roles from unwanted mentions with moderation capabilities." + }, + "configuration": { + "description": "Configure protected users/roles, whitelisted roles/members, ignored channels and the notification message.", + "humanName": "General Configuration", + "categories": { + "protection": { + "displayName": "Protected" + }, + "whitelisted": { + "displayName": "Whitelists" + }, + "rules": { + "displayName": "Ping rules" + }, + "automod": { + "displayName": "AutoMod settings" + }, + "messages": { + "displayName": "Warning message" + } + }, + "content": { + "protectedRoles": { + "humanName": "Protected Roles", + "description": "Specific roles which are protected from pings." + }, + "protectAllUsersWithProtectedRole": { + "humanName": "Protect all users with a protected role", + "description": "if enabled, all users with at least one protected role will be protected from pings, even if they are not specifically listed as protected users." + }, + "protectedUsers": { + "humanName": "Protected Users", + "description": "Specific users who are protected from pings." + }, + "ignoredRoles": { + "humanName": "Whitelisted Roles", + "description": "Roles allowed to ping protected members or roles." + }, + "ignoredChannels": { + "humanName": "Whitelisted Channels", + "description": "Pings in these channels are ignored." + }, + "ignoredUsers": { + "humanName": "Whitelisted Users", + "description": "Pings from these users are ignored." + }, + "allowReplyPings": { + "humanName": "Allow Reply Pings", + "description": "If enabled, replying to a protected user (with mention ON) is allowed." + }, + "selfPingConfiguration": { + "humanName": "Self-Ping configuration", + "description": "Configure what happens when a protected user pings themselves. Note: Automod overrides this setting meaning this setting will not apply if Automod is enabled." + }, + "enableAutomod": { + "humanName": "Enable automod", + "description": "If enabled, the bot will utilise Discord's native AutoMod to block the message with a ping of a protected user/role." + }, + "autoModLogChannel": { + "humanName": "AutoMod Log Channel", + "description": "Channel where AutoMod alerts are sent." + }, + "autoModBlockMessage": { + "humanName": "AutoMod custom message for message block", + "description": "Custom text shown to the user when blocked (Max 150 characters).", + "default": "Your message was blocked because you are trying to ping a protected user/role. The message content might be logged depending on the configuration." + }, + "pingWarningMessage": { + "humanName": "Warning Message", + "description": "The message that gets sent to the user when they ping someone.", + "default": { + "title": "You are not allowed to ping %target-name%!", + "description": "<@%pinger-id%>, You are not allowed to ping %target-mention% due to your role. You can view which roles/members you are not allowed to ping by using the `/ping-protection list protected` command.\n\nIf you were replying, make sure to turn off the mention in the reply.", + "image": "https://scnx-cdn.scootkit.net/1769198862209-rJfCVKzAuo6uQLhPUe9o2P6ArJkDBSVUCEyUQM6bqt5WFKWK.gif", + "color": "#ed4245" + }, + "params": { + "target-name": { + "description": "Name of the pinged user/role" + }, + "target-mention": { + "description": "Mention of the pinged user/role" + }, + "target-id": { + "description": "ID of the pinged user/role" + }, + "pinger-id": { + "description": "ID of the user who pinged" + } + } + } + } + }, + "moderation": { + "description": "Define triggers for punishments.", + "humanName": "Moderation Actions", + "configElementName": { + "one": "punishment", + "more": "punishment" + }, + "content": { + "pingsCount": { + "humanName": "Pings to trigger moderation", + "description": "The amount of pings required to trigger a moderation action." + }, + "useCustomTimeframe": { + "humanName": "Use a custom timeframe", + "description": "If enabled, you can choose your own custom timeframe of days in which the pings must occur to trigger the moderation action." + }, + "timeframeDays": { + "humanName": "Timeframe (Days)", + "description": "In how many days must these pings occur?" + }, + "actionType": { + "humanName": "Action", + "description": "What punishment should be applied?" + }, + "muteDuration": { + "humanName": "Mute Duration (only if action type is MUTE)", + "description": "How long to mute the user? (in minutes)" + }, + "enableActionLogging": { + "humanName": "Enable action logging", + "description": "If enabled, moderation actions will be logged in the channel where a protected user/role got pinged." + }, + "actionLogMessage": { + "humanName": "Action log message", + "description": "The message that will be sent when a user is punished for pinging protected users/roles.", + "default": { + "title": "Moderation action taken against %pinger-name%", + "description": "I have taken action against %pinger-mention% for pinging protected users/roles %pings% times within %timeframe% days.\n **Action:** %action%\n**Duration:** %duration% minutes", + "color": "#ed4245" + }, + "params": { + "pinger-mention": { + "description": "Mention of the user who pinged" + }, + "pinger-name": { + "description": "Name of the user who pinged" + }, + "action": { + "description": "The action that was taken (muted/kicked)" + }, + "pings": { + "description": "Number of pings that triggered the action" + }, + "timeframe": { + "description": "The timeframe in days in which the pings occurred" + }, + "duration": { + "description": "Duration of the mute in minutes (only for the mute action)" + } + } + } + } + }, + "storage": { + "description": "Configure how long moderation logs and leaver data are kept.", + "humanName": "Data Storage", + "categories": { + "pings": { + "displayName": "Ping History" + }, + "moderation": { + "displayName": "Moderation Logs" + }, + "leavers": { + "displayName": "Leaver Data" + } + }, + "content": { + "enablePingHistory": { + "humanName": "Enable Ping History", + "description": "If enabled, the bot will keep a history of pings to enforce moderation actions." + }, + "pingHistoryRetention": { + "humanName": "Ping History Retention", + "description": "Decides on how long to keep ping logs. Minimum is 4 weeks (1 month) with a maximum of 96 weeks (2 years). This is the length factor of the 'Basic' punishment timeframe." + }, + "deleteAllPingHistoryAfterTimeframe": { + "humanName": "Delete all the pings in history after the timeframe?", + "description": "If enabled, the bot will delete ALL the pings history of an user after the timeframe instead of only the ping(s) exceeding the timeframe in the history." + }, + "modLogRetention": { + "humanName": "Moderation Log Retention (Months)", + "description": "How long to keep records of punishments (1 - 24 Months). This is applied when moderation actions are enabled." + }, + "enableLeaverDataRetention": { + "humanName": "Keep user logs after they leave", + "description": "If enabled, the bot will keep a history of the user after they leave." + }, + "leaverRetention": { + "humanName": "Leaver Data Retention (Days)", + "description": "How long to keep data after a user leaves (1-7 Days)." + } + } + } + }, + "polls": { + "_module": { + "humanReadableName": "Polls", + "description": "Simple module to create fresh polls on your server! Supports anonymous polls and more." + }, + "config": { + "description": "Configure the function of the module here", + "humanName": "Configuration", + "content": { + "reactions": { + "humanName": "Emojis", + "description": "You can set the different emojis to use" + } + } + }, + "strings": { + "description": "Edit the messages and strings of the module here", + "humanName": "Messages", + "content": { + "embed": { + "humanName": "Embed", + "description": "You can edit the settings of your embed here" + } + } + } + }, + "quiz": { + "_module": { + "humanReadableName": "Quiz Module", + "description": "Create quiz for your users and let them compete against each other." + }, + "config": { + "description": "Configure the function of the module here", + "humanName": "Configuration", + "content": { + "emojis": { + "humanName": "Emojis", + "description": "You can set the emojis to use" + }, + "dailyQuizLimit": { + "humanName": "Daily quiz limit", + "description": "How many quizzes can be played per day using /quiz play" + }, + "leaderboardChannel": { + "humanName": "Quiz leaderboard channel", + "description": "In which channel the quiz leaderboard is displayed" + }, + "createAllowedRole": { + "humanName": "Role needed to create quizzes", + "description": "Which role a user needs to have to be able to create quizzes with /quiz create/create-bool" + }, + "mode": { + "humanName": "Mode for quiz selection", + "description": "How a /quiz play quiz is selected for users" + }, + "livePreview": { + "humanName": "Live preview of results", + "description": "Whether the live preview of results is enabled" + } + } + }, + "strings": { + "description": "Edit the messages and strings of the module here", + "humanName": "Messages", + "content": { + "embed": { + "humanName": "Embed", + "description": "You can edit the settings of your embed here" + } + } + }, + "quizList": { + "description": "Create and edit the quizzes of the server", + "humanName": "Edit quiz", + "content": { + "description": { + "humanName": "Question or statement", + "description": "Title/Question of the quiz", + "default": "" + }, + "duration": { + "humanName": "Time limit", + "description": "How much time the user has to answer", + "default": "1m" + }, + "correctOptions": { + "humanName": "Correct answers", + "description": "Correct answers" + }, + "wrongOptions": { + "humanName": "Wrong answers", + "description": "Wrong answers" + } + } + } + }, + "reminders": { + "_module": { + "humanReadableName": "Reminders", + "description": "Let users set reminders for themselves - either via DMs or Channels" + }, + "config": { + "description": "Configure the behavior of this module here", + "humanName": "Configuration", + "content": { + "notificationMessage": { + "humanName": "Reminder-Message", + "description": "This message gets send when someone gets remaindered", + "default": { + "title": "🔔 Reminder", + "color": "#F1C40F", + "description": "%message%", + "message": "%mention%" + }, + "params": { + "mention": { + "description": "Mention of the user" + }, + "message": { + "description": "Reminder message set by the user" + }, + "userTag": { + "description": "Tag of the user" + }, + "userAvatarURL": { + "description": "Avatar-URL of the user" + } + } + } + } + } + }, + "rock-paper-scissors": { + "_module": { + "humanReadableName": "Rock Paper Scissors", + "description": "Let your users play Rock Paper Scissors against the bot and each other!" + } + }, + "staff-management-system": { + "_module": { + "humanReadableName": "Staff Management System", + "description": "A powerful, highly customizable staff management system designed to track activity, moderate personnel, and maintain detailed staff records seamlessly." + }, + "configuration": { + "description": "Configure the main staff roles and the default log channel.", + "humanName": "General Configuration", + "categories": { + "roles": { + "displayName": "Staff Roles" + }, + "logging": { + "displayName": "Logging" + } + }, + "content": { + "staffRoles": { + "humanName": "Staff Roles", + "description": "Roles that can use basic staff commands (Shifts, LoA Request and RA Request, reviews etc.)." + }, + "supervisorRoles": { + "humanName": "Supervisor Roles", + "description": "Roles that can manage other staff members (Approve/Deny/Manage LoA's and RA's, Manage Shifts, promote and infract users)." + }, + "managementRoles": { + "humanName": "Management Roles", + "description": "Roles with full access, including data deletion abilities." + }, + "generalLogChannel": { + "humanName": "General Log Channel", + "description": "The default channel where logs happen such as status changes/request and more. This can be overridden in some features." + } + } + }, + "infractions": { + "description": "Configure how staff infractions, strikes, and suspensions are handled.", + "humanName": "Infractions & Suspensions", + "categories": { + "logic": { + "displayName": "General Logic" + }, + "suspensions": { + "displayName": "Suspensions Logic" + }, + "messages": { + "displayName": "Messages & Embeds" + } + }, + "content": { + "enableInfractions": { + "humanName": "Enable Infractions System", + "description": "Enabling this will unlock features such as issuing infractions to staff members, suspensions and more." + }, + "infractionTypes": { + "humanName": "Infraction Types", + "description": "These are the types of infractions that can be issued to staff members. You can customize these to fit your infractions system." + }, + "enableSuspensions": { + "humanName": "Enable Suspensions System", + "description": "Suspensions temporarily strip a staff member of their roles." + }, + "suspensionHierarchyRole": { + "humanName": "Hierarchy Base Role", + "description": "When suspending, the bot will remove all roles above and including this one. This would usually be your lowest 'Staff' role." + }, + "suspensionRole": { + "humanName": "Suspended Role (Optional)", + "description": "A role to assign the user while they are suspended (e.g., 'Suspended Staff')." + }, + "suspensionMessage": { + "humanName": "Suspension Announcement Message", + "description": "The message sent to the log channel when a staff member is suspended.", + "default": { + "_schema": "v3", + "content": "%user%", + "embeds": [ + { + "author": { + "name": "Signed, %issuer-name% • Case #%case-id%", + "iconURL": "%issuer-avatar%" + }, + "title": "⛔ Staff Suspension", + "description": "**Staff Member:** %user%\n**Duration:** %duration%\n**Ends:** %end-date%\n**Reason:** %reason%", + "color": "#ed4245", + "thumbnailURL": "%user-avatar%" + } + ] + }, + "params": { + "user": { + "description": "Mention of the staff member" + }, + "user-avatar": { + "description": "Avatar of the staff member" + }, + "issuer-mention": { + "description": "Mention of the manager issuing it" + }, + "issuer-name": { + "description": "Name of the issuer" + }, + "issuer-avatar": { + "description": "Avatar of the issuer" + }, + "duration": { + "description": "Duration of the suspension" + }, + "end-date": { + "description": "Timestamp of when the suspension ends" + }, + "reason": { + "description": "Reason provided" + }, + "case-id": { + "description": "Database Case ID" + } + } + }, + "infractionLogChannel": { + "humanName": "Infraction Log Channel", + "description": "Where should infractions and suspensions be announced?" + }, + "infractionMessage": { + "humanName": "Infraction Announcement Message", + "description": "The message sent to the log channel for regular infractions.", + "default": { + "_schema": "v3", + "content": "%user%", + "embeds": [ + { + "author": { + "name": "Signed, %issuer-name% • Case #%case-id%", + "iconURL": "%issuer-avatar%" + }, + "title": "⚠️ New infraction", + "description": "**Staff Member:** %user%\n**Action Taken:** %type%\n**Expires:** %end-date%\n**Reason:** %reason%", + "color": "#e67e22", + "thumbnailURL": "%user-avatar%" + } + ] + }, + "params": { + "user": { + "description": "Mention of the staff member" + }, + "user-avatar": { + "description": "Avatar of the staff member" + }, + "issuer-mention": { + "description": "Mention of the manager issuing it" + }, + "issuer-name": { + "description": "Name of the issuer" + }, + "issuer-avatar": { + "description": "Avatar of the issuer" + }, + "type": { + "description": "Type of infraction (e.g., Warning, Strike)" + }, + "end-date": { + "description": "Timestamp of when this infraction expires" + }, + "reason": { + "description": "Reason provided" + }, + "case-id": { + "description": "Database Case ID" + } + } + }, + "dmInfractedUser": { + "humanName": "DM User on infraction?", + "description": "If enabled, the bot will DM the staff member when they receive an infraction or suspension." + }, + "infractionDmMessage": { + "humanName": "Infraction DM Message", + "description": "The message sent directly to the staff member.", + "default": { + "_schema": "v3", + "embeds": [ + { + "author": { + "name": "Signed, %issuer-name% • Case #%case-id%" + }, + "title": "⚠️ You have been infracted", + "description": "**Type:** %type%\n**Reason:** %reason%\n**Expires:** %end-date%", + "color": "#e67e22" + } + ] + }, + "params": { + "user": { + "description": "Mention of the staff member" + }, + "issuer-name": { + "description": "Name of the issuer" + }, + "type": { + "description": "Type of infraction (e.g., Warning, Strike)" + }, + "end-date": { + "description": "Timestamp of when this infraction expires" + }, + "reason": { + "description": "Reason provided" + }, + "case-id": { + "description": "Database Case ID" + } + } + }, + "suspensionDmMessage": { + "humanName": "Suspension DM Message1", + "description": "The message sent directly to the staff member when suspended.", + "default": { + "_schema": "v3", + "embeds": [ + { + "author": { + "name": "Signed, %issuer-name% • Case #%case-id%" + }, + "title": "⛔ Staff Suspension", + "description": "You have been temporarily suspended.\n\n**Duration:** %duration%\n**Returns:** %end-date%\n**Reason:** %reason%", + "color": "#ed4245" + } + ] + }, + "params": { + "user": { + "description": "Mention of the staff member" + }, + "issuer-name": { + "description": "Name of the issuer" + }, + "type": { + "description": "Type of infraction (e.g., Warning, Strike)" + }, + "duration": { + "description": "Duration of the suspension" + }, + "end-date": { + "description": "Timestamp of when this infraction expires" + }, + "reason": { + "description": "Reason provided" + }, + "case-id": { + "description": "Database Case ID" + } + } + } + } + }, + "promotions": { + "description": "Configure how staff promotions are handled and announced.", + "humanName": "Promotions", + "categories": { + "logic": { + "displayName": "General logic" + }, + "messages": { + "displayName": "Announcements" + } + }, + "content": { + "enablePromotions": { + "humanName": "Enable Promotions System", + "description": "If disabled, the /staff-management promote command will not work." + }, + "autoAddRole": { + "humanName": "Auto-Add New Role?", + "description": "If enabled, the bot will automatically give the user the new rank role. Note: This makes your server prone to raids by promoting someone to a role with more dangerous permissions which can be used to do malicious actions. It is recommended to keep this setting disabled." + }, + "promotionsChannel": { + "humanName": "Promotions Channel", + "description": "The channel where promotion announcements will be sent." + }, + "promotionMessage": { + "humanName": "Promotion Announcement Embed", + "description": "This will be the message sent when someone is promoted.", + "default": { + "_schema": "v3", + "content": "%user-mention%", + "embeds": [ + { + "author": { + "name": "Signed, %promoter-name%", + "imageURL": "%promoter-avatar%" + }, + "title": "🎉 New promotion!", + "description": "Congratulations, you have been promoted to **%new-role-name%**!\n\n**Promoted to:** %new-role-mention%\n**On behalf of:** %promoter-mention%\n**Reason:** %reason%", + "color": "#f1c40f", + "thumbnailURL": "%user-avatar%" + } + ] + }, + "params": { + "user-mention": { + "description": "Pings the promoted user." + }, + "new-role-name": { + "description": "The plain text name of the new role." + }, + "new-role-mention": { + "description": "The pingable mention of the new role." + }, + "promoter-mention": { + "description": "Pings the staff member who issued the promotion." + }, + "promoter-name": { + "description": "The username of the staff member who issued the promotion." + }, + "reason": { + "description": "The reason for the promotion." + }, + "user-avatar": { + "description": "The avatar URL of the promoted user." + }, + "promoter-avatar": { + "description": "The avatar URL of the promoter." + } + } + }, + "dmPromotedUser": { + "humanName": "DM Promoted User?", + "description": "If enabled, the user will receive a direct message when promoted." + }, + "promotionDmMessage": { + "humanName": "Promotion DM Embed", + "description": "The message sent directly to the user.", + "default": { + "_schema": "v3", + "content": "%user-mention%", + "embeds": [ + { + "author": { + "name": "Signed, %promoter-name%", + "imageURL": "%promoter-avatar%" + }, + "title": "🎉 New promotion!", + "description": "Congratulations, you have been promoted to **%new-role-name%**!\n\n**Promoted to:** %new-role-mention%\n**On behalf of:** %promoter-mention%\n**Reason:** %reason%", + "color": "#f1c40f", + "thumbnailURL": "%user-avatar%" + } + ] + }, + "params": { + "user-mention": { + "description": "Pings the promoted user." + }, + "new-role-name": { + "description": "The plain text name of the new role." + }, + "new-role-mention": { + "description": "The pingable mention of the new role." + }, + "promoter-mention": { + "description": "Pings the staff member who issued the promotion." + }, + "promoter-name": { + "description": "The username of the staff member who issued the promotion." + }, + "reason": { + "description": "The reason for the promotion." + }, + "user-avatar": { + "description": "The avatar URL of the promoted user." + }, + "promoter-avatar": { + "description": "The avatar URL of the promoter." + } + } + } + } + }, + "reviews": { + "description": "Configure the staff rating system and feedback channels.", + "humanName": "Staff Reviews", + "categories": { + "settings": { + "displayName": "Settings" + }, + "messages": { + "displayName": "Notifications" + } + }, + "content": { + "enableReviews": { + "humanName": "Enable Reviews System", + "description": "Enabling this unlocks the staff review system, allowing users to submit ratings and feedback for staff members." + }, + "reviewLogChannel": { + "humanName": "Reviews Log Channel", + "description": "Channel where new reviews are posted." + }, + "allowSelfRating": { + "humanName": "Allow Self-Rating?", + "description": "If enabled, staff can review themselves. This is not recommended to keep a fair ratings system." + }, + "onlyAllowStaffReview": { + "humanName": "Only let users review staff", + "description": "If enabled, only staff members can review other staff members." + }, + "ratingMessage": { + "humanName": "Review Message", + "description": "The message sent when a review is submitted.", + "default": { + "_schema": "v3", + "content": "%staff%", + "embeds": [ + { + "title": "🌟 New Staff Rating", + "description": "**Staff:** %staff-mention%\n**Rated by:** %reviewer-mention%\n\n**Rating:** %stars% (%rating%/5)\n**Comment:**\n%comment%", + "color": "#f1c40f", + "thumbnailURL": "%staff-avatar%" + } + ] + }, + "params": { + "staff-mention": { + "description": "Mention of the staff member" + }, + "reviewer-mention": { + "description": "Mention of the reviewer" + }, + "stars": { + "description": "Amount of stars rated in emoji's (⭐⭐⭐⭐⭐)" + }, + "rating": { + "description": "Amount of stars rated in text (1-5)" + }, + "comment": { + "description": "The review's text" + }, + "staff-avatar": { + "description": "The staff member's profile picture (URL)" + }, + "reviewer-avatar": { + "description": "The reviewer's profile picture (URL)" + } + } + } + } + }, + "shifts": { + "description": "Configure shift requirements, duty roles, leaderboards, and quotas.", + "humanName": "Shift Management", + "categories": { + "settings": { + "displayName": "Shift Settings" + }, + "leaderboard": { + "displayName": "Leaderboard" + }, + "quotas": { + "displayName": "Quotas" + }, + "logging": { + "displayName": "Logging" + } + }, + "content": { + "enableShifts": { + "humanName": "Enable Shifts", + "description": "This unlocks the ability for staff to use a shifts system, where they can get on-duty, off-duty, take a break and see their total duty time." + }, + "onDutyRole": { + "humanName": "On-Duty Role", + "description": "Role given to users when they are on-duty. This is optional, but recommended to easily identify who is on-duty." + }, + "dutyTypes": { + "humanName": "Duty Types", + "description": "The types of duty a staff member can select when going on-duty." + }, + "minShiftDuration": { + "humanName": "Minimum Shift Duration (minutes)", + "description": "A minimum shift duration for a shift to count towards their duty time. Default is 0, which means all shift time counts." + }, + "enableLeaderboard": { + "humanName": "Enable duty leaderboard", + "description": "If enabled, staff can see a leaderboard of who has the most duty time in the configured timeframe." + }, + "leaderboardLookback": { + "humanName": "Leaderboard Timeframe", + "description": "The timeframe of the duty time shown on the leaderboard." + }, + "enableQuotas": { + "humanName": "Enable Quota System", + "description": "If enabled, you can set a custom quota of hours for staff to meet in the configured timeframe." + }, + "quotaTimeframe": { + "humanName": "Quota Timeframe", + "description": "The timeframe in which the quota must be met." + }, + "quotas": { + "humanName": "Role Quotas", + "description": "Set required hours per role - the left side will be the role, and the right side is a number which is the hours for the quota. The user's highest role counts as their quota." + }, + "logShiftChanges": { + "humanName": "Log Shift Changes", + "description": "When enabled, shift changes (such as going on-duty, on break, or off-duty) will be logged in a custom channel." + }, + "logShiftChangesChannel": { + "humanName": "Channel for shift change logs", + "description": "The channel where shift changes will be logged. You can set this empty to use the general log channel." + } + } + }, + "status": { + "description": "Configure Leave of Absence (LoA) and Reduced Activity (RA) settings.", + "humanName": "LoA & RA Status", + "categories": { + "base": { + "displayName": "Base Settings" + }, + "loa": { + "displayName": "LoA Settings" + }, + "ra": { + "displayName": "RA Settings" + }, + "logging": { + "displayName": "Requests Log" + } + }, + "content": { + "enableStatusSystem": { + "humanName": "Enable Status System", + "description": "Enabling this unlocks the Leave of Absence (LoA) and Reduced Activity (RA) system, allowing staff to request these statuses and have them tracked." + }, + "enableLoa": { + "humanName": "Enable LoA System", + "description": "If enabled, staff can request a Leave of Absence (LoA)." + }, + "loaRole": { + "humanName": "LoA Role", + "description": "Role given to users when they are on a Leave of Absence. This is optional, but recommended to easily identify who is on LoA." + }, + "loaMaxDays": { + "humanName": "Maximum LoA Duration (days)", + "description": "The maximum duration for a Leave of Absence in days. This limits how long staff can request to be on LoA for." + }, + "requireLoaApproval": { + "humanName": "Require Approval for LoA?", + "description": "If enabled, LoA requests will require approval from staff who have supervisor permissions or higher." + }, + "enableRa": { + "humanName": "Enable RA System", + "description": "If enabled, staff can request Reduced Activity (RA) status for when they are still working but at a reduced load." + }, + "raRole": { + "humanName": "RA Role", + "description": "Role given to users when they are on Reduced Activity. This is optional, but recommended to easily identify who is on RA." + }, + "raMaxDays": { + "humanName": "Maximum RA Duration (days)", + "description": "The maximum duration for RA in days. This limits how long staff can request to be on RA for." + }, + "requireRaApproval": { + "humanName": "Require Approval for RA?", + "description": "If enabled, RA requests will require approval from staff who have supervisor permissions or higher." + }, + "statusLogChannel": { + "humanName": "Status Request Channel", + "description": "Channel where requests are sent for approval." + }, + "logStatusChanges": { + "humanName": "Log status changes", + "description": "If enabled, any changes in staff status (going on/off LoA or RA) will be logged in the configured channel." + }, + "statusChangeLogChannel": { + "humanName": "Status Change Log Channel", + "description": "Channel where status changes are logged. By default this uses your main log channel, but you can set a separate channel here." + } + } + }, + "profiles": { + "description": "Configure the staff profile system (Intros, custom nicknames, and stats).", + "humanName": "Staff Profiles", + "categories": { + "settings": { + "displayName": "Profile Settings" + } + }, + "content": { + "enableProfiles": { + "humanName": "Enable Staff Profiles", + "description": "Allows staff to have a profile tracking their shifts, reviews, and a custom introduction." + }, + "onlyAllowStaffProfile": { + "humanName": "Only allow staff and higher to have their own customizable profile", + "description": "If enabled, only staff members and higher will be able to set a custom profile nickname and introduction. If disabled, all members will be able to set a custom profile nickname and introduction." + }, + "managePermission": { + "humanName": "Profile Moderation Permission", + "description": "Which group is allowed to forcibly wipe another staff member's profile?" + }, + "profileEmbedMessage": { + "humanName": "Profile Embed", + "description": "Customize the embed shown when viewing a staff profile.", + "default": { + "_schema": "v3", + "embeds": [ + { + "title": "Staff Profile: %nickname%", + "description": "%intro%", + "color": "#2b2d31", + "thumbnailURL": "%avatar%", + "fields": [ + { + "name": "Status", + "value": "%status%", + "inline": true + }, + { + "name": "Average Rating", + "value": "%rating%", + "inline": true + } + ] + } + ] + }, + "params": { + "user-mention": { + "description": "The user's mention." + }, + "username": { + "description": "The user's standard Discord username." + }, + "nickname": { + "description": "The user's custom profile nickname (uses default username if not set)." + }, + "intro": { + "description": "The user's custom introduction." + }, + "status": { + "description": "The user's current status (LoA, RA, etc.)." + }, + "rating": { + "description": "The user's average review rating." + }, + "avatar": { + "description": "The user's avatar URL." + } + } + } + } + }, + "activity-checks": { + "description": "Configure automated staff activity checks and response logging.", + "humanName": "Activity Checks", + "categories": { + "general": { + "displayName": "General Settings" + }, + "exceptions": { + "displayName": "Exceptions" + }, + "automation": { + "displayName": "Automation" + }, + "results": { + "displayName": "Results & Logging" + } + }, + "content": { + "enableActivityChecks": { + "humanName": "Enable Activity Checks", + "description": "Allows admins to start an activity check to see who is active." + }, + "targetRoles": { + "humanName": "Roles to Check", + "description": "The roles required to respond to the activity check. Anyone with these roles will be expected to click the button. Leave empty to default to the General Staff Roles." + }, + "timeframe": { + "humanName": "Check Duration (Hours)", + "description": "How long staff have to respond to the activity check (Max 168 hours / 1 week)." + }, + "checkMessage": { + "humanName": "Activity Check Embed", + "description": "The message sent when an activity check starts.", + "default": { + "title": "📋 Staff Activity Check", + "description": "Please click the button below to confirm your activity before %endtime%.", + "color": "#3498db" + }, + "params": { + "end-time": { + "description": "The Discord timestamp when the check ends." + }, + "duration": { + "description": "The configured duration in hours." + } + } + }, + "sendingChannel": { + "humanName": "Default Sending Channel", + "description": "The default channel where the activity check message will be posted. This can manually be overridden with the command." + }, + "exceptionsType": { + "humanName": "Exceptions Rule", + "description": "Who are excused from the activity checks?" + }, + "customExceptionRoles": { + "humanName": "Custom Exception Roles", + "description": "Only applies if 'Custom role(s)' is selected above." + }, + "automatedChecks": { + "humanName": "Automated Checks", + "description": "If enabled, the bot will automatically start activity checks at configured intervals." + }, + "automatedCheckInterval": { + "humanName": "Automated Check Interval", + "description": "On which interval to start automatic checks. Choose cronjob for full customzation." + }, + "automatedCheckCronjob": { + "humanName": "Automated Check Cronjob", + "description": "The cronjob schedule for automatic checks. Only applies if 'Cronjob' is selected above.", + "default": "" + }, + "automatedCheckWeekDay": { + "humanName": "Automated Check Week Day", + "description": "The week day to start automatic checks." + }, + "automatedCheckMonthWeek": { + "humanName": "Automated Check Month Week", + "description": "The week of the month to start automatic checks. Only applies if 'Monthly' is selected above." + }, + "logChannel": { + "humanName": "Results Channel", + "description": "Where the final results are posted. Leave empty if you want to use the general log channel." + }, + "pingResults": { + "humanName": "Ping on Results", + "description": "Ping specific roles when the results are posted." + }, + "pingRoles": { + "humanName": "Roles to Ping", + "description": "The roles to ping with the results message." + } + } + } + }, + "starboard": { + "_module": { + "humanReadableName": "Starboard", + "description": "Let users highlight messages into a starboard channel by reacting." + }, + "config": { + "description": "Configure the starboard channel and reaction settings here", + "humanName": "Configuration", + "content": { + "channelId": { + "humanName": "Starboard channel", + "description": "In which channel starred messages are sent" + }, + "emoji": { + "humanName": "Emoji", + "description": "Which emoji should be used to star messages", + "default": "⭐" + }, + "message": { + "humanName": "Message", + "description": "This message gets send into the selected channel", + "default": { + "message": "**%stars%** %emoji% in %channelMention%", + "color": "#f5c91b", + "description": "%content%", + "image": "%image%", + "author": { + "name": "%displayName%", + "img": "%userAvatar%", + "url": "%link%" + } + }, + "params": { + "stars": { + "description": "Amount of reactions on the message" + }, + "content": { + "description": "The content of the starred message" + }, + "link": { + "description": "A link to the starred message" + }, + "userID": { + "description": "The user ID of the author of the starred message" + }, + "userName": { + "description": "The username of the author of the starred message" + }, + "displayName": { + "description": "The nickname of the author" + }, + "userTag": { + "description": "The tag of the author of the starred message" + }, + "userAvatar": { + "description": "The avatar URL of the message author" + }, + "channelName": { + "description": "The name of the channel the starred message was sent in" + }, + "channelMention": { + "description": "The channel mention of the channel the starred message was sent in" + }, + "emoji": { + "description": "The set starboard emoji for lazy users" + }, + "image": { + "description": "The first attachment or the first image url in the message" + } + } + }, + "excludedChannels": { + "humanName": "Excluded channels", + "description": "In which channels messages cannot be starred" + }, + "excludedRoles": { + "humanName": "Excluded roles", + "description": "Users with these roles cannot star messages" + }, + "minStars": { + "humanName": "Minimum stars", + "description": "How many star reactions are needed for a message to land on the starboard" + }, + "starsPerHour": { + "humanName": "Stars per user per hour", + "description": "How many messages a user can star per hour" + }, + "selfStar": { + "humanName": "Self-Star", + "description": "Whether users can star their own messages" + } + } + } + }, + "status-roles": { + "_module": { + "humanReadableName": "Status-roles", + "description": "Simple module to reward users who have an invite to your server in their status!" + }, + "config": { + "description": "Configure the function of the module here", + "humanName": "Configuration", + "content": { + "words": { + "humanName": "Words", + "description": "Words users should have in their status." + }, + "roles": { + "humanName": "Roles", + "description": "Roles to give to users with one of the words in their status" + }, + "remove": { + "humanName": "Remove all other roles", + "description": "Remove all other roles from users with one of the words in their status" + }, + "ignoreOfflineUsers": { + "humanName": "Do not remove roles from offline users", + "description": "When users are offline, they don't have a status, leading to the role being removed. If enabled, the status role won't be removed from offline users, only users that have a different status. Recommended on servers with more than 500 members." + } + } + } + }, + "sticky-messages": { + "_module": { + "humanReadableName": "Sticky messages", + "description": "Let a set message always appear at the end of a channel." + }, + "sticky-messages": { + "description": "Manage the sticky messages here", + "humanName": "Sticky messages", + "content": { + "channelId": { + "humanName": "Channel", + "description": "Channel-ID in which the message should get send" + }, + "message": { + "humanName": "Message", + "description": "Message that should get send", + "default": "" + }, + "respondBots": { + "humanName": "Respond to bots", + "description": "Whether your bot reacts to messages from other bots in the channel" + } + } + } + }, + "suggestions": { + "_module": { + "humanReadableName": "Suggestions", + "description": "Advanced module to manage suggestions on your guild" + }, + "config": { + "description": "Configure the function of the module here", + "humanName": "Configuration", + "content": { + "suggestionChannel": { + "humanName": "Suggestion-Channel", + "description": "Channel in which this module should operate" + }, + "createSuggestionFromMessagesInChannel": { + "humanName": "Create suggestions from messages in channel", + "description": "If enabled, the bot will create thread under each suggestion" + }, + "reactions": { + "humanName": "Reactions", + "description": "Emojis with which the bot should react to a new suggestion" + }, + "allowUserComment": { + "humanName": "User-Comments in Threads", + "description": "If enabled, the bot will create thread under each suggestion" + }, + "threadName": { + "humanName": "Thread-Name", + "description": "Name of the thread", + "default": "Comments" + }, + "successfullySubmitted": { + "humanName": "\"Successfully submitted\"-Message", + "description": "This message gets send if a suggestion is submitted successfully.", + "default": "Suggestion %id% submitted successfully.", + "params": { + "id": { + "description": "ID of the suggestion" + } + } + }, + "notifyRole": { + "humanName": "Notification-Role", + "description": "If set, this role gets pinged when a new suggestion gets created" + }, + "sendPNNotifications": { + "humanName": "Send DM-Notifications", + "description": "If enabled the creator and all commentators get a notification when something changes on a suggestion" + }, + "teamChange": { + "humanName": "DM-Status-Notification", + "description": "This message gets send to the creator and all commentators when a suggestion gets updated and sendPNNotifications is enabled", + "default": "Hi, a suggestion you are subscribed to got updated by a team member - read it here %url%", + "params": { + "url": { + "description": "URL to the suggestion" + }, + "title": { + "description": "Title of the suggestion" + } + } + }, + "unansweredSuggestion": { + "humanName": "Unanswered Suggestion-Message", + "description": "This will be the messages that will get send when the user creates their suggestion and no admin has responded yet", + "default": { + "title": "Suggestion #%id%", + "description": "%suggestion%", + "color": "#F1C40F", + "thumbnail": "%avatarURL%", + "author": { + "name": "%tag%", + "img": "%avatarURL%" + }, + "fields": [ + { + "name": "Suggestion-Status", + "value": "No admin answered to this suggestion yet" + } + ] + }, + "params": { + "id": { + "description": "ID of the suggestion" + }, + "suggestion": { + "description": "Content of the suggestion" + }, + "tag": { + "description": "Tag of the user who created this suggestion" + }, + "avatarURL": { + "description": "Avatar-URL of the user who created this suggestion" + } + } + }, + "deniedSuggestion": { + "humanName": "Denied Suggestion-Message", + "description": "The suggestion will be edited to this message, when an admin denies a suggestion", + "default": { + "title": "Suggestion #%id%", + "description": "%suggestion%", + "color": "#E74C3C", + "thumbnail": "%avatarURL%", + "author": { + "name": "%tag%", + "img": "%avatarURL%" + }, + "fields": [ + { + "name": "Suggestion-Status: DENIED", + "value": "Denied by %adminUser% with the following reason: \"%adminMessage%\"" + } + ] + }, + "params": { + "id": { + "description": "ID of the suggestion" + }, + "suggestion": { + "description": "Content of the suggestion" + }, + "tag": { + "description": "Tag of the user who created this suggestion" + }, + "avatarURL": { + "description": "Avatar-URL of the user who created this suggestion" + }, + "adminUser": { + "description": "Mention of the administrator who denied this suggestion" + }, + "adminMessage": { + "description": "Message by administrator who denied this suggestion" + } + } + }, + "approvedSuggestion": { + "humanName": "Approved Suggestion-Message", + "description": "The suggestion will be edited to this message, when an admin approves a suggestion", + "default": { + "title": "Suggestion #%id%", + "description": "%suggestion%", + "color": "#2ECC71", + "thumbnail": "%avatarURL%", + "author": { + "name": "%tag%", + "img": "%avatarURL%" + }, + "fields": [ + { + "name": "Suggestion-Status: APPROVED", + "value": "Approved by %adminUser% with the following reason: \"%adminMessage%\"" + } + ] + }, + "params": { + "id": { + "description": "ID of the suggestion" + }, + "suggestion": { + "description": "Content of the suggestion" + }, + "tag": { + "description": "Tag of the user who created this suggestion" + }, + "avatarURL": { + "description": "Avatar-URL of the user who created this suggestion" + }, + "adminUser": { + "description": "Mention of the administrator who approved this suggestion" + }, + "adminMessage": { + "description": "Message by administrator who approved this suggestion" + } + } + } + } + } + }, + "team-list": { + "_module": { + "humanReadableName": "Staff-List", + "description": "List all your staff members and explain team roles in always up-to-date embed" + }, + "config": { + "description": "Configure your team list embeds and displayed roles here", + "humanName": "Configuration", + "content": { + "channelID": { + "humanName": "Channel", + "description": "Channel-ID to run all operations in it" + }, + "roles": { + "humanName": "Listed Roles", + "description": "Roles that should be listed in the embed" + }, + "descriptions": { + "humanName": "Descriptions of roles", + "description": "Optional description of a listed role (Field 1: Role-ID, Field 2: Description)" + }, + "embed": { + "humanName": "Embed", + "description": "Configuration of the member-embed" + }, + "nameOverwrites": { + "humanName": "Name-Overwrites", + "description": "optional; Allows to overwrite the displayed name of roles (Field 1: Role-ID, Field 2: Displayed Name)" + }, + "includeStatus": { + "humanName": "Include Online-Status of Staff-Members", + "description": "If enabled, the current online status will be displayed in the staffmember-list" + }, + "onlineShowHighestRole": { + "humanName": "Only list the highest role of a user?", + "description": "If enabled, a staff member will only be listed under their highest role in the list." + } + } + } + }, + "temp-channels": { + "_module": { + "humanReadableName": "Temporary channels", + "description": "Allow users to quickly create voice channels by joining a voice channel" + }, + "config": { + "description": "Configure temporary voice channel creation settings here", + "humanName": "Configuration", + "categories": { + "general": { + "displayName": "General" + }, + "permissions": { + "displayName": "Permissions & Mode" + }, + "features": { + "displayName": "Features" + }, + "messages": { + "displayName": "Messages" + }, + "limits": { + "displayName": "Limits" + }, + "archiving": { + "displayName": "Archiving" + } + }, + "content": { + "channelID": { + "humanName": "Channel", + "description": "Set the channel here where users have to join to create their temp-channel" + }, + "category": { + "humanName": "Category", + "description": "You can set a category here in which the new channel should be created" + }, + "channelname_format": { + "humanName": "Channel name", + "description": "Change the format of the channel name here", + "default": "⏳ %username%", + "params": { + "username": { + "description": "Username of the user" + }, + "nickname": { + "description": "Nickname of the member" + }, + "number": { + "description": "The current number of the channel" + }, + "tag": { + "description": "Tag of the user" + } + } + }, + "timeout": { + "humanName": "Deletion timeout", + "description": "Set a timeout here in which the bot should wait before deleting the voice channel (in seconds)" + }, + "publicChannels": { + "humanName": "Default to public channels", + "description": "If enabled, new temp channels start public (synced with category). If disabled, channels start private (only the creator can join)." + }, + "allowUserToChangeMode": { + "humanName": "Allow change of channel mode", + "description": "If enabled the user has the permission to change the access-mode of the voice channel" + }, + "privateBypassRoles": { + "humanName": "Private Mode Bypass Roles", + "description": "Roles that can always join and see private temporary channels, regardless of who created them." + }, + "allowUserToChangeName": { + "humanName": "Allow editing the channel", + "description": "If enabled the user has the permission to change the name and settings of the voice channel via both, the Discord-integrated menus and the corresponding /-commands" + }, + "create_no_mic_channel": { + "humanName": "Create no-mic-channel", + "description": "If enabled the bot will create a separate text channel for each voice channel, visible only to users in the voice channel. Note: Discord now has built-in text-in-voice channels, so this is usually not needed." + }, + "noMicChannelMessage": { + "humanName": "No-Mic Channel Message", + "description": "You can set a message here that should be send in the no-mic-channel when created", + "default": "Welcome to your no-mic-channel - you can only see this channel if you are in the connected voicechat" + }, + "useNoMic": { + "humanName": "No-Mic Channel for Settings", + "description": "If enabled the settings menu will be sent into the no-mic-channel. If no-mic-channels aren't enabled, the menu will instead be sent to Discord's integrated text-in-voice channels" + }, + "settingsChannel": { + "humanName": "Settings channel", + "description": "You can set a channel here in which the settings menu should be created. Leave this field empty, if you don't want to use this feature." + }, + "send_dm": { + "humanName": "Send DM", + "description": "Should the bot send a direct message to a user when a new channel is created for them?" + }, + "dm": { + "humanName": "DM Message Content", + "description": "The direct message content sent to the user when their temporary channel is created.", + "default": "I have created and moved you to your new voice-channel - have fun ^^", + "params": { + "channelname": { + "description": "Name of the channel" + } + } + }, + "notInChannel": { + "humanName": "Not in Channel Message", + "description": "This message gets sent to a user who tries to edit their channel while not being in it.", + "default": "You have to be in your temp-channel to do this" + }, + "modeSwitched": { + "humanName": "Mode Switched Message", + "description": "This message gets sent to a user, after they changed the mode of their channel", + "default": "The access-mode of your channel has been switched to %mode%", + "params": { + "mode": { + "description": "Mode of the channel" + } + } + }, + "userAdded": { + "humanName": "User Added Message", + "description": "This message gets sent to a user, after they added an user to their channel", + "default": "the user %user% has been added to your channel. They can now access it whenever they like to", + "params": { + "user": { + "description": "The user, that was added" + } + } + }, + "userRemoved": { + "humanName": "User Removed Message", + "description": "This message gets sent to a user, after they removed an user from their channel", + "default": "the user %user% has been removed from your channel. They can no longer access it, while your channel is private", + "params": { + "user": { + "description": "The user, that was removed" + } + } + }, + "listUsers": { + "humanName": "List Users Message", + "description": "The message to be sent when a user requests a list of users with access to their channel.", + "default": "Here is a list of all the users that have access to your channel: %users%", + "params": { + "users": { + "description": "List of users with access" + } + } + }, + "channelEdited": { + "humanName": "Channel Edited Message", + "description": "The message to be sent when a user edits their channel.", + "default": "Your channel was edited" + }, + "edit-error": { + "humanName": "Edit Error Message", + "description": "The message sent when a channel edit fails.", + "default": "An error occurred while editing your channel. One or more of your settings could not be applied. This could be due to missing permissions or an invalid value" + }, + "settingsMessage": { + "humanName": "Settings Panel Message", + "description": "Set the message that should get send in the channel specified above to let the users change the settings of their temp-channels", + "default": "Change the Settings of your temporary channel here" + }, + "enableMaxActiveChannels": { + "humanName": "Enable channel limit", + "description": "If enabled, the bot will limit the number of temporary channels that can exist at the same time." + }, + "maxActiveChannels": { + "humanName": "Maximum active channels", + "description": "Maximum number of temp channels that can exist at the same time." + }, + "maxActiveChannelsMessage": { + "humanName": "Channel Limit Reached Message", + "description": "This message is sent via DM when a user tries to create a temp channel but the limit has been reached.", + "default": "⚠️ The maximum number of temporary channels has been reached. Please try again later." + }, + "enableArchiving": { + "humanName": "Enable channel archiving", + "description": "If enabled, empty temp channels will be moved to an archive category instead of being deleted. Channels are restored when the creator rejoins the trigger channel." + }, + "archiveCategory": { + "humanName": "Archive category", + "description": "Category where archived temp channels are moved to. Make this category hidden from regular users." + }, + "archiveDeleteAfterHours": { + "humanName": "Delete archived channels after (hours)", + "description": "Hours after which archived channels are permanently deleted. Set to 0 to never auto-delete. Default: 168 (7 days)." + } + } + } + }, + "tic-tak-toe": { + "_module": { + "humanReadableName": "Tic Tac Toe", + "description": "Let your users play Tick-Tac-Toe against each other!" + } + }, + "tickets": { + "_module": { + "humanReadableName": "Ticket-System", + "description": "Let users create tickets to message your staff" + }, + "config": { + "description": "Manage the basic settings of this module here", + "humanName": "Configuration", + "configElementName": { + "one": "Ticket-Category", + "more": "Ticket-Categories" + }, + "content": { + "name": { + "humanName": "Name", + "description": "Name of the Ticket type. This will be shown to users", + "default": "Support" + }, + "ticket-create-category": { + "humanName": "Ticket create category", + "description": "Category in which tickets should get created." + }, + "ticket-create-channel": { + "humanName": "Ticket creation channel", + "description": "Channel in which a message with a \"Create Ticket\" button should get send" + }, + "ticketRoles": { + "humanName": "Ticket Roles", + "description": "Users who get pinged in the tickets and who can see tickets" + }, + "logChannel": { + "humanName": "Log channel", + "description": "Channel in which ticket logs should get send" + }, + "ticket-create-message": { + "humanName": "Ticket created message", + "description": "Message that gets send/edited in the ticket-create-channel", + "default": "Click the big button below to contact our staff and create a ticket" + }, + "sendUserDMAfterTicketClose": { + "humanName": "Send user DM after ticket is closed", + "description": "If enabled users get a DM from the bot after someone closes the ticket" + }, + "userDM": { + "humanName": "User DM", + "description": "This message gets send to the user if sendUserDMAfterTicketClose is enabled", + "default": "Thanks for contacting our support for the ticket-category \"%type%\", here is your transcript: %transcriptURL%", + "params": { + "transcriptURL": { + "description": "URL to transcript" + }, + "type": { + "description": "Name of this ticket type" + } + } + }, + "creation-message": { + "humanName": "Ticket-Created Message", + "description": "This message will get sent in new tickets. The close buttons will be added.", + "default": { + "title": "📥 New ticket #%id%", + "color": "#2ECC71", + "message": "%rolePings%", + "fields": [ + { + "name": "👤 User", + "value": "%userMention%", + "inline": true + }, + { + "name": "☕ Ticket-Topic", + "value": "%ticketTopic%", + "inline": true + }, + { + "name": "ℹ️ Information", + "value": "Your issue got solved? Click the button below. You can always find this message pinned." + } + ] + }, + "params": { + "id": { + "description": "Unique identification number of the ticket" + }, + "userMention": { + "description": "Mention of the user who created this ticket" + }, + "rolePings": { + "description": "Mention of the roles you have selected in the \"Ticket roles\" field" + }, + "ticketTopic": { + "description": "Name of the Ticket-Topic" + }, + "userTag": { + "description": "Tag of the user who created this ticket" + } + } + }, + "ticket-create-button": { + "humanName": "Ticket create button", + "description": "Button for creating a ticket", + "default": "Create ticket 🎫" + }, + "ticket-close-button": { + "humanName": "Ticket close button", + "description": "Button for closing a ticket", + "default": "❎ Close ticket" + } + } + } + }, + "twitch-notifications": { + "_module": { + "humanReadableName": "Twitch-Notifications", + "description": "Module that sends a message to a channel, when a streamer goes live on Twitch" + }, + "streamers": { + "description": "Configure here, where for what streamer which message should get send", + "humanName": "Streamers", + "content": { + "liveMessage": { + "humanName": "Live-Messages", + "description": "Message that gets send if the streamer goes live", + "default": "Hey, %streamer% is live on Twitch streaming %game%! Check it out: %url%", + "params": { + "streamer": { + "description": "Name of the Streamer" + }, + "game": { + "description": "Game which is streamed" + }, + "url": { + "description": "Link to the stream" + }, + "title": { + "description": "Title of the Stream" + }, + "thumbnailUrl": { + "description": "The Link to the thumbnail of the Stream" + } + } + }, + "liveMessageChannel": { + "humanName": "Channel", + "description": "Channel in which live-message should get sent" + }, + "streamer": { + "humanName": "Streamer", + "description": "Streamer where a notification should send when they start streaming", + "default": "" + }, + "liveRole": { + "humanName": "Use Live-Role", + "description": "Should the Live-Role be activated?" + }, + "id": { + "humanName": "Discord-User ID", + "description": "ID of the Discord-Account of the Streamer" + }, + "role": { + "humanName": "Live Role", + "description": "ID of the Role that the Streamer should get, when live" + } + } + } + }, + "uno": { + "_module": { + "humanReadableName": "Uno", + "description": "Let your users play Uno against each other!" + } + }, + "welcomer": { + "_module": { + "humanReadableName": "Welcome and Boosts", + "description": "Simple module to say \"Hi\" to new members, give them roles automatically and say \"thanks\" to users who boosted" + }, + "channels": { + "description": "Configure here in which channel which message should get send", + "humanName": "Channel", + "content": { + "channelID": { + "humanName": "Channel", + "description": "Channel in which the message should get send" + }, + "type": { + "humanName": "Channel-Type", + "description": "This sets in which content the channel should get used" + }, + "randomMessages": { + "humanName": "Random messages?", + "description": "If enabled the bot will randomly pick a messages instead of using the message option below" + }, + "message": { + "humanName": "Message", + "description": "Message that should get send", + "default": "", + "params": { + "mention": { + "description": "Mention of the user who unboosted" + }, + "memberProfilePictureUrl": { + "description": "URL of the user's avatar" + }, + "servername": { + "description": "Name of the guild" + }, + "tag": { + "description": "Tag of the user" + }, + "createdAt": { + "description": "Date when account was created" + }, + "memberProfileBannerUrl": { + "description": "URL of the banner's avatar" + }, + "joinedAt": { + "description": "Date when user joined guild" + }, + "guildUserCount": { + "description": "Count of users on the guild" + }, + "guildMemberCount": { + "description": "Count of members (without bots) on the guild" + }, + "boostCount": { + "description": "Total count of boosts" + }, + "guildLevel": { + "description": "Boost-Level of the guild after the boost" + } + } + }, + "welcome-button": { + "humanName": "Welcome-Button (only if \"Channel-Type\" = \"join\")", + "description": "If enabled, a welcome-button will be attached to the welcome message. When a user clicks on it, the bot will send a welcome-ping in a configured channel. The button can be pressed once." + }, + "welcome-button-content": { + "humanName": "Welcome-Button-Content", + "description": "Content of the welcome button", + "default": "Say hi 👋" + }, + "welcome-button-channel": { + "humanName": "Channel in which the welcome-button should send a message", + "description": "The bot will send the configured message in this channel when a user presses the button" + }, + "welcome-button-message": { + "humanName": "Welcome-Button-Message", + "description": "This is the message the bot will send in the configured channel when a user presses the button", + "default": "%clickUserMention% welcomes %userMention% :wave:", + "params": { + "userMention": { + "description": "Mention of the user who joined the server" + }, + "userTag": { + "description": "Tag of the user who joined the server" + }, + "userAvatarURL": { + "description": "Avatar of the user who joined the server" + }, + "clickUserMention": { + "description": "Mention of the user who clicked the button" + }, + "clickUserTag": { + "description": "Tag of the user who clicked the button" + }, + "clickUserAvatarURL": { + "description": "Avatar of the user who clicked the button" + } + } + } + } + }, + "random-messages": { + "description": "Manage the randomly send messages here", + "humanName": "Random messages", + "content": { + "type": { + "humanName": "Message-Type", + "description": "This sets in which content the message should get send" + }, + "message": { + "humanName": "Message", + "description": "Message that should get send", + "default": "", + "params": { + "mention": { + "description": "Mention of the user who unboosted" + }, + "memberProfilePictureUrl": { + "description": "URL of the user's avatar" + }, + "servername": { + "description": "Name of the guild" + }, + "tag": { + "description": "Tag of the user" + }, + "createdAt": { + "description": "Date when account was created" + }, + "joinedAt": { + "description": "Date when user joined guild" + }, + "guildUserCount": { + "description": "Count of users on the guild" + }, + "guildMemberCount": { + "description": "Count of members (without bots) on the guild" + }, + "boostCount": { + "description": "Total count of boosts" + }, + "guildLevel": { + "description": "Boost-Level of the guild after the unboost" + } + } + } + } + }, + "config": { + "description": "Manage the basic settings of this module here", + "humanName": "Configuration", + "categories": { + "welcome": { + "displayName": "Welcome" + }, + "roles": { + "displayName": "Auto-Roles" + }, + "boost": { + "displayName": "Boosts" + } + }, + "content": { + "give-roles-on-join": { + "humanName": "Give roles on join", + "description": "Roles to give to a new member" + }, + "assign-roles-immediately": { + "humanName": "Immediately give roles, instead of waiting for rules acceptance?", + "description": "If enabled, roles will be granted immediately when a user joins your server. Otherwise, no roles will be assigned to users before they complete the Discord onboarding." + }, + "not-send-messages-if-member-is-bot": { + "humanName": "Ignore bots?", + "description": "Should bots get ignored when they join (or leave) the server" + }, + "give-roles-on-boost": { + "humanName": "Give additional roles to boosters", + "description": "Roles to give to members who boosts the server" + }, + "delete-welcome-message": { + "humanName": "Delete welcome message", + "description": "Should their welcome message be deleted, if a user leaves the server within 7 days" + }, + "sendDirectMessageOnJoin": { + "humanName": "Send DM on join? (often experienced by users as spam)", + "description": "If enabled, a DM will be sent to new users. This is often experienced by them as spam and can decrease your new user retention metrics. Please note that not all users will receive this DM, as a huge chunk has DMs disabled." + }, + "joinDM": { + "humanName": "Join DM Message", + "description": "Message that should get send to new users via DMs", + "default": "", + "params": { + "mention": { + "description": "Mention of the user who unboosted" + }, + "memberProfilePictureUrl": { + "description": "URL of the user's avatar" + }, + "servername": { + "description": "Name of the guild" + }, + "tag": { + "description": "Tag of the user" + }, + "createdAt": { + "description": "Date when account was created" + }, + "joinedAt": { + "description": "Date when user joined guild" + }, + "guildUserCount": { + "description": "Count of users on the guild" + }, + "guildMemberCount": { + "description": "Count of members (without bots) on the guild" + }, + "boostCount": { + "description": "Total count of boosts" + }, + "guildLevel": { + "description": "Boost-Level of the guild after the unboost" + } + } + } + } + } + } +} diff --git a/config-localizations/generate-files.js b/config-localizations/generate-files.js new file mode 100644 index 00000000..f06e5569 --- /dev/null +++ b/config-localizations/generate-files.js @@ -0,0 +1,322 @@ +/** + * Extracts English strings from all config JSON files and generates + * config-localizations/en.json for use as the Weblate reference file. + * + * Reads module.json config-example-files to discover ALL config files per module. + * Config files use inline English-only values (plain strings). This script + * extracts them into a structured JSON file that translators can work with. + * + * Also reports warnings for missing humanName/description fields and shows + * how many new strings were added compared to the previous en.json. + * + * Usage: node config-localizations/generate-files.js + */ + +const fs = require('fs'); +const path = require('path'); + +const ROOT = path.resolve(__dirname, '..'); +const OUTPUT_DIR = __dirname; +const OUTPUT_PATH = path.join(OUTPUT_DIR, 'en.json'); + +const extracted = {}; +const warnings = []; + +// Load previous en.json for comparison +let previousData = {}; +try { + previousData = JSON.parse(fs.readFileSync(OUTPUT_PATH, 'utf-8')); +} catch (e) { + // No previous file — everything will be new +} + +/** + * Extract English strings from a config file's top-level and content fields. + */ +function extractFromConfig(configData, filePath) { + const result = {}; + + // Top-level fields + for (const key of ['description', 'humanName', 'warningBanner']) { + if (typeof configData[key] === 'string' && configData[key].length > 0) { + result[key] = configData[key]; + } + } + + // Warn about missing top-level fields + if (!configData.humanName) { + warnings.push(`${filePath}: Missing top-level "humanName"`); + } + if (!configData.description) { + warnings.push(`${filePath}: Missing top-level "description"`); + } + + // informationBanner: can be a string or a complex object with nested strings + if (configData.informationBanner) { + if (typeof configData.informationBanner === 'string') { + result.informationBanner = configData.informationBanner; + } else if (typeof configData.informationBanner === 'object') { + result.informationBanner = configData.informationBanner; + } + } + + // configElementName: after conversion, this is {one: "...", more: "..."} or a string + if (configData.configElementName) { + if (typeof configData.configElementName === 'string') { + result.configElementName = configData.configElementName; + } else if (typeof configData.configElementName === 'object' && !Array.isArray(configData.configElementName)) { + result.configElementName = configData.configElementName; + } + } + + // commandsWarnings.special[].info + if (configData.commandsWarnings && Array.isArray(configData.commandsWarnings.special)) { + const cmdWarnings = {}; + for (const warning of configData.commandsWarnings.special) { + if (typeof warning.info === 'string' && warning.info.length > 0) { + cmdWarnings[warning.name] = {info: warning.info}; + } + } + if (Object.keys(cmdWarnings).length > 0) result.commandsWarnings = cmdWarnings; + } + + // categories[].displayName + if (Array.isArray(configData.categories)) { + const categories = {}; + for (const cat of configData.categories) { + if (typeof cat.displayName === 'string' && cat.displayName.length > 0) { + categories[cat.id] = {displayName: cat.displayName}; + } else if (!cat.displayName) { + warnings.push(`${filePath}: Category "${cat.id}" missing "displayName"`); + } + } + if (Object.keys(categories).length > 0) result.categories = categories; + } + + // content fields + if (Array.isArray(configData.content)) { + const contentResult = {}; + for (const field of configData.content) { + const fieldResult = extractFromField(field, filePath); + if (Object.keys(fieldResult).length > 0) { + contentResult[field.name] = fieldResult; + } + } + if (Object.keys(contentResult).length > 0) result.content = contentResult; + } + + return result; +} + +/** + * Extract English strings from a single content field. + */ +function extractFromField(field, filePath) { + const result = {}; + + // humanName and description + for (const key of ['humanName', 'description']) { + if (typeof field[key] === 'string' && field[key].length > 0) { + result[key] = field[key]; + } + } + + // Warn about missing required field properties + if (!field.humanName) { + warnings.push(`${filePath}: Field "${field.name}" missing "humanName"`); + } + if (!field.description) { + warnings.push(`${filePath}: Field "${field.name}" missing "description"`); + } + + // Only extract defaults for localizable types + if (['string', 'emoji', 'imgURL'].includes(field.type)) { + if (typeof field.default === 'string') { + result.default = field.default; + } else if (field.default && typeof field.default === 'object' && !Array.isArray(field.default)) { + // Embed default object (with title, description, etc.) + result.default = field.default; + } + } + + // params[].description + if (Array.isArray(field.params)) { + const params = {}; + for (const param of field.params) { + if (typeof param.description === 'string' && param.description.length > 0) { + params[param.name] = {description: param.description}; + } else if (!param.description) { + warnings.push(`${filePath}: Field "${field.name}" param "${param.name}" missing "description"`); + } + } + if (Object.keys(params).length > 0) result.params = params; + } + + // select content[].displayName (when content is array of objects) + if (Array.isArray(field.content) && field.content.length > 0 && typeof field.content[0] === 'object' && field.content[0] !== null) { + const selectOptions = {}; + for (const option of field.content) { + if (option && typeof option.displayName === 'string' && option.displayName.length > 0) { + selectOptions[option.value] = {displayName: option.displayName}; + } else if (option && !option.displayName) { + warnings.push(`${filePath}: Field "${field.name}" select option "${option.value}" missing "displayName"`); + } + } + if (Object.keys(selectOptions).length > 0) result.selectOptions = selectOptions; + } + + // links[].label + if (Array.isArray(field.links)) { + const links = {}; + for (let i = 0; i < field.links.length; i++) { + if (typeof field.links[i].label === 'string' && field.links[i].label.length > 0) { + links[field.links[i].url || i] = {label: field.links[i].label}; + } + } + if (Object.keys(links).length > 0) result.links = links; + } + + return result; +} + +/** + * Count all leaf string values in a nested object. + */ +function countStrings(obj) { + if (obj === null || obj === undefined) return 0; + if (typeof obj === 'string') return 1; + if (typeof obj !== 'object') return 0; + if (Array.isArray(obj)) return obj.reduce((sum, v) => sum + countStrings(v), 0); + return Object.values(obj).reduce((sum, v) => sum + countStrings(v), 0); +} + +/** + * Process a single config JSON file. + */ +function processFile(filePath, scope, fileName) { + let configData; + try { + configData = JSON.parse(fs.readFileSync(filePath, 'utf-8')); + } catch (e) { + console.warn(` Skipping ${filePath}: ${e.message}`); + return; + } + + // Skip non-config files + if (Array.isArray(configData) && !configData.content) return; + if (!configData.content && !configData.description && !configData.humanName) return; + + const result = extractFromConfig(configData, `${scope}/${fileName}.json`); + if (Object.keys(result).length === 0) return; + + if (!extracted[scope]) extracted[scope] = {}; + extracted[scope][fileName] = result; +} + +// Process config-generator files +console.log('Scanning config-generator/...'); +const coreDir = path.join(ROOT, 'config-generator'); +if (fs.existsSync(coreDir)) { + for (const file of fs.readdirSync(coreDir).sort()) { + if (!file.endsWith('.json')) continue; + const filePath = path.join(coreDir, file); + const fileName = file.replace('.json', ''); + console.log(` ${file}`); + processFile(filePath, '_core', fileName); + } +} + +// Process module config files using module.json +console.log('Scanning modules/...'); +const modulesDir = path.join(ROOT, 'modules'); +for (const moduleName of fs.readdirSync(modulesDir).sort()) { + const moduleDir = path.join(modulesDir, moduleName); + if (!fs.statSync(moduleDir).isDirectory()) continue; + + const moduleJsonPath = path.join(moduleDir, 'module.json'); + if (!fs.existsSync(moduleJsonPath)) continue; + + let moduleJson; + try { + moduleJson = JSON.parse(fs.readFileSync(moduleJsonPath, 'utf-8')); + } catch (e) { + console.warn(` Skipping ${moduleName}: invalid module.json`); + continue; + } + + // Extract module.json metadata (humanReadableName, description) + const moduleMetadata = {}; + if (typeof moduleJson.humanReadableName === 'string' && moduleJson.humanReadableName.length > 0) { + moduleMetadata.humanReadableName = moduleJson.humanReadableName; + } else if (!moduleJson.humanReadableName) { + warnings.push(`${moduleName}/module.json: Missing "humanReadableName"`); + } + if (typeof moduleJson.description === 'string' && moduleJson.description.length > 0) { + moduleMetadata.description = moduleJson.description; + } else if (!moduleJson.description) { + warnings.push(`${moduleName}/module.json: Missing "description"`); + } + if (typeof moduleJson.legalDisclaimer === 'string' && moduleJson.legalDisclaimer.length > 0) { + moduleMetadata.legalDisclaimer = moduleJson.legalDisclaimer; + } + if (typeof moduleJson.enableWarning === 'string' && moduleJson.enableWarning.length > 0) { + moduleMetadata.enableWarning = moduleJson.enableWarning; + } + if (Object.keys(moduleMetadata).length > 0) { + if (!extracted[moduleName]) extracted[moduleName] = {}; + extracted[moduleName]['_module'] = moduleMetadata; + } + + // Extract config files + const configFiles = moduleJson['config-example-files'] || []; + for (const configFile of configFiles) { + const filePath = path.join(moduleDir, configFile); + if (!fs.existsSync(filePath)) { + console.warn(` Warning: ${moduleName}/${configFile} listed in module.json but not found`); + continue; + } + const fileName = path.basename(configFile, '.json'); + console.log(` ${moduleName}/${configFile}`); + processFile(filePath, moduleName, fileName); + } +} + +// Count strings +const totalStrings = countStrings(extracted); +const previousStrings = countStrings(previousData); + +// Write en.json +fs.writeFileSync(OUTPUT_PATH, JSON.stringify(extracted, null, 2) + '\n'); +const scopeCount = Object.keys(extracted).length; +let fieldCount = 0; +for (const scope of Object.values(extracted)) { + for (const file of Object.values(scope)) { + if (file.content) fieldCount += Object.keys(file.content).length; + } +} + +console.log(`\nWritten ${OUTPUT_PATH}`); +console.log(` ${scopeCount} scopes, ${fieldCount} content fields`); +console.log(` ${totalStrings} total strings`); +if (previousStrings > 0) { + const newStrings = totalStrings - previousStrings; + if (newStrings > 0) { + console.log(` ${newStrings} new strings added since last generation`); + } else if (newStrings < 0) { + console.log(` ${Math.abs(newStrings)} strings removed since last generation`); + } else { + console.log(` No change in string count`); + } +} else { + console.log(` (first generation — all strings are new)`); +} + +// Report warnings +if (warnings.length > 0) { + console.log(`\n${warnings.length} warning(s):`); + for (const w of warnings) { + console.log(` - ${w}`); + } +} + +console.log('\nDone!'); diff --git a/config-localizations/getLocale.js b/config-localizations/getLocale.js new file mode 100644 index 00000000..f38442ad --- /dev/null +++ b/config-localizations/getLocale.js @@ -0,0 +1,449 @@ +/** + * Locale utilities for config-localizations JSON files. + * + * Exports: + * localize(stringName, locale, dir) + * Look up a single localized value by dot-path. + * + * getLocalizedConfig(configName, moduleName, locale, rootCustomBotDir) + * Return a full config file with all values localized. + * + * Usage: + * const { localize, getLocalizedConfig } = require('./config-localizations/getLocale'); + * + * localize('moderation.strings.content.ban_message.default', 'de', '/path/to/branch/config-localizations'); + * + * getLocalizedConfig('configs/config.json', 'moderation', 'de', '/path/to/bot'); + * getLocalizedConfig('config.json', null, 'de', '/path/to/bot'); // core config + */ + +const fs = require('fs'); +const path = require('path'); + +/** Cache TTL in ms (5 minutes). */ +const CACHE_TTL = 5 * 60 * 1000; + +// Keyed by "dir\0locale" to keep per-directory caches separate. +const cache = {}; + +/** + * Load and cache a locale file from a given directory. + * Re-reads from disk if the cache entry is older than CACHE_TTL. + */ +function loadLocale(dir, locale) { + const key = dir + '\0' + locale; + const entry = cache[key]; + if (entry && (Date.now() - entry.ts) < CACHE_TTL) return entry.data; + const filePath = path.join(dir, `${locale}.json`); + let data = null; + try { + data = JSON.parse(fs.readFileSync(filePath, 'utf-8')); + } catch { /* missing/unreadable file → null */ + } + cache[key] = { + data, + ts: Date.now() + }; + return data; +} + +/** + * Walk an object by a dot-separated path. Returns undefined on miss. + */ +function resolve(obj, dotPath) { + const keys = dotPath.split('.'); + let current = obj; + for (const key of keys) { + if (current == null || typeof current !== 'object') return undefined; + current = current[key]; + } + return current; +} + +/** + * Look up a localized string by dot-path. + * + * @param {string} stringName Dot-separated path, e.g. "moderation.strings.content.ban_message.default" + * @param {string} [locale] BCP-47 language code (e.g. "de"). Falls back to "en". + * @param {string} [dir] Directory containing the locale JSON files. Defaults to this file's directory. + * @returns {*} The resolved value, or undefined if not found. + */ +function localize(stringName, locale, dir) { + const configDir = dir || __dirname; + if (locale && locale !== 'en') { + const locData = loadLocale(configDir, locale); + if (locData) { + const value = resolve(locData, stringName); + if (value !== undefined) return value; + } + } + const enData = loadLocale(configDir, 'en'); + if (!enData) return undefined; + return resolve(enData, stringName); +} + +/** + * Return a full config example file with all values replaced by their + * localized equivalents. Falls back to English for missing translations. + * + * @param {string} configName Path to the config file relative to the module dir + * (e.g. "configs/config.json"). For core configs, relative + * to config-generator/ (e.g. "config.json"). + * @param {string|null} moduleName Module name (e.g. "moderation"), or null for core configs. + * @param {string} locale BCP-47 language code (e.g. "de"). Falls back to "en". + * @param {string} rootCustomBotDir Root directory of the custom bot installation. + * @returns {object|null} The localized config object, or null if the file doesn't exist. + */ +function getLocalizedConfig(configName, moduleName, locale, rootCustomBotDir) { + const configPath = moduleName + ? path.join(rootCustomBotDir, 'modules', moduleName, configName) + : path.join(rootCustomBotDir, 'config-generator', configName); + + let config; + try { + config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + } catch { + return null; + } + config = JSON.parse(JSON.stringify(config)); + + if (!locale || locale === 'en') return config; + + const locDir = path.join(rootCustomBotDir, 'config-localizations'); + const locData = loadLocale(locDir, locale); + const enData = loadLocale(locDir, 'en'); + + const scope = moduleName || '_core'; + const fileKey = path.basename(configName, '.json'); + const fileLoc = locData && locData[scope] && locData[scope][fileKey]; + + if (!fileLoc) return config; + + const enFile = enData && enData[scope] && enData[scope][fileKey]; + + function pick(locObj, enObj, key, original) { + if (locObj && locObj[key] !== undefined) return locObj[key]; + if (enObj && enObj[key] !== undefined) return enObj[key]; + return original; + } + + // Top-level metadata + for (const key of ['humanName', 'description', 'informationBanner']) { + if (fileLoc[key] !== undefined) config[key] = fileLoc[key]; + } + + // configElementName (e.g. { one: "punishment", more: "punishments" }) + if (fileLoc.configElementName && config.configElementName) { + const locCE = fileLoc.configElementName; + const enCE = enFile && enFile.configElementName; + for (const k of Object.keys(config.configElementName)) { + config.configElementName[k] = pick(locCE, enCE, k, config.configElementName[k]); + } + } + + // Categories — config: [{id, displayName, ...}], locale: {id: {displayName}} + if (fileLoc.categories && Array.isArray(config.categories)) { + const enCats = enFile && enFile.categories; + for (const cat of config.categories) { + const catLoc = fileLoc.categories[cat.id]; + const catEn = enCats && enCats[cat.id]; + if (catLoc || catEn) { + cat.displayName = pick(catLoc, catEn, 'displayName', cat.displayName); + } + } + } + + // Content fields — config: [{name, humanName, ...}], locale: {name: {humanName, ...}} + if (fileLoc.content && Array.isArray(config.content)) { + const enContent = enFile && enFile.content; + for (const field of config.content) { + const fLoc = fileLoc.content[field.name]; + const fEn = enContent && enContent[field.name]; + if (!fLoc && !fEn) continue; + + for (const key of ['humanName', 'description', 'default']) { + const val = pick(fLoc, fEn, key, undefined); + if (val !== undefined) field[key] = val; + } + + // Params — config: [{name, description}], locale: {name: {description}} + if (Array.isArray(field.params) && (fLoc && fLoc.params || fEn && fEn.params)) { + const pLoc = fLoc && fLoc.params; + const pEn = fEn && fEn.params; + for (const param of field.params) { + const paramLoc = pLoc && pLoc[param.name]; + const paramEn = pEn && pEn[param.name]; + if (paramLoc || paramEn) { + param.description = pick(paramLoc, paramEn, 'description', param.description); + } + } + } + } + } + + return config; +} + +/** + * List config files for a module with localized metadata. + * + * @param {string} locale BCP-47 language code (e.g. "de"). Falls back to "en". + * @param {string} moduleName Module directory name (e.g. "moderation"). + * @param {string} rootCustomBotDir Root directory of the custom bot installation. + * @returns {Array<{filename: string, humanName: string, description: string, fieldCount: number}>|null} + * Array of config summaries, or null if the module doesn't exist. + */ +function listLocalizedConfigs(locale, moduleName, rootCustomBotDir) { + const mjPath = path.join(rootCustomBotDir, 'modules', moduleName, 'module.json'); + let mj; + try { + mj = JSON.parse(fs.readFileSync(mjPath, 'utf-8')); + } catch { + return null; + } + + const configFiles = mj['config-example-files']; + if (!Array.isArray(configFiles) || configFiles.length === 0) return []; + + const locDir = path.join(rootCustomBotDir, 'config-localizations'); + const locData = locale && locale !== 'en' ? loadLocale(locDir, locale) : null; + const enData = loadLocale(locDir, 'en'); + + function pickVal(locObj, enObj, key, fallback) { + if (locObj && locObj[key] !== undefined) return locObj[key]; + if (enObj && enObj[key] !== undefined) return enObj[key]; + return fallback; + } + + const result = []; + for (const cfgPath of configFiles) { + const fullPath = path.join(rootCustomBotDir, 'modules', moduleName, cfgPath); + let cfg; + try { + cfg = JSON.parse(fs.readFileSync(fullPath, 'utf-8')); + } catch { + continue; + } + + const fileKey = path.basename(cfgPath, '.json'); + const fileLoc = locData && locData[moduleName] && locData[moduleName][fileKey]; + const fileEn = enData && enData[moduleName] && enData[moduleName][fileKey]; + + result.push({ + filename: cfgPath, + humanName: pickVal(fileLoc, fileEn, 'humanName', cfg.humanName || fileKey), + description: pickVal(fileLoc, fileEn, 'description', cfg.description || ''), + fieldCount: Array.isArray(cfg.content) ? cfg.content.length : 0 + }); + } + + return result; +} + +/** + * List all config files for every module with localized metadata. + * + * @param {string} locale BCP-47 language code (e.g. "de"). Falls back to "en". + * @param {string} rootCustomBotDir Root directory of the custom bot installation. + * @returns {Array<{moduleName: string, humanReadableName: string, moduleDescription: string, configs: Array<{filename: string, humanName: string, description: string, fieldCount: number}>}>} + */ +function listAllLocalizedConfigs(locale, rootCustomBotDir) { + const modulesDir = path.join(rootCustomBotDir, 'modules'); + let moduleDirs; + try { + moduleDirs = fs.readdirSync(modulesDir).sort(); + } catch { + return []; + } + + const locDir = path.join(rootCustomBotDir, 'config-localizations'); + const locData = loadLocale(locDir, locale && locale !== 'en' ? locale : null); + const enData = loadLocale(locDir, 'en'); + + function pickVal(locScope, enScope, key, fallback) { + if (locScope && locScope[key] !== undefined) return locScope[key]; + if (enScope && enScope[key] !== undefined) return enScope[key]; + return fallback; + } + + const result = []; + + for (const mod of moduleDirs) { + const mjPath = path.join(modulesDir, mod, 'module.json'); + let mj; + try { + mj = JSON.parse(fs.readFileSync(mjPath, 'utf-8')); + } catch { + continue; + } + + const configFiles = mj['config-example-files']; + if (!Array.isArray(configFiles) || configFiles.length === 0) continue; + + // Localized module metadata + const modLoc = locData && locData[mod] && locData[mod]._module; + const modEn = enData && enData[mod] && enData[mod]._module; + + const entry = { + moduleName: mod, + humanReadableName: pickVal(modLoc, modEn, 'humanReadableName', mj.humanReadableName || mod), + moduleDescription: pickVal(modLoc, modEn, 'description', mj.description || ''), + configs: [] + }; + + for (const cfgPath of configFiles) { + const fullPath = path.join(modulesDir, mod, cfgPath); + let cfg; + try { + cfg = JSON.parse(fs.readFileSync(fullPath, 'utf-8')); + } catch { + continue; + } + + const fileKey = path.basename(cfgPath, '.json'); + const fileLoc = locData && locData[mod] && locData[mod][fileKey]; + const fileEn = enData && enData[mod] && enData[mod][fileKey]; + + entry.configs.push({ + name: cfgPath.replaceAll('.json', ''), + filename: cfgPath.replaceAll('.json', '').replaceAll('configs/', ''), + humanName: pickVal(fileLoc, fileEn, 'humanName', cfg.humanName || fileKey), + description: pickVal(fileLoc, fileEn, 'description', cfg.description || ''), + fieldCount: Array.isArray(cfg.content) ? cfg.content.length : 0 + }); + } + + result.push(entry); + } + + return result; +} + +/** + * Return all modules with localized humanReadableName and description, + * plus static metadata from module.json. The author field is redacted to + * only { scnxOrgID } when a scnxOrgID is present. + * + * @param {string} rootCustomBotDir Root directory of the custom bot installation. + * @returns {Array} Array of module summary objects. + */ +function localizedModules(rootCustomBotDir) { + const modulesDir = path.join(rootCustomBotDir, 'modules'); + let moduleDirs; + try { + moduleDirs = fs.readdirSync(modulesDir).sort(); + } catch { + return []; + } + + const locDir = path.join(rootCustomBotDir, 'config-localizations'); + const enData = loadLocale(locDir, 'en'); + + // Collect all available locales + const locales = {}; + try { + for (const file of fs.readdirSync(locDir)) { + if (file.endsWith('.json')) { + const loc = file.replace('.json', ''); + locales[loc] = loadLocale(locDir, loc); + } + } + } catch { /* no localization dir */ + } + + const result = []; + + for (const mod of moduleDirs) { + const mjPath = path.join(modulesDir, mod, 'module.json'); + let mj; + try { + mj = JSON.parse(fs.readFileSync(mjPath, 'utf-8')); + } catch { + continue; + } + + if (mj.hidden) continue; + + // Build localized humanReadableName and description across all locales + const humanReadableName = {}; + const description = {}; + const legalDisclaimer = {}; + + for (const [loc, data] of Object.entries(locales)) { + const modLoc = data && data[mod] && data[mod]._module; + if (modLoc && modLoc.humanReadableName !== undefined) { + humanReadableName[loc] = modLoc.humanReadableName; + } + if (modLoc && modLoc.description !== undefined) { + description[loc] = modLoc.description; + } + if (modLoc && modLoc.legalDisclaimer !== undefined) { + legalDisclaimer[loc] = modLoc.legalDisclaimer; + } + } + + // English fallback from the file itself + if (!humanReadableName.en) humanReadableName.en = mj.humanReadableName || mod; + if (!description.en) description.en = mj.description || ''; + if (!legalDisclaimer.en && mj.legalDisclaimer) legalDisclaimer.en = mj.legalDisclaimer; + + // Author: redact to just scnxOrgID when it's set + let author = mj.author; + if (author && author.scnxOrgID) { + author = {scnxOrgID: author.scnxOrgID}; + } + + // Config file count + const configFiles = mj['config-example-files']; + const configFileCount = Array.isArray(configFiles) ? configFiles.length : 0; + + // Command count: count .js files in commands-dir + let commandCount = 0; + if (mj['commands-dir']) { + const cmdDir = path.join(modulesDir, mod, mj['commands-dir']); + try { + commandCount = fs.readdirSync(cmdDir).filter(f => f.endsWith('.js')).length; + } catch { /* no commands dir */ + } + } + + // Has database models + let hasDB = false; + if (mj['models-dir']) { + const modelsDir = path.join(modulesDir, mod, mj['models-dir']); + try { + hasDB = fs.readdirSync(modelsDir).some(f => f.endsWith('.js')); + } catch { /* no models dir */ + } + } + + const entry = { + name: mj.name || mod, + humanReadableName, + description, + tags: mj.tags || [], + 'fa-icon': mj['fa-icon'] || '', + author, + openSourceURL: mj.openSourceURL || null, + usesAICredits: mj.usesAICredits || false, + earlyAccess: mj.earlyAccess || false, + commandsCount: commandCount, + configFileCount, + hasDB + }; + + if (Object.keys(legalDisclaimer).length > 0) entry.legalDisclaimer = legalDisclaimer; + + result.push(entry); + } + + return result; +} + +module.exports = { + localize, + getLocalizedConfig, + listAllLocalizedConfigs, + listLocalizedConfigs, + localizedModules +}; \ No newline at end of file diff --git a/developer-docs/README.md b/developer-docs/README.md new file mode 100644 index 00000000..c0ed6228 --- /dev/null +++ b/developer-docs/README.md @@ -0,0 +1,42 @@ +# Developer Documentation + +Guides for people writing modules or contributing to the bot core. + +## Module authors + +Start here if you want to add a new feature as a module: + +- [**Writing a module**](./writing-a-module.md) - file layout, `module.json`, lifecycle, end-to-end example. +- [**Events**](./events.md) - event handler shape, lifecycle gates (`botReadyAt`, `allowPartial`, + `ignoreBotReadyCheck`), Discord and custom events you can listen to. +- [**Slash commands**](./commands.md) - `config` / `run` / `subcommands` / `autocomplete`, registration, options. +- [**Database models**](./database-models.md) - Sequelize `Model.init` pattern, `models-dir`, accessing models from + events. +- [**Localization**](./localization.md) - adding strings to `locales/en.json` and using `localize()`. + +## Configuration schema + +For module config files (`config.json`, `streamers.json`, etc.): + +- [**Configuration files**](./configuration.md) - schema reference: field types, defaults, `dependsOn`, `elementToggle`, + validation. +- [**Country localization**](./config-localization.md) - how user-facing strings in config files are extracted and + translated. + +## Message schemas + +The string + embed format used in `allowEmbed` config fields. Canonical reference (v2 / v3 / v4): + +- [V2 schema](https://docs.scnx.xyz/docs/scnx-api/reference/message-schema-v2/) - legacy, still parsed when `_schema` is + absent. +- [V3 schema](https://docs.scnx.xyz/docs/scnx-api/reference/message-schema-v3/) - tag with `"_schema": "v3"`. +- [V4 schema](https://docs.scnx.xyz/docs/scnx-api/reference/message-schema-v4/) - tag with `"_schema": "v4"`. + +## Migration + +- [**Migration**](./migration.md) - upgrading between major bot versions. + +## Validation + +Run `npm run verify-configs` to validate every module's config schema. CI runs this on every PR via +`.github/workflows/verify-configs.yml`. \ No newline at end of file diff --git a/developer-docs/commands.md b/developer-docs/commands.md new file mode 100644 index 00000000..eaf3f946 --- /dev/null +++ b/developer-docs/commands.md @@ -0,0 +1,184 @@ +# Slash Commands + +Commands live in a module's `commands-dir` (typically `commands/`). Each `.js` file is one slash command. The bot +collects all command files and syncs them with Discord at startup. + +## Minimum command + +```js +// modules/example/commands/ping.js +module.exports.config = { + name: 'ping', + description: 'Replies with pong.' +}; + +module.exports.run = async (interaction) => { + await interaction.reply({content: 'Pong!', ephemeral: true}); +}; +``` + +Two exports: + +- **`config`** - the slash command definition Discord registers. `name`, `description`, optional `options`, optional + `defaultMemberPermissions`. +- **`run`** - async function called when a user invokes the command. Receives the `ChatInputCommandInteraction`. + +## Options + +```js +const {ChannelType} = require('discord.js'); + +module.exports.config = { + name: 'archive', + description: 'Archive a channel.', + options: [ + { + type: 'CHANNEL', + name: 'channel', + description: 'Channel to archive.', + required: true, + channelTypes: [ChannelType.GuildText, ChannelType.GuildAnnouncement] + }, + { + type: 'STRING', + name: 'reason', + description: 'Why are you archiving it?', + required: false + } + ] +}; +``` + +Supported `type` strings: `STRING`, `INTEGER`, `BOOLEAN`, `USER`, `CHANNEL`, `ROLE`, `MENTIONABLE`, `NUMBER`, +`ATTACHMENT`, `SUB_COMMAND`, `SUB_COMMAND_GROUP`. (These are mapped to `ApplicationCommandOptionType` internally.) + +Read option values inside `run` with `interaction.options.getString('reason')`, `getChannel('channel', true)`, +`getInteger(...)`, etc. + +## Subcommands + +Use `SUB_COMMAND` options and export a `subcommands` map keyed by subcommand name: + +```js +module.exports.subcommands = { + 'add': async (interaction) => { /* ... */ }, + 'remove': async (interaction) => { /* ... */ }, + 'list': async (interaction) => { /* ... */ } +}; + +module.exports.config = { + name: 'role', + description: 'Manage self-assignable roles.', + options: [ + { + type: 'SUB_COMMAND', + name: 'add', + description: 'Add a role.', + options: [{type: 'ROLE', name: 'role', description: 'Role to add.', required: true}] + }, + { + type: 'SUB_COMMAND', + name: 'remove', + description: 'Remove a role.', + options: [{type: 'ROLE', name: 'role', description: 'Role to remove.', required: true}] + }, + { + type: 'SUB_COMMAND', + name: 'list', + description: 'List configured roles.' + } + ] +}; +``` + +When `subcommands` is exported, the loader dispatches to the matching key automatically - you don't need a top-level +`run`. (You may still export `run` as a fallback for commands that have both subcommands and a no-subcommand +invocation.) + +## Autocomplete + +For `STRING` / `INTEGER` / `NUMBER` options with `autocomplete: true`, export an `autocomplete` function: + +```js +module.exports.config = { + name: 'play', + description: 'Play a sound.', + options: [ + { + type: 'STRING', + name: 'sound', + description: 'Which sound to play.', + required: true, + autocomplete: true + } + ] +}; + +module.exports.autocomplete = async (interaction) => { + const focused = interaction.options.getFocused(); + const sounds = client.configurations['sounds']['catalog'] + .filter(s => s.name.toLowerCase().includes(focused.toLowerCase())) + .slice(0, 25); + await interaction.respond(sounds.map(s => ({name: s.name, value: s.id}))); +}; +``` + +## Permissions + +Restrict who can use a command at the Discord level with `defaultMemberPermissions`: + +```js +const {PermissionFlagsBits} = require('discord.js'); + +module.exports.config = { + name: 'kick', + description: 'Kick a member.', + defaultMemberPermissions: PermissionFlagsBits.KickMembers.toString(), + options: [/* ... */] +}; +``` + +For finer-grained checks (role-based, configurable per-server), do the check inside `run`: + +```js +module.exports.run = async (interaction) => { + const staffRoles = interaction.client.configurations['my-module']['config']['staffRoles']; + if (!interaction.member.roles.cache.some(r => staffRoles.includes(r.id))) { + return interaction.reply({content: '⚠️ Staff only.', ephemeral: true}); + } + // ... +}; +``` + +## Localization + +Use `localize()` for both descriptions and replies - see [localization.md](./localization.md). Descriptions are +evaluated at command registration time, so they always render in `client.locale`: + +```js +const {localize} = require('../../../src/functions/localize'); + +module.exports.config = { + name: 'help', + description: localize('help', 'command-description') +}; +``` + +## Defer when slow + +Discord requires a response within 3 seconds. If your command does anything slow (database lookups, API calls, file +I/O), defer immediately: + +```js +module.exports.run = async (interaction) => { + await interaction.deferReply({ephemeral: true}); + const result = await someSlowThing(); + await interaction.editReply({content: result}); +}; +``` + +## Where commands are registered + +Commands are registered as **guild commands** for the guild configured in `config/config.json`. Global registration is +not supported - this bot is single-guild by design. Reloading happens automatically at startup; new commands appear +within seconds. To force a re-sync without restart, run `/reload`. \ No newline at end of file diff --git a/developer-docs/config-localization.md b/developer-docs/config-localization.md new file mode 100644 index 00000000..44a69906 --- /dev/null +++ b/developer-docs/config-localization.md @@ -0,0 +1,274 @@ +# Config Localization System + +## Overview + +Configuration files (`config.json`) currently embed all translations inline as localized objects: + +```json +{ + "description": { + "en": "Configure settings", + "de": "Einstellungen konfigurieren" + }, + "humanName": { + "en": "Configuration", + "de": "Konfiguration" + } +} +``` + +The new system moves all non-English translations to external files in `config-localizations/.json`, keeping only +the English value inline as a plain string: + +```json +{ + "description": "Configure settings", + "humanName": "Configuration" +} +``` + +German (and any other language) lives in `config-localizations/de.json`: + +```json +{ + "module-name": { + "config": { + "description": "Einstellungen konfigurieren", + "humanName": "Konfiguration" + } + } +} +``` + +## What gets localized + +| Property | Where it appears | Localized? | +|-----------------------------------|-----------------------------------------------------|-------------------------------| +| `description` | Top-level, fields, params | Yes | +| `humanName` | Top-level, fields | Yes | +| `default` (string/embed types) | Fields with `type: "string"`, `"emoji"`, `"imgURL"` | Yes | +| `default` (all other types) | Booleans, integers, IDs, arrays, selects, keyed | **No** - values are universal | +| `displayName` | Categories, select options with object content | Yes | +| `configElementName` | Top-level (configElements files) | Yes | +| `warningBanner` | Top-level | Yes | +| `commandsWarnings.special[].info` | Top-level | Yes | +| `params[].description` | Inside field params | Yes | +| `links[].label` | Inside field links | Yes | + +### Why some defaults are not localized + +- **Booleans**: `true`/`false` - universal +- **Integers/Floats**: Numbers - universal +- **Colors**: Color names like `"GREEN"`, `"ORANGE"` or hex codes - universal +- **Channel/Role/User IDs**: Discord snowflakes - universal +- **Select values**: The stored value is a code (`"daily"`, `"none"`) - universal. The _display name_ of select options + IS localized separately +- **Arrays of IDs**: Lists of snowflakes - universal +- **Keyed maps**: Key-value maps where keys/values are IDs or numbers - universal +- **Timezones**: Timezone strings like `"Europe/Berlin"` - universal + +## Localization file structure + +``` +config-localizations/ + en.json # English (reference/fallback) + de.json # German + generate-files.js # Extraction script +``` + +Each language file follows this structure: + +```json +{ + "_core": { + "": { + "description": "...", + "humanName": "...", + "content": { + "": { + "humanName": "...", + "description": "...", + "default": "..." + } + } + } + }, + "": { + "": { + "description": "...", + "humanName": "...", + "categories": { + "": { + "displayName": "..." + } + }, + "content": { + "": { + "humanName": "...", + "description": "...", + "default": "...", + "params": { + "": { + "description": "..." + } + }, + "selectOptions": { + "": { + "displayName": "..." + } + } + } + } + } + } +} +``` + +- `_core` contains config-generator files (bot-level config, strings) +- Module names match directory names (`birthday`, `moderation`, `activity-streak`, etc.) +- File keys are filenames without `.json` (`config`, `lockdown`, `strings`, etc.) +- Only keys that have a translation are present - missing keys fall back to English + +## Extraction script + +`config-localizations/generate-files.js` scans all config files and extracts localized objects into per-language files: + +```bash +node config-localizations/generate-files.js +``` + +This regenerates ALL language files from the current config sources. Run it after modifying any config file. + +## Implementation plan + +### Phase 1: Generate localization files (done) + +The `generate-files.js` script extracts all existing translations into `en.json` and `de.json`. + +### Phase 2: Modify configuration loader + +Update `src/functions/configuration.js` to resolve translations from the external files. + +The `checkConfigFile` function needs to be updated so that when it reads a config schema, it checks if a field value is +a plain string (new format) or a localized object (old format for backwards compatibility). If it's a plain string, it +looks up the translation from `config-localizations/.json`. + +Specifically, a new function `resolveLocalization(scope, fileName, fieldPath, value, locale)` should: + +1. If `value` is already a localized object (`{en: ..., de: ...}`), use the old behavior (backwards compatible) +2. If `value` is a plain string/value (new format), look up the translation: + - Load `config-localizations/.json` (cache it) + - Navigate to `[scope][fileName][fieldPath]` + - Return the translated value if found, otherwise return the English value + +This must handle: + +- Top-level `description`, `humanName` +- Field-level `humanName`, `description`, `default` +- `params[].description` +- `categories[].displayName` +- `commandsWarnings.special[].info` +- Select option `displayName` +- `configElementName` +- `warningBanner` +- `links[].label` + +### Phase 3: Convert config files to new format + +Write a second script (`config-localizations/convert-configs.js`) that: + +1. Reads each config JSON file +2. For every localized object (`{en: ..., de: ...}`), replaces it with just the English value +3. Skips `default` on non-string types (they already aren't localized objects for boolean/integer/etc, but some may + have `{en: false}` which should become just `false`) +4. Writes the simplified config file back + +This converts: + +```json +{ + "description": { + "en": "Configure here", + "de": "Hier konfigurieren" + }, + "content": [ + { + "name": "enabled", + "type": "boolean", + "default": { + "en": false + }, + "description": { + "en": "Enable?", + "de": "Aktivieren?" + } + } + ] +} +``` + +To: + +```json +{ + "description": "Configure here", + "content": [ + { + "name": "enabled", + "type": "boolean", + "default": false, + "description": "Enable?" + } + ] +} +``` + +Note: `default: { "en": false }` becomes `default: false` - the `{en: ...}` wrapper is removed for ALL defaults, not +just strings. The localization files only store string defaults, but the config files should be cleaned up uniformly. + +### Phase 4: Update SCNX dashboard integration + +The SCNX dashboard reads config schemas directly. It needs to be updated to: + +1. Load the localization files +2. Apply translations when rendering field labels, descriptions, and defaults +3. Fall back to the inline English value when no translation exists + +### Phase 5: Add translation workflow + +- Add `config-localizations/` to the Weblate translation project +- Translators edit the language JSON files directly +- Running `generate-files.js` is only needed to bootstrap new configs or verify the structure +- New languages are added by creating a new `.json` file following the same structure + +## For module developers + +When writing a new config file, use plain English strings everywhere: + +```json +{ + "description": "Configure the example module", + "humanName": "Configuration", + "filename": "config.json", + "content": [ + { + "name": "logChannel", + "type": "channelID", + "humanName": "Log Channel", + "description": "Channel for log messages.", + "default": "" + }, + { + "name": "welcomeMessage", + "type": "string", + "allowEmbed": true, + "humanName": "Welcome Message", + "description": "Message sent when a user joins.", + "default": "Welcome %user%!" + } + ] +} +``` + +Translations are handled externally. After adding your config, run `node config-localizations/generate-files.js` to add +English entries to `en.json`. Translators will add the other languages. \ No newline at end of file diff --git a/developer-docs/configuration.md b/developer-docs/configuration.md new file mode 100644 index 00000000..a0f93487 --- /dev/null +++ b/developer-docs/configuration.md @@ -0,0 +1,567 @@ +# Module Configuration Files + +This guide explains how to write `config.json`, `streamers.json`, etc. - the JSON files in `modules//configs/`that +define a module's settings. The bot reads these to render config editors, validate values, and provide defaults. + +> **Format change.** As of bot v3, config files use **plain English strings** for `humanName`, `description`, defaults, +> etc. The old `{en: "...", de: "..."}` inline-localization format is no longer supported and `npm run verify-configs`will +> reject it. Translations now live in `config-localizations/.json` and are extracted by a separate script. +> See [config-localization.md](./config-localization.md). + +Selected developers can preview how their configuration files render in the SCNX dashboard +at https://scnx.app/developers/configuration after approval. The OSS bot reads the same files - dashboard preview is +optional. + +## File structure + +Every config file has the same top-level shape: + +```json +{ + "filename": "config.json", + "humanName": "Configuration", + "description": "Adjust messages and permissions here.", + "content": [] +} +``` + +| Field | Required | Description | +|---------------|----------|--------------------------------------------------------------------| +| `filename` | Yes | The generated config filename (must match the file's actual name). | +| `humanName` | Yes | Display name shown in the dashboard. | +| `description` | Yes | One-line description shown in the dashboard. | +| `content` | Yes | Array of field definitions (see below). | + +Optional top-level keys: `categories`, `commandsWarnings`, `configElements`, `configElementName`, `warningBanner`, +`hidden`, `skipContentCheck`. Each is documented in its own section below. + +## Field definitions + +Each entry in the `content` array defines one configuration field: + +```json +{ + "name": "staffRoles", + "humanName": "Staff Roles", + "description": "Roles that can manage this module.", + "type": "array", + "content": "roleID", + "default": [] +} +``` + +### Required field properties + +| Property | Description | +|---------------|-------------------------------------------------------------------| +| `name` | Internal key used in code (`moduleConfig.staffRoles`). camelCase. | +| `type` | Data type. See [Field types](#field-types) for the full list. | +| `humanName` | Display name shown in the dashboard. | +| `description` | Sentence explaining what the field does. | +| `default` | Default value. Must match the declared `type`. | + +### Optional field properties + +| Property | Applies to | Description | +|-------------------|--------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `category` | All types | Groups the field under a UI tab (see [Categories](#categories)). | +| `dependsOn` | All types | Only show this field when another named field is truthy. | +| `dependsOnNot` | All types | Only show this field when another named field is falsy. (Opposite of `dependsOn`.) | +| `allowNull` | `channelID`, `roleID`, `userID`, `guildID`, `integer`, `float`, `string` | Allow the field to be empty (`""` or `null`) without failing validation. | +| `allowEmbed` | `string` | Allow the user to configure an embed object instead of plain text. | +| `params` | `string` (with `allowEmbed`) | Document available `%placeholder%` variables (see [Parameters](#parameters)). | +| `content` | `array`, `keyed`, `select`, `channelID` | Sub-type, options, or allowed channel types (meaning depends on parent type). For `channelID`, an array of channel-type identifiers (see `channelID` below). | +| `maxValue` | `integer`, `float` | Maximum allowed numeric value. | +| `minValue` | `integer`, `float` | Minimum allowed numeric value. | +| `maxLength` | `array`, `string` | Maximum number of items (array) or characters (string). | +| `disableKeyEdits` | `keyed` | Prevent users from adding/removing keys; only existing values are editable. | +| `pro` | All types | Mark the field as paid-tier only (SCNX dashboard hides/disables for free plans). | +| `optional` | `string` | Field can be skipped without being explicitly null. | +| `links` | All types | Help links shown next to the field. Format: `[{"label": "...", "url": "..."}]`. | +| `hidden` | All types | Hide the field from the dashboard UI. The value is still loaded - useful for migration shims. | +| `elementToggle` | `boolean` (inside `configElements: true`) | Marks this field as the per-element enable toggle. **Only one allowed per file.** | + +## Field types + +The verifier accepts these `type` values: + +`string`, `emoji`, `imgURL`, `timezone`, `boolean`, `integer`, `float`, `channelID`, `roleID`, `userID`, `guildID`, +`array`, `keyed`, `select`. + +### `string` + +A text field. Set `allowEmbed: true` to also accept an embed object. + +```json +{ + "name": "welcomeMessage", + "humanName": "Welcome message", + "description": "Sent in the welcome channel when someone joins.", + "type": "string", + "allowEmbed": true, + "default": { + "title": "Welcome!", + "description": "Hello %user%" + }, + "params": [ + {"name": "user", "description": "Mention of the new member."} + ] +} +``` + +When `allowEmbed` is true, the value can be a plain string or an embed object. Embed schemas v2/v3/v4 are all +supported - tag v3/v4 explicitly with `"_schema": "v3"` (or `"v4"`). +Reference: [v2](https://docs.scnx.xyz/docs/scnx-api/reference/message-schema-v2/), [v3](https://docs.scnx.xyz/docs/scnx-api/reference/message-schema-v3/), [v4](https://docs.scnx.xyz/docs/scnx-api/reference/message-schema-v4/). + +### `emoji` + +Unicode or custom Discord emoji. + +```json +{ + "name": "starEmoji", + "humanName": "Star emoji", + "description": "Emoji used for the starboard reaction.", + "type": "emoji", + "default": "⭐" +} +``` + +### `imgURL` + +A URL pointing at an image. Treated as a string at runtime, but the dashboard renders an image picker. + +```json +{ + "name": "logo", + "humanName": "Logo", + "description": "URL of the server logo (used in welcome embeds).", + "type": "imgURL", + "default": "" +} +``` + +### `timezone` + +A timezone name like `Europe/Berlin`. Stored as a string; validate with a library (e.g. `Intl.DateTimeFormat`) before +using. + +```json +{ + "name": "guildTimezone", + "humanName": "Server timezone", + "description": "Used for daily reset jobs and date formatting.", + "type": "timezone", + "default": "UTC" +} +``` + +### `boolean` + +```json +{ + "name": "enabled", + "humanName": "Enabled", + "description": "Toggle the module on or off.", + "type": "boolean", + "default": false +} +``` + +### `integer` / `float` + +Numeric fields. Use `minValue` and `maxValue` to constrain the range. + +```json +{ + "name": "cooldownSeconds", + "humanName": "Cooldown (seconds)", + "description": "Minimum time between uses.", + "type": "integer", + "default": 60, + "minValue": 0, + "maxValue": 3600 +} +``` + +### `channelID` + +A channel picker. Use `content` to restrict to specific channel kinds. Without `content`, all common types are accepted. + +```json +{ + "name": "logChannel", + "humanName": "Log channel", + "description": "Channel for log messages.", + "type": "channelID", + "content": ["GUILD_TEXT", "GUILD_NEWS"], + "default": "", + "allowNull": true +} +``` + +Valid channel-type identifiers: `GUILD_TEXT`, `GUILD_VOICE`, `GUILD_CATEGORY`, `GUILD_NEWS` (announcement channels), +`GUILD_STAGE_VOICE`, `GUILD_FORUM`, `GUILD_MEDIA`, `GUILD_NEWS_THREAD`, `GUILD_PUBLIC_THREAD`, `GUILD_PRIVATE_THREAD`. + +### `roleID` + +A role picker. + +```json +{ + "name": "moderatorRole", + "humanName": "Moderator role", + "description": "Role granted access to moderation commands.", + "type": "roleID", + "default": "" +} +``` + +### `userID` + +A user picker. + +```json +{ + "name": "owner", + "humanName": "Bot owner", + "description": "User who receives critical alerts.", + "type": "userID", + "default": "" +} +``` + +### `guildID` + +A Discord guild ID. Use this for cross-guild references (e.g. emoji from another server). + +```json +{ + "name": "emojiGuild", + "humanName": "Emoji guild", + "description": "Server where custom emojis are stored.", + "type": "guildID", + "default": "" +} +``` + +### `array` + +A list of values. The `content` property defines the type of each item. + +```json +{ + "name": "adminRoles", + "humanName": "Admin roles", + "description": "Roles allowed to use admin commands.", + "type": "array", + "content": "roleID", + "default": [] +} +``` + +Valid `content` values: any scalar type (`roleID`, `channelID`, `userID`, `guildID`, `string`, `integer`, `emoji`, ...). +Use `maxLength` to limit the number of items. + +### `select` + +A dropdown. The `content` property defines the options. + +**Simple string options** (the stored value equals the displayed label): + +```json +{ + "name": "streakPeriod", + "humanName": "Streak period", + "description": "How often streak progress resets.", + "type": "select", + "content": ["daily", "weekly", "monthly"], + "default": "daily" +} +``` + +**Labeled options** (stored value differs from the label): + +```json +{ + "name": "curveType", + "humanName": "XP curve", + "description": "Formula used to calculate level requirements.", + "type": "select", + "content": [ + {"value": "LINEAR", "displayName": "Linear (default)"}, + {"value": "EXPONENTIAL", "displayName": "Exponential"}, + {"value": "CUSTOM", "displayName": "Custom formula"} + ], + "default": "LINEAR" +} +``` + +### `keyed` + +A key/value map. The `content` property defines the key and value types. + +```json +{ + "name": "rewardRoles", + "humanName": "Level reward roles", + "description": "Roles granted at specific levels.", + "type": "keyed", + "content": {"key": "integer", "value": "roleID"}, + "default": {} +} +``` + +Common combinations: + +| Key type | Value type | Use case | +|-------------|------------|--------------------------------------| +| `integer` | `roleID` | Level reward roles, milestone roles. | +| `roleID` | `float` | XP multiplier per role. | +| `channelID` | `float` | XP multiplier per channel. | +| `channelID` | `string` | Auto-react emojis per channel. | +| `roleID` | `string` | Descriptions per role. | + +Use `disableKeyEdits: true` when the keys are fixed and users should only edit values. + +## Categories + +Categories group fields into tabs in the dashboard. Without categories, all fields appear in a single list. + +```json +{ + "categories": [ + {"id": "general", "icon": "fas fa-gears", "displayName": "General"}, + {"id": "messages", "icon": "fas fa-comment", "displayName": "Messages"}, + {"id": "roles", "icon": "fas fa-user-shield", "displayName": "Roles & Permissions"} + ], + "content": [ + { + "name": "staffRoles", + "humanName": "Staff roles", + "description": "Roles that can manage this module.", + "type": "array", + "content": "roleID", + "category": "roles", + "default": [] + } + ] +} +``` + +| Property | Description | +|---------------|-----------------------------------------------------------------------------------| +| `id` | Internal identifier referenced by fields via `category: ""`. | +| `icon` | FontAwesome class. Browse and request icons at https://scnx.app/developers/icons. | +| `displayName` | Tab label. | + +Fields without a `category` appear in an uncategorized section. Use categories when your config has 7+ fields or +distinct logical groups; below that, a flat list is cleaner. + +## Conditional fields + +Use `dependsOn` to show a field only when another field is truthy: + +```json +[ + {"name": "enableCooldown", "humanName": "Enable cooldown", "description": "...", "type": "boolean", "default": false}, + {"name": "cooldownDuration", "humanName": "Cooldown (seconds)", "description": "...", "type": "integer", "default": 60, "dependsOn": "enableCooldown"} +] +``` + +`dependsOn` works with: + +- **Boolean fields** - shown when the boolean is `true`. +- **Select fields** - shown when the select is not `""` or `"none"`. + +`dependsOnNot` is the inverse - show the field when the named field is falsy. + +You can chain dependencies: A enables B which enables C. + +## Parameters + +For `string` fields with `allowEmbed: true`, document available `%placeholder%` variables with `params`: + +```json +{ + "name": "endMessage", + "humanName": "End message", + "description": "Posted when the game ends.", + "type": "string", + "allowEmbed": true, + "default": "Congrats %winner%, the number was %number%!", + "params": [ + {"name": "winner", "description": "Mention of the winner."}, + {"name": "number", "description": "The winning number."} + ] +} +``` + +In code, use `embedType()` from `src/functions/helpers.js` to substitute placeholders: + +```js +const {embedType} = require('../../../src/functions/helpers'); + +channel.send(embedType(moduleConfig.endMessage, { + '%winner%': member.toString(), + '%number%': game.number +})); +``` + +Param entries can also have: + +- `isImage: true` - the user can route this param into an embed `image`, `thumbnail`, `author.img`, or `footerImgUrl` + slot. +- `fieldValue: ""` - on a parent `select` field, the param is only available when the select equals this + value. + +## Config elements + +For configs where users create multiple instances of the same schema (ticket categories, team list entries, streamer +entries, ...), set `configElements: true` at the top level: + +```json +{ + "filename": "categories.json", + "humanName": "Ticket categories", + "description": "One entry per ticket category.", + "configElements": true, + "configElementName": {"one": "Ticket Category", "more": "Ticket Categories"}, + "content": [ + {"name": "channelID", "humanName": "Channel", "description": "Where new tickets are opened.", "type": "channelID", "default": ""}, + {"name": "enabled", "humanName": "Enabled", "description": "Toggle this category.", "type": "boolean", "default": true, "elementToggle": true}, + {"name": "message", "humanName": "Initial message", "description": "Sent when a ticket is created.", "type": "string", "allowEmbed": true, "default": "Hello!"} + ] +} +``` + +| Property | Description | +|---------------------|----------------------------------------------------------------------------------------| +| `configElements` | `true` to enable multi-element mode. The stored value is an array of objects. | +| `configElementName` | Singular/plural labels for the dashboard. `{one: "...", more: "..."}`. | +| `elementToggle` | On a single boolean field inside `content`, marks it as the per-element on/off toggle. | + +Add a new element from the CLI: `node add-config-element-object.js `. + +## Commands warnings + +Use `commandsWarnings` to tell users which slash commands need manual permission setup in their server settings: + +```json +{ + "commandsWarnings": { + "normal": ["/manage-levels"], + "special": [ + {"name": "/moderate", "info": "Each moderator needs explicit permission for this command in server settings."} + ] + } +} +``` + +- `normal` - simple list of command names that need permission configuration. +- `special` - commands that need additional explanation beyond just setting permissions. + +## Other top-level properties + +| Property | Description | +|--------------------|--------------------------------------------------------------------------------------------| +| `warningBanner` | Warning banner shown prominently at the top of the dashboard config page. | +| `hidden` | `true` to hide the entire file from the dashboard UI. Useful for credentials-only configs. | +| `skipContentCheck` | `true` to skip default-value normalization for this file. Use when the schema is dynamic. | + +## Validating + +Run `npm run verify-configs` to check every config file in the repo against this schema. CI runs the same script on +every PR via `.github/workflows/verify-configs.yml`. The script catches: + +- Missing required properties (`name`, `type`, `default`). +- Type mismatches between `type` and `default`. +- Unknown `type` values. +- `dependsOn` / `dependsOnNot` referencing non-existent fields. +- Multiple `elementToggle` fields in the same file. +- Duplicate field names. +- Defaults still using the deprecated localized format. +- Embed defaults that look like v3 messages but are missing `"_schema": "v3"`. + +## Full example + +```json +{ + "filename": "config.json", + "humanName": "Configuration", + "description": "Configure the example module.", + "commandsWarnings": { + "normal": [ + "/example" + ] + }, + "categories": [ + { + "id": "general", + "icon": "fas fa-gears", + "displayName": "General" + }, + { + "id": "messages", + "icon": "fas fa-comment", + "displayName": "Messages" + } + ], + "content": [ + { + "name": "enabled", + "humanName": "Enable module?", + "description": "Toggle this module on or off.", + "type": "boolean", + "category": "general", + "default": false + }, + { + "name": "logChannel", + "humanName": "Log channel", + "description": "Channel for log messages. Leave empty to disable.", + "type": "channelID", + "content": [ + "GUILD_TEXT" + ], + "category": "general", + "allowNull": true, + "dependsOn": "enabled", + "default": "" + }, + { + "name": "notificationMessage", + "humanName": "Notification message", + "description": "Sent when a user triggers the module.", + "type": "string", + "allowEmbed": true, + "category": "messages", + "dependsOn": "enabled", + "default": { + "title": "Notification", + "description": "Hello %user%!" + }, + "params": [ + { + "name": "user", + "description": "Mention of the user." + } + ] + } + ] +} +``` + +## Accessing config values in code + +Config values are available at runtime via `client.configurations`: + +```js +const moduleConfig = client.configurations['your-module']['config']; +const logChannel = moduleConfig.logChannel; +const isEnabled = moduleConfig.enabled; +``` + +The key under `client.configurations[moduleName]` is the config filename without `.json`. `configs/config.json` becomes +`client.configurations['your-module']['config']`; `configs/streamers.json` becomes +`client.configurations['your-module']['streamers']`. \ No newline at end of file diff --git a/developer-docs/database-models.md b/developer-docs/database-models.md new file mode 100644 index 00000000..5cde1376 --- /dev/null +++ b/developer-docs/database-models.md @@ -0,0 +1,101 @@ +# Database Models + +The bot uses [Sequelize](https://sequelize.org/) for persistence. The default driver is SQLite (`sqlite3` package), but +any Sequelize-supported database works. Each module declares its own models in `models-dir` (typically `models/`). + +## Defining a model + +A model file exports a class extending `Model` with a static `init(sequelize)` method: + +```js +// modules/welcomer/models/User.js +const {DataTypes, Model} = require('sequelize'); + +module.exports = class WelcomerUser extends Model { + static init(sequelize) { + return super.init({ + id: { + autoIncrement: true, + type: DataTypes.INTEGER, + primaryKey: true + }, + userID: DataTypes.STRING, + channelID: DataTypes.STRING, + messageID: DataTypes.STRING, + timestamp: DataTypes.DATE + }, { + tableName: 'welcomer_User', + timestamps: true, + sequelize + }); + } +}; +``` + +The loader calls `init(sequelize)` for you and registers the model under `client.models[][]`. The +filename without `.js` becomes the key - `User.js` → `client.models['welcomer']['User']`. + +### Conventions + +- **`tableName`**: prefix with the module name, e.g. `welcomer_User`, to avoid collisions across modules. +- **`timestamps: true`** adds `createdAt` and `updatedAt` automatically. Skip if you don't need them. +- **Primary key**: an auto-incrementing `id` is the simplest choice. Use a composite key only when you need it. +- **Class name**: doesn't have to match the filename, but matching keeps stack traces readable. Prefix with the module + if you have multiple modules with similarly-named models (e.g. `WelcomerUser` not just `User`). + +## Using models in handlers + +Models are available on `client.models` after the bot starts: + +```js +// modules/welcomer/events/guildMemberAdd.js +module.exports.run = async (client, member) => { + const User = client.models['welcomer']['User']; + await User.create({ + userID: member.id, + channelID: '...', + messageID: '...', + timestamp: new Date() + }); +}; +``` + +All standard Sequelize methods are available: `findOne`, `findAll`, `findOrCreate`, `update`, `destroy`, `count`, +`bulkCreate`, etc. + +## Migrations + +The bot calls `sequelize.sync()` at startup, which creates missing tables and adds missing columns automatically. **It +does not modify or remove existing columns.** If you change a column's type, rename it, or drop it, you have two +options: + +1. **Manual migration.** Use Sequelize's [umzug](https://github.com/sequelize/umzug) or write SQL by hand. Drop the + bot's table or run `ALTER TABLE` against your database. +2. **Bump the table name.** For breaking schema changes, rename `tableName` (e.g. `welcomer_User_v2`). The old table + stays in place for safety; you migrate data on the side. + +For non-trivial migrations across versions, the bot exposes `module.exports.migrationStart()` / `migrationEnd()` from +`main.js` - call these around long-running migration code so SIGTERM/SIGINT defers shutdown until the migration +finishes. + +## Associations + +Define associations from the module's `botReady` handler, after every model has been initialized: + +```js +// modules/example/events/botReady.js +module.exports.run = (client) => { + const A = client.models['example']['A']; + const B = client.models['example']['B']; + A.hasMany(B, {foreignKey: 'aId'}); + B.belongsTo(A, {foreignKey: 'aId'}); +}; +module.exports.ignoreBotReadyCheck = true; +``` + +## Performance notes + +- Use `attributes: ['col1', 'col2']` to limit returned columns on hot paths. +- Index columns you query on with `indexes: [{fields: ['userID']}]` in the second argument of `super.init`. +- Batch inserts with `bulkCreate` instead of looping `create`. +- For SQLite, write-heavy workloads benefit from `sequelize.transaction()` around batches. \ No newline at end of file diff --git a/developer-docs/events.md b/developer-docs/events.md new file mode 100644 index 00000000..69cc941f --- /dev/null +++ b/developer-docs/events.md @@ -0,0 +1,88 @@ +# Events + +Event handlers live in a module's `events-dir` (typically `events/`). The filename - without the `.js` extension - is +the event name. Discord.js events, custom client events, and submodule events are all handled the same way. + +## Handler shape + +```js +// modules/example/events/messageCreate.js +module.exports.run = async (client, message) => { + if (message.author.bot) return; + // ... +}; +``` + +A handler exports `run`. The bot calls it with `(client, ...args)` where `args` are whatever the underlying event emits. +For `messageCreate` that's a `Message`; for `guildMemberAdd` that's a `GuildMember`; for `voiceStateUpdate` that's +`(oldState, newState)`. + +The filename `messageCreate.js` registers a listener for the `messageCreate` event. You can have one file per event per +module - multiple modules can listen to the same event, and they will all run. + +## Lifecycle flags + +Three optional exports control when your handler runs: + +```js +module.exports.run = async (client, ...args) => { /* ... */ }; +module.exports.ignoreBotReadyCheck = true; // run before bot is fully ready (rare - usually leave false) +module.exports.allowPartial = true; // accept partial Discord structures (e.g. uncached messages) +``` + +Default behavior: + +- **`botReadyAt` gate**: handlers are skipped silently until `client.botReadyAt` is set (i.e. until config is loaded and + the guild is fetched). This prevents your code from running against half-initialized state. Set + `ignoreBotReadyCheck = true` only if you need to react to events during startup itself. +- **Partial gate**: if any argument is a partial structure (for example, a `messageDelete` for an uncached message), the + handler is skipped unless `allowPartial = true`. Set this when you can handle partials gracefully - for example, by + checking `if (message.partial) return;` early. + +## Errors + +Handler errors are caught by the loader and logged via `client.logger.error`. If Sentry is configured (SCNX builds), the +error is also reported. You don't need a top-level try/catch for safety - but you should still catch errors at +meaningful boundaries to log useful context. + +## Custom client events + +The bot emits its own events. Listen to them like any Discord event by naming your file accordingly: + +| Event | When it fires | File name | +|----------------|----------------------------------------------------------------------------------------------------------------------------------------------|-------------------| +| `botReady` | After config and commands have loaded, the guild has been fetched, and the bot is fully online. | `botReady.js` | +| `configReload` | After `config.json` and module configs have been (re-)loaded - including via `/reload`. Use this to invalidate caches that depend on config. | `configReload.js` | + +Example: invalidate a cached compiled formula when the user edits the formula in their config: + +```js +// modules/levels/events/configReload.js +module.exports.run = (client) => { + client.cache = client.cache || {}; + delete client.cache.levelFormula; +}; +module.exports.ignoreBotReadyCheck = true; +``` + +## Common Discord events used in this codebase + +| Event | Args | Typical use | +|---------------------|--------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------| +| `messageCreate` | `(message)` | Reactions, counter modules, AFK pings. | +| `messageDelete` | `(message)` (often partial - set `allowPartial`) | Anti-ghostping, sticky messages. | +| `messageUpdate` | `(oldMessage, newMessage)` | Edit logging, anti-ghostping. | +| `guildMemberAdd` | `(member)` | Welcomers, auto-roles, captcha. | +| `guildMemberRemove` | `(member)` | Goodbye messages, cleanup. | +| `guildMemberUpdate` | `(oldMember, newMember)` | Boost detection, role-driven side effects. | +| `interactionCreate` | `(interaction)` | Button/select-menu/modal handlers within a module. (Slash commands are handled separately - see [commands.md](./commands.md).) | +| `voiceStateUpdate` | `(oldState, newState)` | VC pings, temp channels, channel-stats. | +| `channelDelete` | `(channel)` | Cleanup of channel-bound config. | + +For the full list of Discord.js events, see +the [discord.js docs](https://discord.js.org/docs/packages/discord.js/14.26.2/Client:Class). + +## Module-disabled handling + +Handlers from a disabled module are not registered. If your handler depends on shared state from another module, check +`client.modules[''].enabled` defensively rather than assuming the model exists. \ No newline at end of file diff --git a/developer-docs/localization.md b/developer-docs/localization.md new file mode 100644 index 00000000..c4cf8c74 --- /dev/null +++ b/developer-docs/localization.md @@ -0,0 +1,64 @@ +# Localization + +The bot has two separate localization systems. Don't confuse them: + +| System | Purpose | Lives in | Authored where | +|--------------------|----------------------------------------------------------------------------------------------|--------------------------------------------|-----------------------------------------------------------------------------------------| +| **Code strings** | User-facing strings emitted by event handlers and slash commands (`localize()` calls in JS). | `locales/en.json`, `locales/de.json`, etc. | Hand-edited by developers. | +| **Config strings** | Field names and descriptions inside config files (`humanName`, `description`). | `config-localizations/en.json`, etc. | Generated from inline strings - see [config-localization.md](./config-localization.md). | + +This guide covers **code strings**. For config strings, see [config-localization.md](./config-localization.md). + +## Adding a string + +Strings are namespaced by module. Open `locales/en.json` and add a top-level key matching your module name (or extend an +existing one): + +```json +{ + "hello-world": { + "welcome": "Welcome %u to the server!", + "channel-not-found": "Configured welcome channel %c does not exist." + } +} +``` + +Then call `localize(namespace, key, params?)`: + +```js +const {localize} = require('../../../src/functions/localize'); + +await channel.send(localize('hello-world', 'welcome', {u: member.toString()})); +client.logger.error(localize('hello-world', 'channel-not-found', {c: channelID})); +``` + +`%u` and `%c` are placeholders - `localize()` substitutes them from the third argument (`{u: ..., c: ...}`). +Placeholders are arbitrary single-letter or short identifiers; pick whatever reads well in the source string. + +## Other languages + +This repository ships only `en.json` actively maintained. Translations for German, French, etc. exist in +`locales/.json` and are managed externally via Weblate. **Do not edit non-English locale files in this repository. +** Add new keys only to `en.json`; translations will follow. + +## Behavior at runtime + +`client.locale` is set from `--lang=` on the command line, defaulting to `en`. `localize()` looks up +`client.locale` first; if the key is missing, it falls back to `en`; if still missing, it returns the key itself so +missing translations are visible rather than silently empty. + +## Common mistakes + +- **Don't hard-code English strings in code.** Even one-off log messages should go through `localize()` so + other-language operators get readable logs. +- **Don't reuse a key across namespaces.** `localize('moderation', 'banned')` and `localize('admin-tools', 'banned')` + are independent - translators see them in separate contexts. +- **Don't dynamically build the namespace or key from user input.** That breaks translation tooling and creates + security/typo footguns. +- **Don't add keys for modules other than your own.** Each module owns its namespace. + +## Validation + +`npm run verify-configs` validates config schemas but does not currently lint `locales/*.json` for missing keys. If you +reference a key that doesn't exist, `localize()` returns the literal `.` string at runtime - easy to +spot in logs, but won't fail CI. \ No newline at end of file diff --git a/developer-docs/migration.md b/developer-docs/migration.md new file mode 100644 index 00000000..f9e49295 --- /dev/null +++ b/developer-docs/migration.md @@ -0,0 +1,351 @@ +# Database Migrations + +This guide explains how to write safe database migrations for CustomDCBot modules. + +## Why migrations are needed + +Sequelize's `db.sync()` (called in `main.js` at startup) creates tables that don't exist, but it **does not** add new +columns to existing tables. If you add a new field to a model, existing databases will be missing that column and +queries will fail. + +Migrations solve this by reading existing data, recreating the table with the new schema, and re-inserting the data. + +## Where migrations run + +Migrations go in your module's `events/botReady.js`, at the top of the `run` function - before any other logic. + +## The DatabaseSchemeVersion table + +Every migration is tracked using the shared `DatabaseSchemeVersion` model. Before running a migration, check if it has +already been applied: + +```js +const dbVersion = await client.models['DatabaseSchemeVersion'].findOne({ + where: { + model: 'your-module_YourModel', + version: 'V1' + } +}); +if (!dbVersion) { + // Run migration +} +``` + +After the migration completes, mark it as done: + +```js +await client.models['DatabaseSchemeVersion'].create({ + model: 'your-module_YourModel', + version: 'V1' +}); +``` + +The naming convention for `model` is `moduleName_ModelName` (e.g. `birthday_User`, `activity-streak_StreakUser`). + +## Migration pattern + +```js +const { + migrationStart, + migrationEnd +} = require('../../../main'); + +module.exports.run = async function (client) { + const dbVersion = await client.models['DatabaseSchemeVersion'].findOne({ + where: {model: 'your-module_YourModel'} + }); + if (!dbVersion) { + migrationStart(); + try { + client.logger.info('[your-module] Running V1 migration (adding newField)...'); + + // 1. Read existing data with EXPLICIT attributes (only columns that exist pre-migration) + const data = await client.models['your-module']['YourModel'].findAll({ + attributes: ['id', 'existingField1', 'existingField2'] + }); + + // 2. Drop and recreate the table with the new schema + await client.models['your-module']['YourModel'].sync({force: true}); + + // 3. Re-insert all data with the new field's default value + for (const row of data) { + await client.models['your-module']['YourModel'].create({ + id: row.id, + existingField1: row.existingField1, + existingField2: row.existingField2, + newField: false // default value for the new column + }); + } + + client.logger.info('[your-module] V1 migration complete.'); + await client.models['DatabaseSchemeVersion'].create({ + model: 'your-module_YourModel', + version: 'V1' + }); + } finally { + migrationEnd(); + } + } + + // ... rest of your botReady logic +}; +``` + +## Critical rules + +### Always use explicit attributes in findAll + +```js +// WRONG - will try to SELECT the new column that doesn't exist yet +const data = await client.models['your-module']['YourModel'].findAll(); + +// CORRECT - only selects columns that exist in the pre-migration table +const data = await client.models['your-module']['YourModel'].findAll({ + attributes: ['id', 'existingField1', 'existingField2'] +}); +``` + +Your model already defines the new field, so Sequelize will include it in the `SELECT` statement by default. Since the +column doesn't exist in the database yet, the query will crash. Always list only the columns that exist **before** your +migration. + +### Always wrap migrations in migrationStart/migrationEnd + +```js +const { + migrationStart, + migrationEnd +} = require('../../../main'); +``` + +Call `migrationStart()` before the migration begins and `migrationEnd()` when it finishes. **Always** use `try/finally` +to ensure `migrationEnd()` runs even if the migration throws an error. This prevents the bot from shutting down +mid-migration (which would cause data loss since `sync({force: true})` drops the table before recreating it). + +### Always re-insert with explicit field mapping + +```js +// WRONG - may carry over unexpected fields or miss the new default +await client.models['your-module']['YourModel'].create(row); + +// CORRECT - explicit mapping with new field default +await client.models['your-module']['YourModel'].create({ + id: row.id, + existingField1: row.existingField1, + newField: false +}); +``` + +### Mark the migration version after all data is re-inserted + +The `DatabaseSchemeVersion` entry should be created **after** all data has been successfully migrated. If the migration +fails halfway, it will re-run on next startup (which is safe since it checks the version first). + +## Multiple migrations + +Migrations stack sequentially. Each one runs in order and assumes all previous migrations have already been applied. +This matters for which columns you list in `attributes`. + +### Adding a second migration later + +When a new release needs another schema change, add a new migration block **after** the existing one: + +```js +// V1 migration (existing - added "hidden" field) +const dbVersion = await client.models['DatabaseSchemeVersion'].findOne({ + where: {model: 'your-module_YourModel'} +}); +if (!dbVersion) { + migrationStart(); + try { + const data = await client.models['your-module']['YourModel'].findAll({ + attributes: ['id', 'existingField1', 'existingField2'] + }); + await client.models['your-module']['YourModel'].sync({force: true}); + for (const row of data) { + await client.models['your-module']['YourModel'].create({ + id: row.id, + existingField1: row.existingField1, + existingField2: row.existingField2, + hidden: false + }); + } + await client.models['DatabaseSchemeVersion'].create({ + model: 'your-module_YourModel', + version: 'V1' + }); + } finally { + migrationEnd(); + } +} + +// V2 migration (new - added "priority" field) +const dbVersionV2 = await client.models['DatabaseSchemeVersion'].findOne({ + where: { + model: 'your-module_YourModel', + version: 'V2' + } +}); +if (!dbVersionV2) { + migrationStart(); + try { + // V1 has already run, so "hidden" exists in the table now + const data = await client.models['your-module']['YourModel'].findAll({ + attributes: ['id', 'existingField1', 'existingField2', 'hidden'] + }); + await client.models['your-module']['YourModel'].sync({force: true}); + for (const row of data) { + await client.models['your-module']['YourModel'].create({ + id: row.id, + existingField1: row.existingField1, + existingField2: row.existingField2, + hidden: row.hidden, + priority: 0 + }); + } + await client.models['DatabaseSchemeVersion'].upsert({ + model: 'your-module_YourModel', + version: 'V2' + }); + } finally { + migrationEnd(); + } +} +``` + +V2's `attributes` includes `hidden` because V1 has already added it by the time V2 runs. + +### Adding multiple fields in a single release + +If you're adding multiple new fields at the same time (e.g. both `hidden` and `priority` in the same release), you only +need **one** migration. Don't create separate migrations for each field - just handle them all in one version bump: + +```js +const dbVersion = await client.models['DatabaseSchemeVersion'].findOne({ + where: {model: 'your-module_YourModel'} +}); +if (!dbVersion) { + migrationStart(); + try { + const data = await client.models['your-module']['YourModel'].findAll({ + attributes: ['id', 'existingField1', 'existingField2'] + }); + await client.models['your-module']['YourModel'].sync({force: true}); + for (const row of data) { + await client.models['your-module']['YourModel'].create({ + id: row.id, + existingField1: row.existingField1, + existingField2: row.existingField2, + hidden: false, // new field 1 + priority: 0 // new field 2 + }); + } + await client.models['DatabaseSchemeVersion'].create({ + model: 'your-module_YourModel', + version: 'V1' + }); + } finally { + migrationEnd(); + } +} +``` + +### Fresh installs vs. existing databases + +On a fresh install (no existing database), `db.sync()` in `main.js` creates all tables with all columns from the model +definition. The migration check finds no existing rows and no `DatabaseSchemeVersion` entry. The migration runs but +`findAll` returns an empty array, so it effectively just creates the version entry. This is fine - the migration is a +no-op on empty tables. + +### Removing or renaming fields + +If you need to **remove** a column, the same pattern works - just don't include the removed field in the re-insert step. +The `sync({force: true})` recreates the table from the model definition (which no longer has the field), so the column +disappears. + +If you need to **rename** a column, read the old column name in `attributes` and write to the new column name during +re-insert: + +```js +const data = await client.models['your-module']['YourModel'].findAll({ + attributes: ['id', 'oldFieldName'] +}); +await client.models['your-module']['YourModel'].sync({force: true}); +for (const row of data) { + await client.models['your-module']['YourModel'].create({ + id: row.id, + newFieldName: row.oldFieldName // renamed + }); +} +``` + +### Changing a field's type + +Same approach - read the old data, recreate the table, convert during re-insert: + +```js +const data = await client.models['your-module']['YourModel'].findAll({ + attributes: ['id', 'count'] // was STRING, now INTEGER +}); +await client.models['your-module']['YourModel'].sync({force: true}); +for (const row of data) { + await client.models['your-module']['YourModel'].create({ + id: row.id, + count: parseInt(row.count, 10) || 0 + }); +} +``` + +### Multiple models in one module + +If your module has multiple models that both need migrations, run them independently with separate version keys: + +```js +// Model A migration +const dbVersionA = await client.models['DatabaseSchemeVersion'].findOne({ + where: {model: 'your-module_ModelA'} +}); +if (!dbVersionA) { + migrationStart(); + try { + // ... migrate ModelA ... + await client.models['DatabaseSchemeVersion'].create({ + model: 'your-module_ModelA', + version: 'V1' + }); + } finally { + migrationEnd(); + } +} + +// Model B migration +const dbVersionB = await client.models['DatabaseSchemeVersion'].findOne({ + where: {model: 'your-module_ModelB'} +}); +if (!dbVersionB) { + migrationStart(); + try { + // ... migrate ModelB ... + await client.models['DatabaseSchemeVersion'].create({ + model: 'your-module_ModelB', + version: 'V1' + }); + } finally { + migrationEnd(); + } +} +``` + +Each model tracks its own version independently. They don't need to share version numbers. + +## Checklist + +Before submitting a migration: + +- [ ] `findAll` uses explicit `attributes` listing only pre-migration columns +- [ ] Migration is wrapped in `migrationStart()` / `migrationEnd()` with `try/finally` +- [ ] New fields are explicitly set with their default value during re-insert +- [ ] `DatabaseSchemeVersion` entry is created **after** all data is re-inserted +- [ ] Version string follows the pattern `V1`, `V2`, etc. +- [ ] Model name follows the pattern `moduleName_ModelName` +- [ ] Migration runs at the top of `botReady.js` before any other module logic \ No newline at end of file diff --git a/developer-docs/writing-a-module.md b/developer-docs/writing-a-module.md new file mode 100644 index 00000000..bf1b857b --- /dev/null +++ b/developer-docs/writing-a-module.md @@ -0,0 +1,173 @@ +# Writing a Module + +A module is a self-contained folder under `modules/` that bundles together event handlers, slash commands, database +models, and configuration. The bot discovers and loads modules at startup based on each folder's `module.json`. + +## Minimum file layout + +``` +modules/ + hello-world/ + module.json # required - describes the module + events/ # optional - Discord & custom event handlers + messageCreate.js + commands/ # optional - slash commands + hello.js + models/ # optional - Sequelize models + Greeting.js + configs/ # optional - user-editable config files + config.json +``` + +Only `module.json` is mandatory. Everything else is opt-in via the matching `module.json` field. + +## `module.json` reference + +```json +{ + "name": "hello-world", + "humanReadableName": "Hello World", + "description": "Greets new members.", + "fa-icon": "fas fa-hand-wave", + "author": { + "name": "Your Name", + "link": "https://github.com/your-handle" + }, + "openSourceURL": "https://github.com/ScootKit/CustomDCBot/tree/main/modules/hello-world", + "tags": [ + "fun" + ], + "events-dir": "/events", + "commands-dir": "/commands", + "models-dir": "/models", + "config-example-files": [ + "configs/config.json" + ] +} +``` + +| Field | Required | Purpose | +|------------------------|----------|--------------------------------------------------------------------------------------------------------------| +| `name` | Yes | Internal id. Must match the folder name. Used as the namespace for `localize()` and `client.configurations`. | +| `humanReadableName` | Yes | Display name shown in dashboards and `/help`. | +| `description` | Yes | One-line summary. | +| `fa-icon` | No | FontAwesome class. Browse the supported set at https://scnx.app/developers/icons. | +| `author` | No | `{name, link}` shown in `/help`. `scnxOrgID` is dashboard-specific and ignored otherwise. | +| `openSourceURL` | No | Link to source in `/help`. | +| `tags` | No | Used by the dashboard to group modules. Free-form strings. | +| `events-dir` | No | Folder (relative to the module) scanned for event handlers. Convention: `/events`. | +| `commands-dir` | No | Folder scanned for slash commands. Convention: `/commands`. | +| `models-dir` | No | Folder scanned for Sequelize models. Convention: `/models`. | +| `config-example-files` | No | Paths (relative to the module) of config schema files. See [configuration.md](./configuration.md). | + +If you omit a `*-dir` key, that subsystem is skipped - there's no default. A module with only events doesn't need +`commands-dir`. + +## Lifecycle + +Bot startup, in order: + +1. Read `config/config.json` (the user's main config). +2. Discover modules - read each `module.json`, mark enabled/disabled. +3. Load core models, then each module's models (`models-dir`). +4. Load and validate each module's `config-example-files` against the user's actual config files in + `config//`. +5. Fire `client.emit('configReload')`. +6. Load core events, then each module's events (`events-dir`). +7. Connect to Discord, fetch the configured guild. +8. Load core commands, then each module's commands (`commands-dir`); sync slash commands with Discord. +9. Set `client.botReadyAt = new Date()` and fire `client.emit('botReady')`. + +After `botReadyAt` is set, queued events start firing. Until then, handlers without `ignoreBotReadyCheck = true` are +silently skipped - see [events.md](./events.md). + +## Accessing module state at runtime + +Inside any handler, the `client` object exposes everything the loader registered: + +```js +client.configurations['hello-world']['config'] // parsed configs/config.json +client.models['hello-world']['Greeting'] // Sequelize model class +client.modules['hello-world'] // {enabled, events: [...], ...} +client.guild // the configured guild (set after botReady) +client.logger // log4js logger - use this, not console +``` + +`client.configurations[][]` is keyed by the config filename without `.json`. +`configs/config.json` becomes `client.configurations['hello-world']['config']`; `configs/streamers.json` becomes +`client.configurations['hello-world']['streamers']`. + +## A complete minimal module + +``` +modules/hello-world/ +├── module.json +├── configs/config.json +└── events/guildMemberAdd.js +``` + +`module.json`: + +```json +{ + "name": "hello-world", + "humanReadableName": "Hello World", + "description": "Welcome message in a configured channel.", + "events-dir": "/events", + "config-example-files": [ + "configs/config.json" + ] +} +``` + +`configs/config.json`: + +```json +{ + "filename": "config.json", + "humanName": "Configuration", + "description": "Where to send the welcome message.", + "content": [ + { + "name": "channel", + "humanName": "Welcome channel", + "description": "Channel new members are greeted in.", + "type": "channelID", + "default": "" + } + ] +} +``` + +`events/guildMemberAdd.js`: + +```js +const {localize} = require('../../../src/functions/localize'); + +module.exports.run = async (client, member) => { + const {channel: channelID} = client.configurations['hello-world']['config']; + if (!channelID) return; + const channel = await client.channels.fetch(channelID).catch(() => null); + if (!channel) return; + await channel.send(localize('hello-world', 'welcome', {u: member.toString()})); +}; +``` + +`locales/en.json` (add a top-level key): + +```json +"hello-world": { +"welcome": "Welcome %u to the server!" +} +``` + +That's a working module. Run `npm run verify-configs` to confirm the config schema is valid, then start the bot with +`npm start`. + +## What to read next + +- [Events](./events.md) for handler patterns and the lifecycle gates that decide when your code runs. +- [Slash commands](./commands.md) when your module needs user-invokable commands. +- [Database models](./database-models.md) for persistent state. +- [Localization](./localization.md) for adding user-facing strings. +- [Configuration files](./configuration.md) for the full config schema reference. \ No newline at end of file diff --git a/generate-config.js b/generate-config.js index 656141ca..38dbaf30 100644 --- a/generate-config.js +++ b/generate-config.js @@ -14,12 +14,12 @@ if (args[0]) { try { require(`${confDir}/config.json`); console.error('Seems like you already have an config file! You can start the bot now with "npm start"!'); - process.exit(1); + process.exit(0); } catch (e) { console.log('[INFO] Starting generation...'); exampleFile.content.forEach(async field => { if (!field.name) return; - config[field.name] = field.default.en; + config[field.name] = (typeof field.default === 'object' && field.default !== null && 'en' in field.default) ? field.default.en : field.default; }); if (!fs.existsSync(`${confDir}`)) { diff --git a/locales/en.json b/locales/en.json index f0fec168..b71ae7f8 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1,1419 +1,1439 @@ { - "main": { - "startup-info": "SCNX-CustomBot v2 - Log-Level: %l", - "missing-moduleconf": "Missing moduleConfig-file. Automatically disabling all modules and overwriting modules.json later", - "sync-db": "Synced database", - "login-error": "Bot could not log in. Error: %e", - "login-error-token": "Bot could not log in because the provided token is invalid. Please update your token.", - "login-error-intents": "Bot could not log in because the intents were not enabled correctly. Please enable \"PRESENCE INTENT\", \"SERVER MEMBERS INTENT\" and \"MESSAGE CONTENT INTENT\" in your Discord-Developer-Dashboard: %url", - "not-invited": "Please invite the bot to your Discord server before continuing: %inv", - "require-code-grant-active": "You might be unable to invite your bot to your server as you have enabled the \"Require public code grant\" option in your Discord Developer Dashboard. Please disable this option: %d", - "interactions-endpoint-active": "You bot will be unable to respond to interactions, because the field \"Interactions Endpoint URL\" has a value in your Discord Developer Dashboard. Please remove any content from this field and restart your bot: %d", - "logged-in": "Bot logged in as %tag and is now online.", - "logchannel-wrong-type": "There is no Log-Channel set or it has the wrong type (only text-channels are supported).", - "config-check-failed": "Configuration-Check failed. You can find more information in your log. The bot exited.", - "bot-ready": "The bot initiated successfully and is now listening to commands", - "no-command-permissions": "Could not update server commands. Please give us permissions to performe this critical action: %inv", - "perm-sync": "Synced permissions for /%c", - "perm-sync-failed": "Failed to synced permissions for /%c: %e", - "loading-module": "Loading module %m", - "hidden-module": "Module %m is hidden, meaning that it is not available. Skipping…", - "module-disabled": "Module %m is disabled", - "command-loaded": "Loaded command %d/%f", - "command-dir": "Loading commands in %d/%f", - "global-command-sync": "Synced global application commands", - "guild-command-sync": "Synced server application commands", - "guild-command-no-sync-required": "Server application commands are up to date - no syncing required", - "global-command-no-sync-required": "Global application commands are up to date - no syncing required", - "event-loaded": "Loaded events %d/%f", - "event-dir": "Loading events in %d/%f", - "model-loaded": "Loaded database model %d/%f", - "model-dir": "Loading database model in %d/%f", - "loaded-cli": "Loaded API-Action %c in %p", - "channel-lock": "Locked channel", - "channel-unlock": "Unlocked channel", - "channel-unlock-data-not-found": "Unlocking channel with ID %c failed because it was never locked (which is weird to begin with).", - "module-disable": "Module %m got disabled because %r", - "migrate-success": "Migration from %o to %m finished successfully.", - "migrate-start": "Migration from %o to %m started... Please do not stop the bot" - }, - "reload": { - "reloading-config": "Reloading configuration…", - "reloading-config-with-name": "User %tag is reloading the configuration…", - "reloaded-config": "Configuration reloaded successfully.\nOut of %totalModules modules, %enabled were enabled, %configDisabled were disabled because their configuration was wrong.", - "reload-failed": "Configuration reloaded failed. Bot shutting down.", - "reload-successful-syncing-commands": "Configuration reloaded successfully, syncing commands, to make sure permissions are up-to-date…", - "reload-failed-message": "**FAILED**\n```%r```\n**Please read your log to find more information**\nThe bot will kill itself now, bye :wave:", - "command-description": "Reloads the configuration" - }, - "config": { - "checking-config": "Checking configurations...", - "done-with-checking": "Done with checking. Out of %totalModules modules, %enabled were enabled, %configDisabled were disabled because their configuration was wrong.", - "creating-file": "Config %m/%f does not exist - I'm going to create it, please stand by...", - "checking-of-field-failed": "An error occurred while checking the content of field \"%fieldName\" in %m/%f", - "saved-file": "Configuration-File %f in %m was saved successfully.", - "moduleconf-regeneration": "Regenerating module configuration, no settings will be overwritten, don't worry.", - "moduleconf-regeneration-success": "Module configuration regeneration successfully finished.", - "channel-not-found": "Channel with ID \"%id\" could not be found", - "user-not-found": "User with ID \"%id\" could not be found", - "channel-not-on-guild": "Channel with ID \"%id\" is not on your server", - "channel-invalid-type": "Channel with ID \"%id\" has a type that can not be used for this field", - "role-not-found": "Role with ID \"%id\" could not be found on your server", - "config-reload": "Reloading all configuration..." - }, - "helpers": { - "timestamp": "%dd.%mm.%yyyy at %hh:%min", - "you-did-not-run-this-command": "You did not run this command. If you want to use the buttons, try running the command yourself.", - "next": "Next", - "back": "Back", - "toggle-data-fetch-error": "SC Network Release: Toggle-Data could not be fetched", - "toggle-data-fetch": "SC Network Release: Toggle-Data fetched successfully" - }, - "command": { - "startup": "The bot is currently starting up. Please try again in a few minutes.", - "not-found": "Command not found", - "used": "%tag (%id) used command /%c", - "message-used": "%tag (%id) used command %p%c", - "execution-failed": "Execution of command /%c %g %s failed (Tracing: %t): %e", - "message-execution-failed": "Execution of command %p%c failed (Tracing: %t): %e", - "wrong-guild": "This command is only available on the server **%g**.", - "autcomplete-execution-failed": "Execution of auto-complete on command /%c %g %s with option %f failed: %e", - "execution-failed-message": "## 🔴 Command execution failed 🔴\nThis usually happens either due to misconfiguration or due to an error on our site. Please send a screenshot of this message in #support on the [ScootKit Discord](https://scootk.it/dc-en), to resolve this.\n\n### Internal Tracing ID\n`%t`\n### Debugging-Information\n```%e```", - "error-giving-role": "An error occurred when trying to give you your roles ):\nPlease ask the server administrators to confirm that the highest role of the bot is above the role that the bot is supposed to assign.", - "description-too-long": "The following command description of %c was too long to sync: %s", - "module-disabled": "This command is part of the \"%m\" which is disabled. This can either be intended by the server-admins (and slash-commands haven't synced yet) or this could be caused by a configuration error. Please check (or ask the admins) to check the bot's configuration and logs for details.", - "command-disabled": "This command is currently disabled by the server configuration. If you believe this is an error, please contact a server administrator." - }, - "help": { - "bot-info-titel": "ℹ️ Bot-Info", - "bot-info-description": "This bot is part of [SCNX](https://scnx.xyz/de?ref=custombot_help_embed), a plattform from [ScootKit](https://scootkit.net) allowing the creation of fully customizable for Discord communities, and is being hosted for \"%g\".", - "stats-title": "📊 Stats", - "stats-content": "Active modules: %am\nRegistered commands: %rc\nBot-Version: %v\nRunning on server %si\n[Server-Plan](https://scnx.xyz/plan): %pl\nLast restart: %lr\nLast reload: %lR", - "command-description": "Show every commands", - "slash-commands-title": "Slash-Commands", - "select-module-placeholder": "Select a module to view its commands", - "select-module-hint": "👇 Use the dropdown below to browse commands by module.", - "back-to-overview": "Back to overview", - "modules-overview": "📋 Modules & Commands", - "built-in-description": "Core commands built into the bot" - }, - "bot-feedback": { - "command-description": "Send feedback about the bot to the bot developer", - "submitted-successfully": "Thanks so much for your feedback! It has been carefully recorded and our team will review it soon. If we have any questions, we may contact you via DM (or if you are on our [Support Server]() we'll open a ticket). Thank you for making [CustomBots]() better for everyone <3\n\nYour feedback is subject to our [Terms of service]() and [Privacy Policy]().", - "failed-to-submit": "Sorry, but I couldn't send your feedback to our staff. This could be, because you got blocked or because of some server issue we are having. You can always report bugs and submit feedback in our [Feature-Board](https://features.sc-network.net). Thank you.", - "feedback-description": "Your feedback. Make sure it's neutral, constructive and helpful" - }, - "admin-tools": { - "position": "%i has the position %p.", - "position-changed": "Changed %i's position to %p.", - "category-can-not-have-category": "A Category can not have a category", - "not-category": "Can not change category of channel to a not category channel", - "changed-category": "%c's category got set to %cat", - "command-description": "Execute some actions for admins via commands", - "new-position-description": "New position", - "movechannel-description": "See the position of a channel or change the position of a channel", - "moverole-description": "See the position of a role or change the position of a role", - "setcategory-description": "Sets the category of a channel", - "channel-description": "Channel on which this action should be executed", - "role-description": "Role on which this action should be executed", - "category-description": "New category of the channel", - "emoji-too-much-data": "Please **only** enter one emoji and nothing else", - "emoji-import": "Imported \"%e\" successfully.", - "stealemote-description": "Steals a emote from another server", - "emote-description": "Emote to steal", - "role-command-description": "Assign or remove roles permanently or temporarily", - "role-give-description": "Assign someone a role permanently or temporarily", - "role-user-add-description": "Member that you want to assign the role to", - "role-add-role-description": "Role you want to assign to the member", - "role-add-duration-description": "If you set this parameter, the role will be removed from this user after this duration expires", - "role-user-status-description": "User you want to see temporary roles from", - "role-remove-description": "Remove a role from someone permanently or temporarily", - "role-user-remove-description": "Member that you want to remove the role from", - "role-remove-role-description": "Role you want to remove from the member", - "role-remove-duration-description": "If you set this parameter, the role will be added back to this user after this duration expires", - "role-status-description": "Shows which roles of a user are temporary and when they will be removed", - "role-not-high-enough": "The highest role of the bot is not above %e. The highest role of the bot needs to be above the role you want to remove or assign.", - "unable-to-change-roles": "Changing role %r to %u failed. Error message obtained by Discord:\n```%e```", - "user-not-found": "The user has not been found on your server.", - "duration-wrong": "The value of the duration argument is wrong. Learn more [in our docs]()", - "audit-log-add": "[admin-tools] %u added a role using a command.", - "audit-log-remove": "[admin-tools] %u removed a role using a command.", - "audit-log-add-duration": "[admin-tools] %u added a temporary role using a command that will be removed at %t.", - "audit-log-remove-duration": "[admin-tools] %u removed a temporary role using a command that will be added back at %t.", - "audit-log-temporary-remove": "[admin-tools] This role was added temporarily and has removed since the temporary timeframe expired.", - "audit-log-temporary-add": "[admin-tools] This role has been removed temporarily and has been added back since the temporary timeframe expired.", - "role-add": "%u has been given the role %r.", - "role-remove": "%u has removed the role %r.", - "role-add-duration": "%u has been given the role %r. It will be removed at %t.", - "role-remove-duration": "%r has been removed from %u. It will be given back at %t.", - "user-without-temporary-action": "%u has no roles that are temporary.", - "user-temporary-action-header": "Temporary roles of %u", - "status-remove": "%r will be removed on %t.", - "status-add": "%r will be added back on %t.", - "users-trying-to-manage-higher-role": "Your highest role, %t, is not below %e. To manage a user's role, you the role you are managing needs to be below your highest role." - }, - "welcomer": { - "channel-not-found": "[welcomer] Channel not found: %c", - "welcome-yourself-error": "Welcome, nice to meet you! This button is reversed for a special member of this server who want's to say \"Hi\" to you ^^" - }, - "birthdays": { - "channel-not-found": "[birthdays] Channel not found: %c", - "sync-error": "[birthdays] %u's state was set to \"sync\", but there was no syncing candidate, so I disabled the synchronization", - "age-hover": "%a years old", - "sync-enabled-hover": "Birthday synchronized", - "verified-hover": "Birthday verified", - "no-bd-this-month": "No birthdays this month ):", - "no-birthday-set": "You don't currently have a registered birthday on this server. Set a birthday with `/birthday set`.", - "birthday-status": "Your birthday is currently set to **%dd.%mm%yyyy**%age.", - "your-age": "which means that you are **%age** years old", - "sync-on": "Your birthday is being synced via your [SC Network Account](https://sc-network.net/dashboard).", - "sync-off": "Your birthday is set locally on this server and will not be synchronized", - "no-sync-account": "It seems like you either don't have an [SC Network Account]() or you haven't entered any information about your birthday in it yet.", - "auto-sync-on": "It seems that you have autoSync in your [SC Network Account]() enabled. This means that your birthday will be synchronized all the time on every server. [Learn more]().\nYour birthday isn't showing up? It can take up to 24 hours (usually it's less than two hours) for it to be synced, so stay calm and wait just a bit longer.", - "enabled-sync": "Successfully set. The synchronization is now enabled :+1:", - "disabled-sync": "Successfully set. The synchronization is disabled, you can now change or remove your birthday from this server.", - "delete-but-sync-is-on": "You currently have sync enabled. Please disable sync to delete your birthday.", - "deleted-successfully": "Birthday deleted successfully.", - "only-sync-allowed": "This server only allows synchronization of your birthday with a [SC Network Account]()", - "invalid-date": "Invalid date provided", - "against-tos": "You have to be at least 13 years old to use Discord. Please read Discord's [Terms of Service]() and if you are under the age of 13 please [delete your account]() to comply with Discord's [Terms of Service]() and wait %waitTime (or for the age for your country, listed [here]()) years before creating a new account.", - "too-old": "It seems like you are too old to be alive", - "command-description": "View, edit and delete your birthday", - "status-command-description": "Shows the current status of your birthday", - "sync-command-description": "Manage the synchronization on this server", - "sync-command-action-description": "Action which should be performed on your synchronization", - "sync-command-action-enable-description": "Enable synchronization", - "sync-command-action-disable-description": "Disable synchronization", - "set-command-description": "Sets your birthday", - "set-command-day-description": "Day of your birthday", - "set-command-month-description": "Month of your birthday", - "set-command-year-description": "Year of your birthday", - "delete-command-description": "Deletes your birthday from this server", - "migration-happening": "Database-Schema not up-to-date. Migration database... This could take a while. Do not restart your bot to avoid data loss.", - "migration-done": "Successfully migrated database to newest version." - }, - "months": { - "1": "January", - "2": "February", - "3": "March", - "4": "April", - "5": "May", - "6": "June", - "7": "July", - "8": "August", - "9": "September", - "10": "October", - "11": "November", - "12": "December" - }, - "levels": { - "leaderboard-channel-not-found": "Leaderboard-Channel not found or wrong type", - "leaderboard-notation": "%p. %u: Level %l - %xp XP", - "list-location": "[Level System] The live leaderboard is currently located here: %l. Delete the message and restart the bot, to re-send it.", - "leaderboard": "Leaderboard", - "no-user-on-leaderboard": "Can't generate a leaderboard, because no one has any XP which is odd, but that's how it is ¯\\_(ツ)_/¯", - "and-x-other-users": "and %uc other users", - "level": "Level %l", - "users": "Users", - "leaderboard-command-description": "Shows the leaderboard of this server", - "leaderboard-sortby-description": "How to sort the leaderboard (default: %d)", - "profile-command-description": "Shows the profile of you or an an user", - "profile-user-description": "User to see the profile from (default: you)", - "please-send-a-message": "Please send some messages before I can show you some data", - "no-role": "None", - "are-you-sure-you-want-to-delete-user-xp": "Okay, do you really want to screw with %u? If you hate them so much, feel free to run `/manage-levels reset-xp confirm:True user:%ut` to run this irreversible action.", - "are-you-sure-you-want-to-delete-server-xp": "Do you really want to delete all XP and Levels from this server? This action is irreversible and everyone on this server will hate you. Decided that it's worth it? Enter `/manage-levels reset-xp confirm:True`", - "user-not-found": "User not found", - "user-deleted-users-xp": "%t deleted the XP of the user with id %u", - "removed-xp-successfully": "`Removed %u's XP and level successfully.`", - "deleted-server-xp": "%u deleted the XP of all users", - "successfully-deleted-all-xp-of-users": "Successfully deleted all the XP of all users", - "cheat-no-profile": "This user doesn't have a profile (yet), please force them to write a message before trying to betrayal your community by manipulating level scores.", - "manipulated": "%u manipulated the XP of %m to %v (level %l)", - "successfully-changed": "Successfully edited the XP of %u - they are now **level %l** with **%x XP**.\nRemember, every change you make destroys the experience of other users on this server as the levelsystem isn't fair anymore.", - "edit-xp-command-description": "Manage the levels of your server", - "negative-xp": "This user would have a negative XP value which is not possible", - "negative-level": "This user would have a level below one which is not possible", - "reset-xp-description": "Reset the XP of a user or of the whole server", - "reset-xp-user-description": "User to reset the XP from (default: whole server)", - "reset-xp-confirm-description": "Do you really want to delete the data?", - "edit-xp-user-description": "User to edit", - "edit-xp-value-description": "New XP value of the user", - "edit-xp-description": "Betrays your community and edits a user's XP", - "no-custom-formula": "No valid custom formula was entered. Using default formula.", - "invalid-custom-formula": "Invalid custom formula was entered. Please either fix the syntax of your custom formula or remove the value of the custom formula field.", - "role-factors-total": "Multiplied together, the user receives **%f times more XP** for every message.", - "edit-level-description": "Betrays your community and edits a user's levels", - "random-messages-enabled-but-non-configured": "You have random messages enabled, but have non configured. Ignoring config.randomMessages configuration.", - "granted-rewards-audit-log": "Updated roles to make sure, they have the level role they need" - }, - "team-list": { - "channel-not-found": "Could not find channel with ID %c or the channel has a wrong type (only text-channels supported)", - "role-not-found": "Could not find role with ID %r", - "no-users-with-role": "No users on this server have the %r role yet.", - "no-roles-selected": "No roles listed yet.", - "offline": "Offline", - "dnd": "Do not disturb", - "idle": "Away", - "online": "Online" - }, - "ping-on-vc-join": { - "channel-not-found": "Notify channel %c not found", - "could-not-send-pn": "Could not send PN to %m" - }, - "suggestions": { - "suggestion-not-found": "Suggestion not found", - "updated-suggestion": "Successfully updated suggestion", - "suggest-description": "Create and comment on suggestions", - "suggest-content": "Content you want to suggest", - "loading": "A wild new suggestion appeared, loading..", - "manage-suggestion-command-description": "Manage suggestions as an admin", - "manage-suggestion-accept-description": "Accepts a suggestion", - "manage-suggestion-deny-description": "Denies a suggestion", - "manage-suggestion-id-description": "ID of the suggestion", - "manage-suggestion-comment-description": "Explain why you made this choice" - }, - "auto-delete": { - "could-not-fetch-channel": "Could not fetch channel with ID %c", - "could-not-fetch-messages": "Could not fetch messages from channel with ID %c" - }, - "auto-thread": { - "thread-create-reason": "This thread got created, because you configured auto-thread to do so" - }, - "auto-messager": { - "channel-not-found": "Channel with ID %id not found" - }, - "polls": { - "what-have-i-votet": "What have I voted?", - "vote": "Vote!", - "vote-this": "Click on this option to place your vote here", - "voted-successfully": "Successfully voted. Thanks for your participation.", - "not-voted-yet": "You have not voted yet, so I can't show you what you voted.", - "you-voted": "You have voted for **%o**.", - "remove-vote": "Remove my vote", - "removed-vote": "Your vote was removed successfully.", - "change-opinion": "You can change your opinion anytime by just selecting something else above the button you just clicked.", - "command-poll-description": "Create and end polls", - "command-poll-create-description": "Create a new poll", - "command-poll-end-description": "Ends an existing poll", - "command-poll-end-msgid-description": "ID of the poll", - "command-poll-create-description-description": "Topic / Description of this poll", - "command-poll-create-channel-description": "Channel in which the poll should get created", - "command-poll-create-option-description": "Option number %o", - "command-poll-create-endAt-description": "Duration of the poll (if not set the poll will not end automatically)", - "command-poll-create-public-description": "If enabled (disabled by default) the votes of users will be displayed publicly", - "created-poll": "Successfully created poll in %c.", - "not-found": "Poll could not be found", - "no-votes-for-this-option": "Nobody voted this option yet", - "ended-poll": "Poll ended successfully", - "view-public-votes": "View current voters", - "not-public": "This poll does not appear to be public, no results can be displayed.", - "poll-private": "\uD83D\uDD12 This poll is **anonymous**, meaning that no one can see what you voted (not even the admins).", - "poll-public": "\uD83D\uDD13 This poll is **public**, meaning that everyone can see what you voted.", - "not-text-channel": "You need to select a text-channel that is not an announcement-channel." - }, - "channel-stats": { - "audit-log-reason-interval": "Updated channel because of interval", - "audit-log-reason-startup": "Updated channel because of startup", - "not-voice-channel-info": "Channel \"%c\" (%id) is a %t and not a voice-channel as recommended" - }, - "info-commands": { - "info-command-description": "Find information about parts of this server", - "command-userinfo-description": "Find more information about a user on this server", - "argument-userinfo-user-description": "User you want to see information about (default: you)", - "command-roleinfo-description": "Find more information about a role on this server", - "argument-roleinfo-role-description": "Role you want to see information about", - "command-channelinfo-description": "Find more information about a channel on this server", - "argument-channelinfo-channel-description": "Channel you want to see information about", - "command-serverinfo-description": "Find more information about this server", - "information-about-role": "Information about the role %r", - "hoisted": "Hoisted", - "mentionable": "Mentionable", - "managed": "Managed", - "information-about-channel": "Information about the channel %c", - "information-about-user": "Information about the user %u", - "information-about-server": "Information about %s", - "boostLevel": "Level", - "boostCount": "Boosts", - "userCount": "Users", - "memberCount": "Members", - "onlineCount": "Online", - "textChannel": "Text", - "voiceChannel": "Voice", - "categoryChannel": "Categories", - "otherChannel": "Other", - "total-invites": "Total", - "active-invites": "Active", - "left-invites": "Left" - }, - "channelType": { - "GUILD_TEXT": "Text-Channel", - "GUILD_VOICE": "Voice-Channel", - "GUILD_CATEGORY": "Category", - "GUILD_NEWS": "News-Channel", - "GUILD_STORE": "Store-Channel", - "GUILD_NEWS_THREAD": "News-Channel-Thread", - "GUILD_PUBLIC_THREAD": "Public Thread", - "GUILD_PRIVATE_THREAD": "Private Thread", - "GUILD_STAGE_VOICE": "Stage-Channel", - "DM": "Direct-Message", - "GROUP_DM": "Group-Direct-Message", - "UNKNOWN": "Unknown" - }, - "stagePrivacy": { - "PUBLIC": "Publicly accessible", - "GUILD_ONLY": "Only server members can join" - }, - "guildVerification": { - "0": "None", - "1": "Low", - "2": "Medium", - "3": "High", - "4": "Very high" - }, - "boostTier": { - "0": "None", - "1": "Level 1", - "2": "Level 2", - "3": "Level 3" - }, - "temp-channels": { - "removed-audit-log-reason": "Removed temp channel, because no one was in it", - "permission-update-audit-log-reason": "Updated permissions, to make sure only people in the VC can see the no-mic-channel", - "created-audit-log-reason": "Created Temp-Channel for %u", - "move-audit-log-reason": "Moved user to their voice channel", - "no-mic-channel-topic": "Welcome to %u's no-mic-channel. You will see this channel as long as you are connected to this temp-channel.", - "disconnect-audit-log-reason": "The old channel of the user could not be found - disconnecting them - hopefully they join again", - "command-description": "Manage your temp-channel", - "mode-subcommand-description": "Change the mode of your channel", - "public-option-description": "If enabled, anyone can join your temp-channel", - "add-subcommand-description": "Add users, that will be able to join your channel, while it is private", - "remove-subcommand-description": "Remove users from you channel", - "add-user-option-description": "The user to be added", - "remove-user-option-description": "The user to be removed", - "list-subcommand-description": "List the users with access to your channel", - "edit-subcommand-description": "Edit various settings of your channel", - "user-limit-option-description": "Change the user-limit of your channel", - "bitrate-option-description": "Change the bitrate of your channel (min. 8000)", - "name-option-description": "Change the name of your channel", - "nsfw-option-description": "Change, whether your channel is age-restricted or not", - "no-added-user": "There are no users to be displayed here", - "nothing-changed": "Your channel already had these settings.", - "no-disconnect": "Couldn't disconnect the user from your channel. This could be due to missing permissions, or the user not being in your voice-channel", - "edit-error": "An error occurred while editing your channel. one or more of your settings couldn't be applied. This could be due to missing permissions or an invalid value.", - "add-user": "Add user", - "remove-user": "Remove user", - "list-users": "List users", - "private-channel": "Private", - "public-channel": "Public", - "edit-channel": "Edit channel", - "add-modal-title": "Add an user to your temp-channel", - "add-modal-prompt": "The user you want to add (tag or user-id)", - "remove-modal-title": "Remove an user from your temp-channel", - "remove-modal-prompt": "The user you want to remove (tag or user-id)", - "edit-modal-title": "Edit your temp-channel", - "edit-modal-nsfw-prompt": "Mark temp-channel as age-restricted?", - "edit-modal-nsfw-placeholder": "\"true\" (yes) or \"false\" (no)", - "edit-modal-bitrate-prompt": "Bitrate of your Temp-channel?", - "edit-modal-bitrate-placeholder": "A number over 8000", - "edit-modal-limit-prompt": "Limit of users in your temp-channel", - "edit-modal-limit-placeholder": "Number between 0 and 99; 0 = unlimited", - "edit-modal-name-prompt": "How should your channel be called?", - "edit-modal-name-placeholder": "A very creative channel name", - "edit-modal-username-placeholder": "Username of the user", - "user-not-found": "User not found" - }, - "guess-the-number": { - "command-description": "Manage your guess-the-number-games", - "status-command-description": "Shows the current status of a guess-the-number-game in this channel", - "create-command-description": "Create a new guess-the-number-game in this channel", - "create-min-description": "Minimal value users can guess", - "create-max-description": "Maximal value users can guess", - "create-number-description": "Number users should guess to win", - "end-command-description": "Ends the current game", - "session-already-running": "There is a session already running in this channel. Please end it with /guess-the-number end", - "session-not-running": "There is currently no session running.", - "gamechannel-modus": "You can't use this command in a gamechannel. To end a game, disable the gamechannel modus and try using the end command.", - "session-ended-successfully": "Ended session successfully. Locked channel successfully.", - "current-session": "Current session", - "number": "Number", - "min-val": "Min-Value", - "max-val": "Max-Value", - "owner": "Owner", - "guess-count": "Count of guesses", - "min-max-discrepancy": "`min` can't be bigger or equal to `max`", - "max-discrepancy": "`number` can't be bigger than `max`.", - "min-discrepancy": "`number` can't be smaller than `min`.", - "emoji-guide-button": "What does the reaction under my guess mean?", - "guide-wrong-guess": "Your guess was wrong (but your entry was valid)", - "guide-win": "You guessed correctly - you win :tada:", - "guide-admin-guess": "Your guess was invalid, because you are an admin - admins can't participate because they can see the correct number", - "guide-invalid-guess": "Your guess was invalid (e.g. below the minimal / over the maximal number, not a number, …)", - "created-successfully": "Created game successfully. Users can now start guessing in this channel. The winning number is **%n**. You can always check the status by running `/guess-the-number-status`. Note that you as an admin can not guess.", - "game-ended": "Game ended", - "game-started": "Game started" - }, - "massrole": { - "command-description": "Manage roles for all members", - "add-subcommand-description": "Add a role to all members", - "remove-subcommand-description": "Remove a role from all members", - "remove-all-subcommand-description": "Remove all roles from all members", - "role-option-add-description": "The role, that will be given to all members", - "role-option-remove-description": "The role, that will be removed from all members", - "target-option-description": "Determines whether bots should be included or not", - "all-users": "All Users", - "bots": "Bots", - "humans": "Humans", - "not-admin": "⚠ To use this command, you need to be added to the adminRoles option in the SCNX-Dashboard. If you are the owner of this bot please remember to create an override in the server settings to prevent abuse of this command.", - "add-reason": "Mass role addition by %u", - "remove-reason": "Mass role removal by %u" - }, - "twitch-notifications": { - "channel-not-found": "Channel with ID %c could not be found", - "user-not-on-twitch": "Could not find user %u on twitch" - }, - "hunt-the-code": { - "admin-command-description": "Manage the current Code-Hunt", - "create-code-description": "Create a new code for the current code-hunt", - "display-name-description": "Name of the code that will be displayed to user when they redeem the code", - "code-description": "Set the code that will be used to redeem it (default: randomly generated)", - "code-created": "Code \"%displayName\" successfully created: \"%code\"", - "error-creating-code": "Error creating code \"{{displayName}}\". Maybe the entered code is already in the database?", - "successful-reset": "Successfully ended the current Code-Hunt-Game - [here](%url)'s your report - save the URL if you want to access it later.", - "end-description": "Ends the current Code-Hunt (will clear users and codes and generates a report)", - "command-description": "Redeem or see data about the current Code-Hunt", - "redeem-description": "Redeem a code you found", - "code-redeem-description": "The code you want to redeem", - "leaderboard-description": "See the current leaderboard", - "profile-description": "See your current count of found codes", - "no-codes-found": "No codes redeemed yet ):", - "no-users": "No users redeemed codes yet ):", - "report-header": "Report for the Hunt-The-Code game on %s", - "user-header": "Participating users", - "code-header": "Codes", - "report-description": "Generates a report", - "report": "You can find the report [here](%url)." - }, - "fun": { - "slap-command-description": "Slap a user in the face", - "user-argument-description": "User to performe this action on", - "no-no-not-slapping-yourself": "You can not punch yourself lol (well technically you can, but our gifs do not support that, so deal with it ¯\\_(ツ)_/¯)", - "pat-command-description": "Pat someone nicely", - "no-no-not-patting-yourself": "Well, good try, but we don't do this here", - "no-no-not-kissing-yourself": "Uah, that's gross, you should try paying somebody to do that (well you should not, but better then kissing yourself)", - "kiss-command-description": "Kiss someone", - "hug-command-description": "Hug someone <3", - "no-no-not-hugging-yourself": "You are quite lonely aren't you? Try hugging a tree, that should work. Unless you live in a desert. Then hug a cactus. That's a bit more painful, but trust me.", - "random-command-description": "Helps you select random things", - "random-number-command-description": "Selects a random number", - "min-argument-description": "Minimal number (default: 1)", - "max-argument-description": "Maximal number (default: 42)", - "random-ikeaname-command-description": "Generates a random name for a IKEA-Name", - "syllable-count-argument-description": "Count of syllables to generate name from (default: random)", - "random-dice-command-description": "Roll a dice", - "random-coinflip-command-description": "Flip a coin", - "random-8ball-command-description": "Generates an answer to a yes/no question", - "dice-site-1": "Heads", - "dice-site-2": "Tails" - }, - "moderation": { - "moderate-command-description": "Moderate users on your server", - "moderate-notes-command-description": "Set or see moderator's notes of a user", - "moderate-notes-command-view": "View a user's notes", - "moderate-notes-command-create": "Create a new note about a user", - "moderate-notes-command-edit": "Edit one of your existing notes about a user", - "moderate-notes-command-delete": "Delete one of your existing notes about a user", - "moderate-ban-command-description": "Ban a user on your server", - "moderate-reason-description": "Reason for your action", - "moderate-proof-description": "Proof for your action", - "report-user-not-found-on-guild": "This user could not be found on \"%s\". You can only report users that are members of our server.", - "proof": "Proof", - "report-proof-description": "Attach an optional (image) proof to your report", - "file": "File uploaded", - "anti-grief-reason": "Too many actions of type \"%type\" in the last %h hours. Maximum amount allowed: %n", - "anti-grief-user-message": "Sorry, but it seems like you are abusing your moderative powers. We've taken actions to prevent this from happening.", - "moderate-duration-description": "Duration of the action (max: 28 days, default: 14 days)", - "mute-max-duration": "Discord limits the maximal duration of a timeout to 28 days. Please enter an amount equal or less than this", - "moderate-quarantine-command-description": "Quarantine a user on your server", - "moderate-unquarantine-command-description": "Removes a user from the quarantine", - "moderate-unban-command-description": "Revokes an existing ban", - "moderate-clear-command-description": "Clears messages in the current channel", - "moderate-clear-amount-description": "How many messages should get cleared?", - "moderate-kick-command-description": "Kick a user from your server", - "moderate-unwarn-command-description": "Revokes a warning", - "moderate-mute-command-description": "Mute a user on your server", - "moderate-unmute-command-description": "Unmutes a user on your server", - "moderate-warn-command-description": "Warn a user", - "moderate-channel-mute-description": "Mutes a user from the current channel", - "moderate-unchannel-mute-description": "Removes a channel-mute from this channel", - "moderate-lock-command-description": "Lock the current channel", - "moderate-unlock-command-description": "Unlock the current channel", - "moderate-lockdown-command-description": "Activate or lift server-wide lockdown", - "moderate-lockdown-enable-description": "True to activate lockdown, false to lift it", - "lockdown-not-enabled": "The lockdown system is not enabled. Enable it in the lockdown configuration.", - "lockdown-already-active": "A lockdown is already active.", - "lockdown-not-active": "No lockdown is currently active.", - "lockdown-activated": "Server Lockdown Activated", - "lockdown-lifted": "Server Lockdown Lifted", - "lockdown-activated-reply": "Lockdown activated. %c channels have been locked.", - "lockdown-lifted-reply": "Lockdown lifted. %c channels have been restored.", - "lockdown-log-description": "**Reason:** %r\n**Triggered by:** %u\n**Type:** %t\n**Affected channels:** %c", - "lockdown-lift-log-description": "**Reason:** %r\n**Lifted by:** %u\n**Restored channels:** %c", - "lockdown-automatic": "Automatic", - "lockdown-manual": "Manual", - "lockdown-system": "System", - "lockdown-auto-lift-reason": "Auto-lift timer expired", - "lockdown-restored": "Lockdown state restored from database after restart", - "lockdown-joinraid-trigger": "Join raid detected", - "lockdown-spam-trigger": "Excessive spam detected", - "lockdown-joingate-trigger": "Excessive join-gate violations detected", - "lockdown-restore-failed": "Failed to restore permissions for channel %c: %e", - "lockdown-users-kicked": "Users Kicked", - "lockdown-users-kicked-description": "%k non-moderator users were disconnected from voice channels.", - "moderate-user-description": "User on who the action should get performed", - "moderate-userid-description": "ID of a user", - "moderate-days-description": "Number of days of messages to delete", - "invalid-days": "Days can only be between 0 and 7 (inclusive)", - "moderate-notes-description": "Notes to set / update", - "moderate-note-id-description": "ID of one of your notes you want to edit (leave blank to create a new one)", - "moderate-warnid-description": "ID of a warn (run /moderate actions to get it)", - "moderate-actions-command-description": "Show all recorded actions against a user", - "report-command-description": "Reports a user and sends a snapshot of the chat to server staff", - "report-reason-description": "Please describe what the user did wrong", - "report-user-description": "User you want to report", - "no-reason": "Not set", - "muterole-not-found": "Could not find muterole. Can not perform this action", - "quarantinerole-not-found": "Could not find quarantinerole. Can not perform this action", - "mute-audit-log-reason": "Got muted by %u because of \"%r\"", - "unmute-audit-log-reason": "Got unmuted by %u because of \"%r\"", - "quarantine-audit-log-reason": "Got quarantined by %u because of \"%r\"", - "kicked-audit-log-reason": "Got kicked by %u because of \"%r\"", - "banned-audit-log-reason": "Got banned by %u because of \"%r\"", - "channelmute-audit-log-reason": "Got channel-mutet by %u because of \"%r\"", - "unchannelmute-audit-log-reason": "The Channel-Mute got removed by %u because of \"%r\"", - "unbanned-audit-log-reason": "Got unbanned by %u because of \"%r\"", - "unquarantine-audit-log-reason": "Got unquarantined by %u because of \"%r\"", - "action-expired": "Action expired", - "auto-mod": "Auto-Mod", - "batch-role-remove-failed": "Could not remove all roles from %i (trying to remove roles one by one): %e", - "batch-role-add-failed": "Could not add all roles to %i (trying to remove roles one by one): %e", - "could-not-remove-role": "Could not remove role %r from %i: %e", - "could-not-add-role": "Could not add role %r to %i: %e", - "reason": "Reason", - "join-gate": "Join-Gate", - "expires-at": "Action expires on", - "action": "Action", - "case": "Case", - "victim": "Victim", - "missing-logchannel": "LogChannel could not be found", - "reached-warns": "Reached %w warns", - "restored-punishment-audit-log-reason": "Restored punishment", - "anti-join-raid": "ANTI-JOIN-RAID", - "raid-detected": "Raid detected", - "joingate-for-everyone": "Join-Gate-Modus: Catch all users", - "account-age-to-low": "Account creation age of %a days is to low (required are more then %c)", - "no-profile-picture": "Account has no profile picture (required)", - "join-gate-fail": "Account failed Join-Gate (%r)", - "blacklisted-word": "Posted blacklisted word in %c", - "invite-sent": "Sent invite in %c", - "scam-url-sent": "Sent scam-url in %c", - "anti-spam": "Anti-Spam", - "reached-messages-in-timeframe": "Reached %m (normal) messages in less than %t seconds", - "reached-duplicated-content-messages": "Reached %m messages with the same content in less than %t", - "reached-ping-messages": "Reached %m messages with (user) pings in less then %t seconds", - "reached-massping-messages": "Reached %m messages with mass pings in less than %t seconds", - "action-done": "Executed action successfully. Action-ID: #%i", - "expiring-action-done": "Done. Action will expire on %d. Action-ID: #%i", - "cleared-channel": "Cleared channel successfully.\nNote: Messages older than 14 days can not be deleted using this method.", - "clear-failed": "An error occurred. You can only delete 100 messages at once.", - "no-quarantine-action-found": "Sorry, but I couldn't find any records of quarantining this users.", - "locked-channel-successfully": "Locked channel successfully. Only moderators (and admins) can write messages here now.", - "unlocked-channel-successfully": "Unlocked channel successfully. Permissions got restored to the permission-state before the lock occurred.", - "unlock-audit-log-reason": "User %u unlocked this channel by running /moderate unlock", - "warning-not-found": "I could not find this warning. Please make sure you are actually using a warning-id and not a userid.", - "can-not-report-mod": "You can not report moderators.", - "action-description-format": "%reason\nby %u on %t", - "no-actions-title": "None found", - "no-actions-value": "No actions against %u found.", - "actions-embed-title": "Mod-Actions against %u - Site %i", - "actions-embed-description": "You can find every action against %u here.", - "report-embed-title": "New report", - "report-embed-description": "A user reported another user. Please review the case and take actions if needed.", - "reported-user": "Reported user", - "report-reason": "Reason for the report", - "report-user": "User who submitted report", - "message-log": "Last 100 messages", - "message-log-description": "You can find an encrypted message-log [here](%u).", - "channel": "Channel", - "no-report-pings": "No pings configured. Check your configuration to ping your staff.", - "not-allowed-to-see-own-notes": "Sorry, but you are not allowed to see your own notes.", - "note-added": "Note added successfully", - "note-edited": "Edited note successfully", - "note-deleted": "Note deleted successfully", - "note-not-found-or-no-permissions": "Note not found or no permissions to edit this note.", - "notes-embed-title": "Notes about %u", - "info-field-title": "ℹ️ Information", - "no-notes-found": "No notes about this user. Create a new note with `/moderate notes create` and set the notes attribute.", - "more-notes": "%x other moderator also added notes about this user. Notes are sorted in reverse chronology, so you will see the newest notes first.", - "user-notes-field-title": "%t's notes", - "user-not-on-server": "I can't perform this action on this user, as they are not currently on your server.", - "verification": "VERIFICATION", - "verification-failed": "Verification failed", - "verification-started": "Verification got started", - "verification-completed": "Verification completed", - "user": "User", - "manual-verification-needed": "Manual verification needed", - "verification-deny": "Deny verification", - "verification-approve": "Approve verification", - "verification-skip": "Skip verification", - "captcha-verification-pending": "Captcha-Verification is pending. You can either wait for the user to complete it or skip it manually.", - "verification-update-proceeded": "Successfully update verification status", - "verify-channel-set-but-not-found-or-wrong-type": "The configured verify-channel could not be found or it's type is not supported.", - "generating-message": "We are preparing some stuff, this message should get edited shortly...", - "restart-verification-button": "Restart verification process", - "member-not-found": "This user could not be found, maybe they already left?", - "already-verified": "Seems like you are already verified... Why would you want to repeat this process?", - "restarted-verification": "I have sent you another DM about your verification process. Please read it carefully and follow the actions described in it. Please not that this action did not re-trigger the manual-verification (if enabled), so spamming this button is useless.", - "dms-still-disabled": "It seems like your DMs are still disabled. Please enable your DMs to start the verification. This is not optional, you need to do this in order to get access to %g.", - "dms-not-enabled-ping": "%p, it seems like you have your DMs disabled. Please enable them and hit the button below this message to verify yourself. You have two minutes to complete this process." - }, - "counter": { - "created-db-entry": "Initialized database entry for %i", - "not-a-number": "This is not a number. You can not chat here. Try creating a thread if your message is that important.", - "restriction-audit-log": "This user proceeded to abuse the counter channel after five warnings, so we locked them out.", - "only-one-message-per-person": "Users have to take turns counting: You can not count two times in a row.", - "not-the-next-number": "That's not the next number. The next number is **%n**, please make sure you are counting up one by one.", - "channel-topic-change-reason": "Someone counted, so we updated the description as required by the configuration" - }, - "tickets": { - "channel-not-found": "Ticket-Create-Channel could not be found", - "existing-ticket": "You already have a ticket open: %c", - "ticket-created-audit-log": "%u created a new ticket by clicking the button", - "ticket-created": "Successfully created ticket and notified staff. Head over to it: %c", - "no-admin-pings": "No pings configured. Check your configuration to ping your staff.", - "ticket-closed-successfully": "Closed ticket successfully. This channel will be deleted in a few seconds, thanks for reaching out to our support.", - "ticket-closed-audit-log": "%u closed ticket", - "closing-ticket": "Closing ticket as requested by %u...", - "ticket-with-user": "👤 Ticket-User", - "could-not-dm": "Could not DM %u: %r", - "no-log-channel": "Log-Channel not found", - "ticket-log-embed-title": "📎 Ticket %i closed", - "ticket-log": "Ticket-Log", - "ticket-type": "☕ Ticket-Topic", - "ticket-log-value": "Transcript with %n messages can be found [here](%u).", - "closed-by": "👷 Ticket closed by" - }, - "reminders": { - "command-description": "Set a reminder for yourself", - "in-description": "After what time should we remind you? (eg. \"2h 30m\")", - "what-description": "What should we remind you about?", - "dm-description": "Should we send you a DM instead of reminding your in this channel?", - "one-minute-in-future": "Your reminder needs to be at least one minute in the future", - "reminder-set": "Reminder set. We'll remind you at %d." - }, - "afk-system": { - "command-description": "Manage your AFK-Status on this server", - "end-command-description": "End your current AFK-Session", - "start-command-description": "Start a new AFK-Session", - "reason-option-description": "Explain why you started this session", - "autoend-option-description": "If enabled, the bot will auto-end your AFK Session when your write a message (default: enabled)", - "no-running-session": "You don't have any session running.", - "already-running-session": "You already have an AFK-Session running, try ending it with `/afk-system end`.", - "afk-nickname-change-audit-log": "Updated user nickname because they started an AFK-Session", - "can-not-edit-nickname": "Can not edit nickname of %u: %e" - }, - "tic-tac-toe": { - "command-description": "Play tic-tac-toe against someone in the chat", - "user-description": "User to play against", - "challenge-message": "%t, %u challenged you to a game of tic-tac-toe! Hit the button below to join the battle! This invitation will expire in about 2 minutes, so don't hesitate to much.", - "accept-invite": "Join game", - "deny-invite": "No thanks", - "self-invite-not-possible": "Are you really that lonely? Even Simon, a complete introvert with no friends and developer of this bot, can find another user to play tic-tac-toe with... You should be able to do that too, try inviting %r for example, maybe they want to play a round?", - "invite-expired": "Sorry, %u, %i didn't accept your request to play tic-tac-toe in time ):", - "invite-denied": "Sorry, %u, but %i denied your request to play a round of tic-tac-toe ):", - "you-are-not-the-invited-one": "Sorry, but this invite doesn't belong to you. You can start your own game with `/tic-tac-toe`.", - "playing-header": "**TIC-TAC-TOE GAME IS RUNNING**\n\n%u (🟢) VS %i (🟡)\nCurrently on turn: %t\n\n%t, click a button with a white circle below to place your marker", - "win-header": "**TIC-TOE-GAME ENDED**\n\n%u (🟢) VS %i (🟡)\n\n%w won the game - GG!\n\n*You can start a new round by using `/tic-tac-toe`*", - "draw-header": "**TIC-TOE-GAME ENDED**\n\n%u (🟢) VS %i (🟡)\n\nDraw - no one won this game.", - "not-your-turn": "It's not your turn, take a coffee and return later" - }, - "duel": { - "command-description": "Play duel against someone in the chat", - "user-description": "User to play against", - "challenge-message": "%t, %u challenged you to a game of duel! Hit the button below to join the battle! This invitation will expire in about 2 minutes, so don't hesitate to much.", - "accept-invite": "Join game", - "deny-invite": "No thanks", - "self-invite-not-possible": "Are you really that lonely? Even Simon, a complete introvert with no friends and developer of this bot, can find another user to play duel with... You should be able to do that too, try inviting %r for example, maybe they want to play a round?", - "invite-expired": "Sorry, %u, %i didn't accept your request to play duel in time ):", - "invite-denied": "Sorry, %u, but %i denied your request to play a round of duel ):", - "you-are-not-the-invited-one": "Sorry, but this invite doesn't belong to you. You can start your own game with `/duel`.", - "game-running-header": "🎮 Game running", - "what-do-you-want-to-do": "**Select your action!**", - "pending": "⏳ Waiting for selection…", - "ready": "✅ Ready", - "continues-info": "The game continues once both parties have selected their next action.", - "how-does-this-game-work": "Wondering how this game works? Read our short explanation [here]().", - "use-gun": "Use gun", - "guard": "Guard", - "reload": "Load gun", - "game-ended": "🎮 Game ended", - "no-bullets": "Sorry, but you haven't loaded any bullets yet, so you can't use your gun yet.", - "bullets-full": "Sorry, but your gun only has place for 5 bullets at a time.", - "gun-gun": "Both %g1 and %g1 draw their guns. They stare each other and their eyes and slowly lower their weapons. No, the duell won't be resolved if both die - there can only be one winner.", - "guard-gun": "%g1 draws their gun and shoot - %d1 dodged the bullet successfully.", - "guard-guard": "Both %d1 and %d2 wait for each other to fire the shot - but nothing happens.", - "reload-gun": "While %r1 starts reloading their gun, %g1 draws their weapon and shoots - it's a head-shot. %r1 drops to the ground. %g1 should celebrate because they won, but they are left feeling bad for murdering their old friend.", - "guard-over-reload-gun": "As this is %r1's fifth guard in a row, they are tired and are to slow - %g1 shoots them directly into their head and %r1 drops to the ground. It's a win for %g1 - but at what price?", - "reload-reload": "Both %r1 and %r2 stare each other in the eyes while taking a short break to load one bullet each in their chamber.", - "reload-guard": "%d1 prepares to doge a bullet - but %r1 uses the time to load their weapon - no shots get fired.", - "ended-state": "This game ended. You can start a new duel with `/duel`.", - "not-your-game": "You are not one of players - you can start a new game with `/duel`." - }, - "economy-system": { - "work-earned-money": "The user %u gained %m %c by working", - "crime-earned-money": "The user %u gained %m %c by committing a crime", - "message-drop-earned-money": "The user %u gained %m %c by getting a message drop", - "rob-earned-money": "The user %u gained %m %c by robbing from %v", - "weekly-earned-money": "The user %u gained %m %c by cashing in their weekly reward", - "daily-earned-money": "The user %u gained %m %c by cashing in their daily reward", - "admin-self-abuse": "The admin %a wanted to abuse their permissions by giving them self even more money! This can't and should not be ignored!", - "admin-self-abuse-answer": "What a bad admin you are, %u. I'm disappointed with you! I need to report this. If I wish I could ban you!", - "added-money": "%i %c has been added to the balance of %u", - "removed-money": "%i %c has been removed from the balance of %u", - "set-money": "The balance of %u has been set to %i.", - "added-money-log": "The user %u added %i %c to the balance of %v", - "removed-money-log": "The user %u removed %i %c from the balance of %v", - "set-money-log": "The user %u set %v's balance to %i %c", - "command-description-main": "Use the economy-system", - "command-description-work": "Earn some cash by working", - "command-description-crime": "Earn some cash by committing a crime", - "command-description-rob": "Rob some cash from another user", - "option-description-rob-user": "User to rob from", - "crime-loose-money": "The user %u lost %m %c by committing a crime", - "command-description-daily": "Cash in your daily rewards", - "command-description-weekly": "Cash in your weekly rewards", - "command-description-balance": "Show the balance of a user", - "option-description-user": "User to execute action upon", - "command-description-add": "Add some cash to a user", - "command-description-remove": "Remove some cash from a user", - "option-description-amount": "Amount to manipulate", - "command-description-set": "Set a user's balance", - "option-description-balance": "Balance to set user to", - "message-drop": "Message-Drop: You earned %m %c simply by chatting!", - "created-item": "The user %u has created a new shop item: %i", - "item-duplicate": "The item already exist", - "role-to-high": "The specified role is higher than the highest role of the bot. Therefore the bot can't give the role to users. The item was **not** created.", - "delete-item": "The user %u has deleted the shop item %i", - "edit-item": "The user %u has edited the item %i. Possible changes are:\nNew name: %n\nNew price: %p\nNew role: %r", - "user-purchase": "The user %u has purchased the shop item %i for %p.", - "shop-command-description": "Use the shop-system", - "shop-command-description-add": "Create a new item in the shop (admins only)", - "shop-option-description-itemName": "Name of the item", - "shop-option-description-newItemName": "New name of the Item", - "shop-option-description-itemID": "ID of the Item", - "shop-option-description-price": "Price of the item", - "shop-option-description-role": "Role to give to users who buy the item", - "shop-command-description-buy": "Buy an item", - "shop-command-description-list": "List all items in the shop", - "shop-command-description-delete": "Remove an item from the shop", - "shop-command-description-edit": "Edit an item", - "channel-not-found": "Can't find the leaderboard channel with the ID %c", - "command-description-deposit": "Deposit xyz to your bank", - "option-description-amount-deposit": "Amount to deposit", - "command-description-withdraw": "Withdraw xyz from your Bank", - "option-description-amount-withdraw": "Amount to withdraw", - "command-group-description-msg-drop-msg": "Enable/ Disable the Message-Drop-Message", - "command-description-msg-drop-msg-enable": "Enable the Message-Drop-Message", - "command-description-msg-drop-msg-disable": "Disable the Message-Drop-Message", - "command-description-destroy": "Destroy the whole economy (deletes all Database-Entries)", - "option-description-confirm": "Confirm, that you really want to destroy the whole economy", - "destroy-cancel-reply": "You're lucky. You stopped me in the last moment before I destroyed the economy", - "destroy-reply": "Ok... I'll destroy the whole economy", - "destroy": "%u destroyed the economy", - "migration-happening": "Database not up-to-date. Migrating database...", - "migration-done": "Migrated database successfully.", - "nothing-selected": "Select an item to buy it", - "select-menu-price": "Price: %p", - "price-less-than-zero": "The price can't be less or equal to zero" - }, - "status-role": { - "fulfilled": "Status-role condition is fulfilled", - "not-fulfilled": "Status-role condition is no longer fulfilled" - }, - "color-me": { - "create-log-reason": "%user redeemed their boosting-rewards by requesting the creation of this role", - "edit-log-reason": "%user edited their boosting-reward-role", - "delete-unboost-log-reason": "%user stopped boosting, so their role got deleted", - "delete-manual-log-reason": "%user deleted their role manually", - "command-description": "Request a Custom role as a reward for boosting. This has a cooldown of 24 hours", - "manage-subcommand-description": "Create or edit your custom role", - "name-option-description": "The name of your custom role", - "color-option-description": "The color of your custom role", - "remove-subcommand-description": "Remove your custom role", - "icon-option-description": "Your role-icon", - "confirm-option-remove-description": "Do you really want to delete your custom role? This will not reset any running cooldowns" - }, - "rock-paper-scissors": { - "stone": "Stone", - "paper": "Paper", - "scissors": "Scissors", - "won": "won", - "lost": "lost", - "tie": "tie", - "play-again": "Play again", - "challenge-message": "%t, %u challenged you to a game of rock-paper-scissors! Hit the button below to join the game! This invitation will expire in about 2 minutes, so don't hesitate to much.", - "invite-expired": "Sorry, %u, %i didn't accept your request to play rock-paper-scissors in time ):", - "invite-denied": "Sorry, %u, but %i denied your request to play a round of rock-paper-scissors ):", - "rps-title": "Rock Paper Scissors", - "rps-description": "Choose your weapon!", - "its-a-tie-try-again": "It's a tie! Try again!", - "command-description": "Play rock-paper-scissors against the bot or someone in the chat" - }, - "connect-four": { - "tie": "It's a tie!", - "win": "%u has won the game!", - "not-turn": "Sorry, but it's not your turn!", - "game-message": "Connect Four game of %u1 and %u2\nCurrent turn: %c %t.\n\n%g", - "challenge-message": "%t, %u challenged you to a game of Connect Four! Hit the button below to join the game! This invitation will expire in about 2 minutes, so don't hesitate to much.", - "invite-expired": "Sorry, %u, %i didn't accept your request to play Connect Four in time ):", - "invite-denied": "Sorry, %u, but %i denied your request to play a round of Connect Four ):", - "command-description": "Play Connect Four against someone in the chat", - "field-size-description": "The size of the playfield (default: 7)", - "challenge-yourself": "You cannot challenge yourself!", - "challenge-bot": "You cannot challenge bots!" - }, - "uno": { - "command-description": "Play Uno against users in the chat", - "challenge-message": "%u invites to a round of Uno! Click the button below this message to join! The game starts %timestamp with %count players.", - "not-enough-players": "Not enough players joined for a round of Uno!", - "user-cards": "%u: %cards cards", - "already-joined": "You're already in!", - "view-deck": "View deck", - "draw": "Draw card", - "uno": "Uno!", - "turn": "It's %u turn!", - "update-button": "Update", - "use-drawn": "Do you want to use the drawn card?", - "dont-use-drawn": "Dont use", - "win": "%u won the game! %turns cards were played.", - "win-you": "You've won the game!", - "missing-uno": "⚠️ You must use the Uno! button before you use your second last card!", - "choose-color": "Select a color:", - "pending-draws": "Use a Draw 2/4 card, otherwise you have to draw %count cards!", - "not-ingame": "You're not in this game!", - "skip": "Skip", - "reverse": "Reverse", - "color": "Color choice", - "draw2": "Draw 2", - "colordraw4": "Color choice and draw 4", - "cant-uno": "You cannot use Uno currently.", - "done-uno": "You've called Uno!", - "auto-drawn-skip": "Your turn was skipped because you would have had to draw the cards anyway.", - "start-game": "Start game now", - "not-host": "You're not the host of the game!", - "max-players": "The game is full!", - "previous-cards": "Previous cards: ", - "used-card": "You've already used the card %c! Use the Update button and play a valid card.", - "invalid-card": "You cannot play the card %c right now! Please select a valid card.", - "inactive-warn": "%u, it's your turn in the uno game!", - "inactive-win": "The uno game has ended. %u won as all others have been eliminated!" - }, - "quiz": { - "what-have-i-voted": "What have I voted?", - "vote": "Vote!", - "vote-this": "Select this option if you think it's correct.", - "voted-successfully": "Selected successfully.", - "not-voted-yet": "You have not selected an option yet, so I can't show you what you selected.", - "you-voted": "You've selected **%o** as correct answer.", - "change-opinion": "You can change your opinion at any time by selecting another option above the button you just clicked.", - "cannot-change-opinion": "You cannot change your selection as the creator of this quiz disabled it.", - "select-correct": "Select all correct answers", - "this-correct": "Mark this answer as correct", - "cmd-description": "Create or play server quiz", - "cmd-create-normal-description": "Create a quiz with up to 10 answers", - "cmd-create-bool-description": "Create a quiz with true or false answers", - "cmd-play-description": "Play a server quiz", - "cmd-leaderboard-description": "Shows the quiz leaderboard of the server", - "cmd-create-description-description": "Title / description of the quiz", - "cmd-create-channel-description": "Channel in which the quiz should be created", - "cmd-create-endAt-description": "How long the quiz will last", - "cmd-create-option-description": "Option number %o", - "cmd-create-canchange-description": "If the players can change their opinion after voting (default: no)", - "daily-quiz-limit": "You've reached the limit of **%l** daily playable quizzes. You can play again %timestamp.", - "created": "Quiz created successfully in %c.", - "correct-highlighted": "All correct answers were highlighted.", - "answer-correct": "✅ Your answer was correct and you've received one point for the leaderboard!", - "answer-wrong": "❌ Your answer was wrong!", - "bool-true": "Statement is correct", - "bool-false": "Statement is wrong", - "leaderboard-channel-not-found": "The leaderboard channel couldn't be found or it's type is invalid.", - "leaderboard-notation": "**%p. %u**: %xp XP", - "your-rank": "You've collected **%xp** points in quiz!", - "no-rank": "You've never finished a quiz successfully!", - "no-quiz": "No quizzes have been created for this server. Trusted admins can create them on https://scnx.app/glink?page=bot/configuration?query=quiz&file=quiz%7Cconfigs%2FquizList .", - "no-permission": "You don't have enough permissions to create quiz using the command." - }, - "starboard": { - "invalid-minstars": "Invalid minimum stars %stars", - "star-limit": "You've reached the hourly starboard limit of %limitEmoji on the server which is why you cannot react on the message %msgUrl .\nTry again %time!" - }, - "nicknames": { - "owner-cannot-be-renamed": "The owner of the server (%u) cannot be renamed.", - "nickname-error": "An error occurred while trying to change the nickname of %u: %e" - }, - "ping-protection": { - "log-not-a-member": "[Ping Protection] Punishment failed: The pinger is not a member.", - "log-punish-role-error": "[Ping Protection] Punishment failed: I cannot punish %tag because their role is higher than or equal to my highest role.", - "log-mute-error": "[Ping Protection] Punishment failed: I cannot mute %tag: %e", - "log-kick-error": "[Ping Protection] Punishment failed: I cannot kick %tag: %e", - "log-action-log-failed": "[Ping Protection] Punishment logging failed: %e", - "log-data-deletion": "[Ping Protection] All data for the user with ID %u has been deleted successfully.", - "log-automod-keyword-limit": "[Ping Protection] Automod keywords exceed 1000 characters limit. Keywords were truncated.", - "punish-log-failed-title": "Punishment failed for user %u", - "punish-log-failed-desc": "An error occured while trying to punish the user %m. Please check the bot's permissions and role hierarchy. See the message below for the error.", - "punish-log-error": "Error: ```%e```", - "punish-role-error": "I cannot punish %tag because their role is higher than or equal to my highest role.", - "reason-basic": "User reached %c pings in the last %w weeks.", - "reason-advanced": "User reached %c pings in the last %d days (Custom timeframe).", - "cmd-desc-module": "Ping protection related commands", - "cmd-desc-group-user": "Every command related to the users", - "cmd-desc-history": "View the ping history of a user", - "cmd-opt-user": "The user to check", - "cmd-desc-actions": "View the moderation action history of a user", - "cmd-desc-panel": "Admin: Open the user management panel", - "cmd-desc-group-list": "Lists protected or whitelisted entities", - "cmd-desc-list-protected": "List of all the protected users and roles", - "cmd-desc-list-wl": "List of all the whitelisted roles, channels and users", - "embed-history-title": "Ping history of %u", - "no-data-found": "No logs found for this user.", - "embed-actions-title": "Moderation history of %u", - "label-reason": "Reason", - "actions-retention-note": "Note: Moderation actions are retained for 1 - 12 months based on the configuration.", - "no-permission": "You don't have sufficient permissions to use this command.", - "panel-title": "User Panel: %u", - "panel-description": "Manage and view data for %u (%i). View a quick recap of their ping and moderation history, or delete all data stored for this user (Risky).", - "btn-history": "Ping history", - "btn-actions": "Actions history", - "btn-delete": "Delete all data (Risky)", - "list-protected-title": "Protected Users and Roles", - "list-protected-desc": "View all protected users and roles here. When someone pings one of these protected user(s)/role(s), a warning will be sent. Exceptions are when pinged by someone with a whitelisted role/as a whitelisted user or when it's sent in a whitelisted channel.", - "field-protected-users": "Protected Users", - "field-protected-roles": "Protected Roles", - "list-whitelist-title": "Whitelisted Roles, Users and Channels", - "list-whitelist-desc": "View all whitelisted roles, users and channels here. Whitelisted roles and users will not get a warning for pinging a protected entity, and pings from them or in whitelisted channels will be ignored.", - "field-wl-roles": "Whitelisted Roles", - "field-wl-channels": "Whitelisted Channels", - "field-wl-users": "Whitelisted Users", - "list-none": "None are configured.", - "modal-title": "Confirm data deletion for this user", - "modal-label": "Confirm data deletion by typing this phrase:", - "modal-phrase": "I understand that all data of this user will be deleted and that this action cannot be undone.", - "modal-failed": "The phrase you entered is incorrect. Data deletion cancelled.", - "modal-success-data-deletion": "All data for the user <@%u> (%u) has been deleted successfully", - "field-quick-history": "Quick history view (Last %w weeks)", - "field-quick-desc": "Pings history amount: %p\nModeration actions amount: %m", - "history-disabled": "History logging has been disabled by a bot-configurator.\nAre you (one of) the bot-configurators? You can enable history logging in the \"Data Storage\" tab in the 'ping-protection' module ^^", - "leaver-warning-long": "This user left the server at %d. These logs will stay until automatic deletion.", - "leaver-warning-short": "This user left the server at %d.", - "meme-why": "😐 [Why are you the way that you are?]() - You just pinged yourself..", - "meme-played": "🔑 [Congratulations, you played yourself.]()", - "meme-spider": "🕷️ [Is this you?]() - You just pinged yourself.", - "meme-rick": "🎵 [Never gonna give you up, never gonna let you down...]() You just Rick Rolled yourself. Also congrats you unlocked the secret easter egg that only has a 1% chance of appearing!!1!1!!", - "meme-grind": "Why are you even pinging yourself 5 times in a row? Anyways continue some more to possibly get the secret meme\n-# (good luck grinding, only a 1% chance of getting it and during testing I had it once after 83 pings)", - "label-jump": "Jump to Message", - "no-message-link": "This ping was blocked by AutoMod", - "list-entry-text": "%index. **Pinged %target** at %time\n%link" - }, - "staff-management-system": { - "time-zero": "0 seconds", - "time-hours": "hours", - "time-hour": "hour", - "time-mins": "minutes", - "time-min": "minute", - "time-secs": "seconds", - "time-sec": "second", - "stat-brk": "🟡 On Break", - "stat-on": "🟢 On-Duty", - "stat-off": "🔴 Off-Duty", - "duty-panel-title": "Duty Panel - %type", - "duty-stats": "📊 Statistics", - "duty-stat-desc": "**Total Shift Duration:** %duration\n**Total Shifts:** %count\n**Average Shift Duration:** %average", - "btn-duty-on": "On-Duty", - "btn-duty-res": "Resume Duty", - "btn-duty-brk": "Toggle Break", - "btn-duty-off": "Off-Duty", - "duty-breakdown": "Shift Breakdown", - "duty-quota-str": "\n\n**Quota (%timeframe):** %duration / %hours hours\n*%result*", - "quota-met": "✅ Quota Met", - "quota-fail": "❌ Quota Not Met", - "duty-time-title": "Shift Time - %type", - "duty-time-desc": "**Total Shifts:** %count\n**Total Duration:** %duration", - "btn-hist": "View History", - "err-no-lb": "ℹ️ No shift data found for **%type**.", - "duty-lb-title": "Leaderboard - %type", - "duty-lb-desc": "**%lookback Top Shifts**\n\n%lines", - "page-count": "Page %page/%total", - "info-no-sh-hi": "ℹ️ No completed shifts found.", - "duty-hi-title": "Shift History - %type", - "duty-adm-title": "Admin Duty Panel - %user", - "btn-f-off": "Force Off-Duty", - "btn-v-act": "Void Active Shift", - "btn-add-t": "Add Time", - "btn-v-all": "Void All Shifts", - "err-not-yours": "❌ This panel is not yours.", - "err-alr-on": "❌ You are already on a shift.", - "err-not-on": "❌ You are not on a shift.", - "err-hist-oth": "❌ You can only view your own history.", - "mod-v-all-title": "Confirm: Void All Shifts", - "mod-v-all-lbl": "Type CONFIRM to delete all shift data", - "err-conf-fail": "❌ Data deletion confirmation failed. You must type the phrase exactly.", - "succ-v-all": "All shift data for <@%user> has been deleted successfully.", - "mod-add-t": "Add Duty Time", - "mod-add-min": "Minutes to add", - "mod-add-type": "Shift Type", - "err-inv-min": "❌ Invalid number of minutes.", - "err-inv-type": "❌ Invalid shift type. Available: %types", - "err-sh-dis": "❌ Shift tracking is disabled.", - "info-no-act-sh": "ℹ️ There are no active shifts right now.", - "duty-act-title": "Active Shifts", - "duty-act-desc": "**Total Shifts:** %count", - "err-no-perm": "❌ You do not have permission to do this.", - "err-no-mem": "❌ Could not find that member.", - "ph-sel-type": "Select a Shift Type", - "msg-sel-type": "👇 Please choose your shift type:", - "err-prof-dis": "❌ Staff Profiles are disabled.", - "err-prof-cfg": "❌ Configuration is missing. Please make sure the message is not empty.", - "err-prof-no-own": "❌ You do not have a staff profile.", - "err-prof-no-tgt": "❌ That user does not have a profile.", - "rev-dis-text": "*Reviews disabled*", - "rev-no-rate": "No ratings yet", - "stat-offl": "⚫ Offline", - "stat-onl": "🟢 Online", - "stat-idl": "🟡 Away", - "stat-dnd": "🔴 Do Not Disturb", - "stat-prof-ond": "⏱️ On duty", - "stat-prof-loa": "🌙 On LoA", - "stat-prof-ra": "⛱️ On RA", - "prof-no-intro": "*No introduction set.*", - "err-prof-empty": "❌ Profile embed is empty.", - "err-prof-perm": "❌ You must be a staff member to have a profile.", - "prof-edit-title": "Edit Profile", - "prof-edit-nick": "Custom Nickname", - "prof-edit-intro": "Introduction", - "succ-prof-wipe": "✅ Profile wiped for %u.", - "succ-prof-upd": "✅ Profile updated!", - "general-chan": "Channel", - "general-ends": "Ends", - "ac-tot-res": "Total Responded", - "err-ac-noact": "❌ There is no active activity check.", - "succ-ac-end": "✅ Activity check ended manually.", - "err-gen-no-user": "❌ Could not find that user.", - "del-conf-phrase": "I understand that this will delete the specified data for this user and it cannot be undone.", - "mod-del-title": "Confirm Data Deletion", - "mod-del-lbl": "Type confirmation phrase:", - "del-all-title": "Confirm total data deletion", - "del-all-desc": "You are about to delete ALL data for this user. Reminder that this ***cannot be undone***. This is the last chance to back out. If you are sure, click the button below.\nThis action will automatically cancel in 30 seconds.", - "btn-conf-del": "Confirm deletion", - "btn-cancel": "Cancel", - "succ-del-canc": "✅ Data deletion cancelled.", - "succ-del-all": "✅ ALL data has been permanently wiped.", - "err-del-time": "⏳ Data deletion timed out.", - "succ-del-tgt": "✅ Target data has been permanently wiped.", - "err-gen-no-perm": "❌ You do not have permission.", - "err-no-req": "❌ Request not found.", - "err-req-hndl": "❌ Request is already %status.", - "mod-deny-req": "Deny Request", - "general-rsn": "Reason", - "label-appr-by": "This was approved by", - "req-appr-by": "✅ Approved by %user", - "req-deny-by": "❌ Denied by %user", - "general-stat": "Status", - "err-ac-alr-end": "❌ This activity check has already ended.", - "info-ac-alr-conf": "ℹ️ You already confirmed your activity!", - "succ-ac-log": "✅ Activity logged successfully!", - "err-internal": "❌ An internal error occurred.", - "dm-appr-title": "Your %label request got approved!", - "dm-appr-desc": "Your %label request got approved by %approver!\nYou are now on LoA until %endFmt.\nYou can view your LoA status by using the %viewCmd command.", - "dm-deny-title": "Your %label request was denied", - "dm-deny-desc": "Your %label request was denied by %denier.\n**Reason:** %reason", - "dm-ext-title": "Your %label got extended", - "dm-ext-desc": "Your %label got extended by %extender.\nThis extension is for **%days day(s)** - your %label now ends at %endFmt.\n**Reason for extension:** %reason\nYou can view your updated %label status by using the %viewCmd command.", - "dm-early-title": "Your %label ended early", - "dm-early-desc": "Your %label got ended early by %ender - your %label is now over and your role has been removed.\n**Reason for early end:** %reason.", - "dm-end-title": "Your %label has ended", - "dm-end-desc": "Your %label has now ended and your role has been removed.", - "log-start-title": "%label started for %username", - "log-start-desc": "%label started for %mention.%apprText", - "log-info-hdr": "%label Information", - "general-start": "Start", - "general-end": "End", - "log-end-title": "%label ended for %username", - "log-end-desc": "%label ended for %mention.", - "general-started": "Started", - "general-ended": "Ended", - "log-adj-title": "%label adjusted for %username", - "log-adj-desc": "The %label of %mention was adjusted by <@%executor>.", - "log-changes": "Changes made:", - "err-feat-disabled": "❌ %feature disabled.", - "err-use-susp": "❌ Please use `/staff-management infraction suspend`.", - "err-inv-dur": "❌ Invalid duration format or value.", - "label-never": "Never", - "succ-infract": "✅ Issued **%type** (Case #%caseId) to %user.", - "label-days": "days", - "succ-susp": "✅ Issued Suspension (Case #%caseId) to %user for %duration.", - "err-no-case-ref": "❌ No case found for %reference.", - "err-case-inact": "⚠️ Case #%caseId is inactive.", - "succ-void-fail": "✅ Case #%caseId voided, role restore failed.", - "succ-void": "✅ Voided Case #%caseId.", - "info-clean-rec": "ℹ️ %username has a clean record.", - "rec-title": "Record: %username", - "icon-voided": "⚪", - "label-exp": "Expires", - "label-case": "Case", - "label-date": "Date", - "label-iss": "Issuer", - "err-role-hier": "❌ I cannot assign a role higher than my highest role.", - "err-add-role": "❌ Failed to add role: %e", - "succ-promo": "✅ Promoted %user to %role.", - "info-no-promo": "ℹ️ No promotion history found for %username.", - "prom-hist-title": "Promotion History: %username", - "label-role": "Role", - "label-prom-by": "Promoted by", - "panel-title": "User Panel: %username", - "panel-desc": "Manage and view all data for the user %mention (%id).", - "panel-ph": "Select a category...", - "opt-over": "Overview", - "opt-act": "Activity Checks", - "opt-inf": "Infractions", - "opt-prom": "Promotions", - "opt-rev": "Reviews", - "opt-shi": "Shifts", - "opt-sta": "Status", - "opt-del": "Data Deletion", - "p-inf-title": "Infractions: %username", - "p-inf-desc": "Total: **%count**\n%types\n", - "info-none": "*None*", - "p-no-hist": "*No history on this page.*", - "p-prom-title": "Promotions: %username", - "p-prom-desc": "Total: **%count**\n", - "p-rev-title": "Reviews: %username", - "p-rev-desc": "Total: **%count**\nAverage rating: **%avg ⭐**\n", - "label-by": "by", - "p-sta-title": "Status: %username", - "p-sta-desc": "Total requests: **%count**\nActive: %active\n", - "p-act-title": "Activity Checks: %username", - "p-act-desc": "Responses: **%count**\n", - "label-chk": "Check on", - "label-end": "Ends", - "label-chan": "Channel", - "p-shi-title": "Shifts: %username", - "no-quota-configured": "No quota", - "duty-quota-met": "✅ Quota Met", - "duty-quota-failed": "❌ Quota Not Met", - "label-unranked": "Unranked", - "panel-shifts-desc": "**Total Shifts:** %totalShifts\n**Duration:** %totalSeconds\n**Rank:** %lbRank\n**Breakdown:**\n%breakdownStr\n\n%quotaStr", - "err-shift-data-unavailable": "Shift data unavailable: %error", - "btn-view-history": "View History", - "panel-deletion-title": "Data Deletion: %tag", - "panel-deletion-desc": "⚠️ DANGEROUS AREA ⚠️\nYou are now entering a dangerous zone. At this place, you are able to delete specific or all data for the selected user. These actions ***CANNOT BE UNDONE*** and should only be used if you are absolutely sure about what you are doing. If you only want to delete specific entries, please use the respective command for that entry instead.\nIf you are unsure, click 'Go Back' from the dropdown now.\n\nUse the dropdown below to choose which data you want to delete or delete all data. Choose wisely and gracefully.", - "panel-deletion-placeholder": "Select data to delete...", - "panel-opt-back": "Go Back", - "panel-opt-del-act": "Delete Activity Checks", - "panel-opt-del-inf": "Delete Infractions", - "panel-opt-del-prom": "Delete Promotions", - "panel-opt-del-rev": "Delete Reviews", - "panel-opt-del-shifts": "Delete Shifts", - "panel-opt-del-status": "Delete Status", - "panel-opt-del-all": "Delete ALL data", - "status-active-loa": "🟢 On LoA", - "status-active-ra": "🟠 On RA", - "status-hist-loa": "LoA History", - "status-hist-ra": "RA History", - "err-status-disabled": "❌ %type system disabled.", - "err-invalid-duration": "❌ Invalid duration.", - "err-duration-max": "❌ Max duration is %max days.", - "err-status-exists": "❌ You have an active %type request.", - "status-request-title": "New %type Request", - "status-req-user": "User", - "status-req-duration": "Duration", - "btn-approve": "Approve", - "btn-deny": "Deny", - "success-status-request": "✅ %type request created (%state).", - "state-pending": "Pending", - "state-auto": "Auto-Approved", - "no-active-status": "ℹ️ %user has no active %type.", - "label-stat": "Status", - "filter-active": " (Active)", - "filter-expired": " (Expired)", - "filter-history": " (History)", - "err-no-recs": "No records found.", - "manage-status-title": "Manage %label - %username", - "manage-stat-desc": "%status\nPrevious %label's: %count", - "no-act-stat": "⚫ No active %label", - "manage-active-details": "📋 Active %label Details", - "label-auto": "Auto", - "manage-no-active-user": "No active %label.", - "btn-end-early": "End %label Early", - "btn-extend": "Extend %label", - "err-no-active-end": "❌ No active %label to end.", - "modal-end-early-title": "End %label Early", - "modal-end-early-reason": "Reason for ending", - "err-stat-inact": "❌ This %label is inactive.", - "status-ended-embed-desc": "⚫ %label ended by %user\nReason: %reason", - "err-no-active-extend": "❌ No active %label.", - "modal-extend-title": "Extend %label", - "modal-extend-days": "Additional days, maximum of 180 days", - "modal-extend-reason": "Reason for extension", - "status-adjusted-log": "**%label extended** - the %label now ends at %newEnd.\n**Reason:** %reason", - "mod-stat-ext": "**Start:** %s\n**End:** %e (+%d days)\n**Status:** %t\n**Approved by:** %a\n**Reason:** %r", - "info-no-status-history": "ℹ️ No %label history.", - "status-history-desc": "Showing %count of %total %label records.", - "err-ac-act": "❌ Active check already running.", - "err-ac-norole": "❌ No target roles configured.", - "err-ac-invchan": "❌ Invalid channel.", - "ac-confirm-btn": "Confirm Activity", - "succ-ac-start": "✅ Check started in <#%channel> for %hours hours.", - "err-ac-perms": "❌ Missing permissions in <#%channel>.", - "ac-title-end": "📋 Activity Check (Ended)", - "ac-res-title": "📊 Activity Results", - "ac-f-res": "✅ Responded (%count)", - "ac-f-fail": "❌ Failed (%count)", - "ac-f-exc": "🛡️ Exceptions (%count)", - "log-ac-send-fail": "Failed to send activity check results message: %error", - "err-not-mem": "❌ That is not a member.", - "err-self-rate": "A good detective never investigates themselves. Neither do you.", - "err-staff-rate": "❌ You can only rate staff.", - "succ-review": "✅ Rated %tag %stars stars.", - "rev-title": "Reviews: %username", - "rev-desc": "**Average:** %avg ⭐ (%count reviews)", - "label-hist": "History", - "info-ac-none": "There are no active activity checks. Please check recent results in %c.", - "log-sched-fail": "[Staff Management] Failed to init expiry schedules: %error", - "log-susp-end": "[Staff Management] Automatically ended suspension for %tag", - "log-susp-err": "[Staff Management] Error expiring suspension: %error", - "log-leave-err": "[Staff Management] Error handling member leave: %error", - "log-del-all": "[Staff Management] Data deletion (ALL) executed for user %target by admin %admin.", - "log-del-type": "[Staff Management] Data deletion (%type) executed for user %target by admin %admin.", - "log-int-error": "[Staff Management] Interaction Error: %error", - "log-void-all": "[Staff management] All shift data for the user with ID %target has been deleted by admin %admin.", - "log-add-time": "[Staff Management] %admin added %min mins of %type duty time to %target.", - "log-stat-dm-error": "[Staff Management] Failed to send status DM to %u: %e", - "log-status-adj-error": "[Staff Management] Logging status adjustment failed: %e", - "log-promo-msg-error": "[Staff Management] Failed to send promotion announcement: %e", - "lbl-log-chan": "the configured log channel", - "ac-live-title": "Live Activity Check Status", - "err-ac-not-req": "❌ You are not required to respond to this activity check.", - "cmd-desc-status": "Manage Leave of Absence (LoA) and Reduced Activity (RA).", - "cmd-desc-loa": "Manage Leave of Absence (LoA).", - "cmd-desc-loa-request": "Request a Leave of Absence.", - "cmd-desc-loar-duration": "The duration for your LoA (e.g. 3d, 2w, 1m)", - "cmd-desc-loar-reason": "Reason for your LoA", - "cmd-desc-loa-view": "View your Leave of Absence status.", - "cmd-desc-loav-user": "The user to view the LoA status", - "cmd-desc-loa-list": "List of all Leave of Absences", - "cmd-desc-loal-filter": "Filter the LoA list on active, expired or all", - "cmd-desc-loa-admin": "Manage a user's Leave of Absence.", - "cmd-desc-loaa-user": "The user to manage their LoA", - "cmd-desc-ra": "Manage Reduced Activity (RA).", - "cmd-desc-ra-request": "Request Reduced Activity.", - "cmd-desc-rar-duration": "The duration for your RA (e.g. 3d, 2w, 1m)", - "cmd-desc-rar-reason": "Reason for your RA", - "cmd-desc-ra-view": "View your Reduced Activity status.", - "cmd-desc-rav-user": "The user to view the RA status", - "cmd-desc-ra-list": "List of all Reduced Activities", - "cmd-desc-ral-filter": "Filter the RA list on active, expired or all", - "cmd-desc-ra-admin": "Manage a user's Reduced Activity.", - "cmd-desc-raa-user": "The user to manage their RA", - "cmd-desc-duty": "Manage your duty status and view statistics.", - "cmd-desc-duty-manage": "Manage your duty status.", - "cmd-desc-duty-manage-type": "The duty type", - "cmd-desc-duty-active": "View all staff currently on duty.", - "cmd-desc-duty-lb": "View the duty time leaderboard.", - "cmd-desc-duty-lb-type": "The duty type for the leaderboard.", - "cmd-desc-duty-time": "View your total duty time.", - "cmd-desc-duty-time-type": "The duty type", - "cmd-desc-duty-admin": "Manage a user's shift.", - "cmd-desc-duty-admin-user": "The user to manage their shift", - "cmd-desc-smg": "Access the staff management system.", - "cmd-desc-panel": "Open the staff management panel for a user.", - "cmd-desc-panel-user": "The user to open the staff panel for.", - "cmd-desc-infractions": "Manage staff infractions.", - "cmd-desc-issue": "Issue an infraction to a staff member.", - "cmd-desc-issue-user": "The user receiving the infraction.", - "cmd-desc-issue-type": "The type of infraction to issue.", - "cmd-desc-issue-reason": "The reason for issuing this infraction.", - "cmd-desc-issue-expiry": "When the infraction should expire.", - "cmd-desc-suspend": "Suspend a staff member.", - "cmd-desc-suspend-user": "The user to suspend.", - "cmd-desc-suspend-duration": "How long the suspension should last.", - "cmd-desc-suspend-reason": "The reason for the suspension.", - "cmd-desc-history": "View a user's history.", - "cmd-desc-history-user": "The user whose history you want to view.", - "cmd-desc-void": "Void an infraction case.", - "cmd-desc-void-case-ref": "The case ID or message link of the infraction to void.", - "cmd-desc-promotion": "Manage staff promotions.", - "cmd-desc-promote": "Promote a staff member to a new rank.", - "cmd-desc-promote-user": "The user to promote.", - "cmd-desc-promote-rank": "The rank to promote the user to.", - "cmd-desc-promote-reason": "The reason for the promotion.", - "cmd-desc-promote-channel": "The channel to announce the promotion in.", - "cmd-desc-prom-history": "View the promotion history of a staff member.", - "cmd-desc-prom-history-user": "The user whose promotion history you want to view.", - "cmd-desc-ac": "Manage activity checks.", - "cmd-desc-ac-start": "Start a new activity check.", - "cmd-desc-ac-start-channel": "The channel where the activity check will be posted.", - "cmd-desc-ac-view": "View the current activity check status.", - "cmd-desc-ac-end": "End the current activity check.", - "cmd-desc-profile": "Manage staff profiles.", - "cmd-desc-profile-view": "View a staff member's profile.", - "cmd-desc-profile-view-user": "The user whose profile you want to view.", - "cmd-desc-profile-edit": "Edit your staff profile.", - "cmd-desc-profile-wipe": "Wipe a staff member's profile data.", - "cmd-desc-profile-wipe-user": "The user whose profile will be wiped.", - "cmd-desc-review": "Manage staff reviews.", - "cmd-desc-review-submit": "Submit a review for a staff member.", - "cmd-desc-review-submit-user": "The user you are reviewing.", - "cmd-desc-review-submit-stars": "The star rating for the review.", - "cmd-desc-review-submit-comment": "Your review comment.", - "cmd-desc-review-history": "View the review history of a staff member.", - "cmd-desc-review-history-user": "The user whose review history you want to view.", - "del-no-perm": "You do not have sufficient permissions to perform data deletion.", - "log-err-exp-susp": "Suspension check failed: %error", - "duty-admin-target-left": "The action was completed, but the user is no longer in the server.", - "err-shift-too-short": "Your shift was not counted because it was shorter than the minimum required duration of %min minute(s).", - "log-status-expiry-fail": "[Staff Management] Failed to process automatic status expiry: %error", - "none-provided": "No reason provided.", - "log-infract-dm-fail": "[Staff Management] Failed to send infraction DM to %user: %error", - "log-susp-dm-fail": "[Staff Management] Failed to send suspension DM to %user: %error", - "log-promo-dm-fail": "[Staff Management] Failed to send promotion DM to %user: %error", - "duty-started-title": "⏲️ Shift Started", - "duty-break-title": "⏸️ On Break", - "duty-ended-title": "↩️ Off-Duty", - "duty-shift-overview": "Shift Overview", - "duty-shift-report-title": "Shift Report", - "duty-shift-information": "Shift Information", - "label-started": "Started", - "label-ended": "Ended", - "label-elapsed-time": "Elapsed Time", - "label-shift-type": "Shift Type", - "log-duty-dm-fail": "[Staff Management] Failed to send shift report DM to %user: %error", - "label-breaks": "Breaks", - "log-duty-start-title": "%username went on-duty", - "log-duty-start-desc": "%mention has started a duty shift.", - "log-duty-break-title": "%username went on break", - "log-duty-break-desc": "%mention is now on break.", - "log-duty-resume-title": "%username resumed duty", - "log-duty-resume-desc": "%mention is back on duty.", - "log-duty-end-title": "%username went off-duty", - "log-duty-end-desc": "%mention has ended their duty shift.", - "log-duty-void-title": "%username's active shift was voided", - "log-duty-void-desc": "%mention's active shift was voided by %executor.", - "log-duty-info-hdr": "Information", - "label-ended-by": "Ended by", - "log-duty-log-fail": "[Staff Management] Failed to log duty change (%action): %error", - "err-self-infract": "That's not in the code... well, it's more of a guideline anyway. Still no.\n-# You cannot infract yourself", - "err-self-promo": "You can't promote yourself through a black hole of audacity and expect it to work.", - "general-req-reason": "Reason for request", - "status-expired-auto": "Ended automatically because the status expired." - } + "main": { + "startup-info": "SCNX-CustomBot v2 - Log-Level: %l", + "missing-moduleconf": "Missing moduleConfig-file. Automatically disabling all modules and overwriting modules.json later", + "sync-db": "Synced database", + "login-error": "Bot could not log in. Error: %e", + "login-error-token": "Bot could not log in because the provided token is invalid. Please update your token.", + "login-error-intents": "Bot could not log in because the intents were not enabled correctly. Please enable \"PRESENCE INTENT\", \"SERVER MEMBERS INTENT\" and \"MESSAGE CONTENT INTENT\" in your Discord-Developer-Dashboard: %url", + "not-invited": "Please invite the bot to your Discord server before continuing: %inv", + "require-code-grant-active": "You might be unable to invite your bot to your server as you have enabled the \"Require public code grant\" option in your Discord Developer Dashboard. Please disable this option: %d", + "interactions-endpoint-active": "You bot will be unable to respond to interactions, because the field \"Interactions Endpoint URL\" has a value in your Discord Developer Dashboard. Please remove any content from this field and restart your bot: %d", + "logged-in": "Bot logged in as %tag and is now online.", + "logchannel-wrong-type": "There is no Log-Channel set or it has the wrong type (only text-channels are supported).", + "config-check-failed": "Configuration-Check failed. You can find more information in your log. The bot exited.", + "bot-ready": "The bot initiated successfully and is now listening to commands", + "no-command-permissions": "Could not update server commands. Please give us permissions to performe this critical action: %inv", + "perm-sync": "Synced permissions for /%c", + "perm-sync-failed": "Failed to synced permissions for /%c: %e", + "loading-module": "Loading module %m", + "hidden-module": "Module %m is hidden, meaning that it is not available. Skipping…", + "module-disabled": "Module %m is disabled", + "command-loaded": "Loaded command %d/%f", + "command-dir": "Loading commands in %d/%f", + "global-command-sync": "Synced global application commands", + "guild-command-sync": "Synced server application commands", + "guild-command-no-sync-required": "Server application commands are up to date - no syncing required", + "global-command-no-sync-required": "Global application commands are up to date - no syncing required", + "event-loaded": "Loaded events %d/%f", + "event-dir": "Loading events in %d/%f", + "model-loaded": "Loaded database model %d/%f", + "model-dir": "Loading database model in %d/%f", + "loaded-cli": "Loaded API-Action %c in %p", + "channel-lock": "Locked channel", + "channel-unlock": "Unlocked channel", + "channel-unlock-data-not-found": "Unlocking channel with ID %c failed because it was never locked (which is weird to begin with).", + "module-disable": "Module %m got disabled because %r", + "migrate-success": "Migration from %o to %m finished successfully.", + "migrate-start": "Migration from %o to %m started... Please do not stop the bot", + "shutdown-deferred": "Shutdown requested but a database migration is in progress. Will shut down after migration completes.", + "shutdown-after-migration": "Migration complete, proceeding with shutdown.", + "uncaught-exception": "Uncaught exception: %e — continuing execution.", + "unhandled-rejection": "Unhandled promise rejection: %e — continuing execution.", + "discord-error": "Discord.js error: %e", + "shard-error": "Discord shard error: %e", + "shard-disconnect": "Disconnected from Discord (close event code: %c). Reconnection will be attempted automatically.", + "shard-reconnecting": "Reconnecting to Discord…", + "db-connect-error": "Could not connect to the database: %e — the bot will now exit.", + "cli-command-error": "CLI command error: %e", + "discord-api-error": "Could not reach the Discord API during startup: %e — some checks were skipped." + }, + "reload": { + "reloading-config": "Reloading configuration…", + "reloading-config-with-name": "User %tag is reloading the configuration…", + "reloaded-config": "Configuration reloaded successfully.\nOut of %totalModules modules, %enabled were enabled, %configDisabled were disabled because their configuration was wrong.", + "reload-failed": "Configuration reloaded failed. Bot shutting down.", + "reload-successful-syncing-commands": "Configuration reloaded successfully, syncing commands, to make sure permissions are up-to-date…", + "reload-failed-message": "**FAILED**\n```%r```\n**Please read your log to find more information**\nThe bot will kill itself now, bye :wave:", + "command-description": "Reloads the configuration" + }, + "config": { + "checking-config": "Checking configurations...", + "done-with-checking": "Done with checking. Out of %totalModules modules, %enabled were enabled, %configDisabled were disabled because their configuration was wrong.", + "creating-file": "Config %m/%f does not exist - I'm going to create it, please stand by...", + "checking-of-field-failed": "An error occurred while checking the content of field \"%fieldName\" in %m/%f", + "saved-file": "Configuration-File %f in %m was saved successfully.", + "moduleconf-regeneration": "Regenerating module configuration, no settings will be overwritten, don't worry.", + "moduleconf-regeneration-success": "Module configuration regeneration successfully finished.", + "channel-not-found": "Channel with ID \"%id\" could not be found", + "user-not-found": "User with ID \"%id\" could not be found", + "channel-not-on-guild": "Channel with ID \"%id\" is not on your server", + "channel-invalid-type": "Channel with ID \"%id\" has a type that can not be used for this field", + "role-not-found": "Role with ID \"%id\" could not be found on your server", + "config-reload": "Reloading all configuration..." + }, + "helpers": { + "timestamp": "%dd.%mm.%yyyy at %hh:%min", + "you-did-not-run-this-command": "You did not run this command. If you want to use the buttons, try running the command yourself.", + "next": "Next", + "back": "Back", + "toggle-data-fetch-error": "SC Network Release: Toggle-Data could not be fetched", + "toggle-data-fetch": "SC Network Release: Toggle-Data fetched successfully", + "duration-just-now": "just now", + "duration-minute": "%i minute", + "duration-minutes": "%i minutes", + "duration-hour": "%i hour", + "duration-hours": "%i hours", + "duration-day": "%i day", + "duration-days": "%i days", + "duration-month": "%i month", + "duration-months": "%i months", + "duration-year": "%i year", + "duration-years": "%i years" + }, + "command": { + "startup": "The bot is currently starting up. Please try again in a few minutes.", + "not-found": "Command not found", + "used": "%tag (%id) used command /%c", + "message-used": "%tag (%id) used command %p%c", + "execution-failed": "Execution of command /%c %g %s failed (Tracing: %t): %e", + "message-execution-failed": "Execution of command %p%c failed (Tracing: %t): %e", + "wrong-guild": "This command is only available on the server **%g**.", + "autcomplete-execution-failed": "Execution of auto-complete on command /%c %g %s with option %f failed: %e", + "execution-failed-message": "## 🔴 Command execution failed 🔴\nThis usually happens either due to misconfiguration or due to an error on our site. Please send a screenshot of this message in #support on the [ScootKit Discord](https://scootk.it/dc-en), to resolve this.\n\n### Internal Tracing ID\n`%t`\n### Debugging-Information\n```%e```", + "error-giving-role": "An error occurred when trying to give you your roles ):\nPlease ask the server administrators to confirm that the highest role of the bot is above the role that the bot is supposed to assign.", + "description-too-long": "The following command description of %c was too long to sync: %s", + "module-disabled": "This command is part of the \"%m\" which is disabled. This can either be intended by the server-admins (and slash-commands haven't synced yet) or this could be caused by a configuration error. Please check (or ask the admins) to check the bot's configuration and logs for details.", + "command-disabled": "This command is currently disabled by the server configuration. If you believe this is an error, please contact a server administrator." + }, + "help": { + "bot-info-titel": "ℹ️ Bot-Info", + "bot-info-description": "This bot is part of [SCNX](https://scnx.xyz/de?ref=custombot_help_embed), a plattform from [ScootKit](https://scootkit.net) allowing the creation of fully customizable for Discord communities, and is being hosted for \"%g\".", + "stats-title": "📊 Stats", + "stats-content": "Active modules: %am\nRegistered commands: %rc\nBot-Version: %v\nRunning on server %si\n[Server-Plan](https://scnx.xyz/plan): %pl\nLast restart: %lr\nLast reload: %lR", + "command-description": "Show every commands", + "slash-commands-title": "Slash-Commands", + "select-module-placeholder": "Select a module to view its commands", + "select-module-hint": "👇 Use the dropdown below to browse commands by module.", + "back-to-overview": "Back to overview", + "modules-overview": "📋 Modules & Commands", + "built-in-description": "Core commands built into the bot", + "custom-commands-label": "Custom Commands", + "custom-commands-description": "User-created custom slash commands" + }, + "bot-feedback": { + "command-description": "Send feedback about the bot to the bot developer", + "submitted-successfully": "Thanks so much for your feedback! It has been carefully recorded and our team will review it soon. If we have any questions, we may contact you via DM (or if you are on our [Support Server]() we'll open a ticket). Thank you for making [CustomBots]() better for everyone <3\n\nYour feedback is subject to our [Terms of service]() and [Privacy Policy]().", + "failed-to-submit": "Sorry, but I couldn't send your feedback to our staff. This could be, because you got blocked or because of some server issue we are having. You can always report bugs and submit feedback in our [Feature-Board](https://features.sc-network.net). Thank you.", + "feedback-description": "Your feedback. Make sure it's neutral, constructive and helpful" + }, + "admin-tools": { + "position": "%i has the position %p.", + "position-changed": "Changed %i's position to %p.", + "category-can-not-have-category": "A Category can not have a category", + "not-category": "Can not change category of channel to a not category channel", + "changed-category": "%c's category got set to %cat", + "command-description": "Execute some actions for admins via commands", + "new-position-description": "New position", + "movechannel-description": "See the position of a channel or change the position of a channel", + "moverole-description": "See the position of a role or change the position of a role", + "setcategory-description": "Sets the category of a channel", + "channel-description": "Channel on which this action should be executed", + "role-description": "Role on which this action should be executed", + "category-description": "New category of the channel", + "emoji-too-much-data": "Please **only** enter one emoji and nothing else", + "emoji-import": "Imported \"%e\" successfully.", + "stealemote-description": "Steals a emote from another server", + "emote-description": "Emote to steal", + "role-command-description": "Assign or remove roles permanently or temporarily", + "role-give-description": "Assign someone a role permanently or temporarily", + "role-user-add-description": "Member that you want to assign the role to", + "role-add-role-description": "Role you want to assign to the member", + "role-add-duration-description": "If you set this parameter, the role will be removed from this user after this duration expires", + "role-user-status-description": "User you want to see temporary roles from", + "role-remove-description": "Remove a role from someone permanently or temporarily", + "role-user-remove-description": "Member that you want to remove the role from", + "role-remove-role-description": "Role you want to remove from the member", + "role-remove-duration-description": "If you set this parameter, the role will be added back to this user after this duration expires", + "role-status-description": "Shows which roles of a user are temporary and when they will be removed", + "role-not-high-enough": "The highest role of the bot is not above %e. The highest role of the bot needs to be above the role you want to remove or assign.", + "unable-to-change-roles": "Changing role %r to %u failed. Error message obtained by Discord:\n```%e```", + "user-not-found": "The user has not been found on your server.", + "duration-wrong": "The value of the duration argument is wrong. Learn more [in our docs]()", + "audit-log-add": "[admin-tools] %u added a role using a command.", + "audit-log-remove": "[admin-tools] %u removed a role using a command.", + "audit-log-add-duration": "[admin-tools] %u added a temporary role using a command that will be removed at %t.", + "audit-log-remove-duration": "[admin-tools] %u removed a temporary role using a command that will be added back at %t.", + "audit-log-temporary-remove": "[admin-tools] This role was added temporarily and has removed since the temporary timeframe expired.", + "audit-log-temporary-add": "[admin-tools] This role has been removed temporarily and has been added back since the temporary timeframe expired.", + "role-add": "%u has been given the role %r.", + "role-remove": "%u has removed the role %r.", + "role-add-duration": "%u has been given the role %r. It will be removed at %t.", + "role-remove-duration": "%r has been removed from %u. It will be given back at %t.", + "user-without-temporary-action": "%u has no roles that are temporary.", + "user-temporary-action-header": "Temporary roles of %u", + "status-remove": "%r will be removed on %t.", + "status-add": "%r will be added back on %t.", + "users-trying-to-manage-higher-role": "Your highest role, %t, is not below %e. To manage a user's role, you the role you are managing needs to be below your highest role.", + "audit-log-role-ban": "[admin-tools] User banned for receiving the \"%r\" role. Reason: %reason" + }, + "welcomer": { + "channel-not-found": "[welcomer] Channel not found: %c", + "welcome-yourself-error": "Welcome, nice to meet you! This button is reversed for a special member of this server who want's to say \"Hi\" to you ^^" + }, + "months": { + "1": "January", + "2": "February", + "3": "March", + "4": "April", + "5": "May", + "6": "June", + "7": "July", + "8": "August", + "9": "September", + "10": "October", + "11": "November", + "12": "December" + }, + "levels": { + "leaderboard-channel-not-found": "Leaderboard-Channel not found or wrong type", + "leaderboard-notation": "%p. %u: Level %l - %xp XP", + "list-location": "[Level System] The live leaderboard is currently located here: %l. Delete the message and restart the bot, to re-send it.", + "leaderboard": "Leaderboard", + "no-user-on-leaderboard": "Can't generate a leaderboard, because no one has any XP which is odd, but that's how it is ¯\\_(ツ)_/¯", + "and-x-other-users": "and %uc other users", + "level": "Level %l", + "users": "Users", + "leaderboard-command-description": "Shows the leaderboard of this server", + "leaderboard-sortby-description": "How to sort the leaderboard (default: %d)", + "profile-command-description": "Shows the profile of you or an an user", + "profile-user-description": "User to see the profile from (default: you)", + "please-send-a-message": "Please send some messages before I can show you some data", + "no-role": "None", + "are-you-sure-you-want-to-delete-user-xp": "Okay, do you really want to screw with %u? If you hate them so much, feel free to run `/manage-levels reset-xp confirm:True user:%ut` to run this irreversible action.", + "are-you-sure-you-want-to-delete-server-xp": "Do you really want to delete all XP and Levels from this server? This action is irreversible and everyone on this server will hate you. Decided that it's worth it? Enter `/manage-levels reset-xp confirm:True`", + "user-not-found": "User not found", + "user-deleted-users-xp": "%t deleted the XP of the user with id %u", + "removed-xp-successfully": "`Removed %u's XP and level successfully.`", + "deleted-server-xp": "%u deleted the XP of all users", + "successfully-deleted-all-xp-of-users": "Successfully deleted all the XP of all users", + "cheat-no-profile": "This user doesn't have a profile (yet), please force them to write a message before trying to betrayal your community by manipulating level scores.", + "manipulated": "%u manipulated the XP of %m to %v (level %l)", + "successfully-changed": "Successfully edited the XP of %u - they are now **level %l** with **%x XP**.\nRemember, every change you make destroys the experience of other users on this server as the levelsystem isn't fair anymore.", + "edit-xp-command-description": "Manage the levels of your server", + "negative-xp": "This user would have a negative XP value which is not possible", + "negative-level": "This user would have a level below one which is not possible", + "xp-out-of-range": "This XP value is too large. Please choose a value below 1,000,000,000,000.", + "level-out-of-range": "This level value is too large. Please choose a value below 1,000,000.", + "reset-xp-description": "Reset the XP of a user or of the whole server", + "reset-xp-user-description": "User to reset the XP from (default: whole server)", + "reset-xp-confirm-description": "Do you really want to delete the data?", + "edit-xp-user-description": "User to edit", + "edit-xp-value-description": "New XP value of the user", + "edit-xp-description": "Betrays your community and edits a user's XP", + "no-custom-formula": "No valid custom formula was entered. Using default formula.", + "invalid-custom-formula": "Invalid custom formula was entered. Please either fix the syntax of your custom formula or remove the value of the custom formula field.", + "role-factors-total": "Multiplied together, the user receives **%f times more XP** for every message.", + "edit-level-description": "Betrays your community and edits a user's levels", + "random-messages-enabled-but-non-configured": "You have random messages enabled, but have non configured. Ignoring config.randomMessages configuration.", + "granted-rewards-audit-log": "Updated roles to make sure, they have the level role they need" + }, + "team-list": { + "channel-not-found": "Could not find channel with ID %c or the channel has a wrong type (only text-channels supported)", + "role-not-found": "Could not find role with ID %r", + "no-users-with-role": "No users on this server have the %r role yet.", + "no-roles-selected": "No roles listed yet.", + "offline": "Offline", + "dnd": "Do not disturb", + "idle": "Away", + "online": "Online" + }, + "ping-on-vc-join": { + "channel-not-found": "Notify channel %c not found", + "could-not-send-pn": "Could not send PN to %m" + }, + "suggestions": { + "suggestion-not-found": "Suggestion not found", + "updated-suggestion": "Successfully updated suggestion", + "suggest-description": "Create and comment on suggestions", + "suggest-content": "Content you want to suggest", + "loading": "A wild new suggestion appeared, loading..", + "manage-suggestion-command-description": "Manage suggestions as an admin", + "manage-suggestion-accept-description": "Accepts a suggestion", + "manage-suggestion-deny-description": "Denies a suggestion", + "manage-suggestion-id-description": "ID of the suggestion", + "manage-suggestion-comment-description": "Explain why you made this choice" + }, + "auto-delete": { + "could-not-fetch-channel": "Could not fetch channel with ID %c", + "could-not-fetch-messages": "Could not fetch messages from channel with ID %c" + }, + "auto-thread": { + "thread-create-reason": "This thread got created, because you configured auto-thread to do so" + }, + "auto-messager": { + "channel-not-found": "Channel with ID %id not found" + }, + "polls": { + "what-have-i-votet": "What have I voted?", + "vote": "Vote!", + "vote-this": "Click on this option to place your vote here", + "voted-successfully": "Successfully voted. Thanks for your participation.", + "not-voted-yet": "You have not voted yet, so I can't show you what you voted.", + "you-voted": "You have voted for **%o**.", + "remove-vote": "Remove my vote", + "removed-vote": "Your vote was removed successfully.", + "change-opinion": "You can change your opinion anytime by just selecting something else above the button you just clicked.", + "command-poll-description": "Create and end polls", + "command-poll-create-description": "Create a new poll", + "command-poll-end-description": "Ends an existing poll", + "command-poll-end-msgid-description": "ID of the poll", + "command-poll-create-description-description": "Topic / Description of this poll", + "command-poll-create-channel-description": "Channel in which the poll should get created", + "command-poll-create-option-description": "Option number %o", + "command-poll-create-endAt-description": "Duration of the poll (if not set the poll will not end automatically)", + "command-poll-create-public-description": "If enabled (disabled by default) the votes of users will be displayed publicly", + "created-poll": "Successfully created poll in %c.", + "not-found": "Poll could not be found", + "no-votes-for-this-option": "Nobody voted this option yet", + "ended-poll": "Poll ended successfully", + "view-public-votes": "View current voters", + "not-public": "This poll does not appear to be public, no results can be displayed.", + "poll-private": "🔒 This poll is **anonymous**, meaning that no one can see what you voted (not even the admins).", + "poll-public": "🔓 This poll is **public**, meaning that everyone can see what you voted.", + "not-text-channel": "You need to select a text-channel that is not an announcement-channel." + }, + "channel-stats": { + "audit-log-reason-interval": "Updated channel because of interval", + "audit-log-reason-startup": "Updated channel because of startup", + "not-voice-channel-info": "Channel \"%c\" (%id) is a %t and not a voice-channel as recommended" + }, + "info-commands": { + "info-command-description": "Find information about parts of this server", + "command-userinfo-description": "Find more information about a user on this server", + "argument-userinfo-user-description": "User you want to see information about (default: you)", + "command-roleinfo-description": "Find more information about a role on this server", + "argument-roleinfo-role-description": "Role you want to see information about", + "command-channelinfo-description": "Find more information about a channel on this server", + "argument-channelinfo-channel-description": "Channel you want to see information about", + "command-serverinfo-description": "Find more information about this server", + "information-about-role": "Information about the role %r", + "hoisted": "Hoisted", + "mentionable": "Mentionable", + "managed": "Managed", + "information-about-channel": "Information about the channel %c", + "information-about-user": "Information about the user %u", + "information-about-server": "Information about %s", + "boostLevel": "Level", + "boostCount": "Boosts", + "userCount": "Users", + "memberCount": "Members", + "onlineCount": "Online", + "textChannel": "Text", + "voiceChannel": "Voice", + "categoryChannel": "Categories", + "otherChannel": "Other", + "total-invites": "Total", + "active-invites": "Active", + "left-invites": "Left" + }, + "channelType": { + "GUILD_TEXT": "Text-Channel", + "GUILD_VOICE": "Voice-Channel", + "GUILD_CATEGORY": "Category", + "GUILD_NEWS": "News-Channel", + "GUILD_STORE": "Store-Channel", + "GUILD_NEWS_THREAD": "News-Channel-Thread", + "GUILD_PUBLIC_THREAD": "Public Thread", + "GUILD_PRIVATE_THREAD": "Private Thread", + "GUILD_STAGE_VOICE": "Stage-Channel", + "DM": "Direct-Message", + "GROUP_DM": "Group-Direct-Message", + "UNKNOWN": "Unknown" + }, + "stagePrivacy": { + "PUBLIC": "Publicly accessible", + "GUILD_ONLY": "Only server members can join" + }, + "guildVerification": { + "0": "None", + "1": "Low", + "2": "Medium", + "3": "High", + "4": "Very high" + }, + "boostTier": { + "0": "None", + "1": "Level 1", + "2": "Level 2", + "3": "Level 3" + }, + "temp-channels": { + "removed-audit-log-reason": "Removed temp channel, because no one was in it", + "permission-update-audit-log-reason": "Updated permissions, to make sure only people in the VC can see the no-mic-channel", + "created-audit-log-reason": "Created Temp-Channel for %u", + "move-audit-log-reason": "Moved user to their voice channel", + "no-mic-channel-topic": "Welcome to %u's no-mic-channel. You will see this channel as long as you are connected to this temp-channel.", + "disconnect-audit-log-reason": "The old channel of the user could not be found - disconnecting them - hopefully they join again", + "command-description": "Manage your temp-channel", + "mode-subcommand-description": "Change the mode of your channel", + "public-option-description": "If enabled, anyone can join your temp-channel", + "add-subcommand-description": "Add users, that will be able to join your channel, while it is private", + "remove-subcommand-description": "Remove users from you channel", + "add-user-option-description": "The user to be added", + "remove-user-option-description": "The user to be removed", + "list-subcommand-description": "List the users with access to your channel", + "edit-subcommand-description": "Edit various settings of your channel", + "user-limit-option-description": "Change the user-limit of your channel", + "bitrate-option-description": "Change the bitrate of your channel (min. 8000)", + "name-option-description": "Change the name of your channel", + "nsfw-option-description": "Change, whether your channel is age-restricted or not", + "no-added-user": "There are no users to be displayed here", + "nothing-changed": "Your channel already had these settings.", + "no-disconnect": "Couldn't disconnect the user from your channel. This could be due to missing permissions, or the user not being in your voice-channel", + "edit-error": "An error occurred while editing your channel. one or more of your settings couldn't be applied. This could be due to missing permissions or an invalid value.", + "add-user": "Add user", + "remove-user": "Remove user", + "list-users": "List users", + "private-channel": "Private", + "public-channel": "Public", + "edit-channel": "Edit channel", + "add-modal-title": "Add an user to your temp-channel", + "add-modal-prompt": "The user you want to add (tag or user-id)", + "remove-modal-title": "Remove an user from your temp-channel", + "remove-modal-prompt": "The user you want to remove (tag or user-id)", + "edit-modal-title": "Edit your temp-channel", + "edit-modal-nsfw-prompt": "Mark temp-channel as age-restricted?", + "edit-modal-nsfw-placeholder": "\"true\" (yes) or \"false\" (no)", + "edit-modal-nsfw-on": "Yes (age-restricted)", + "edit-modal-nsfw-off": "No (not age-restricted)", + "edit-modal-bitrate-prompt": "Bitrate of your Temp-channel?", + "edit-modal-bitrate-placeholder": "A number over 8000", + "edit-modal-limit-prompt": "Limit of users in your temp-channel", + "edit-modal-limit-placeholder": "Number between 0 and 99; 0 = unlimited", + "edit-modal-name-prompt": "How should your channel be called?", + "edit-modal-name-placeholder": "A very creative channel name", + "edit-modal-username-placeholder": "Username of the user", + "user-not-found": "User not found" + }, + "guess-the-number": { + "command-description": "Manage your guess-the-number-games", + "status-command-description": "Shows the current status of a guess-the-number-game in this channel", + "create-command-description": "Create a new guess-the-number-game in this channel", + "create-min-description": "Minimal value users can guess", + "create-max-description": "Maximal value users can guess", + "create-number-description": "Number users should guess to win", + "end-command-description": "Ends the current game", + "session-already-running": "There is a session already running in this channel. Please end it with /guess-the-number end", + "session-not-running": "There is currently no session running.", + "gamechannel-modus": "You can't use this command in a gamechannel. To end a game, disable the gamechannel modus and try using the end command.", + "session-ended-successfully": "Ended session successfully. Locked channel successfully.", + "current-session": "Current session", + "number": "Number", + "min-val": "Min-Value", + "max-val": "Max-Value", + "owner": "Owner", + "guess-count": "Count of guesses", + "min-max-discrepancy": "`min` can't be bigger or equal to `max`", + "max-discrepancy": "`number` can't be bigger than `max`.", + "min-discrepancy": "`number` can't be smaller than `min`.", + "emoji-guide-button": "What does the reaction under my guess mean?", + "guide-wrong-guess": "Your guess was wrong (but your entry was valid)", + "guide-win": "You guessed correctly - you win :tada:", + "guide-admin-guess": "Your guess was invalid, because you are an admin - admins can't participate because they can see the correct number", + "guide-invalid-guess": "Your guess was invalid (e.g. below the minimal / over the maximal number, not a number, …)", + "created-successfully": "Created game successfully. Users can now start guessing in this channel. The winning number is **%n**. You can always check the status by running `/guess-the-number-status`. Note that you as an admin can not guess.", + "game-ended": "Game ended", + "game-started": "Game started", + "leaderboard-button": "Leaderboard", + "leaderboard-title": "Guess-the-Number Leaderboard", + "leaderboard-empty": "No games have been won yet.", + "wins": "wins", + "guesses": "guesses" + }, + "massrole": { + "command-description": "Manage roles for all members", + "add-subcommand-description": "Add a role to all members", + "remove-subcommand-description": "Remove a role from all members", + "remove-all-subcommand-description": "Remove all roles from all members", + "role-option-add-description": "The role, that will be given to all members", + "role-option-remove-description": "The role, that will be removed from all members", + "target-option-description": "Determines whether bots should be included or not", + "all-users": "All Users", + "bots": "Bots", + "humans": "Humans", + "not-admin": "⚠ To use this command, you need to be added to the adminRoles option in the SCNX-Dashboard. If you are the owner of this bot please remember to create an override in the server settings to prevent abuse of this command.", + "add-reason": "Mass role addition by %u", + "remove-reason": "Mass role removal by %u" + }, + "twitch-notifications": { + "channel-not-found": "Channel with ID %c could not be found", + "user-not-on-twitch": "Could not find user %u on twitch", + "message-not-found": "No live message configured for streamer %s" + }, + "fun": { + "slap-command-description": "Slap a user in the face", + "user-argument-description": "User to performe this action on", + "no-no-not-slapping-yourself": "You can not punch yourself lol (well technically you can, but our gifs do not support that, so deal with it ¯\\_(ツ)_/¯)", + "pat-command-description": "Pat someone nicely", + "no-no-not-patting-yourself": "Well, good try, but we don't do this here", + "no-no-not-kissing-yourself": "Uah, that's gross, you should try paying somebody to do that (well you should not, but better then kissing yourself)", + "kiss-command-description": "Kiss someone", + "hug-command-description": "Hug someone <3", + "no-no-not-hugging-yourself": "You are quite lonely aren't you? Try hugging a tree, that should work. Unless you live in a desert. Then hug a cactus. That's a bit more painful, but trust me.", + "random-command-description": "Helps you select random things", + "random-number-command-description": "Selects a random number", + "min-argument-description": "Minimal number (default: 1)", + "max-argument-description": "Maximal number (default: 42)", + "random-ikeaname-command-description": "Generates a random name for a IKEA-Name", + "syllable-count-argument-description": "Count of syllables to generate name from (default: random)", + "random-dice-command-description": "Roll a dice", + "random-coinflip-command-description": "Flip a coin", + "random-8ball-command-description": "Generates an answer to a yes/no question", + "dice-site-1": "Heads", + "dice-site-2": "Tails" + }, + "moderation": { + "moderate-command-description": "Moderate users on your server", + "moderate-notes-command-description": "Set or see moderator's notes of a user", + "moderate-notes-command-view": "View a user's notes", + "moderate-notes-command-create": "Create a new note about a user", + "moderate-notes-command-edit": "Edit one of your existing notes about a user", + "moderate-notes-command-delete": "Delete one of your existing notes about a user", + "moderate-ban-command-description": "Ban a user on your server", + "moderate-reason-description": "Reason for your action", + "moderate-proof-description": "Proof for your action", + "report-user-not-found-on-guild": "This user could not be found on \"%s\". You can only report users that are members of our server.", + "proof": "Proof", + "report-proof-description": "Attach an optional (image) proof to your report", + "file": "File uploaded", + "anti-grief-reason": "Too many actions of type \"%type\" in the last %h hours. Maximum amount allowed: %n", + "anti-grief-user-message": "Sorry, but it seems like you are abusing your moderative powers. We've taken actions to prevent this from happening.", + "moderate-duration-description": "Duration of the action (max: 28 days, default: 14 days)", + "mute-max-duration": "Discord limits the maximal duration of a timeout to 28 days. Please enter an amount equal or less than this", + "moderate-quarantine-command-description": "Quarantine a user on your server", + "moderate-unquarantine-command-description": "Removes a user from the quarantine", + "moderate-unban-command-description": "Revokes an existing ban", + "moderate-clear-command-description": "Clears messages in the current channel", + "moderate-clear-amount-description": "How many messages should get cleared?", + "moderate-kick-command-description": "Kick a user from your server", + "moderate-unwarn-command-description": "Revokes a warning", + "moderate-mute-command-description": "Mute a user on your server", + "moderate-unmute-command-description": "Unmutes a user on your server", + "moderate-warn-command-description": "Warn a user", + "moderate-channel-mute-description": "Mutes a user from the current channel", + "moderate-unchannel-mute-description": "Removes a channel-mute from this channel", + "moderate-lock-command-description": "Lock the current channel", + "moderate-unlock-command-description": "Unlock the current channel", + "moderate-lockdown-command-description": "Activate or lift server-wide lockdown", + "moderate-lockdown-enable-description": "True to activate lockdown, false to lift it", + "lockdown-not-enabled": "The lockdown system is not enabled. Enable it in the lockdown configuration.", + "lockdown-already-active": "A lockdown is already active.", + "lockdown-not-active": "No lockdown is currently active.", + "lockdown-activated": "Server Lockdown Activated", + "lockdown-lifted": "Server Lockdown Lifted", + "lockdown-activated-reply": "Lockdown activated. %c channels have been locked.", + "lockdown-lifted-reply": "Lockdown lifted. %c channels have been restored.", + "lockdown-log-description": "**Reason:** %r\n**Triggered by:** %u\n**Type:** %t\n**Affected channels:** %c", + "lockdown-lift-log-description": "**Reason:** %r\n**Lifted by:** %u\n**Restored channels:** %c", + "lockdown-automatic": "Automatic", + "lockdown-manual": "Manual", + "lockdown-system": "System", + "lockdown-auto-lift-reason": "Auto-lift timer expired", + "lockdown-restored": "Lockdown state restored from database after restart", + "lockdown-joinraid-trigger": "Join raid detected", + "lockdown-spam-trigger": "Excessive spam detected", + "lockdown-joingate-trigger": "Excessive join-gate violations detected", + "lockdown-restore-failed": "Failed to restore permissions for channel %c: %e", + "lockdown-users-kicked": "Users Kicked", + "lockdown-users-kicked-description": "%k non-moderator users were disconnected from voice channels.", + "moderate-user-description": "User on who the action should get performed", + "moderate-userid-description": "ID of a user", + "moderate-days-description": "Number of days of messages to delete", + "invalid-days": "Days can only be between 0 and 7 (inclusive)", + "moderate-notes-description": "Notes to set / update", + "moderate-note-id-description": "ID of one of your notes you want to edit (leave blank to create a new one)", + "moderate-warnid-description": "ID of a warn (run /moderate actions to get it)", + "moderate-actions-command-description": "Show all recorded actions against a user", + "report-command-description": "Reports a user and sends a snapshot of the chat to server staff", + "report-reason-description": "Please describe what the user did wrong", + "report-user-description": "User you want to report", + "no-reason": "Not set", + "muterole-not-found": "Could not find muterole. Can not perform this action", + "quarantinerole-not-found": "Could not find quarantinerole. Can not perform this action", + "mute-audit-log-reason": "Got muted by %u because of \"%r\"", + "unmute-audit-log-reason": "Got unmuted by %u because of \"%r\"", + "quarantine-audit-log-reason": "Got quarantined by %u because of \"%r\"", + "kicked-audit-log-reason": "Got kicked by %u because of \"%r\"", + "banned-audit-log-reason": "Got banned by %u because of \"%r\"", + "channelmute-audit-log-reason": "Got channel-mutet by %u because of \"%r\"", + "unchannelmute-audit-log-reason": "The Channel-Mute got removed by %u because of \"%r\"", + "unbanned-audit-log-reason": "Got unbanned by %u because of \"%r\"", + "unquarantine-audit-log-reason": "Got unquarantined by %u because of \"%r\"", + "action-expired": "Action expired", + "auto-mod": "Auto-Mod", + "batch-role-remove-failed": "Could not remove all roles from %i (trying to remove roles one by one): %e", + "batch-role-add-failed": "Could not add all roles to %i (trying to remove roles one by one): %e", + "could-not-remove-role": "Could not remove role %r from %i: %e", + "could-not-add-role": "Could not add role %r to %i: %e", + "reason": "Reason", + "join-gate": "Join-Gate", + "expires-at": "Action expires on", + "action": "Action", + "case": "Case", + "victim": "Victim", + "missing-logchannel": "LogChannel could not be found", + "reached-warns": "Reached %w warns", + "restored-punishment-audit-log-reason": "Restored punishment", + "anti-join-raid": "ANTI-JOIN-RAID", + "raid-detected": "Raid detected", + "joingate-for-everyone": "Join-Gate-Modus: Catch all users", + "account-age-to-low": "Account creation age of %a days is to low (required are more then %c)", + "no-profile-picture": "Account has no profile picture (required)", + "join-gate-fail": "Account failed Join-Gate (%r)", + "blacklisted-word": "Posted blacklisted word in %c", + "invite-sent": "Sent invite in %c", + "scam-url-sent": "Sent scam-url in %c", + "anti-spam": "Anti-Spam", + "reached-messages-in-timeframe": "Reached %m (normal) messages in less than %t seconds", + "reached-duplicated-content-messages": "Reached %m messages with the same content in less than %t", + "reached-ping-messages": "Reached %m messages with (user) pings in less then %t seconds", + "reached-massping-messages": "Reached %m messages with mass pings in less than %t seconds", + "action-done": "Executed action successfully. Action-ID: #%i", + "expiring-action-done": "Done. Action will expire on %d. Action-ID: #%i", + "cleared-channel": "Cleared channel successfully.\nNote: Messages older than 14 days can not be deleted using this method.", + "clear-failed": "An error occurred. You can only delete 100 messages at once.", + "no-quarantine-action-found": "Sorry, but I couldn't find any records of quarantining this users.", + "locked-channel-successfully": "Locked channel successfully. Only moderators (and admins) can write messages here now.", + "unlocked-channel-successfully": "Unlocked channel successfully. Permissions got restored to the permission-state before the lock occurred.", + "unlock-audit-log-reason": "User %u unlocked this channel by running /moderate unlock", + "warning-not-found": "I could not find this warning. Please make sure you are actually using a warning-id and not a userid.", + "can-not-report-mod": "You can not report moderators.", + "action-description-format": "%reason\nby %u on %t", + "no-actions-title": "None found", + "no-actions-value": "No actions against %u found.", + "actions-embed-title": "Mod-Actions against %u - Site %i", + "actions-embed-description": "You can find every action against %u here.", + "report-embed-title": "New report", + "report-embed-description": "A user reported another user. Please review the case and take actions if needed.", + "reported-user": "Reported user", + "report-reason": "Reason for the report", + "report-user": "User who submitted report", + "message-log": "Last 100 messages", + "message-log-description": "You can find an encrypted message-log [here](%u).", + "channel": "Channel", + "no-report-pings": "No pings configured. Check your configuration to ping your staff.", + "not-allowed-to-see-own-notes": "Sorry, but you are not allowed to see your own notes.", + "note-added": "Note added successfully", + "note-edited": "Edited note successfully", + "note-deleted": "Note deleted successfully", + "note-not-found-or-no-permissions": "Note not found or no permissions to edit this note.", + "notes-embed-title": "Notes about %u", + "info-field-title": "ℹ️ Information", + "no-notes-found": "No notes about this user. Create a new note with `/moderate notes create` and set the notes attribute.", + "more-notes": "%x other moderator also added notes about this user. Notes are sorted in reverse chronology, so you will see the newest notes first.", + "user-notes-field-title": "%t's notes", + "user-not-on-server": "I can't perform this action on this user, as they are not currently on your server.", + "verification": "VERIFICATION", + "verification-failed": "Verification failed", + "verification-started": "Verification got started", + "verification-completed": "Verification completed", + "user": "User", + "manual-verification-needed": "Manual verification needed", + "verification-deny": "Deny verification", + "verification-approve": "Approve verification", + "verification-skip": "Skip verification", + "captcha-verification-pending": "Captcha-Verification is pending. You can either wait for the user to complete it or skip it manually.", + "verification-update-proceeded": "Successfully update verification status", + "verify-channel-set-but-not-found-or-wrong-type": "The configured verify-channel could not be found or it's type is not supported.", + "generating-message": "We are preparing some stuff, this message should get edited shortly...", + "restart-verification-button": "Restart verification process", + "member-not-found": "This user could not be found, maybe they already left?", + "already-verified": "Seems like you are already verified... Why would you want to repeat this process?", + "restarted-verification": "I have sent you another DM about your verification process. Please read it carefully and follow the actions described in it. Please not that this action did not re-trigger the manual-verification (if enabled), so spamming this button is useless.", + "dms-still-disabled": "It seems like your DMs are still disabled. Please enable your DMs to start the verification. This is not optional, you need to do this in order to get access to %g.", + "dms-not-enabled-ping": "%p, it seems like you have your DMs disabled. Please enable them and hit the button below this message to verify yourself. You have two minutes to complete this process.", + "verify-me-button": "Verify Me", + "enter-solution-button": "Enter Solution", + "verification-submitted": "Your verification request has been submitted. A moderator will review it shortly.", + "already-pending-review": "Your verification request is already being reviewed by a moderator.", + "captcha-expired": "Your captcha has expired. Please click Verify Me again.", + "retry-message": "Wrong answer. You can try again in %t. (Attempt %a/%m)", + "cooldown-message": "⏳ Please wait %t% before trying again.", + "retries-exhausted": "You have exhausted all verification attempts.", + "simple-math-challenge": "What is %a %op %b?", + "simple-word-challenge": "Type the following word: %w", + "captcha-solution-label": "Enter the captcha solution", + "simple-solution-label": "Enter your answer", + "verification-modal-title": "Verification" + }, + "counter": { + "created-db-entry": "Initialized database entry for %i", + "not-a-number": "This is not a number. You can not chat here. Try creating a thread if your message is that important.", + "restriction-audit-log": "This user proceeded to abuse the counter channel after five warnings, so we locked them out.", + "only-one-message-per-person": "Users have to take turns counting: You can not count two times in a row.", + "not-the-next-number": "That's not the next number. The next number is **%n**, please make sure you are counting up one by one.", + "channel-topic-change-reason": "Someone counted, so we updated the description as required by the configuration" + }, + "tickets": { + "channel-not-found": "Ticket-Create-Channel could not be found", + "existing-ticket": "You already have a ticket open: %c", + "ticket-created-audit-log": "%u created a new ticket by clicking the button", + "ticket-created": "Successfully created ticket and notified staff. Head over to it: %c", + "no-admin-pings": "No pings configured. Check your configuration to ping your staff.", + "ticket-closed-successfully": "Closed ticket successfully. This channel will be deleted in a few seconds, thanks for reaching out to our support.", + "ticket-closed-audit-log": "%u closed ticket", + "closing-ticket": "Closing ticket as requested by %u...", + "ticket-with-user": "👤 Ticket-User", + "could-not-dm": "Could not DM %u: %r", + "no-log-channel": "Log-Channel not found", + "ticket-log-embed-title": "📎 Ticket %i closed", + "ticket-log": "Ticket-Log", + "ticket-type": "☕ Ticket-Topic", + "ticket-log-value": "Transcript with %n messages can be found [here](%u).", + "closed-by": "👷 Ticket closed by" + }, + "reminders": { + "command-description": "Set a reminder for yourself", + "in-description": "After what time should we remind you? (eg. \"2h 30m\")", + "what-description": "What should we remind you about?", + "dm-description": "Should we send you a DM instead of reminding your in this channel?", + "one-minute-in-future": "Your reminder needs to be at least one minute in the future", + "reminder-set": "Reminder set. We'll remind you at %d.", + "snooze-10m": "10 min", + "snooze-30m": "30 min", + "snooze-1h": "1 hour", + "snooze-1d": "1 day", + "snoozed": "Reminder snoozed. We'll remind you again at %d.", + "snooze-not-allowed": "You can only snooze your own reminders." + }, + "afk-system": { + "command-description": "Manage your AFK-Status on this server", + "end-command-description": "End your current AFK-Session", + "start-command-description": "Start a new AFK-Session", + "reason-option-description": "Explain why you started this session", + "autoend-option-description": "If enabled, the bot will auto-end your AFK Session when your write a message (default: enabled)", + "no-running-session": "You don't have any session running.", + "already-running-session": "You already have an AFK-Session running, try ending it with `/afk-system end`.", + "afk-nickname-change-audit-log": "Updated user nickname because they started an AFK-Session", + "can-not-edit-nickname": "Can not edit nickname of %u: %e" + }, + "tic-tac-toe": { + "command-description": "Play tic-tac-toe against someone in the chat", + "user-description": "User to play against", + "challenge-message": "%t, %u challenged you to a game of tic-tac-toe! Hit the button below to join the battle! This invitation will expire in about 2 minutes, so don't hesitate to much.", + "accept-invite": "Join game", + "deny-invite": "No thanks", + "self-invite-not-possible": "Are you really that lonely? Even Simon, a complete introvert with no friends and developer of this bot, can find another user to play tic-tac-toe with... You should be able to do that too, try inviting %r for example, maybe they want to play a round?", + "invite-expired": "Sorry, %u, %i didn't accept your request to play tic-tac-toe in time ):", + "invite-denied": "Sorry, %u, but %i denied your request to play a round of tic-tac-toe ):", + "you-are-not-the-invited-one": "Sorry, but this invite doesn't belong to you. You can start your own game with `/tic-tac-toe`.", + "playing-header": "**TIC-TAC-TOE GAME IS RUNNING**\n\n%u (🟢) VS %i (🟡)\nCurrently on turn: %t\n\n%t, click a button with a white circle below to place your marker", + "win-header": "**TIC-TOE-GAME ENDED**\n\n%u (🟢) VS %i (🟡)\n\n%w won the game - GG!\n\n*You can start a new round by using `/tic-tac-toe`*", + "draw-header": "**TIC-TOE-GAME ENDED**\n\n%u (🟢) VS %i (🟡)\n\nDraw - no one won this game.", + "not-your-turn": "It's not your turn, take a coffee and return later" + }, + "duel": { + "command-description": "Play duel against someone in the chat", + "user-description": "User to play against", + "challenge-message": "%t, %u challenged you to a game of duel! Hit the button below to join the battle! This invitation will expire in about 2 minutes, so don't hesitate to much.", + "accept-invite": "Join game", + "deny-invite": "No thanks", + "self-invite-not-possible": "Are you really that lonely? Even Simon, a complete introvert with no friends and developer of this bot, can find another user to play duel with... You should be able to do that too, try inviting %r for example, maybe they want to play a round?", + "invite-expired": "Sorry, %u, %i didn't accept your request to play duel in time ):", + "invite-denied": "Sorry, %u, but %i denied your request to play a round of duel ):", + "you-are-not-the-invited-one": "Sorry, but this invite doesn't belong to you. You can start your own game with `/duel`.", + "game-running-header": "🎮 Game running", + "what-do-you-want-to-do": "**Select your action!**", + "pending": "⏳ Waiting for selection…", + "ready": "✅ Ready", + "continues-info": "The game continues once both parties have selected their next action.", + "how-does-this-game-work": "Wondering how this game works? Read our short explanation [here]().", + "use-gun": "Use gun", + "guard": "Guard", + "reload": "Load gun", + "game-ended": "🎮 Game ended", + "no-bullets": "Sorry, but you haven't loaded any bullets yet, so you can't use your gun yet.", + "bullets-full": "Sorry, but your gun only has place for 5 bullets at a time.", + "gun-gun": "Both %g1 and %g1 draw their guns. They stare each other and their eyes and slowly lower their weapons. No, the duell won't be resolved if both die - there can only be one winner.", + "guard-gun": "%g1 draws their gun and shoot - %d1 dodged the bullet successfully.", + "guard-guard": "Both %d1 and %d2 wait for each other to fire the shot - but nothing happens.", + "reload-gun": "While %r1 starts reloading their gun, %g1 draws their weapon and shoots - it's a head-shot. %r1 drops to the ground. %g1 should celebrate because they won, but they are left feeling bad for murdering their old friend.", + "guard-over-reload-gun": "As this is %r1's fifth guard in a row, they are tired and are to slow - %g1 shoots them directly into their head and %r1 drops to the ground. It's a win for %g1 - but at what price?", + "reload-reload": "Both %r1 and %r2 stare each other in the eyes while taking a short break to load one bullet each in their chamber.", + "reload-guard": "%d1 prepares to doge a bullet - but %r1 uses the time to load their weapon - no shots get fired.", + "ended-state": "This game ended. You can start a new duel with `/duel`.", + "not-your-game": "You are not one of players - you can start a new game with `/duel`." + }, + "economy-system": { + "work-earned-money": "The user %u gained %m %c by working", + "crime-earned-money": "The user %u gained %m %c by committing a crime", + "message-drop-earned-money": "The user %u gained %m %c by getting a message drop", + "rob-earned-money": "The user %u gained %m %c by robbing from %v", + "weekly-earned-money": "The user %u gained %m %c by cashing in their weekly reward", + "daily-earned-money": "The user %u gained %m %c by cashing in their daily reward", + "admin-self-abuse": "The admin %a wanted to abuse their permissions by giving them self even more money! This can't and should not be ignored!", + "admin-self-abuse-answer": "What a bad admin you are, %u. I'm disappointed with you! I need to report this. If I wish I could ban you!", + "added-money": "%i %c has been added to the balance of %u", + "removed-money": "%i %c has been removed from the balance of %u", + "set-money": "The balance of %u has been set to %i.", + "added-money-log": "The user %u added %i %c to the balance of %v", + "removed-money-log": "The user %u removed %i %c from the balance of %v", + "set-money-log": "The user %u set %v's balance to %i %c", + "command-description-main": "Use the economy-system", + "command-description-work": "Earn some cash by working", + "command-description-crime": "Earn some cash by committing a crime", + "command-description-rob": "Rob some cash from another user", + "option-description-rob-user": "User to rob from", + "crime-loose-money": "The user %u lost %m %c by committing a crime", + "command-description-daily": "Cash in your daily rewards", + "command-description-weekly": "Cash in your weekly rewards", + "command-description-balance": "Show the balance of a user", + "option-description-user": "User to execute action upon", + "command-description-add": "Add some cash to a user", + "command-description-remove": "Remove some cash from a user", + "option-description-amount": "Amount to manipulate", + "command-description-set": "Set a user's balance", + "option-description-balance": "Balance to set user to", + "message-drop": "Message-Drop: You earned %m %c simply by chatting!", + "created-item": "The user %u has created a new shop item: %i", + "item-duplicate": "The item already exist", + "role-to-high": "The specified role is higher than the highest role of the bot. Therefore the bot can't give the role to users. The item was **not** created.", + "delete-item": "The user %u has deleted the shop item %i", + "edit-item": "The user %u has edited the item %i. Possible changes are:\nNew name: %n\nNew price: %p\nNew role: %r", + "user-purchase": "The user %u has purchased the shop item %i for %p.", + "shop-command-description": "Use the shop-system", + "shop-command-description-add": "Create a new item in the shop (admins only)", + "shop-option-description-itemName": "Name of the item", + "shop-option-description-newItemName": "New name of the Item", + "shop-option-description-itemID": "ID of the Item", + "shop-option-description-price": "Price of the item", + "shop-option-description-role": "Role to give to users who buy the item", + "shop-command-description-buy": "Buy an item", + "shop-command-description-list": "List all items in the shop", + "shop-command-description-delete": "Remove an item from the shop", + "shop-command-description-edit": "Edit an item", + "channel-not-found": "Can't find the leaderboard channel with the ID %c", + "command-description-deposit": "Deposit xyz to your bank", + "option-description-amount-deposit": "Amount to deposit", + "command-description-withdraw": "Withdraw xyz from your Bank", + "option-description-amount-withdraw": "Amount to withdraw", + "command-group-description-msg-drop-msg": "Enable/ Disable the Message-Drop-Message", + "command-description-msg-drop-msg-enable": "Enable the Message-Drop-Message", + "command-description-msg-drop-msg-disable": "Disable the Message-Drop-Message", + "command-description-destroy": "Destroy the whole economy (deletes all Database-Entries)", + "option-description-confirm": "Confirm, that you really want to destroy the whole economy", + "destroy-cancel-reply": "You're lucky. You stopped me in the last moment before I destroyed the economy", + "destroy-reply": "Ok... I'll destroy the whole economy", + "destroy": "%u destroyed the economy", + "migration-happening": "Database not up-to-date. Migrating database...", + "migration-done": "Migrated database successfully.", + "nothing-selected": "Select an item to buy it", + "select-menu-price": "Price: %p", + "price-less-than-zero": "The price can't be less or equal to zero" + }, + "status-role": { + "fulfilled": "Status-role condition is fulfilled", + "not-fulfilled": "Status-role condition is no longer fulfilled" + }, + "color-me": { + "create-log-reason": "%user redeemed their boosting-rewards by requesting the creation of this role", + "edit-log-reason": "%user edited their boosting-reward-role", + "delete-unboost-log-reason": "%user stopped boosting, so their role got deleted", + "delete-manual-log-reason": "%user deleted their role manually", + "command-description": "Request a Custom role as a reward for boosting. This has a cooldown of 24 hours", + "manage-subcommand-description": "Create or edit your custom role", + "name-option-description": "The name of your custom role", + "color-option-description": "The color of your custom role", + "remove-subcommand-description": "Remove your custom role", + "icon-option-description": "Your role-icon", + "confirm-option-remove-description": "Do you really want to delete your custom role? This will not reset any running cooldowns" + }, + "rock-paper-scissors": { + "stone": "Stone", + "paper": "Paper", + "scissors": "Scissors", + "won": "won", + "lost": "lost", + "tie": "tie", + "play-again": "Play again", + "challenge-message": "%t, %u challenged you to a game of rock-paper-scissors! Hit the button below to join the game! This invitation will expire in about 2 minutes, so don't hesitate to much.", + "invite-expired": "Sorry, %u, %i didn't accept your request to play rock-paper-scissors in time ):", + "invite-denied": "Sorry, %u, but %i denied your request to play a round of rock-paper-scissors ):", + "rps-title": "Rock Paper Scissors", + "rps-description": "Choose your weapon!", + "its-a-tie-try-again": "It's a tie! Try again!", + "command-description": "Play rock-paper-scissors against the bot or someone in the chat" + }, + "connect-four": { + "tie": "It's a tie!", + "win": "%u has won the game!", + "not-turn": "Sorry, but it's not your turn!", + "game-message": "Connect Four game of %u1 and %u2\nCurrent turn: %c %t.\n\n%g", + "challenge-message": "%t, %u challenged you to a game of Connect Four! Hit the button below to join the game! This invitation will expire in about 2 minutes, so don't hesitate to much.", + "invite-expired": "Sorry, %u, %i didn't accept your request to play Connect Four in time ):", + "invite-denied": "Sorry, %u, but %i denied your request to play a round of Connect Four ):", + "command-description": "Play Connect Four against someone in the chat", + "field-size-description": "The size of the playfield (default: 7)", + "challenge-yourself": "You cannot challenge yourself!", + "challenge-bot": "You cannot challenge bots!" + }, + "uno": { + "command-description": "Play Uno against users in the chat", + "challenge-message": "%u invites to a round of Uno! Click the button below this message to join! The game starts %timestamp with %count players.", + "not-enough-players": "Not enough players joined for a round of Uno!", + "user-cards": "%u: %cards cards", + "already-joined": "You're already in!", + "view-deck": "View deck", + "draw": "Draw card", + "uno": "Uno!", + "turn": "It's %u turn!", + "update-button": "Update", + "use-drawn": "Do you want to use the drawn card?", + "dont-use-drawn": "Dont use", + "win": "%u won the game! %turns cards were played.", + "win-you": "You've won the game!", + "missing-uno": "⚠️ You must use the Uno! button before you use your second last card!", + "choose-color": "Select a color:", + "pending-draws": "Use a Draw 2/4 card, otherwise you have to draw %count cards!", + "not-ingame": "You're not in this game!", + "skip": "Skip", + "reverse": "Reverse", + "color": "Color choice", + "draw2": "Draw 2", + "colordraw4": "Color choice and draw 4", + "cant-uno": "You cannot use Uno currently.", + "done-uno": "You've called Uno!", + "auto-drawn-skip": "Your turn was skipped because you would have had to draw the cards anyway.", + "start-game": "Start game now", + "not-host": "You're not the host of the game!", + "max-players": "The game is full!", + "previous-cards": "Previous cards: ", + "used-card": "You've already used the card %c! Use the Update button and play a valid card.", + "invalid-card": "You cannot play the card %c right now! Please select a valid card.", + "inactive-warn": "%u, it's your turn in the uno game!", + "inactive-win": "The uno game has ended. %u won as all others have been eliminated!" + }, + "quiz": { + "what-have-i-voted": "What have I voted?", + "vote": "Vote!", + "vote-this": "Select this option if you think it's correct.", + "voted-successfully": "Selected successfully.", + "not-voted-yet": "You have not selected an option yet, so I can't show you what you selected.", + "you-voted": "You've selected **%o** as correct answer.", + "change-opinion": "You can change your opinion at any time by selecting another option above the button you just clicked.", + "cannot-change-opinion": "You cannot change your selection as the creator of this quiz disabled it.", + "select-correct": "Select all correct answers", + "this-correct": "Mark this answer as correct", + "cmd-description": "Create or play server quiz", + "cmd-create-normal-description": "Create a quiz with up to 10 answers", + "cmd-create-bool-description": "Create a quiz with true or false answers", + "cmd-play-description": "Play a server quiz", + "cmd-leaderboard-description": "Shows the quiz leaderboard of the server", + "cmd-create-description-description": "Title / description of the quiz", + "cmd-create-channel-description": "Channel in which the quiz should be created", + "cmd-create-endAt-description": "How long the quiz will last", + "cmd-create-option-description": "Option number %o", + "cmd-create-canchange-description": "If the players can change their opinion after voting (default: no)", + "daily-quiz-limit": "You've reached the limit of **%l** daily playable quizzes. You can play again %timestamp.", + "created": "Quiz created successfully in %c.", + "correct-highlighted": "All correct answers were highlighted.", + "answer-correct": "✅ Your answer was correct and you've received one point for the leaderboard!", + "answer-wrong": "❌ Your answer was wrong!", + "bool-true": "Statement is correct", + "bool-false": "Statement is wrong", + "leaderboard-channel-not-found": "The leaderboard channel couldn't be found or it's type is invalid.", + "leaderboard-notation": "**%p. %u**: %xp XP", + "your-rank": "You've collected **%xp** points in quiz!", + "no-rank": "You've never finished a quiz successfully!", + "no-quiz": "No quizzes have been created for this server. Trusted admins can create them on https://scnx.app/glink?page=bot/configuration?query=quiz&file=quiz%7Cconfigs%2FquizList .", + "no-permission": "You don't have enough permissions to create quiz using the command." + }, + "topgg": { + "channel-not-found": "The configured channel with the ID \"%c\" was not found", + "testvote-header": "This was a test vote", + "voterole-reached": "Voted on top.gg and received Voter-Role", + "voterole-ended": "Vote on top.gg expired and got Voter-Role removed", + "opt-in": "Enable notifications when you can vote again", + "opt-out": "Disable notifications when you can vote again", + "opted-in": "Successfully opted in into receiving notifications when you can vote again", + "opted-out": "Successfully opted out of receiving notifications when you can vote again", + "already-opted-in": "You are already opted-in and will receive notifications when you can vote again", + "already-opted-out": "You are already opted-out and will **not** receive notifications when you can vote again", + "voteamount-reached": "The user reached %k votes which resulted in this role to be given.", + "testvote-description": "This vote was triggered in the top.gg dashboard and does not count towards any votecount of anyone and won't be used for reminders." + }, + "starboard": { + "invalid-minstars": "Invalid minimum stars %stars", + "star-limit": "You've reached the hourly starboard limit of %limitEmoji on the server which is why you cannot react on the message %msgUrl .\nTry again %time!" + }, + "nicknames": { + "owner-cannot-be-renamed": "The owner of the server (%u) cannot be renamed.", + "nickname-error": "An error occurred while trying to change the nickname of %u: %e" + }, + "ping-protection": { + "log-not-a-member": "[Ping Protection] Punishment failed: The pinger is not a member.", + "log-punish-role-error": "[Ping Protection] Punishment failed: I cannot punish %tag because their role is higher than or equal to my highest role.", + "log-mute-error": "[Ping Protection] Punishment failed: I cannot mute %tag: %e", + "log-kick-error": "[Ping Protection] Punishment failed: I cannot kick %tag: %e", + "log-action-log-failed": "[Ping Protection] Punishment logging failed: %e", + "log-data-deletion": "[Ping Protection] All data for the user with ID %u has been deleted successfully.", + "log-automod-keyword-limit": "[Ping Protection] Automod keywords exceed 1000 characters limit. Keywords were truncated.", + "punish-log-failed-title": "Punishment failed for user %u", + "punish-log-failed-desc": "An error occured while trying to punish the user %m. Please check the bot's permissions and role hierarchy. See the message below for the error.", + "punish-log-error": "Error: ```%e```", + "punish-role-error": "I cannot punish %tag because their role is higher than or equal to my highest role.", + "reason-basic": "User reached %c pings in the last %w weeks.", + "reason-advanced": "User reached %c pings in the last %d days (Custom timeframe).", + "cmd-desc-module": "Ping protection related commands", + "cmd-desc-group-user": "Every command related to the users", + "cmd-desc-history": "View the ping history of a user", + "cmd-opt-user": "The user to check", + "cmd-desc-actions": "View the moderation action history of a user", + "cmd-desc-panel": "Admin: Open the user management panel", + "cmd-desc-group-list": "Lists protected or whitelisted entities", + "cmd-desc-list-protected": "List of all the protected users and roles", + "cmd-desc-list-wl": "List of all the whitelisted roles, channels and users", + "embed-history-title": "Ping history of %u", + "no-data-found": "No logs found for this user.", + "embed-actions-title": "Moderation history of %u", + "label-reason": "Reason", + "actions-retention-note": "Note: Moderation actions are retained for 1 - 12 months based on the configuration.", + "no-permission": "You don't have sufficient permissions to use this command.", + "panel-title": "User Panel: %u", + "panel-description": "Manage and view data for %u (%i). View a quick recap of their ping and moderation history, or delete all data stored for this user (Risky).", + "btn-history": "Ping history", + "btn-actions": "Actions history", + "btn-delete": "Delete all data (Risky)", + "list-protected-title": "Protected Users and Roles", + "list-protected-desc": "View all protected users and roles here. When someone pings one of these protected user(s)/role(s), a warning will be sent. Exceptions are when pinged by someone with a whitelisted role/as a whitelisted user or when it's sent in a whitelisted channel.", + "field-protected-users": "Protected Users", + "field-protected-roles": "Protected Roles", + "list-whitelist-title": "Whitelisted Roles, Users and Channels", + "list-whitelist-desc": "View all whitelisted roles, users and channels here. Whitelisted roles and users will not get a warning for pinging a protected entity, and pings from them or in whitelisted channels will be ignored.", + "field-wl-roles": "Whitelisted Roles", + "field-wl-channels": "Whitelisted Channels", + "field-wl-users": "Whitelisted Users", + "list-none": "None are configured.", + "modal-title": "Confirm data deletion for this user", + "modal-label": "Confirm data deletion by typing this phrase:", + "modal-phrase": "I understand that all data of this user will be deleted and that this action cannot be undone.", + "modal-failed": "The phrase you entered is incorrect. Data deletion cancelled.", + "modal-success-data-deletion": "All data for the user <@%u> (%u) has been deleted successfully", + "field-quick-history": "Quick history view (Last %w weeks)", + "field-quick-desc": "Pings history amount: %p\nModeration actions amount: %m", + "history-disabled": "History logging has been disabled by a bot-configurator.\nAre you (one of) the bot-configurators? You can enable history logging in the \"Data Storage\" tab in the 'ping-protection' module ^^", + "leaver-warning-long": "This user left the server at %d. These logs will stay until automatic deletion.", + "leaver-warning-short": "This user left the server at %d.", + "meme-why": "😐 [Why are you the way that you are?]() - You just pinged yourself..", + "meme-played": "🔑 [Congratulations, you played yourself.]()", + "meme-spider": "🕷️ [Is this you?]() - You just pinged yourself.", + "meme-rick": "🎵 [Never gonna give you up, never gonna let you down...]() You just Rick Rolled yourself. Also congrats you unlocked the secret easter egg that only has a 1% chance of appearing!!1!1!!", + "meme-grind": "Why are you even pinging yourself 5 times in a row? Anyways continue some more to possibly get the secret meme\n-# (good luck grinding, only a 1% chance of getting it and during testing I had it once after 83 pings)", + "label-jump": "Jump to Message", + "no-message-link": "This ping was blocked by AutoMod", + "list-entry-text": "%index. **Pinged %target** at %time\n%link" + }, + "betterstatus": { + "command-description": "Change the bot's status", + "command-disabled": "The /status command is not enabled. An administrator needs to enable it in the betterstatus module configuration.", + "text-description": "The status text to display", + "activity-type-description": "The activity type (Playing, Watching, etc.)", + "bot-status-description": "The bot's online status (Online, Idle, DND)", + "streaming-link-description": "Streaming URL (only used when activity type is Streaming)", + "status-changed": "Bot status has been changed to: %s" + }, + "staff-management-system": { + "time-zero": "0 seconds", + "time-hours": "hours", + "time-hour": "hour", + "time-mins": "minutes", + "time-min": "minute", + "time-secs": "seconds", + "time-sec": "second", + "stat-brk": "🟡 On Break", + "stat-on": "🟢 On-Duty", + "stat-off": "🔴 Off-Duty", + "duty-panel-title": "Duty Panel - %type", + "duty-stats": "📊 Statistics", + "duty-stat-desc": "**Total Shift Duration:** %duration\n**Total Shifts:** %count\n**Average Shift Duration:** %average", + "btn-duty-on": "On-Duty", + "btn-duty-res": "Resume Duty", + "btn-duty-brk": "Toggle Break", + "btn-duty-off": "Off-Duty", + "duty-breakdown": "Shift Breakdown", + "duty-quota-str": "\n\n**Quota (%timeframe):** %duration / %hours hours\n*%result*", + "quota-met": "✅ Quota Met", + "quota-fail": "❌ Quota Not Met", + "duty-time-title": "Shift Time - %type", + "duty-time-desc": "**Total Shifts:** %count\n**Total Duration:** %duration", + "btn-hist": "View History", + "err-no-lb": "ℹ️ No shift data found for **%type**.", + "duty-lb-title": "Leaderboard - %type", + "duty-lb-desc": "**%lookback Top Shifts**\n\n%lines", + "page-count": "Page %page/%total", + "info-no-sh-hi": "ℹ️ No completed shifts found.", + "duty-hi-title": "Shift History - %type", + "duty-adm-title": "Admin Duty Panel - %user", + "btn-f-off": "Force Off-Duty", + "btn-v-act": "Void Active Shift", + "btn-add-t": "Add Time", + "btn-v-all": "Void All Shifts", + "err-not-yours": "❌ This panel is not yours.", + "err-alr-on": "❌ You are already on a shift.", + "err-not-on": "❌ You are not on a shift.", + "err-hist-oth": "❌ You can only view your own history.", + "mod-v-all-title": "Confirm: Void All Shifts", + "mod-v-all-lbl": "Type CONFIRM to delete all shift data", + "err-conf-fail": "❌ Data deletion confirmation failed. You must type the phrase exactly.", + "succ-v-all": "All shift data for <@%user> has been deleted successfully.", + "mod-add-t": "Add Duty Time", + "mod-add-min": "Minutes to add", + "mod-add-type": "Shift Type", + "err-inv-min": "❌ Invalid number of minutes.", + "err-inv-type": "❌ Invalid shift type. Available: %types", + "err-sh-dis": "❌ Shift tracking is disabled.", + "info-no-act-sh": "ℹ️ There are no active shifts right now.", + "duty-act-title": "Active Shifts", + "duty-act-desc": "**Total Shifts:** %count", + "err-no-perm": "❌ You do not have permission to do this.", + "err-no-mem": "❌ Could not find that member.", + "ph-sel-type": "Select a Shift Type", + "msg-sel-type": "👇 Please choose your shift type:", + "err-prof-dis": "❌ Staff Profiles are disabled.", + "err-prof-cfg": "❌ Configuration is missing. Please make sure the message is not empty.", + "err-prof-no-own": "❌ You do not have a staff profile.", + "err-prof-no-tgt": "❌ That user does not have a profile.", + "rev-dis-text": "*Reviews disabled*", + "rev-no-rate": "No ratings yet", + "stat-offl": "⚫ Offline", + "stat-onl": "🟢 Online", + "stat-idl": "🟡 Away", + "stat-dnd": "🔴 Do Not Disturb", + "stat-prof-ond": "⏱️ On duty", + "stat-prof-loa": "🌙 On LoA", + "stat-prof-ra": "⛱️ On RA", + "prof-no-intro": "*No introduction set.*", + "err-prof-empty": "❌ Profile embed is empty.", + "err-prof-perm": "❌ You must be a staff member to have a profile.", + "prof-edit-title": "Edit Profile", + "prof-edit-nick": "Custom Nickname", + "prof-edit-intro": "Introduction", + "succ-prof-wipe": "✅ Profile wiped for %u.", + "succ-prof-upd": "✅ Profile updated!", + "general-chan": "Channel", + "general-ends": "Ends", + "ac-tot-res": "Total Responded", + "err-ac-noact": "❌ There is no active activity check.", + "succ-ac-end": "✅ Activity check ended manually.", + "err-gen-no-user": "❌ Could not find that user.", + "del-conf-phrase": "I understand that this will delete the specified data for this user and it cannot be undone.", + "mod-del-title": "Confirm Data Deletion", + "mod-del-lbl": "Type confirmation phrase:", + "del-all-title": "Confirm total data deletion", + "del-all-desc": "You are about to delete ALL data for this user. Reminder that this ***cannot be undone***. This is the last chance to back out. If you are sure, click the button below.\nThis action will automatically cancel in 30 seconds.", + "btn-conf-del": "Confirm deletion", + "btn-cancel": "Cancel", + "succ-del-canc": "✅ Data deletion cancelled.", + "succ-del-all": "✅ ALL data has been permanently wiped.", + "err-del-time": "⏳ Data deletion timed out.", + "succ-del-tgt": "✅ Target data has been permanently wiped.", + "err-gen-no-perm": "❌ You do not have permission.", + "err-no-req": "❌ Request not found.", + "err-req-hndl": "❌ Request is already %status.", + "mod-deny-req": "Deny Request", + "general-rsn": "Reason", + "general-req-reason": "Reason for request", + "label-appr-by": "This was approved by", + "req-appr-by": "✅ Approved by %user", + "req-deny-by": "❌ Denied by %user", + "general-stat": "Status", + "err-ac-alr-end": "❌ This activity check has already ended.", + "info-ac-alr-conf": "ℹ️ You already confirmed your activity!", + "succ-ac-log": "✅ Activity logged successfully!", + "err-internal": "❌ An internal error occurred.", + "dm-appr-title": "Your %label request got approved!", + "dm-appr-desc": "Your %label request got approved by %approver!\nYou are now on LoA until %endFmt.\nYou can view your LoA status by using the %viewCmd command.", + "dm-deny-title": "Your %label request was denied", + "dm-deny-desc": "Your %label request was denied by %denier.\n**Reason:** %reason", + "dm-ext-title": "Your %label got extended", + "dm-ext-desc": "Your %label got extended by %extender.\nThis extension is for **%days day(s)** - your %label now ends at %endFmt.\n**Reason for extension:** %reason\nYou can view your updated %label status by using the %viewCmd command.", + "dm-early-title": "Your %label ended early", + "dm-early-desc": "Your %label got ended early by %ender - your %label is now over and your role has been removed.\n**Reason for early end:** %reason.", + "dm-end-title": "Your %label has ended", + "dm-end-desc": "Your %label has now ended and your role has been removed.", + "log-start-title": "%label started for %username", + "log-start-desc": "%label started for %mention.%apprText", + "log-info-hdr": "%label Information", + "general-start": "Start", + "general-end": "End", + "log-end-title": "%label ended for %username", + "log-end-desc": "%label ended for %mention.", + "general-started": "Started", + "general-ended": "Ended", + "log-adj-title": "%label adjusted for %username", + "log-adj-desc": "The %label of %mention was adjusted by <@%executor>.", + "log-changes": "Changes made:", + "err-feat-disabled": "❌ %feature disabled.", + "err-use-susp": "❌ Please use `/staff-management infraction suspend`.", + "err-inv-dur": "❌ Invalid duration format or value.", + "label-never": "Never", + "succ-infract": "✅ Issued **%type** (Case #%caseId) to %user.", + "label-days": "days", + "succ-susp": "✅ Issued Suspension (Case #%caseId) to %user for %duration.", + "err-no-case": "❌ Case #%caseId does not exist.", + "err-no-case-ref": "❌ No case found for %reference.", + "err-case-inact": "⚠️ Case #%caseId is inactive.", + "succ-void-fail": "✅ Case #%caseId voided, role restore failed.", + "succ-void": "✅ Voided Case #%caseId.", + "info-clean-rec": "ℹ️ %username has a clean record.", + "rec-title": "Record: %username", + "icon-voided": "⚪", + "label-exp": "Expires", + "label-case": "Case", + "label-date": "Date", + "label-iss": "Issuer", + "err-role-hier": "❌ I cannot assign a role higher than my highest role.", + "err-add-role": "❌ Failed to add role: %e", + "succ-promo": "✅ Promoted %user to %role.", + "info-no-promo": "ℹ️ No promotion history found for %username.", + "prom-hist-title": "Promotion History: %username", + "label-role": "Role", + "label-prom-by": "Promoted by", + "panel-title": "User Panel: %username", + "panel-desc": "Manage and view all data for the user %mention (%id).", + "panel-ph": "Select a category...", + "opt-over": "Overview", + "opt-act": "Activity Checks", + "opt-inf": "Infractions", + "opt-prom": "Promotions", + "opt-rev": "Reviews", + "opt-shi": "Shifts", + "opt-sta": "Status", + "opt-del": "Data Deletion", + "p-inf-title": "Infractions: %username", + "p-inf-desc": "Total: **%count**\n%types\n", + "info-none": "*None*", + "p-no-hist": "*No history on this page.*", + "p-prom-title": "Promotions: %username", + "p-prom-desc": "Total: **%count**\n", + "p-rev-title": "Reviews: %username", + "p-rev-desc": "Total: **%count**\nAverage rating: **%avg ⭐**\n", + "label-by": "by", + "p-sta-title": "Status: %username", + "p-sta-desc": "Total requests: **%count**\nActive: %active\n", + "p-act-title": "Activity Checks: %username", + "p-act-desc": "Responses: **%count**\n", + "label-chk": "Check on", + "label-end": "Ends", + "label-chan": "Channel", + "p-shi-title": "Shifts: %username", + "no-quota-configured": "No quota", + "duty-quota-met": "✅ Quota Met", + "duty-quota-failed": "❌ Quota Not Met", + "label-unranked": "Unranked", + "panel-shifts-desc": "**Total Shifts:** %totalShifts\n**Duration:** %totalSeconds\n**Rank:** %lbRank\n**Breakdown:**\n%breakdownStr\n\n%quotaStr", + "err-shift-data-unavailable": "Shift data unavailable: %error", + "btn-view-history": "View History", + "panel-deletion-title": "Data Deletion: %tag", + "panel-deletion-desc": "⚠️ DANGEROUS AREA ⚠️\nYou are now entering a dangerous zone. At this place, you are able to delete specific or all data for the selected user. These actions ***CANNOT BE UNDONE*** and should only be used if you are absolutely sure about what you are doing. If you only want to delete specific entries, please use the respective command for that entry instead.\nIf you are unsure, click 'Go Back' from the dropdown now.\n\nUse the dropdown below to choose which data you want to delete or delete all data. Choose wisely and gracefully.", + "panel-deletion-placeholder": "Select data to delete...", + "panel-opt-back": "Go Back", + "panel-opt-del-act": "Delete Activity Checks", + "panel-opt-del-inf": "Delete Infractions", + "panel-opt-del-prom": "Delete Promotions", + "panel-opt-del-rev": "Delete Reviews", + "panel-opt-del-shifts": "Delete Shifts", + "panel-opt-del-status": "Delete Status", + "panel-opt-del-all": "Delete ALL data", + "status-active-loa": "🟢 On LoA", + "status-active-ra": "🟠 On RA", + "status-hist-loa": "LoA History", + "status-hist-ra": "RA History", + "err-status-disabled": "❌ %type system disabled.", + "err-invalid-duration": "❌ Invalid duration.", + "err-duration-max": "❌ Max duration is %max days.", + "err-status-exists": "❌ You have an active %type request.", + "status-request-title": "New %type Request", + "status-req-user": "User", + "status-req-duration": "Duration", + "btn-approve": "Approve", + "btn-deny": "Deny", + "success-status-request": "✅ %type request created (%state).", + "state-pending": "Pending", + "state-auto": "Auto-Approved", + "no-active-status": "ℹ️ %user has no active %type.", + "label-stat": "Status", + "filter-active": " (Active)", + "filter-expired": " (Expired)", + "filter-history": " (History)", + "err-no-recs": "No records found.", + "manage-status-title": "Manage %label - %username", + "manage-stat-desc": "%status\nPrevious %label's: %count", + "no-act-stat": "⚫ No active %label", + "manage-active-details": "📋 Active %label Details", + "label-auto": "Auto", + "manage-no-active-user": "No active %label.", + "btn-end-early": "End %label Early", + "btn-extend": "Extend %label", + "err-no-active-end": "❌ No active %label to end.", + "modal-end-early-title": "End %label Early", + "modal-end-early-reason": "Reason for ending", + "err-stat-inact": "❌ This %label is inactive.", + "status-ended-embed-desc": "⚫ %label ended by %user\nReason: %reason", + "err-no-active-extend": "❌ No active %label.", + "modal-extend-title": "Extend %label", + "modal-extend-days": "Additional days, maximum of 180 days", + "modal-extend-reason": "Reason for extension", + "status-adjusted-log": "**%label extended** - the %label now ends at %newEnd.\n**Reason:** %reason", + "mod-stat-ext": "**Start:** %s\n**End:** %e (+%d days)\n**Status:** %t\n**Approved by:** %a\n**Reason:** %r", + "info-no-status-history": "ℹ️ No %label history.", + "status-history-desc": "Showing %count of %total %label records.", + "err-ac-act": "❌ Active check already running.", + "err-ac-norole": "❌ No target roles configured.", + "err-ac-invchan": "❌ Invalid channel.", + "ac-confirm-btn": "Confirm Activity", + "succ-ac-start": "✅ Check started in <#%channel> for %hours hours.", + "err-ac-perms": "❌ Missing permissions in <#%channel>.", + "ac-title-end": "📋 Activity Check (Ended)", + "ac-res-title": "📊 Activity Results", + "ac-f-res": "✅ Responded (%count)", + "ac-f-fail": "❌ Failed (%count)", + "ac-f-exc": "🛡️ Exceptions (%count)", + "log-ac-send-fail": "Failed to send activity check results message: %error", + "err-not-mem": "❌ That is not a member.", + "err-self-rate": "A good detective never investigates themselves. Neither do you.", + "err-staff-rate": "❌ You can only rate staff.", + "succ-review": "✅ Rated %tag %stars stars.", + "rev-title": "Reviews: %username", + "rev-desc": "**Average:** %avg ⭐ (%count reviews)", + "label-hist": "History", + "info-ac-none": "There are no active activity checks. Please check recent results in %c.", + "log-sched-fail": "[Staff Management] Failed to init expiry schedules: %error", + "log-susp-end": "[Staff Management] Automatically ended suspension for %tag", + "log-susp-err": "[Staff Management] Error expiring suspension: %error", + "log-leave-err": "[Staff Management] Error handling member leave: %error", + "log-del-all": "[Staff Management] Data deletion (ALL) executed for user %target by admin %admin.", + "log-del-type": "[Staff Management] Data deletion (%type) executed for user %target by admin %admin.", + "log-int-error": "[Staff Management] Interaction Error: %error", + "log-void-all": "[Staff management] All shift data for the user with ID %target has been deleted by admin %admin.", + "log-add-time": "[Staff Management] %admin added %min mins of %type duty time to %target.", + "log-stat-dm-error": "[Staff Management] Failed to send status DM to %u: %e", + "log-status-adj-error": "[Staff Management] Logging status adjustment failed: %e", + "log-promo-msg-error": "[Staff Management] Failed to send promotion announcement: %e", + "lbl-log-chan": "the configured log channel", + "ac-live-title": "Live Activity Check Status", + "err-ac-not-req": "❌ You are not required to respond to this activity check.", + "cmd-desc-status": "Manage Leave of Absence (LoA) and Reduced Activity (RA).", + "cmd-desc-loa": "Manage Leave of Absence (LoA).", + "cmd-desc-loa-request": "Request a Leave of Absence.", + "cmd-desc-loar-duration": "The duration for your LoA (e.g. 3d, 2w, 1m)", + "cmd-desc-loar-reason": "Reason for your LoA", + "cmd-desc-loa-view": "View your Leave of Absence status.", + "cmd-desc-loav-user": "The user to view the LoA status", + "cmd-desc-loa-list": "List of all Leave of Absences", + "cmd-desc-loal-filter": "Filter the LoA list on active, expired or all", + "cmd-desc-loa-admin": "Manage a user's Leave of Absence.", + "cmd-desc-loaa-user": "The user to manage their LoA", + "cmd-desc-ra": "Manage Reduced Activity (RA).", + "cmd-desc-ra-request": "Request Reduced Activity.", + "cmd-desc-rar-duration": "The duration for your RA (e.g. 3d, 2w, 1m)", + "cmd-desc-rar-reason": "Reason for your RA", + "cmd-desc-ra-view": "View your Reduced Activity status.", + "cmd-desc-rav-user": "The user to view the RA status", + "cmd-desc-ra-list": "List of all Reduced Activities", + "cmd-desc-ral-filter": "Filter the RA list on active, expired or all", + "cmd-desc-ra-admin": "Manage a user's Reduced Activity.", + "cmd-desc-raa-user": "The user to manage their RA", + "cmd-desc-duty": "Manage your duty status and view statistics.", + "cmd-desc-duty-manage": "Manage your duty status.", + "cmd-desc-duty-manage-type": "The duty type", + "cmd-desc-duty-active": "View all staff currently on duty.", + "cmd-desc-duty-lb": "View the duty time leaderboard.", + "cmd-desc-duty-lb-type": "The duty type for the leaderboard.", + "cmd-desc-duty-time": "View your total duty time.", + "cmd-desc-duty-time-type": "The duty type", + "cmd-desc-duty-admin": "Manage a user's shift.", + "cmd-desc-duty-admin-user": "The user to manage their shift", + "cmd-desc-smg": "Access the staff management system.", + "cmd-desc-panel": "Open the staff management panel for a user.", + "cmd-desc-panel-user": "The user to open the staff panel for.", + "cmd-desc-infractions": "Manage staff infractions.", + "cmd-desc-issue": "Issue an infraction to a staff member.", + "cmd-desc-issue-user": "The user receiving the infraction.", + "cmd-desc-issue-type": "The type of infraction to issue.", + "cmd-desc-issue-reason": "The reason for issuing this infraction.", + "cmd-desc-issue-expiry": "When the infraction should expire.", + "cmd-desc-suspend": "Suspend a staff member.", + "cmd-desc-suspend-user": "The user to suspend.", + "cmd-desc-suspend-duration": "How long the suspension should last.", + "cmd-desc-suspend-reason": "The reason for the suspension.", + "cmd-desc-history": "View a user's history.", + "cmd-desc-history-user": "The user whose history you want to view.", + "cmd-desc-void": "Void an infraction case.", + "cmd-desc-void-case-ref": "The case ID or message link of the infraction to void.", + "cmd-desc-promotion": "Manage staff promotions.", + "cmd-desc-promote": "Promote a staff member to a new rank.", + "cmd-desc-promote-user": "The user to promote.", + "cmd-desc-promote-rank": "The rank to promote the user to.", + "cmd-desc-promote-reason": "The reason for the promotion.", + "cmd-desc-promote-channel": "The channel to announce the promotion in.", + "cmd-desc-prom-history": "View the promotion history of a staff member.", + "cmd-desc-prom-history-user": "The user whose promotion history you want to view.", + "cmd-desc-ac": "Manage activity checks.", + "cmd-desc-ac-start": "Start a new activity check.", + "cmd-desc-ac-start-channel": "The channel where the activity check will be posted.", + "cmd-desc-ac-view": "View the current activity check status.", + "cmd-desc-ac-end": "End the current activity check.", + "cmd-desc-profile": "Manage staff profiles.", + "cmd-desc-profile-view": "View a staff member's profile.", + "cmd-desc-profile-view-user": "The user whose profile you want to view.", + "cmd-desc-profile-edit": "Edit your staff profile.", + "cmd-desc-profile-wipe": "Wipe a staff member's profile data.", + "cmd-desc-profile-wipe-user": "The user whose profile will be wiped.", + "cmd-desc-review": "Manage staff reviews.", + "cmd-desc-review-submit": "Submit a review for a staff member.", + "cmd-desc-review-submit-user": "The user you are reviewing.", + "cmd-desc-review-submit-stars": "The star rating for the review.", + "cmd-desc-review-submit-comment": "Your review comment.", + "cmd-desc-review-history": "View the review history of a staff member.", + "cmd-desc-review-history-user": "The user whose review history you want to view.", + "del-no-perm": "You do not have sufficient permissions to perform data deletion.", + "log-err-exp-susp": "Suspension check failed: %error", + "duty-admin-target-left": "The action was completed, but the user is no longer in the server.", + "err-shift-too-short": "Your shift was not counted because it was shorter than the minimum required duration of %min minute(s).", + "log-status-expiry-fail": "[Staff Management] Failed to process automatic status expiry: %error", + "none-provided": "No reason provided.", + "log-infract-dm-fail": "[Staff Management] Failed to send infraction DM to %user: %error", + "log-susp-dm-fail": "[Staff Management] Failed to send suspension DM to %user: %error", + "log-promo-dm-fail": "[Staff Management] Failed to send promotion DM to %user: %error", + "duty-started-title": "⏲️ Shift Started", + "duty-break-title": "⏸️ On Break", + "duty-ended-title": "↩️ Off-Duty", + "duty-shift-overview": "Shift Overview", + "duty-shift-report-title": "Shift Report", + "duty-shift-information": "Shift Information", + "label-started": "Started", + "label-ended": "Ended", + "label-elapsed-time": "Elapsed Time", + "label-shift-type": "Shift Type", + "log-duty-dm-fail": "[Staff Management] Failed to send shift report DM to %user: %error", + "label-breaks": "Breaks", + "log-duty-start-title": "%username went on-duty", + "log-duty-start-desc": "%mention has started a duty shift.", + "log-duty-break-title": "%username went on break", + "log-duty-break-desc": "%mention is now on break.", + "log-duty-resume-title": "%username resumed duty", + "log-duty-resume-desc": "%mention is back on duty.", + "log-duty-end-title": "%username went off-duty", + "log-duty-end-desc": "%mention has ended their duty shift.", + "log-duty-void-title": "%username's active shift was voided", + "log-duty-void-desc": "%mention's active shift was voided by %executor.", + "log-duty-info-hdr": "Information", + "label-ended-by": "Ended by", + "log-duty-log-fail": "[Staff Management] Failed to log duty change (%action): %error", + "err-self-infract": "That's not in the code... well, it's more of a guideline anyway. Still no.\n-# You cannot infract yourself", + "err-self-promo": "You can't promote yourself through a black hole of audacity and expect it to work.", + "status-expired-auto": "Ended automatically because the status expired." + } } diff --git a/main.js b/main.js index 6191a4a7..6f096984 100644 --- a/main.js +++ b/main.js @@ -11,11 +11,31 @@ const { const client = new Discord.Client({ partials: [Partials.Message, Partials.GuildMember, Partials.GuildScheduledEvent, Partials.Reaction, Partials.User, Partials.Channel], // Most of these are not needed, but enabling them does not increase CPU / RAM usage and does not introduce problems, as we handle them in the event emitter system allowedMentions: {parse: ['users', 'roles']}, // Disables @everyone mentions because everyone hates them - intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildBans, GatewayIntentBits.DirectMessages, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, - GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.GuildPresences, GatewayIntentBits.GuildInvites, GatewayIntentBits.GuildEmojisAndStickers, GatewayIntentBits.GuildMessageReactions, GatewayIntentBits.GuildEmojisAndStickers, GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildWebhooks, GatewayIntentBits.AutoModerationExecution] + intents: [GatewayIntentBits.Guilds, GatewayIntentBits.DirectMessages, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, + GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.GuildPresences, GatewayIntentBits.GuildInvites, GatewayIntentBits.GuildEmojisAndStickers, GatewayIntentBits.GuildMessageReactions, GatewayIntentBits.GuildEmojisAndStickers, GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildWebhooks, GatewayIntentBits.AutoModerationExecution, GatewayIntentBits.GuildModeration] +}); +client.on('error', (err) => { + const {localize: loc} = require('./src/functions/localize'); + const sentryId = client.captureException ? client.captureException(err, {source: 'discord-client-error'}) : null; + client.logger ? client.logger.error(client.sanitizePath(loc('main', 'discord-error', {e: err.stack || err})) + (sentryId ? ` [Sentry: ${sentryId}]` : '')) : console.error(err); +}); +client.on('shardError', (err) => { + const {localize: loc} = require('./src/functions/localize'); + const sentryId = client.captureException ? client.captureException(err, {source: 'shard-error'}) : null; + client.logger ? client.logger.error(client.sanitizePath(loc('main', 'shard-error', {e: err.stack || err})) + (sentryId ? ` [Sentry: ${sentryId}]` : '')) : console.error(err); +}); +client.on('shardDisconnect', (event) => { + const {localize: loc} = require('./src/functions/localize'); + client.logger ? client.logger.warn(loc('main', 'shard-disconnect', {c: event ? event.code : 'unknown'})) : console.warn('Disconnected from Discord'); +}); +client.on('shardReconnecting', () => { + const {localize: loc} = require('./src/functions/localize'); + client.logger ? client.logger.info(loc('main', 'shard-reconnecting')) : console.info('Reconnecting to Discord'); }); client.intervals = []; client.jobs = []; +client._migrationCount = 0; +client._shutdownRequested = false; const fs = require('fs'); const {Sequelize} = require('sequelize'); const log4js = require('log4js'); @@ -31,6 +51,16 @@ const args = process.argv.slice(2); let scnxSetup = false; // If enabled some other (closed-sourced) files get imported and executed if (process.argv.includes('--scnx-enabled')) scnxSetup = true; client.scnxSetup = scnxSetup; +if (scnxSetup) { + const instrument = require('./instrument'); + client.sentry = instrument; + client.sanitizePath = instrument.sanitizePath; + client.captureException = function (err, data) { + return instrument.captureException(err, {contexts: {'extra-data': data}}); + }; +} else { + client.sanitizePath = (s) => s; +} if (args[0] === '--help' || args[0] === '-h') { process.exit(); } @@ -144,13 +174,13 @@ async function startUp() { if (scnxSetup) await require('./src/functions/scnx-integration').beforeInit(client); if (!client.isReady()) { await client.login(config.token).catch(async (e) => { - if (e.code === 'TOKEN_INVALID') { + if (e.code === 'TokenInvalid' || e.message === 'Authentication failed') { if (scnxSetup) await require('./src/functions/scnx-integration').reportIssue(client, { type: 'CORE_FAILURE', errorDescription: 'invalid_token' }); logger.fatal(localize('main', 'login-error-token')); - } else if (e.code === 'DISALLOWED_INTENTS') { + } else if (e.code === 'DisallowedIntents' || e.message === 'Used disallowed intents') { if (scnxSetup) await require('./src/functions/scnx-integration').reportIssue(client, { type: 'CORE_FAILURE', errorDescription: 'disallowed_intents' @@ -160,7 +190,12 @@ async function startUp() { process.exit(); }); } - const app = JSON.parse((await centra(`https://discord.com/api/applications/@me`, 'GET').header('Authorization', `Bot ${client.token}`).send()).body.toString()); + let app = {}; + try { + app = JSON.parse((await centra(`https://discord.com/api/applications/@me`, 'GET').header('Authorization', `Bot ${client.token}`).send()).body.toString()); + } catch (e) { + logger.warn(localize('main', 'discord-api-error', {e: e.message || e})); + } if (app.bot_require_code_grant) { if (scnxSetup) await require('./src/functions/scnx-integration').reportIssue(client, { type: 'CORE_ISSUE', @@ -235,14 +270,60 @@ async function startUp() { client.strings = jsonfile.readFileSync(`${confDir}/strings.json`); client.botReadyAt = new Date(); client.emit('botReady'); + await client.guild.members.fetch({withPresences: true}).catch(() => { + }); if (scnxSetup) await require('./src/functions/scnx-integration').init(client); logger.info(localize('main', 'bot-ready')); if (client.logChannel) client.logChannel.send('🚀 ' + localize('main', 'bot-ready')); await checkForUpdates(client); } +// Prevent shutdown during database migrations +function handleShutdownSignal(signal) { + if (client._migrationCount > 0) { + client._shutdownRequested = true; + logger.warn(localize('main', 'shutdown-deferred')); + return; + } + process.exit(0); +} + +process.on('SIGINT', handleShutdownSignal); +process.on('SIGTERM', handleShutdownSignal); + +process.on('uncaughtException', (err) => { + const sentryId = client.captureException ? client.captureException(err, {source: 'uncaught-exception'}) : null; + logger.error(client.sanitizePath(localize('main', 'uncaught-exception', {e: err.stack || err})) + (sentryId ? ` [Sentry: ${sentryId}]` : '')); +}); + +process.on('unhandledRejection', (reason) => { + const sentryId = client.captureException ? client.captureException(reason instanceof Error ? reason : new Error(String(reason)), {source: 'unhandled-rejection'}) : null; + logger.error(client.sanitizePath(localize('main', 'unhandled-rejection', {e: reason instanceof Error ? reason.stack : reason})) + (sentryId ? ` [Sentry: ${sentryId}]` : '')); +}); + +/** + * Call before starting a migration to prevent shutdown + */ +module.exports.migrationStart = function () { + client._migrationCount++; +}; + +/** + * Call after a migration completes to allow shutdown again + */ +module.exports.migrationEnd = function () { + client._migrationCount--; + if (client._migrationCount <= 0 && client._shutdownRequested) { + logger.info(localize('main', 'shutdown-after-migration')); + process.exit(0); + } +}; + // Starting bot -db.authenticate().then(startUp); +db.authenticate().then(startUp).catch((e) => { + logger.fatal(localize('main', 'db-connect-error', {e: e.message || e})); + process.exit(1); +}); // CLI-COMMANDS const cliCommands = []; @@ -250,6 +331,10 @@ const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); +rl.on('error', (err) => { + const sentryId = client.captureException ? client.captureException(err, {source: 'readline-error'}) : null; + logger.error(client.sanitizePath(localize('main', 'cli-command-error', {e: err.message || err})) + (sentryId ? ` [Sentry: ${sentryId}]` : '')); +}); rl.on('line', (input) => { if (!client.botReadyAt) { return console.error('The bot is not ready yet. Please wait until the bot gets ready to use the cli.'); @@ -260,12 +345,17 @@ rl.on('line', (input) => { if (!command) return console.error('Command not found. Use "help" to see all available commands.'); console.log('\n'); - command.run({ - input, - args: input.split(' '), - client, - cliCommands - }); + try { + command.run({ + input, + args: input.split(' '), + client, + cliCommands + }); + } catch (e) { + const sentryId = client.captureException ? client.captureException(e, {source: 'cli-command'}) : null; + logger.error(client.sanitizePath(localize('main', 'cli-command-error', {e: e.stack || e})) + (sentryId ? ` [Sentry: ${sentryId}]` : '')); + } }); /** @@ -423,14 +513,17 @@ async function syncCommandsIfNeeded() { break; } - if (oldCommand.description !== command.description || (oldCommand.options || []).length !== (command.options || []).length) { + if (oldCommand.description !== command.description || oldCommand.type !== command.type || (oldCommand.options || []).length !== (command.options || []).length) { needSync = true; break; } const newPerms = new PermissionsBitField(command.defaultMemberPermissions || []).bitfield; const oldPerms = new PermissionsBitField(oldCommand.defaultMemberPermissions || []).bitfield; - if (newPerms !== oldPerms) needSync = true; + if (newPerms !== oldPerms) { + needSync = true; + break; + } for (const option of (command.options || [])) { const oldOptionOption = (oldCommand.options || []).find(o => o.name === option.name); @@ -454,6 +547,9 @@ async function syncCommandsIfNeeded() { function checkOption(oldOption, newOption) { if (oldOption.name !== newOption.name || oldOption.autocomplete !== newOption.autocomplete || oldOption.description !== newOption.description || oldOption.type !== newOption.type || (typeof oldOption.required === 'undefined' ? false : oldOption.required) !== (typeof newOption.required === 'undefined' ? false : newOption.required)) return true; if (!compareArrays(oldOption.choices || [], newOption.choices || [])) return true; + if (!compareArrays(oldOption.channelTypes || [], newOption.channelTypes || [])) return true; + if (oldOption.minValue !== newOption.minValue || oldOption.maxValue !== newOption.maxValue) return true; + if (oldOption.minLength !== newOption.minLength || oldOption.maxLength !== newOption.maxLength) return true; if ((oldOption.options || []).length !== (newOption.options || []).length) return true; for (const option of (newOption.options || [])) { const oldOptionOption = (oldOption.options || []).find(o => o.name === option.name); @@ -547,11 +643,11 @@ async function loadEventsInDir(dir, moduleName = null) { if (!eData.moduleName) eData.eventFunction.run(client, ...cArgs); else if (client.modules[eData.moduleName].enabled) eData.eventFunction.run(client, ...cArgs); } catch (e) { - if (client.captureException) client.captureException(e, { + const sentryId = client.captureException ? client.captureException(e, { module: eData.moduleName, event: eventName - }); - client.logger.error(`Error on event ${(eData.moduleName ? eData.moduleName + '/' : '') + eventName}: ${e}`); + }) : null; + client.logger.error(client.sanitizePath(`Error on event ${(eData.moduleName ? eData.moduleName + '/' : '') + eventName}: ${e}${sentryId ? ` [Sentry: ${sentryId}]` : ''}`)); } } }); diff --git a/modules/admin-tools/always-temporary-roles.json b/modules/admin-tools/always-temporary-roles.json new file mode 100644 index 00000000..6f6f91af --- /dev/null +++ b/modules/admin-tools/always-temporary-roles.json @@ -0,0 +1,32 @@ +{ + "filename": "always-temporary-roles.json", + "humanName": "Always-Temporary Roles", + "configElementName": { + "one": "Always-Temporary Role", + "more": "Always-Temporary Roles" + }, + "description": "Configure roles that are always temporary. When a user receives one of these roles (by any means), the role will automatically be removed after the configured duration.", + "configElements": true, + "content": [ + { + "type": "roleID", + "name": "roleID", + "default": "", + "humanName": "Role", + "description": "The role that should always be temporary. When a user receives this role, it will be automatically removed after the configured duration." + }, + { + "type": "string", + "name": "duration", + "default": "24h", + "humanName": "Duration", + "description": "How long the role should last before being automatically removed. Examples: 1h, 12h, 1d, 7d, 30m", + "links": [ + { + "label": "Duration format", + "url": "https://scootk.it/custombot-durations" + } + ] + } + ] +} diff --git a/modules/admin-tools/commands/admin.js b/modules/admin-tools/commands/admin.js index a0d6ecda..6fed5221 100644 --- a/modules/admin-tools/commands/admin.js +++ b/modules/admin-tools/commands/admin.js @@ -105,6 +105,7 @@ module.exports.config = { channel_types: [ChannelType.GuildCategory], required: true, name: 'category', + channelTypes: [ChannelType.GuildCategory], description: localize('admin-tools', 'category-description') } ] diff --git a/modules/admin-tools/config.json b/modules/admin-tools/config.json index c34fdcf6..03368a49 100644 --- a/modules/admin-tools/config.json +++ b/modules/admin-tools/config.json @@ -1,12 +1,6 @@ { - "description": { - "en": "Configure the behaviour of the module here", - "de": "Stelle hier die Funktionen des Modules ein" - }, - "humanName": { - "en": "Configuration", - "de": "Konfiguration" - }, + "description": "Configure the behaviour of the module here", + "humanName": "Configuration", "filename": "config.json", "commandsWarnings": { "normal": [ diff --git a/modules/admin-tools/events/guildMemberUpdate.js b/modules/admin-tools/events/guildMemberUpdate.js new file mode 100644 index 00000000..7f3dc950 --- /dev/null +++ b/modules/admin-tools/events/guildMemberUpdate.js @@ -0,0 +1,49 @@ +const {createTemporaryRoleChangeAction} = require('../temporaryRoles'); +const durationParser = require('parse-duration'); +const {localize} = require('../../../src/functions/localize'); + +module.exports.run = async function (client, oldMember, newMember) { + if (!client.botReadyAt) return; + if (newMember.guild.id !== client.guild.id) return; + + const addedRoles = newMember.roles.cache.filter(r => !oldMember.roles.cache.has(r.id)); + if (addedRoles.size === 0) return; + + await handleRoleBans(client, newMember); + await handleAlwaysTemporaryRoles(client, newMember, addedRoles); +}; + +async function handleRoleBans(client, newMember) { + const config = client.configurations['admin-tools']['role-bans']; + if (!config || !Array.isArray(config) || config.length === 0) return; + + if (newMember.permissions.has('ManageRoles')) return; + + for (const role of newMember.roles.cache.values()) { + const entry = config.find(c => c.roleID === role.id); + if (!entry) continue; + + const deleteMessageSeconds = Math.min(Math.max((entry.deleteMessageDays || 0), 0), 7) * 86400; + await newMember.ban({ + deleteMessageSeconds, + reason: localize('admin-tools', 'audit-log-role-ban', {r: role.name, reason: entry.reason || ''}) + }); + return; + } +} + +async function handleAlwaysTemporaryRoles(client, newMember, addedRoles) { + const config = client.configurations['admin-tools']['always-temporary-roles']; + if (!config || !Array.isArray(config) || config.length === 0) return; + + for (const role of addedRoles.values()) { + const entry = config.find(c => c.roleID === role.id); + if (!entry) continue; + + const ms = durationParser(entry.duration); + if (!ms || ms < 20000) continue; + + const removeDate = new Date(Date.now() + ms); + await createTemporaryRoleChangeAction(client, 'remove', removeDate, role.id, newMember.id); + } +} diff --git a/modules/admin-tools/module.json b/modules/admin-tools/module.json index 65542085..d4bdd144 100644 --- a/modules/admin-tools/module.json +++ b/modules/admin-tools/module.json @@ -10,16 +10,14 @@ "models-dir": "/models", "events-dir": "/events", "config-example-files": [ - "config.json" + "config.json", + "always-temporary-roles.json", + "role-bans.json" ], "tags": [ "administration" ], - "humanReadableName": { - "en": "Admin-Tools" - }, - "description": { - "en": "Simple tools for admins - move channels and roles via commands, assign temporary roles or copy an emoji from another server to your server.", - "de": "Einfache Tools für Admins, um Channel und Rollen per Command zu verschieben, temporäre Rollen zu vergeben und Emojis zu leihen." - } -} \ No newline at end of file + "fa-icon": "fas fa-screwdriver-wrench", + "humanReadableName": "Admin-Tools", + "description": "Simple tools for admins - move channels and roles via commands, assign temporary roles, configure role bans or copy an emoji from another server to your server." +} diff --git a/modules/admin-tools/role-bans.json b/modules/admin-tools/role-bans.json new file mode 100644 index 00000000..d7c56b79 --- /dev/null +++ b/modules/admin-tools/role-bans.json @@ -0,0 +1,33 @@ +{ + "filename": "role-bans.json", + "humanName": "Role Bans", + "configElementName": { + "one": "Role Ban", + "more": "Role Bans" + }, + "description": "Configure roles that automatically ban users when assigned. When a user receives one of these roles, they will be immediately banned from the server. Users with the \"Manage Roles\" permission are exempt.", + "configElements": true, + "content": [ + { + "type": "roleID", + "name": "roleID", + "default": "", + "humanName": "Role", + "description": "When a user receives this role, they will be immediately banned from the server. Users with the \"Manage Roles\" permission are exempt." + }, + { + "type": "string", + "name": "reason", + "default": "Received a banned role", + "humanName": "Ban Reason", + "description": "The reason shown in the audit log when a user is banned for receiving this role." + }, + { + "type": "integer", + "name": "deleteMessageDays", + "default": 0, + "humanName": "Delete Message Days", + "description": "Number of days of messages to delete when banning the user (0-7)." + } + ] +} diff --git a/modules/admin-tools/temporaryRoles.js b/modules/admin-tools/temporaryRoles.js index 04ee8b65..1d86e250 100644 --- a/modules/admin-tools/temporaryRoles.js +++ b/modules/admin-tools/temporaryRoles.js @@ -16,8 +16,8 @@ module.exports.createTemporaryRoleChangeAction = async function (client, type, c } }); if (duplicate) { - duplicate.destroy(); if (jobCache.has(duplicate.id)) jobCache.get(duplicate.id).cancel(); + await duplicate.destroy(); } const res = await client.models['admin-tools']['TemporaryRoleChange'].create({ userID, diff --git a/modules/afk-system/config.json b/modules/afk-system/config.json index d8447340..6107acd5 100644 --- a/modules/afk-system/config.json +++ b/modules/afk-system/config.json @@ -1,130 +1,67 @@ { - "description": { - "en": "Configure the behaviour of the module here", - "de": "Stelle hier die Funktionen des Modules ein" - }, - "humanName": { - "en": "Configuration", - "de": "Konfiguration" - }, + "description": "Configure the behaviour of the module here", + "humanName": "Configuration", "filename": "config.json", "content": [ { "name": "sessionEndedSuccessfully", - "humanName": { - "en": "AFK-Session ended successfully", - "de": "AFK-Sitzung erfolgreich beendet" - }, - "default": { - "en": "✅ Your AFK status has been removed. Welcome back!", - "de": "✅ Dein Status ist jetzt nicht mehr \"AFK\". Willkommen zurück!" - }, - "description": { - "en": "This message gets send if a user ended their AFK-session successfully.", - "de": "Diese Nachricht wird gesendet, wenn der Nutzer seine AFK-Sitzung erfolgreich beendet." - }, + "humanName": "AFK-Session ended successfully", + "default": "✅ Your AFK status has been removed. Welcome back!", + "description": "This message gets send if a user ended their AFK-session successfully.", "type": "string", "allowEmbed": true }, { "name": "sessionStartedSuccessfully", - "humanName": { - "en": "AFK-Session started successfully", - "de": "AFK-Sitzung erfolgreich gestartet" - }, - "default": { - "en": "✅ Your status has been updated to AFK. If another member mentions you while your AFK, we're going to notify them about your status.", - "de": "✅ Dein Status wurde auf \"AFK\" aktualisiert. Wenn dich ein anderer Nutzer erwähnt, während du AFK bist, werden wir ihn über deinen Status informieren." - }, - "description": { - "en": "This message gets send if a user started their session successfully.", - "de": "Diese Nachricht wird Nutzern angezeigt, wenn sie ihren Status auf AFK wechseln." - }, + "humanName": "AFK-Session started successfully", + "default": "✅ Your status has been updated to AFK. If another member mentions you while your AFK, we're going to notify them about your status.", + "description": "This message gets send if a user started their session successfully.", "type": "string", "allowEmbed": true }, { "name": "afkUserWithReason", - "humanName": { - "en": "User is AFK with reason", - "de": "Nutzer ist mit Begründung AFK" - }, - "default": { - "en": "ℹ %user% is currently AFK and specified the following reason: \"%reason%\".", - "de": "ℹ %user% ist aktuell AFK und hat folgenden Grund angegeben: \"%reason%\"." - }, - "description": { - "en": "This message gets send if a pinged user is currently AFK with a previously specified reason.", - "de": "Diese Nachricht wird gesendet, wenn ein Nutzer erwähnt wird, der AFK ist und zuvor eine Begründung dafür angegeben hat." - }, + "humanName": "User is AFK with reason", + "default": "ℹ %user% is currently AFK and specified the following reason: \"%reason%\".", + "description": "This message gets send if a pinged user is currently AFK with a previously specified reason.", "type": "string", "allowEmbed": true, "params": [ { "name": "reason", - "description": { - "de": "Begründung für die Abwesenheit des Nutzers", - "en": "Reason for their absence" - } + "description": "Reason for their absence" }, { "name": "user", - "description": { - "de": "Erwähnung des AFK Nutzers", - "en": "Mention of the user who is AFK" - } + "description": "Mention of the user who is AFK" } ] }, { "name": "afkUserWithoutReason", - "humanName": { - "en": "User is AFK without reason", - "de": "Nutzer ist ohne Begründung AFK" - }, - "default": { - "en": "ℹ %user% is currently AFK.", - "de": "ℹ %user% ist aktuell AFK." - }, - "description": { - "en": "This message gets send if a pinged user is currently AFK without a previously specified reason.", - "de": "Diese Nachricht wird gesendet, wenn ein Nutzer erwähnt wird, der AFK ist und zuvor keine Begründung dafür angegeben hat." - }, + "humanName": "User is AFK without reason", + "default": "ℹ %user% is currently AFK.", + "description": "This message gets send if a pinged user is currently AFK without a previously specified reason.", "type": "string", "allowEmbed": true, "params": [ { "name": "user", - "description": { - "de": "Erwähnung des AFK Nutzers", - "en": "Mention of the user who is AFK" - } + "description": "Mention of the user who is AFK" } ] }, { "name": "autoEndMessage", - "humanName": { - "en": "AFK Session ended automatically", - "de": "AFK Sitzung automatisch beendet" - }, - "default": { - "en": "Welcome back \uD83D\uDC4B!\nYou are not longer AFK because you wrote a message. You can start a new session with `/afk start` and disable `auto-end` if you don't want your sessions to be ended automatically.", - "de": "Willkommen zurück \uD83D\uDC4B!\nDu bist nun nicht mehr AFK, da du eine Nachricht geschrieben hast. Um eine neue Sitzung zu starten gebe bitte `/afk start` ein; solltest du dieses Verhalten deaktivieren wollen, setze außerdem den `auto-end` Parameter." - }, - "description": { - "en": "This message gets send if a user who is AFK and hasn't disabled auto-ending their sessions posts a message on the server.", - "de": "Diese Nachricht wird verschickt, wenn ein Nutzer, der aktuell AFK ist und automatisches Beenden von Sitzungen nicht deaktiviert hat, eine Nachricht auf dem Server sendet." - }, + "humanName": "AFK Session ended automatically", + "default": "Welcome back 👋!\nYou are no longer AFK because you wrote a message. You can start a new session with `/afk start` and disable `auto-end` if you don't want your sessions to be ended automatically.", + "description": "This message gets send if a user who is AFK and hasn't disabled auto-ending their sessions posts a message on the server.", "type": "string", "allowEmbed": true, "params": [ { "name": "user", - "description": { - "en": "Mention of the user who was AFK", - "de": "Erwähnung des Nutzers, der AFK war" - } + "description": "Mention of the user who was AFK" } ] } diff --git a/modules/afk-system/module.json b/modules/afk-system/module.json index aa1a47d9..44d7b73c 100644 --- a/modules/afk-system/module.json +++ b/modules/afk-system/module.json @@ -14,12 +14,8 @@ "tags": [ "tools" ], + "fa-icon": "fas fa-moon-stars", "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/afk-system", - "humanReadableName": { - "en": "AFK-System" - }, - "description": { - "en": "Allow users to set their AFK-Status and notify other users if they try to reach them", - "de": "Erlaubt es deinen Nutzern, ihren AFK-Status zu setzen und benachrichtigt andere Nutzer, wenn sie diesen versuchen zu erreichen" - } -} \ No newline at end of file + "humanReadableName": "AFK-System", + "description": "Allow users to set their AFK-Status and notify other users if they try to reach them" +} diff --git a/modules/anti-ghostping/config.json b/modules/anti-ghostping/config.json index b06f072e..2cfcec58 100644 --- a/modules/anti-ghostping/config.json +++ b/modules/anti-ghostping/config.json @@ -1,81 +1,42 @@ { - "description": {}, - "humanName": { - "en": "Configuration", - "de": "Konfiguration" - }, + "description": "Configure the behaviour of the module here", + "humanName": "Configuration", "filename": "config.json", "content": [ { "name": "awaitBotMessages", - "humanName": { - "de": "Botnachrichten abwarten", - "en": "Wait for Bot-Messages" - }, - "default": { - "en": true, - "de": true - }, - "description": { - "en": "If enabled, the bot will wait ~2 Seconds to make sure no bot like NQN deleted the messages and answered afterwards", - "de": "Wenn diese Option aktiviert ist, wird der Bot ~2 Sekunden warten, um sicherzustellen, dass kein Bot wie NQN die Nachricht gelöscht und danach geantwortet hat" - }, + "humanName": "Wait for Bot-Messages", + "default": true, + "description": "If enabled, the bot will wait ~2 Seconds to make sure no bot like NQN deleted the messages and answered afterwards", "type": "boolean" }, { "name": "ignoredChannels", - "humanName": { - "en": "Ignored Channels", - "de": "Ignorierte Channel" - }, - "default": { - "en": [], - "de": [] - }, - "description": { - "en": "If a ghost ping gets send in one of these configured channels, the bot will not run anti-ghost-ping", - "de": "Wenn ein Ghost-Ping in einem dieser konfigurierten Channel gesendet wird, wird der Bot nicht anti-ghost-ping ausführen" - }, + "humanName": "Ignored Channels", + "default": [], + "description": "If a ghost ping gets send in one of these configured channels, the bot will not run anti-ghost-ping", "type": "array", "content": "channelID" }, { "name": "youJustGotGhostPinged", - "humanName": { - "en": "Ghostping-Message", - "de": "Ghostping-Nachricht" - }, - "default": { - "en": "%mentions%,\nYou just got ghost-pinged by %authorMention% with the following message: \"%msgContent%\"", - "de": "%mentions%,\nDu wurdest gerade von %authorMention% mit folgender Nachricht geghost-pinged: \"%msgContent%\"" - }, - "description": { - "en": "This message gets send if a member pings another user and deletes the message afterwards", - "de": "Diese Nachricht wird gesendet, wenn ein Nutzer einen anderen Nutzer pingt und die Nachricht danach löscht" - }, + "humanName": "Ghostping-Message", + "default": "%mentions%,\nYou just got ghost-pinged by %authorMention% with the following message: \"%msgContent%\"", + "description": "This message gets send if a member pings another user and deletes the message afterwards", "type": "string", "allowEmbed": true, "params": [ { "name": "mentions", - "description": { - "en": "Mentions of every user that got pinged in the original message", - "de": "Erwähnung von jedem, in der Originalnachricht gepingten, Nutzer" - } + "description": "Mentions of every user that got pinged in the original message" }, { "name": "authorMention", - "description": { - "en": "Mention of the original message-author.", - "de": "Erwähnung des Autors der Originalnachricht." - } + "description": "Mention of the original message-author." }, { "name": "msgContent", - "description": { - "en": "Content of the original message", - "de": "Inhalt der Originalnachricht" - } + "description": "Content of the original message" } ] } diff --git a/modules/anti-ghostping/events/messageCreate.js b/modules/anti-ghostping/events/messageCreate.js index 271bdc1b..0c1763e0 100644 --- a/modules/anti-ghostping/events/messageCreate.js +++ b/modules/anti-ghostping/events/messageCreate.js @@ -7,7 +7,7 @@ module.exports.run = async function (client, msg) { if (moduleConfig.ignoredChannels.includes(msg.channel.id)) return; if (msg.mentions.members.filter(f => f.id !== msg.author.id && !f.user.bot).size !== 0) msgsWithMention[msg.id] = msg; setTimeout(() => { - msgsWithMention[msg.id] = null; + delete msgsWithMention[msg.id]; }, 60000); }; module.exports.messageWithMentions = msgsWithMention; \ No newline at end of file diff --git a/modules/anti-ghostping/events/messageDelete.js b/modules/anti-ghostping/events/messageDelete.js index d175233f..da81ac0d 100644 --- a/modules/anti-ghostping/events/messageDelete.js +++ b/modules/anti-ghostping/events/messageDelete.js @@ -11,6 +11,7 @@ module.exports.run = async function (client, msg) { if (messageWithMentions[msg.id].guild.id !== client.config.guildID) return; if (!moduleStrings.awaitBotMessages) return executeGhostPingMessage(); setTimeout(async () => { + if (!messageWithMentions[msg.id]) return; const messages = await msg.channel.messages.fetch({after: msg.id}); if (messages.filter(m => m.author.bot).size !== 0) return; await executeGhostPingMessage(); @@ -22,6 +23,7 @@ module.exports.run = async function (client, msg) { * @return {Promise} */ async function executeGhostPingMessage() { + if (!messageWithMentions[msg.id]) return; let mentionString = ''; messageWithMentions[msg.id].mentions.members.filter(f => f.id !== messageWithMentions[msg.id].author.id && !f.user.bot).forEach(m => { mentionString = mentionString + `<@${m.id}>, `; diff --git a/modules/anti-ghostping/module.json b/modules/anti-ghostping/module.json index 86ca6e5f..cae717b7 100644 --- a/modules/anti-ghostping/module.json +++ b/modules/anti-ghostping/module.json @@ -6,6 +6,7 @@ "link": "https://github.com/SCDerox" }, "events-dir": "/events", + "fa-icon": "fa fa-bell-exclamation", "config-example-files": [ "config.json" ], @@ -13,11 +14,6 @@ "moderation" ], "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/anti-ghostping", - "humanReadableName": { - "en": "Anti-Ghostping" - }, - "description": { - "en": "This module detects ghost-pings and sends a message if one occurs", - "de": "Dieses Modul erkennt automatisch Ghost-Pings und schickt eine Nachricht, wenn einer erkannt wird" - } -} \ No newline at end of file + "humanReadableName": "Anti-Ghostping", + "description": "This module detects ghost-pings and sends a message if one occurs" +} diff --git a/modules/auto-delete/channels.json b/modules/auto-delete/channels.json index 14888788..a7460382 100644 --- a/modules/auto-delete/channels.json +++ b/modules/auto-delete/channels.json @@ -1,28 +1,14 @@ { - "description": { - "en": "Set up channels to delete text-messages from", - "de": "Stelle hier Text-Kanäle ein, aus welchen gelöscht werden soll" - }, - "humanName": { - "en": "Text-Channels", - "de": "Text-Kanäle" - }, + "description": "Set up channels to delete text-messages from", + "humanName": "Text-Channels", "filename": "channels.json", "configElements": true, "content": [ { "name": "channelID", - "humanName": { - "en": "Channel", - "de": "Kanal" - }, - "default": { - "en": "" - }, - "description": { - "en": "The Channel you want messages to be deleted from.", - "de": "Wähle den Kanal aus, aus welchen Nachrichten automatisch gelöscht werden sollen." - }, + "humanName": "Channel", + "default": "", + "description": "The Channel you want messages to be deleted from.", "type": "channelID", "content": [ "GUILD_TEXT", @@ -31,33 +17,17 @@ }, { "name": "timeout", - "humanName": { - "en": "Timeout", - "de": "Timeout" - }, - "default": { - "en": "5" - }, - "description": { - "en": "Timeout (in minutes) after which the messages in a channel will be deleted.", - "de": "Timeout (in Minuten), nachdem die Nachrichten in einem Kanal automatisch gelöscht werden sollen." - }, + "humanName": "Timeout", + "default": 5, + "description": "Timeout (in minutes) after which the messages in a channel will be deleted.", "type": "integer" }, { "name": "keepMessageCount", - "default": { - "en": 0 - }, - "humanName": { - "en": "Amount of messages to keep", - "de": "Anzahl von zu behaltenden Nachrichten" - }, + "default": 0, + "humanName": "Amount of messages to keep", "type": "integer", - "description": { - "en": "Set up a number here to always have x messages in your channel left (newest messages are kept). The number has to below 50.", - "de": "Stelle hier eine Anzahl an Nachrichten ein, die auch nach einer Löschung in dem Kanal behalten werden sollen (neuere Nachrichten werden behalten). Die Zahl muss unter 50 liegen." - } + "description": "Set up a number here to always have x messages in your channel left (newest messages are kept). The number has to below 50." } ] } \ No newline at end of file diff --git a/modules/auto-delete/module.json b/modules/auto-delete/module.json index 8a0b73ef..963de1a6 100644 --- a/modules/auto-delete/module.json +++ b/modules/auto-delete/module.json @@ -5,6 +5,7 @@ "name": "SCDerox (SC Network Team)", "link": "https://github.com/SCDerox" }, + "fa-icon": "fa-regular fa-trash-can", "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/auto-delete", "events-dir": "/events", "config-example-files": [ @@ -14,12 +15,6 @@ "tags": [ "administration" ], - "humanReadableName": { - "en": "Auto-Message-Delete", - "de": "Automatisches Löschen" - }, - "description": { - "en": "This module allows you to delete messages from a channel after a specified timeout to keep your channel clean", - "de": "Halte deine Channel sauber, in dem du alle Nachrichten nach einem bestimmten Intervall in einem Channel löschst" - } -} \ No newline at end of file + "humanReadableName": "Auto-Message-Delete", + "description": "This module allows you to delete messages from a channel after a specified timeout to keep your channel clean" +} diff --git a/modules/auto-delete/voice-channels.json b/modules/auto-delete/voice-channels.json index 688205a2..aee6516f 100644 --- a/modules/auto-delete/voice-channels.json +++ b/modules/auto-delete/voice-channels.json @@ -1,28 +1,14 @@ { - "description": { - "en": "Set up voice-channels to delete messages from", - "de": "Stelle hier Sprach-Kanäle ein, aus welchen gelöscht werden soll" - }, - "humanName": { - "en": "Voice-Channels", - "de": "Sprach-Kanäle" - }, + "description": "Set up voice-channels to delete messages from", + "humanName": "Voice-Channels", "filename": "voice-channels.json", "configElements": true, "content": [ { "name": "channelID", - "humanName": { - "en": "Voice-Channel", - "de": "Sprachkanal" - }, - "default": { - "en": "" - }, - "description": { - "en": "The Voice-Channel you want the auto-deleter to clear if there are no channel members left.", - "de": "Wähle den Sprachkanal aus, den der Bot leeren soll, sobald keine Mitglieder mehr im Sprachkanal sind." - }, + "humanName": "Voice-Channel", + "default": "", + "description": "The Voice-Channel you want the auto-deleter to clear if there are no channel members left.", "type": "channelID", "content": [ "GUILD_VOICE" @@ -30,17 +16,9 @@ }, { "name": "timeout", - "humanName": { - "en": "Timeout", - "de": "Timeout" - }, - "default": { - "en": "5" - }, - "description": { - "en": "Timeout (in minutes) after which the messages in a Voice-Channel are deleted after the last member left the channel. Entering '0' will result in an instant deletion.", - "de": "Timeout (in Minuten), nachdem die Nachrichten gelöscht werden, wenn das letzte Mitglied den Sprachkanal verlassen hat. Wenn du eine '0' verwendest, werden die Nachrichten sofort gelöscht." - }, + "humanName": "Timeout", + "default": 5, + "description": "Timeout (in minutes) after which the messages in a Voice-Channel are deleted after the last member left the channel. Entering '0' will result in an instant deletion.", "type": "integer" } ] diff --git a/modules/auto-messager/cronjob.json b/modules/auto-messager/cronjob.json index 18e9373b..bd40ed91 100644 --- a/modules/auto-messager/cronjob.json +++ b/modules/auto-messager/cronjob.json @@ -1,78 +1,34 @@ { - "description": { - "en": "Advanced users can unleash the full potential of automatic message with cronejobs", - "de": "Nur für fortgeschrittene Nutzer - mit cronjob's kannst hast du die volle Kontrolle über die Nachrichten" - }, - "elementLimits": { - "STARTER": 2, - "ACTIVE_GUILD": 5, - "PRO": 15, - "UNLIMITED": 5, - "PROFESSIONAL": 15 - }, - "humanName": { - "en": "Cronjob (advanced)", - "de": "Cronjobs (fortgeschritten)" - }, + "description": "Advanced users can unleash the full potential of automatic message with cronejobs", + "humanName": "Cronjob (advanced)", "configElementName": { - "de": { - "one": "Automatische Nachricht", - "more": "Automatische Nachrichten" - }, - "en": { - "one": "Automatic message", - "more": "Automatic messages" - } + "one": "Automatic message", + "more": "Automatic messages" }, "filename": "cronjob.json", "configElements": true, "content": [ { "name": "channelID", - "humanName": { - "de": "Kanal", - "en": "Channel" - }, - "default": { - "en": "" - }, - "description": { - "en": "ID of the channel in which the message should be send", - "de": "ID des Kanals, in welchen die Nachricht gesendet werden soll" - }, + "humanName": "Channel", + "default": "", + "description": "ID of the channel in which the message should be send", "type": "channelID" }, { "name": "message", - "humanName": { - "de": "Nachricht", - "en": "Message" - }, - "default": { - "en": "" - }, - "description": { - "en": "Message that should be send", - "de": "Nachricht, die gesendet werden soll" - }, + "humanName": "Message", + "default": "", + "description": "Message that should be send", "type": "string", "allowEmbed": true }, { "name": "expression", - "humanName": { - "de": "Ausdruck", - "en": "Expression" - }, - "default": { - "en": "1 6 1-31 * *", - "de": "1 6 1-31 * *" - }, - "description": { - "en": "The message gets scheduled for this expression", - "de": "Die Nachricht wird für diesen Ausdruck geplant" - }, + "humanName": "Expression", + "default": "1 6 1-31 * *", + "description": "The message gets scheduled for this expression", "type": "string" } ] -} \ No newline at end of file +} diff --git a/modules/auto-messager/daily.json b/modules/auto-messager/daily.json index 4e32ae27..b52456cd 100644 --- a/modules/auto-messager/daily.json +++ b/modules/auto-messager/daily.json @@ -1,96 +1,43 @@ { - "description": { - "en": "You can send on a daily basic here - this can be once a week or month", - "de": "Hier kannst du Nachrichten auf täglicher Basis versenden lassen - das kann auch nur einmal pro Woche oder Monat sein" - }, - "elementLimits": { - "STARTER": 2, - "ACTIVE_GUILD": 5, - "PRO": 15, - "UNLIMITED": 5, - "PROFESSIONAL": 15 - }, + "description": "You can send on a daily basic here - this can be once a week or month", "configElementName": { - "de": { - "one": "Automatische Nachricht", - "more": "Automatische Nachrichten" - }, - "en": { - "one": "Automatic message", - "more": "Automatic messages" - } - }, - "humanName": { - "en": "Daily Basic", - "de": "Tägliche Basis" + "one": "Automatic message", + "more": "Automatic messages" }, + "humanName": "Daily Basic", "filename": "daily.json", "configElements": true, "content": [ { "name": "channelID", - "humanName": { - "de": "Kanal", - "en": "Channel" - }, - "default": { - "en": "" - }, - "description": { - "en": "ID of the channel in which the message should be send", - "de": "ID des Kanals, in welchen die Nachricht gesendet werden soll" - }, + "humanName": "Channel", + "default": "", + "description": "ID of the channel in which the message should be send", "type": "channelID" }, { "name": "message", - "humanName": { - "de": "Nachricht", - "en": "Message" - }, - "default": { - "en": "" - }, - "description": { - "en": "Message that should be send", - "de": "Nachricht, die gesendet werden soll" - }, + "humanName": "Message", + "default": "", + "description": "Message that should be send", "type": "string", "allowEmbed": true }, { "name": "limitWeekDaysTo", - "humanName": { - "de": "Wochentage begrenzen auf", - "en": "Limit Week-Days to" - }, - "default": { - "en": [], - "de": [] - }, - "description": { - "en": "If one or more values are set, the message will only get send when the current week-day is included in this field", - "de": "Wenn ein oder mehrere Werte gesetzt sind, wird die Nachricht nur gesendet, wenn der aktuelle Wochentag hier enthalten ist" - }, + "humanName": "Limit Week-Days to", + "default": [], + "description": "If one or more values are set, the message will only get send when the current week-day is included in this field", "type": "array", "content": "integer" }, { "name": "limitDaysTo", - "humanName": { - "de": "Tage begrenzen auf", - "en": "Limit days to" - }, - "default": { - "en": [], - "de": [] - }, - "description": { - "en": "If one or more values are set, the message will only get send when the current day (of the month) is included in this field", - "de": "Wenn ein oder mehrere Werte gesetzt sind, wird die Nachricht nur gesendet, wenn der aktuelle Tag (des Monats) hier enthalten ist" - }, + "humanName": "Limit days to", + "default": [], + "description": "If one or more values are set, the message will only get send when the current day (of the month) is included in this field", "type": "array", "content": "integer" } ] -} \ No newline at end of file +} diff --git a/modules/auto-messager/hourly.json b/modules/auto-messager/hourly.json index 9b9c2882..29b557cb 100644 --- a/modules/auto-messager/hourly.json +++ b/modules/auto-messager/hourly.json @@ -1,79 +1,35 @@ { - "description": { - "en": "You can send messages on an hourly basic here - this can be once or 24 times a day", - "de": "Hier kannst du Nachrichten auf stündlicher Basis schicken lassen - das kann alles von 1-24x pro Tag sein" - }, - "humanName": { - "en": "Hourly basic", - "de": "Stündliche Basis" - }, - "elementLimits": { - "STARTER": 1, - "ACTIVE_GUILD": 4, - "PRO": 14, - "UNLIMITED": 4, - "PROFESSIONAL": 14 - }, + "description": "You can send messages on an hourly basic here - this can be once or 24 times a day", + "humanName": "Hourly basic", "configElementName": { - "de": { - "one": "Automatische Nachricht", - "more": "Automatische Nachrichten" - }, - "en": { - "one": "Automatic message", - "more": "Automatic messages" - } + "one": "Automatic message", + "more": "Automatic messages" }, "filename": "hourly.json", "configElements": true, "content": [ { "name": "channelID", - "humanName": { - "de": "Kanal", - "en": "Channel" - }, - "default": { - "en": "" - }, - "description": { - "en": "ID of the channel in which the message should be send", - "de": "ID des Kanals, in welchen die Nachricht gesendet werden soll" - }, + "humanName": "Channel", + "default": "", + "description": "ID of the channel in which the message should be send", "type": "channelID" }, { "name": "message", - "humanName": { - "de": "Nachricht", - "en": "Message" - }, - "default": { - "en": "" - }, - "description": { - "en": "Message that should be send", - "de": "Nachricht, die gesendet werden soll" - }, + "humanName": "Message", + "default": "", + "description": "Message that should be send", "type": "string", "allowEmbed": true }, { "name": "limitHoursTo", - "humanName": { - "de": "Stunden begrenzen auf", - "en": "Limit hours to" - }, - "default": { - "en": [], - "de": [] - }, - "description": { - "en": "If one or more values are set, the message will only get send when the current hour is included in this field", - "de": "Wenn ein oder mehrere Werte gesetzt sind, wird die Nachricht nur gesendet, wenn die aktuelle Stunde hier enthalten ist" - }, + "humanName": "Limit hours to", + "default": [], + "description": "If one or more values are set, the message will only get send when the current hour is included in this field", "type": "array", "content": "integer" } ] -} \ No newline at end of file +} diff --git a/modules/auto-messager/module.json b/modules/auto-messager/module.json index e332ccd6..3e073869 100644 --- a/modules/auto-messager/module.json +++ b/modules/auto-messager/module.json @@ -1,5 +1,6 @@ { "name": "auto-messager", + "fa-icon": "fas fa-comment-dots", "author": { "scnxOrgID": "1", "name": "SCDerox (SC Network Team)", @@ -15,12 +16,6 @@ "tags": [ "tools" ], - "humanReadableName": { - "en": "Automatic Messages", - "de": "Automatische Nachrichten" - }, - "description": { - "en": "You can - with this module - send automatic messages", - "de": "Dieses Modul erlaubt dir es dir, automatisch versenden zu lassen" - } -} \ No newline at end of file + "humanReadableName": "Automatic Messages", + "description": "You can - with this module - send automatic messages" +} diff --git a/modules/auto-publisher/config.json b/modules/auto-publisher/config.json index f067cfde..b5f631e1 100644 --- a/modules/auto-publisher/config.json +++ b/modules/auto-publisher/config.json @@ -1,27 +1,13 @@ { - "description": { - "en": "Configure the behaviour of the module here", - "de": "Stelle hier die Funktionen des Modules ein" - }, - "humanName": { - "en": "Configuration", - "de": "Konfiguration" - }, + "description": "Configure the behaviour of the module here", + "humanName": "Configuration", "filename": "config.json", "content": [ { "name": "mode", - "humanName": { - "en": "Message-Publishing-Mode", - "de": "Nachrichten-Veröffentlichung-Modus" - }, - "default": { - "en": "all" - }, - "description": { - "en": "Modus in which this module should operate", - "de": "Modus in welchem dieses Modul arbeiten sollte" - }, + "humanName": "Message-Publishing-Mode", + "default": "all", + "description": "Modus in which this module should operate", "type": "select", "content": [ "all", @@ -31,47 +17,25 @@ }, { "name": "blacklist", - "humanName": { - "en": "Blacklist" - }, - "default": { - "en": [] - }, - "description": { - "en": "Channel to be ignored (only if Message-Publishing-Mode = \"blacklist\")", - "de": "Kanäle, die ignoriert werden sollen (nur wenn Nachrichten-Veröffentlichung-Modus = \"blacklist\")" - }, + "humanName": "Blacklist", + "default": [], + "description": "Channel to be ignored (only if Message-Publishing-Mode = \"blacklist\")", "type": "array", "content": "channelID" }, { "name": "whitelist", - "humanName": { - "en": "Whitelist" - }, - "default": { - "en": [] - }, - "description": { - "en": "Channel in which messages should get published (only if Message-Publishing-Mode = \"whitelist\")", - "de": "Kanäle, in denen Nachrichten veröffentlicht werden sollen (nur wenn Message-Publishing-Mode = \"whitelist\")" - }, + "humanName": "Whitelist", + "default": [], + "description": "Channel in which messages should get published (only if Message-Publishing-Mode = \"whitelist\")", "type": "array", "content": "channelID" }, { "name": "ignoreBots", - "humanName": { - "en": "Ignore bots?", - "de": "Bots ignorieren?" - }, - "default": { - "en": true - }, - "description": { - "en": "Should bots get ignored when they post a message", - "de": "Sollen Bots ignoriert werden, wenn sie eine Nachricht senden" - }, + "humanName": "Ignore bots?", + "default": true, + "description": "Should bots get ignored when they post a message", "type": "boolean" } ] diff --git a/modules/auto-publisher/events/messageCreate.js b/modules/auto-publisher/events/messageCreate.js index 77f1a4fe..1edec8e3 100644 --- a/modules/auto-publisher/events/messageCreate.js +++ b/modules/auto-publisher/events/messageCreate.js @@ -13,11 +13,12 @@ module.exports.run = async (client, msg) => { if (!config.mode) config.mode = 'all'; if (config.mode === 'blacklist' && config.blacklist.includes(msg.channel.id)) return; if (config.mode === 'whitelist' && !config.whitelist.includes(msg.channel.id)) return; - if (msg.crosspostable) await msg.crosspost(); + if (msg.crosspostable) await msg.crosspost().catch(() => { + }); await msg.react('✅').then((r) => { setTimeout(() => { r.remove(); }, 2500); }); } -}; +}; \ No newline at end of file diff --git a/modules/auto-publisher/module.json b/modules/auto-publisher/module.json index 958a7891..6be0fe8c 100644 --- a/modules/auto-publisher/module.json +++ b/modules/auto-publisher/module.json @@ -1,5 +1,6 @@ { "name": "auto-publisher", + "fa-icon": "fas fa-bullhorn", "author": { "scnxOrgID": "1", "name": "SCDerox (SC Network Team)", @@ -13,12 +14,6 @@ "tags": [ "tools" ], - "humanReadableName": { - "en": "Automatic Publishing", - "de": "Automatische Veröffentlichung" - }, - "description": { - "en": "Publishes messages in announcement channels", - "de": "Veröffentlicht Nachrichten in Ankündigungskanälen" - } -} \ No newline at end of file + "humanReadableName": "Automatic Publishing", + "description": "Publishes messages in announcement channels" +} diff --git a/modules/auto-thread/config.json b/modules/auto-thread/config.json index 1062e8b2..5c5ecef7 100644 --- a/modules/auto-thread/config.json +++ b/modules/auto-thread/config.json @@ -1,56 +1,28 @@ { - "description": {}, - "humanName": { - "en": "Configuration", - "de": "Konfiguration" - }, + "description": "Configure the behaviour of the module here", + "humanName": "Configuration", "filename": "config.json", "content": [ { "name": "channels", - "humanName": { - "en": "Channels", - "de": "Kanäle" - }, - "default": { - "en": [], - "de": [] - }, - "description": { - "en": "Here you can add channels in which the bot should create a thread under every message", - "de": "Hier kannst du Kanäle hinzufügen, in welchen der Bot automatisch unter jeder Nachricht einen Thread erstellen soll" - }, + "humanName": "Channels", + "default": [], + "description": "Here you can add channels in which the bot should create a thread under every message", "type": "array", "content": "channelID" }, { "name": "threadName", - "humanName": { - "de": "Threadname" - }, - "default": { - "en": "Comments", - "de": "Kommentare" - }, - "description": { - "en": "Name of every thread", - "de": "Name jedes Threads" - }, + "humanName": "Thread Name", + "default": "Comments", + "description": "Name of every thread", "type": "string" }, { "name": "threadArchiveDuration", - "humanName": { - "de": "Archivierungsdauer" - }, - "default": { - "en": "MAX", - "de": "MAX" - }, - "description": { - "en": "Inactivity after which a thread is automatically archived (in minutes, some values are limited by guild boost level; select \"max\" for the longest possible duration)", - "de": "Inaktivität nach welcher ein Thread automatisch archiviert wird (in Minutes, manche Werte sind durch das Boostlevel des Servers eingeschränkt; verwende \"max\" für die längstmögliche Dauer)" - }, + "humanName": "Archive Duration", + "default": "MAX", + "description": "Inactivity after which a thread is automatically archived (in minutes, some values are limited by guild boost level; select \"max\" for the longest possible duration)", "type": "select", "content": [ "MAX", diff --git a/modules/auto-thread/events/messageCreate.js b/modules/auto-thread/events/messageCreate.js index 276cf745..1cf58fc4 100644 --- a/modules/auto-thread/events/messageCreate.js +++ b/modules/auto-thread/events/messageCreate.js @@ -1,5 +1,15 @@ const {localize} = require('../../../src/functions/localize'); +const {ThreadAutoArchiveDuration} = require('discord.js'); + +const d = { + 'MAX': ThreadAutoArchiveDuration.OneWeek, + '60': ThreadAutoArchiveDuration.OneHour, + '1440': ThreadAutoArchiveDuration.OneDay, + '4320': ThreadAutoArchiveDuration.ThreeDays, + '10080': ThreadAutoArchiveDuration.OneWeek +}; + module.exports.run = async (client, msg) => { if (!client.botReadyAt) return; if (msg.interaction || msg.system) return; @@ -7,7 +17,8 @@ module.exports.run = async (client, msg) => { if (!(moduleConfig.channels || []).includes(msg.channel.id)) return; if (!msg.hasThread) await msg.startThread({ name: moduleConfig.threadName, - autoArchiveDuration: moduleConfig.threadArchiveDuration, + + autoArchiveDuration: d[moduleConfig.threadArchiveDuration], reason: `[auto-thread] ${localize('auto-thread', 'thread-create-reason')}` }); }; \ No newline at end of file diff --git a/modules/auto-thread/module.json b/modules/auto-thread/module.json index 8416f7f4..4d93ad0a 100644 --- a/modules/auto-thread/module.json +++ b/modules/auto-thread/module.json @@ -1,5 +1,6 @@ { "name": "auto-thread", + "fa-icon": "fa-regular fa-comment", "author": { "scnxOrgID": "1", "name": "SCDerox (SC Network Team)", @@ -13,12 +14,6 @@ "tools" ], "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/auto-thread", - "humanReadableName": { - "en": "Automatic Thread-Creation", - "de": "Automatisches Thread-Erstellen" - }, - "description": { - "en": "Automatically creates a thread under each message that gets posted in a selected channel", - "de": "Erstellt einen Thread unter jeder Nachricht, die in einem bestimmten Channel gesendet wird" - } -} \ No newline at end of file + "humanReadableName": "Automatic Thread-Creation", + "description": "Automatically creates a thread under each message that gets posted in a selected channel" +} diff --git a/modules/betterstatus/commands/status.js b/modules/betterstatus/commands/status.js new file mode 100644 index 00000000..dcc87b7d --- /dev/null +++ b/modules/betterstatus/commands/status.js @@ -0,0 +1,84 @@ +const {localize} = require('../../../src/functions/localize'); +const {ActivityType} = require('discord.js'); + +const activityTypes = { + 'PLAYING': ActivityType.Playing, + 'STREAMING': ActivityType.Streaming, + 'WATCHING': ActivityType.Watching, + 'COMPETING': ActivityType.Competing, + 'LISTENING': ActivityType.Listening, + 'CUSTOM': ActivityType.Custom +}; + +/** + * Handle /status command to change bot status + * @param {Interaction} interaction Discord interaction + */ +module.exports.run = async function (interaction) { + const activityType = interaction.options.getString('activity-type'); + const botStatus = interaction.options.getString('bot-status'); + const statusText = interaction.options.getString('text'); + const streamingLink = interaction.options.getString('streaming-link'); + + await interaction.client.user.setPresence({ + status: botStatus, + activities: [{ + name: statusText, + type: activityTypes[activityType], + url: (activityType === 'STREAMING' && streamingLink) ? streamingLink : null + }] + }); + + interaction.reply({ + ephemeral: true, + content: '✅ ' + localize('betterstatus', 'status-changed', {s: statusText}) + }); +}; + +module.exports.config = { + name: 'status', + description: localize('betterstatus', 'command-description'), + defaultMemberPermissions: ['ADMINISTRATOR'], + disabled: function (client) { + return !client.configurations['betterstatus']['config'].enableStatusCommand; + }, + options: [ + { + type: 'STRING', + name: 'text', + required: true, + description: localize('betterstatus', 'text-description') + }, + { + type: 'STRING', + name: 'activity-type', + required: true, + description: localize('betterstatus', 'activity-type-description'), + choices: [ + {name: 'Playing', value: 'PLAYING'}, + {name: 'Streaming', value: 'STREAMING'}, + {name: 'Watching', value: 'WATCHING'}, + {name: 'Competing', value: 'COMPETING'}, + {name: 'Listening', value: 'LISTENING'}, + {name: 'Custom', value: 'CUSTOM'} + ] + }, + { + type: 'STRING', + name: 'bot-status', + required: true, + description: localize('betterstatus', 'bot-status-description'), + choices: [ + {name: 'Online', value: 'online'}, + {name: 'Idle', value: 'idle'}, + {name: 'Do Not Disturb', value: 'dnd'} + ] + }, + { + type: 'STRING', + name: 'streaming-link', + required: false, + description: localize('betterstatus', 'streaming-link-description') + } + ] +}; \ No newline at end of file diff --git a/modules/betterstatus/config.json b/modules/betterstatus/config.json index 95e9da39..4cfeb18d 100644 --- a/modules/betterstatus/config.json +++ b/modules/betterstatus/config.json @@ -1,100 +1,62 @@ { - "description": {}, - "humanName": { - "en": "Configuration", - "de": "Konfiguration" - }, + "description": "Configure the bot status, activity type and interval settings here", + "humanName": "Configuration", "filename": "config.json", "content": [ + { + "name": "enableStatusCommand", + "humanName": "Enable /status command?", + "default": false, + "description": "If enabled, administrators can change the bot status using the /status slash command", + "type": "boolean" + }, { "name": "enableInterval", - "humanName": { - "en": "Enable interval?", - "de": "Interval aktivieren?" - }, - "default": { - "en": false - }, - "description": { - "en": "If enabled the bot will change its status every x seconds", - "de": "Wenn aktiviert wird sich der Status des Bots alle x Sekunden ändern" - }, + "humanName": "Enable interval?", + "default": false, + "description": "If enabled the bot will change its status every x seconds", "type": "boolean" }, { "name": "intervalStatuses", "dependsOn": "enableInterval", - "humanName": { - "en": "Interval-Statuses", - "de": "Interval-Status" - }, - "default": { - "en": [] - }, - "description": { - "en": "Statuses from which the bot should randomly choose one", - "de": "Die Status von denen der Bot einen zufällig wählen soll" - }, + "humanName": "Interval-Statuses", + "default": [], + "description": "Statuses from which the bot should randomly choose one", "type": "array", "content": "string", "params": [ { "name": "onlineMemberCount", - "description": { - "en": "Count of online members on your guild (will not work if presence intent not enabled)", - "de": "Anzahl der Online-Mitglieder auf deinem Server" - } + "description": "Count of online members on your guild (will not work if presence intent not enabled)" }, { "name": "memberCount", - "description": { - "en": "Count of members on your guild", - "de": "Anzahl der Mitglieder auf deinem Server" - } + "description": "Count of members on your guild" }, { "name": "randomMemberTag", - "description": { - "en": "Tag of one random member on your guild", - "de": "Erwähnung eines zufälligen Nutzern auf deinem Server" - } + "description": "Tag of one random member on your guild" }, { "name": "randomOnlineMemberTag", - "description": { - "en": "Tag of one random member who is online on your guild", - "de": "Erwähnung eines zufälligen online Nutzern auf deinem Server" - } + "description": "Tag of one random member who is online on your guild" }, { "name": "channelCount", - "description": { - "en": "Count of channels on your guild", - "de": "Anzahl Channel auf deinem Server" - } + "description": "Count of channels on your guild" }, { "name": "roleCount", - "description": { - "en": "Count of roles on your guild", - "de": "Anzahl Rollen auf deinem Server" - } + "description": "Count of roles on your guild" } ] }, { "name": "activityType", - "humanName": { - "en": "Activity-Type", - "de": "Aktivität-Typ" - }, - "default": { - "en": "PLAYING" - }, - "description": { - "en": "Type of the user activity", - "de": "Type der Aktivität deines Bots" - }, + "humanName": "Activity-Type", + "default": "PLAYING", + "description": "Type of the user activity", "type": "select", "content": [ "CUSTOM", @@ -107,17 +69,9 @@ }, { "name": "botStatus", - "humanName": { - "en": "Bot-Status", - "de": "Bot-Status" - }, - "default": { - "en": "online" - }, - "description": { - "en": "Status of your bot", - "de": "Status deines Bots" - }, + "humanName": "Bot-Status", + "default": "online", + "description": "Status of your bot", "type": "select", "content": [ "idle", @@ -127,88 +81,47 @@ }, { "name": "interval", - "humanName": { - "en": "Status-Interval", - "de": "Statusänderung-Interval" - }, - "default": { - "en": 15 - }, - "description": { - "en": "The interval in seconds (at least 10 seconds)", - "de": "Das Intervall der Statusänderungen in Sekunden (mindestens 10 Sekunden)" - }, + "humanName": "Status-Interval", + "default": 15, + "description": "The interval in seconds (at least 10 seconds)", "minValue": 10, "type": "integer" }, { "name": "changeOnUserJoin", - "humanName": { - "en": "Change status on user join?", - "de": "Beim Beitreten Status ändern?" - }, - "default": { - "en": false - }, - "description": { - "en": "If the status should be changed if someone joins your guild", - "de": "Wenn aktiviert wird sich der Status des Bots ändern, wenn jemand deinem Server beitritt" - }, + "humanName": "Change status on user join?", + "default": false, + "description": "If the status should be changed if someone joins your guild", "type": "boolean" }, { "name": "userJoinStatus", "dependsOn": "changeOnUserJoin", - "humanName": { - "en": "User-Join-Status", - "de": "Nutzer-Join-Status" - }, - "default": { - "en": "Welcome %tag%!" - }, - "description": { - "en": "Status that will be set if a user joins", - "de": "Dieser Status wird gesetzt, wenn jemand deinem Server beitritt" - }, + "humanName": "User-Join-Status", + "default": "Welcome %tag%!", + "description": "Status that will be set if a user joins", "type": "string", "params": [ { "name": "tag", - "description": { - "en": "Tag of the new user", - "de": "Tag des Nutzers" - } + "description": "Tag of the new user" }, { "name": "username", - "description": { - "en": "Username of the new user", - "de": "Nutzername des Nutzers" - } + "description": "Username of the new user" }, { "name": "memberCount", - "description": { - "en": "New member count of your guild", - "de": "Anzahl der Mitglieder auf deinem Server" - } + "description": "New member count of your guild" } ] }, { "name": "streamingLink", "type": "string", - "humanName": { - "en": "Streaming Link", - "de": "Stream-Link" - }, - "default": { - "en": "" - }, - "description": { - "de": "Wird angezeigt, wenn der Aktivität-Typ auf streaming ist und der Link von Discord unterstützt wird", - "en": "Will be shown, if the activity-typ is streaming and your link is supported by Discord" - } + "humanName": "Streaming Link", + "default": "", + "description": "Will be shown, if the activity-typ is streaming and your link is supported by Discord" } ] } \ No newline at end of file diff --git a/modules/betterstatus/events/botReady.js b/modules/betterstatus/events/botReady.js index 2dab5e48..5773e573 100644 --- a/modules/betterstatus/events/botReady.js +++ b/modules/betterstatus/events/botReady.js @@ -24,7 +24,7 @@ module.exports.run = async function (client) { type: activityTypes[moduleConf['activityType']], url: (moduleConf['streamingLink'] && moduleConf.activityType === 'STREAMING') ? moduleConf['streamingLink'] : null }); - }, moduleConf.interval < 5 ? 5000 : moduleConf.interval * 1000); // At least 5 seconds to prevent rate limiting + }, Math.min(moduleConf.interval < 5 ? 5000 : moduleConf.interval * 1000, 0x7FFFFFFF)); // At least 5 seconds to prevent rate limiting client.intervals.push(interval); } diff --git a/modules/betterstatus/module.json b/modules/betterstatus/module.json index 5fe9f6fa..dd90089e 100644 --- a/modules/betterstatus/module.json +++ b/modules/betterstatus/module.json @@ -5,7 +5,9 @@ "link": "https://github.com/SCDerox", "scnxOrgID": "1" }, + "fa-icon": "far fa-user-circle", "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/betterstatus", + "commands-dir": "/commands", "events-dir": "/events", "config-example-files": [ "config.json" @@ -13,11 +15,6 @@ "tags": [ "bot" ], - "humanReadableName": { - "en": "Betterstatus" - }, - "description": { - "en": "Give you more features to make your status even better - change it when someone joins, change it every x seconds and more!", - "de": "Mache den Status deines Bots noch besser - Nutze Variablen, Intervalle und vieles mehr!" - } -} \ No newline at end of file + "humanReadableName": "Betterstatus", + "description": "Give you more features to make your status even better - change it when someone joins, change it every x seconds and more!" +} diff --git a/modules/channel-stats/channels.json b/modules/channel-stats/channels.json index 89f67b7b..6935dd95 100644 --- a/modules/channel-stats/channels.json +++ b/modules/channel-stats/channels.json @@ -1,187 +1,102 @@ { - "description": {}, - "humanName": { - "en": "Configuration", - "de": "Konfiguration" - }, + "description": "Configure voice channels that display live server statistics", + "humanName": "Configuration", "configElementName": { - "de": { - "one": "Statistik-Kanal", - "more": "Statistik-Kanäle" - }, - "en": { - "one": "Statistics-Channel", - "more": "Statistics-Channels" - } + "one": "Statistics-Channel", + "more": "Statistics-Channels" }, "filename": "channels.json", "configElements": true, "content": [ { "name": "channelID", - "humanName": { - "de": "Kanal", - "en": "Channel" - }, - "default": { - "en": "" - }, - "description": { - "en": "ID of the voice channel", - "de": "ID des Sprachkanals" - }, + "humanName": "Channel", + "default": "", + "description": "ID of the voice channel", "type": "channelID" }, { "name": "channelName", - "humanName": { - "de": "Kanalname", - "en": "Channel-Name" - }, - "default": { - "en": "" - }, - "description": { - "en": "Name of Channel", - "de": "Name des Kanals" - }, + "humanName": "Channel-Name", + "default": "", + "description": "Name of Channel", "type": "string", "params": [ { "name": "userCount", - "description": { - "en": "Total count of users on your server", - "de": "Anzahl an Nutzern auf dem Server" - } + "description": "Total count of users on your server" }, { "name": "memberCount", - "description": { - "en": "Total count of members (not bots) on your server", - "de": "Anzahl an Mitgliedern (keine Bots) auf dem Server" - } + "description": "Total count of members (not bots) on your server" }, { "name": "onlineUserCount", - "description": { - "en": "Total count of online (dnd or online status) users on your server", - "de": "Anzahl an Mitgliedern (keine Bots) auf dem Server, welche Online sind (Bitte nicht stören oder Online Status)" - } + "description": "Total count of online (dnd or online status) users on your server" }, { "name": "channelCount", - "description": { - "en": "Total count of channels on your server", - "de": "Anzahl der Kanäle auf dem Server" - } + "description": "Total count of channels on your server" }, { "name": "roleCount", - "description": { - "en": "Total count of roles on your server", - "de": "Anzahl der Rollen auf dem Server" - } + "description": "Total count of roles on your server" }, { "name": "botCount", - "description": { - "en": "Count of Bots on your server", - "de": "Anzahl der Bots auf dem Server" - } + "description": "Count of Bots on your server" }, { "name": "dndCount", - "description": { - "en": "Count of members (not bots) with DND as status", - "de": "Anzahl der Mitglieder (keine Bots) mit Bitte nicht stören als Status" - } + "description": "Count of members (not bots) with DND as status" }, { "name": "onlineMemberCount", - "description": { - "en": "Count of members (not bots) with online (and only online) as status", - "de": "Anzahl der Mitglieder (keine Bots) mit Online (und NUR Online) als Status" - } + "description": "Count of members (not bots) with online (and only online) as status" }, { "name": "awayCount", - "description": { - "en": "Count of members (not bots) with away status", - "de": "Anzahl der Mitglieder (keine Bots) mit \"Abwesend\" Status" - } + "description": "Count of members (not bots) with away status" }, { "name": "offlineCount", - "description": { - "en": "Count of members (not bots) with offline status", - "de": "Anzahl der Mitglieder (keine Bots) mit \"Offline\" status" - } + "description": "Count of members (not bots) with offline status" }, { "name": "guildBoosts", - "description": { - "en": "Show how often this guild was boosted", - "de": "Zeigt, wie oft der Server geboostet wurde" - } + "description": "Show how often this guild was boosted" }, { "name": "boostLevel", - "description": { - "en": "Shows the current boost-level of this guild", - "de": "Zeigt das aktuelle Boost-Level des Servers" - } + "description": "Shows the current boost-level of this guild" }, { "name": "boosterCount", - "description": { - "en": "Count of boosters on this guild", - "de": "Anzahl an Boostern auf dem Server" - } + "description": "Count of boosters on this guild" }, { "name": "emojiCount", - "description": { - "en": "Count of emojis on this guild", - "de": "Anzahl an Emojis auf dem Server" - } + "description": "Count of emojis on this guild" }, { "name": "currentTime", - "description": { - "en": "Current time and date", - "de": "Aktuelles Datum und Uhrzeit" - } + "description": "Current time and date" }, { "name": "userWithRoleCount-", - "description": { - "en": "Count of members with a specific role (replace \"\" with an actual role-id)", - "de": "Anzahl von Nutzern mit einer bestimmen Rolle (bitte \"\" mit einer echten Rollen-ID ersetzen)" - } + "description": "Count of members with a specific role (replace \"\" with an actual role-id)" }, { "name": "onlineUserWithRoleCount-", - "description": { - "en": "Count of members with a specific role who are online (replace \"\" with an actual role-id)", - "de": "Anzahl von Nutzern mit einer bestimmen Rolle, die online sind (bitte \"\" mit einer echten Rollen-ID ersetzen)" - } + "description": "Count of members with a specific role who are online (replace \"\" with an actual role-id)" } ] }, { "name": "updateInterval", - "humanName": { - "de": "Aktualisierungsintervall", - "en": "Update-Interval" - }, - "default": { - "en": 15, - "de": 15 - }, - "description": { - "en": "You can set an interval here in which the bot should update the channels. Must be higher than seven; in minutes.", - "de": "Du kannst hier ein Intervall einstellen, in welchem der Bot die Kanäle aktualisieren soll. Muss höher als sieben sein; in Minuten." - }, + "humanName": "Update-Interval", + "default": 15, + "description": "You can set an interval here in which the bot should update the channels. Must be higher than seven; in minutes.", "type": "integer" } ] diff --git a/modules/channel-stats/events/botReady.js b/modules/channel-stats/events/botReady.js index 4d5f8532..53814d74 100644 --- a/modules/channel-stats/events/botReady.js +++ b/modules/channel-stats/events/botReady.js @@ -14,11 +14,20 @@ module.exports.run = async (client) => { t: dcChannel.type })); const res = await channelNameReplacer(client, dcChannel, channel.channelName); - if (res !== dcChannel.name) dcChannel.setName(res, '[channel-stats] ' + localize('channel-stats', 'audit-log-reason-startup')); + if (res !== dcChannel.name) await dcChannel.setName(res, '[channel-stats] ' + localize('channel-stats', 'audit-log-reason-startup')).catch(() => { + }); + let updating = false; client.intervals.push(setInterval(async () => { - const repName = await channelNameReplacer(client, dcChannel, channel.channelName); - if (repName !== dcChannel.name) dcChannel.setName(repName, '[channel-stats] ' + localize('channel-stats', 'audit-log-reason-interval')); - }, (channel.updateInterval || 5) < 5 ? 5 * 60000 : (channel.updateInterval || 5) * 60000)); + if (updating) return; + updating = true; + try { + const repName = await channelNameReplacer(client, dcChannel, channel.channelName); + if (repName !== dcChannel.name) await dcChannel.setName(repName, '[channel-stats] ' + localize('channel-stats', 'audit-log-reason-interval')).catch(() => { + }); + } finally { + updating = false; + } + }, Math.min(((channel.updateInterval || 5) < 5 ? 5 : (channel.updateInterval || 5)) * 60000, 0x7FFFFFFF))); } }; @@ -71,4 +80,4 @@ async function channelNameReplacer(client, channel, input) { .split('%boosterCount%').join(members.filter(m => !!m.premiumSinceTimestamp).size) .split('%emojiCount%').join(channel.guild.emojis.cache.size) .split('%currentTime%').join(formatDate(new Date(), true)).trim(); -} \ No newline at end of file +} diff --git a/modules/channel-stats/module.json b/modules/channel-stats/module.json index e2a3bf61..13f9697c 100644 --- a/modules/channel-stats/module.json +++ b/modules/channel-stats/module.json @@ -1,5 +1,6 @@ { "name": "channel-stats", + "fa-icon": "fas fa-stream", "author": { "scnxOrgID": "1", "name": "SCDerox (SC Network Team)", @@ -13,12 +14,6 @@ "administration" ], "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/channel-stats", - "humanReadableName": { - "en": "Channel-Stats", - "de": "Channel-Statistiken" - }, - "description": { - "en": "Create channels containing stats about your server - updated automatically.", - "de": "Modul, um Channel mit automatisch aktualisierenden Namen mit Statistiken zu erstellen" - } -} \ No newline at end of file + "humanReadableName": "Channel-Stats", + "description": "Create channels containing stats about your server - updated automatically." +} diff --git a/modules/color-me/commands/color-me.js b/modules/color-me/commands/color-me.js index 168c7588..41b45cf1 100644 --- a/modules/color-me/commands/color-me.js +++ b/modules/color-me/commands/color-me.js @@ -1,12 +1,6 @@ const {localize} = require('../../../src/functions/localize'); const {client} = require('../../../main'); const {embedType, dateToDiscordTimestamp} = require('../../../src/functions/helpers'); -let roleColor; -let roleIcon; -let pos; -let cooldownModel; -let cancel = false; -let iconW = true; module.exports.beforeSubcommand = async function (interaction) { await interaction.deferReply({ephemeral: true}); @@ -14,6 +8,8 @@ module.exports.beforeSubcommand = async function (interaction) { module.exports.subcommands = { 'manage': async function (interaction) { + let roleIcon; + let iconW = true; if (interaction.options.getAttachment('icon') !== null) { if (client.guild.features.includes('ROLE_ICONS')) { roleIcon = interaction.options.getAttachment('icon').url; @@ -26,124 +22,129 @@ module.exports.subcommands = { const moduleStrings = interaction.client.configurations['color-me']['strings']; const moduleModel = interaction.client.models['color-me']['Role']; - if (moduleConf.rolePosition) { - pos = interaction.guild.roles.resolve(moduleConf.rolePosition).position; - } else { - pos = 0; + const pos = moduleConf.rolePosition + ? interaction.guild.roles.resolve(moduleConf.rolePosition).position + : 0; + + const { + allowed, + cooldownModel + } = await cooldown(moduleConf['updateCooldown'] * 3600000, interaction.user.id); + if (!allowed) { + await interaction.editReply(embedType(moduleStrings['cooldown'], { + '%cooldown%': dateToDiscordTimestamp(new Date(cooldownModel.timestamp.getTime() + moduleConf['updateCooldown'] * 3600000), 'R') + })); + return; } - if (await cooldown(moduleConf['updateCooldown'] * 3600000, interaction.user.id)) { - let role = await moduleModel.findOne({ - attributes: ['roleID'], - raw: true, - where: { - userID: interaction.user.id - } - }); - if (role) { - role = role.roleID; - await color(interaction, moduleStrings); - if (cancel) return; - if (interaction.guild.roles.cache.find(r => r.id === role)) { - role = interaction.guild.roles.resolve(role); - role.edit( - { - name: interaction.options.getString('name'), - color: roleColor, - icon: roleIcon, - reason: localize('color-me', 'edit-log-reason', { - user: interaction.user.username - }) - } - ); - if (iconW) { - await interaction.editReply(embedType(moduleStrings['updated'], {})); - } else { - await interaction.editReply(embedType(moduleStrings['updatedNoIcon'], {})); + + let role = await moduleModel.findOne({ + attributes: ['roleID'], + raw: true, + where: { + userID: interaction.user.id + } + }); + if (role) { + role = role.roleID; + const { + roleColor, + cancel + } = await color(interaction, moduleStrings); + if (cancel) return; + if (interaction.guild.roles.cache.find(r => r.id === role)) { + role = interaction.guild.roles.resolve(role); + role.edit( + { + name: interaction.options.getString('name'), + color: roleColor, + icon: roleIcon, + reason: localize('color-me', 'edit-log-reason', { + user: interaction.user.username + }) } + ); + if (iconW) { + await interaction.editReply(embedType(moduleStrings['updated'], {})); } else { - if (interaction.guild.roles.cache.size < 250) { - role = await interaction.guild.roles.create( - { - name: interaction.options.getString('name'), - color: roleColor, - icon: roleIcon, - hoist: moduleConf.listRoles, - permissions: '', - position: pos, - mentionable: false, - reason: localize('color-me', 'create-log-reason', { - user: interaction.user.username - }) - } - ); - } else { - await interaction.editReply(embedType(moduleStrings['roleLimit'], {})); - } - await moduleModel.update({ - userID: interaction.user.id, - roleID: role.id, - name: role.name, - color: role.hexColor, - timestamp: new Date() - }, { - where: { - userID: interaction.user.id - } - }); - if (!interaction.member.roles.cache.has(role)) { - await interaction.member.roles.add(role); - } - if (iconW) { - await interaction.editReply(embedType(moduleStrings['updated'], {})); - } else { - await interaction.editReply(embedType(moduleStrings['updatedNoIcon'], {})); - } + await interaction.editReply(embedType(moduleStrings['updatedNoIcon'], {})); } } else { - await color(interaction, moduleStrings); - if (cancel) return; - try { - role = await interaction.guild.roles.create( - { - name: interaction.options.getString('name'), - color: roleColor, - icon: roleIcon, - hoist: moduleConf.listRoles, - permissions: '', - position: pos, - mentionable: false, - reason: localize('color-me', 'create-log-reason', { - user: interaction.user.username - }) - } - ); - await moduleModel.create({ - userID: interaction.user.id, - roleID: role.id, - name: role.name, - color: role.hexColor, - timestamp: new Date() - }); - await interaction.member.roles.add(role); - if (iconW) { - await interaction.editReply(embedType(moduleStrings['created'], {})); - } else { - await interaction.editReply(embedType(moduleStrings['createdNoIcon'], {})); - } - } catch (e) { + if (interaction.guild.roles.cache.size >= 250) { await interaction.editReply(embedType(moduleStrings['roleLimit'], {})); + return; + } + role = await interaction.guild.roles.create( + { + name: interaction.options.getString('name'), + color: roleColor, + icon: roleIcon, + hoist: moduleConf.listRoles, + permissions: '', + position: pos, + mentionable: false, + reason: localize('color-me', 'create-log-reason', { + user: interaction.user.username + }) + } + ); + await moduleModel.update({ + userID: interaction.user.id, + roleID: role.id, + name: role.name, + color: role.hexColor, + timestamp: new Date() + }, { + where: { + userID: interaction.user.id + } + }); + if (!interaction.member.roles.cache.has(role)) { + await interaction.member.roles.add(role); + } + if (iconW) { + await interaction.editReply(embedType(moduleStrings['updated'], {})); + } else { + await interaction.editReply(embedType(moduleStrings['updatedNoIcon'], {})); } - } } else { - cooldownModel = await moduleModel.findOne({ - where: { - userId: interaction.member.id + const { + roleColor, + cancel + } = await color(interaction, moduleStrings); + if (cancel) return; + try { + role = await interaction.guild.roles.create( + { + name: interaction.options.getString('name'), + color: roleColor, + icon: roleIcon, + hoist: moduleConf.listRoles, + permissions: '', + position: pos, + mentionable: false, + reason: localize('color-me', 'create-log-reason', { + user: interaction.user.username + }) + } + ); + await moduleModel.create({ + userID: interaction.user.id, + roleID: role.id, + name: role.name, + color: role.hexColor, + timestamp: new Date() + }); + await interaction.member.roles.add(role); + if (iconW) { + await interaction.editReply(embedType(moduleStrings['created'], {})); + } else { + await interaction.editReply(embedType(moduleStrings['createdNoIcon'], {})); } - }); - await interaction.editReply((embedType(moduleStrings['cooldown'], { - '%cooldown%': dateToDiscordTimestamp(new Date(cooldownModel.timestamp.getTime() + moduleConf['updateCooldown'] * 3600000), 'R') - }))); + } catch (e) { + await interaction.editReply(embedType(moduleStrings['roleLimit'], {})); + } + } }, @@ -219,40 +220,54 @@ module.exports.config = { /** * Gets a color from the String of a command option + * @returns {Promise<{roleColor: string|number, cancel: boolean}>} */ async function color(interaction, moduleStrings) { if (interaction.options.getString('color')) { - roleColor = interaction.options.getString('color'); + let roleColor = interaction.options.getString('color'); if (!roleColor.startsWith('#')) { roleColor = '#' + roleColor; } if (!(/^#[0-9A-F]{6}$/i).test(roleColor)) { await interaction.editReply(embedType(moduleStrings['invalidColor'], {})); - cancel = true; + return { + roleColor, + cancel: true + }; } - } else { - roleColor = 0xF1C40F; + return { + roleColor, + cancel: false + }; } + return { + roleColor: 0xF1C40F, + cancel: false + }; } /** ** Function to handle the cooldown stuff * @private * @param {number} duration The duration of the cooldown (in ms) - * @param {userId} userId Id of the User - * @returns {Promise} + * @param {string} userId Id of the User + * @returns {Promise<{allowed: boolean, cooldownModel: object|null}>} */ async function cooldown(duration, userId) { const model = client.models['color-me']['Role']; - cooldownModel = await model.findOne({ + const cooldownModel = await model.findOne({ where: { - userId: userId + userID: userId } }); if (cooldownModel && cooldownModel.timestamp) { - // check cooldown duration - return cooldownModel.timestamp.getTime() + duration <= Date.now(); - } else { - return true; + return { + allowed: cooldownModel.timestamp.getTime() + duration <= Date.now(), + cooldownModel + }; } + return { + allowed: true, + cooldownModel: null + }; } \ No newline at end of file diff --git a/modules/color-me/configs/config.json b/modules/color-me/configs/config.json index c5668c2a..155bd206 100644 --- a/modules/color-me/configs/config.json +++ b/modules/color-me/configs/config.json @@ -1,87 +1,41 @@ { - "description": { - "en": "Configure the function of the module here", - "de": "Stelle hier die Funktionen des Modules ein" - }, - "humanName": { - "en": "Configuration", - "de": "Konfiguration" - }, + "description": "Configure the function of the module here", + "humanName": "Configuration", "filename": "config.json", "content": [ { "name": "recreateRole", - "humanName": { - "en": "Recreate roles", - "de": "Rollen wiederherstellen" - }, - "default": { - "en": true - }, - "description": { - "en": "Should the role be created again if the user boosts again?", - "de": "Soll die Rolle wiederhergestellt werden, wenn ein Nutzer erneut boostet?" - }, + "humanName": "Recreate roles", + "default": true, + "description": "Should the role be created again if the user boosts again?", "type": "boolean" }, { "name": "listRoles", - "humanName": { - "en": "Separate roles in member-list", - "de": "Rollen in Mitgliederliste separieren" - }, - "default": { - "en": false - }, - "description": { - "en": "Should the role be listed separately in the member-list?", - "de": "Soll die Rolle in der Mitgliederliste separat gelistet werden?" - }, + "humanName": "Separate roles in member-list", + "default": false, + "description": "Should the role be listed separately in the member-list?", "type": "boolean" }, { "name": "removeOnUnboost", - "humanName": { - "en": "Remove role on unboost", - "de": "Rolle bei Unboost entfernen" - }, - "default": { - "en": false - }, - "description": { - "en": "Should the role be deleted automatically, if the user stops boosting your server? (disable, if also non-boosters should be able to use this command)", - "de": "Soll die Rolle automatisch gelöscht werden, wenn der Nutzer den Server nicht mehr boostet? (deaktivieren, wenn auch nicht-Booster den Befehl verwenden können sollen)" - }, + "humanName": "Remove role on unboost", + "default": false, + "description": "Should the role be deleted automatically, if the user stops boosting your server? (disable, if also non-boosters should be able to use this command)", "type": "boolean" }, { "name": "updateCooldown", - "humanName": { - "en": "Role update cooldown", - "de": "Rollenbearbeitungscooldown" - }, - "default": { - "en": 24 - }, - "description": { - "en": "The amount of time a user needs to wait util they can edit their role again (in hours)", - "de": "Die Dauer, die Benutzer*innen warten müssen, bevor ihre Rolle wieder bearbeitet werden kann (in Stunden)" - }, + "humanName": "Role update cooldown", + "default": 24, + "description": "The amount of time a user needs to wait util they can edit their role again (in hours)", "type": "integer" }, { "name": "rolePosition", - "humanName": { - "en": "Role position", - "de": "Rollenposition" - }, - "default": { - "en": "" - }, - "description": { - "en": "The role, beneath which the custom-roles should be created", - "de": "Die Rolle unter welcher die Custom-Rollen erstellt werden sollen" - }, + "humanName": "Role position", + "default": "", + "description": "The role, beneath which the custom-roles should be created", "type": "roleID" } ] diff --git a/modules/color-me/configs/strings.json b/modules/color-me/configs/strings.json index cbc205b2..b43014c8 100644 --- a/modules/color-me/configs/strings.json +++ b/modules/color-me/configs/strings.json @@ -1,158 +1,77 @@ { - "description": { - "en": "Edit the messages and strings of the module here", - "de": "Stelle hier die Nachrichten des Modules ein" - }, - "humanName": { - "en": "Messages", - "de": "Nachrichten" - }, + "description": "Edit the messages and strings of the module here", + "humanName": "Messages", "filename": "strings.json", "content": [ { "name": "created", - "humanName": { - "en": "Role created", - "de": "Rolle erstellt" - }, - "default": { - "en": "Your role was created successfully.", - "de": "Deine Rolle wurde erfolgreich erstellt." - }, - "description": { - "en": "This messages gets send when a booster sucessfully created their custom role", - "de": "Diese Nachricht wird verschickt, wenn ein Booster seine/ihre Custom-Rolle erstellt hat" - }, + "humanName": "Role created", + "default": "Your role was created successfully.", + "description": "This messages gets send when a booster sucessfully created their custom role", "type": "string", "allowEmbed": true }, { "name": "createdNoIcon", - "humanName": { - "en": "Role created without icon", - "de": "Rolle ohne Icon erstellt" - }, - "default": { - "en": "Your role was created successfully, but your role icon was not used, as this requires the guild to be boost level 2 or higher.", - "de": "Deine Rolle wurde erfolgreich erstellt, aber dein Rollen-Icon wurde nicht verwendet, da hierfür der Server mindestens Boostlevel 2 besitzen muss." - }, - "description": { - "en": "This message gets send when a booster successfully created their custom role, but the guild has not enough boosts to use role icons", - "de": "Diese Nachricht wird verschickt, wenn ein Booster seine/ihre Custom-Rolle erstellt hat, der Server aber nicht ausreichend Boosts hat, um Rollenicons zu verwenden" - }, + "humanName": "Role created without icon", + "default": "Your role was created successfully, but your role icon was not used, as this requires the guild to be boost level 2 or higher.", + "description": "This message gets send when a booster successfully created their custom role, but the guild has not enough boosts to use role icons", "type": "string", "allowEmbed": true }, { "name": "updated", - "humanName": { - "en": "Role updated", - "de": "Rolle aktualisiert" - }, - "default": { - "en": "Your role was updated successfully.", - "de": "Deine Rolle wurde erfolgreich aktualisiert." - }, - "description": { - "en": "This messages gets send when a booster sucessfully updates their custom role", - "de": "Diese Nachricht wird verschickt, wenn ein Booster seine/ihre Custom-Rolle aktualisiert hat" - }, + "humanName": "Role updated", + "default": "Your role was updated successfully.", + "description": "This messages gets send when a booster sucessfully updates their custom role", "type": "string", "allowEmbed": true }, { "name": "updatedNoIcon", - "humanName": { - "en": "Role updated without icon", - "de": "Rolle ohne Icon aktualisiert" - }, - "default": { - "en": "Your role was updated successfully, but your role icon was not used, as this requires the guild to be boost level 2 or higher.", - "de": "Deine Rolle wurde erfolgreich aktualisiert aber dein Rollen-Icon wurde nicht verwendet, da hierfür der Server mindestens Boostlevel 2 besitzen muss." - }, - "description": { - "en": "This messages gets send when a booster sucessfully updates their custom role, but the guild has not enough boosts to use role icons", - "de": "Diese Nachricht wird verschickt, wenn ein Booster seine/ihre Custom-Rolle aktualisiert hat, der Server aber nicht genug boosts hat um Rollenicons zu verwenden" - }, + "humanName": "Role updated without icon", + "default": "Your role was updated successfully, but your role icon was not used, as this requires the guild to be boost level 2 or higher.", + "description": "This messages gets send when a booster sucessfully updates their custom role, but the guild has not enough boosts to use role icons", "type": "string", "allowEmbed": true }, { "name": "removed", - "humanName": { - "en": "Role removed", - "de": "Rolle entfernt" - }, - "default": { - "en": "Your role was removed successfully.", - "de": "Deine Rolle wurde erfolgreich entfernt." - }, - "description": { - "en": "This messages gets send when a booster deleted their custom role", - "de": "Diese Nachricht wird verschickt, wenn ein Booster seine/ihre Custom-Rolle entfernt hat" - }, + "humanName": "Role removed", + "default": "Your role was removed successfully.", + "description": "This messages gets send when a booster deleted their custom role", "type": "string", "allowEmbed": true }, { "name": "roleLimit", - "humanName": { - "en": "Role-limit reached", - "de": "Rollenlimit erreicht" - }, - "default": { - "en": "Your role couldn't be created. This could be, because this server has reached the maximum of roles set by Discord. Ask the staff to delete an unnecessary role to make space for your role or try again later.", - "de": "Deine Rolle konnte nicht erstellt werden. Das kann daran liegen, dass dieser Server die von Discord vorgegebene maximale Rollenzahl erreicht hat. Frag das Team nach der Löschung einer überflüssigen Rolle um Platz zu machen, oder versuche es später erneut." - }, - "description": { - "en": "This messages gets send when a booster-role couldn't be created", - "de": "Diese Nachricht wird verschickt, wenn eine Booster-Rolle nicht erstellt werden konnte" - }, + "humanName": "Role-limit reached", + "default": "Your role couldn't be created. This could be, because this server has reached the maximum of roles set by Discord. Ask the staff to delete an unnecessary role to make space for your role or try again later.", + "description": "This messages gets send when a booster-role couldn't be created", "type": "string", "allowEmbed": true }, { "name": "cooldown", - "humanName": { - "en": "Cooldown", - "de": "Cooldown" - }, - "default": { - "en": "Your role couldn't be edited, since you have to wait until %cooldown% for the cooldown to expire.", - "de": "Deine Rolle konnte nicht bearbeitet werden, da du bis %cooldown% warten musst, dass der Cooldown abläuft." - }, - "description": { - "en": "This messages gets send when a booster-role couldn't be edited, since the user is on cooldown", - "de": "Diese Nachricht wird verschickt, wenn eine Booster-Rolle nicht bearbeitet werden konnte, da der Nutzer den Cooldown abwarten muss" - }, + "humanName": "Cooldown", + "default": "Your role couldn't be edited, since you have to wait until %cooldown% for the cooldown to expire.", + "description": "This messages gets send when a booster-role couldn't be edited, since the user is on cooldown", "type": "string", "allowEmbed": true, "params": [ { "name": "cooldown", - "description": { - "en": "Timestamp the cooldown expires at", - "de": "Zeitpunkt, an welchem der Cooldown abläuft" - } + "description": "Timestamp the cooldown expires at" } ] }, { "name": "invalidColor", - "humanName": { - "en": "Invalid Color", - "de": "Falsche Farbe" - }, - "default": { - "en": "The color you provided is not a valid HEX-Code.", - "de": "Die angegebene Farbe ist kein gültiger HEX-Code." - }, - "description": { - "en": "This messages gets send when the user provides a wrong color code", - "de": "Diese Nachricht wird verschickt, wenn der Nutzer einen falschen Farbcode angibt" - }, + "humanName": "Invalid Color", + "default": "The color you provided is not a valid HEX-Code.", + "description": "This messages gets send when the user provides a wrong color code", "type": "string", "allowEmbed": true } ] -} +} \ No newline at end of file diff --git a/modules/color-me/module.json b/modules/color-me/module.json index e64fa361..95eec666 100644 --- a/modules/color-me/module.json +++ b/modules/color-me/module.json @@ -1,8 +1,6 @@ { "name": "color-me", - "humanReadableName": { - "en": "Color me" - }, + "humanReadableName": "Color me", "author": { "name": "hfgd", "link": "https://github.com/hfgd123", @@ -11,6 +9,7 @@ "openSourceURL": "https://github.com/hfgd123/CustomDCBot/tree/main/modules/color-me", "commands-dir": "/commands", "events-dir": "/events", + "fa-icon": "fas fa-palette", "models-dir": "/models", "config-example-files": [ "configs/config.json", @@ -19,8 +18,5 @@ "tags": [ "community" ], - "description": { - "en": "Simple module to reward users who have boosted your server with a custom role!", - "de": "Einfaches Modul, um Nutzer mit einer eigenen Rolle zu belohnen, die deinen Server boosten!" - } -} \ No newline at end of file + "description": "Simple module to reward users who have boosted your server with a custom role!" +} diff --git a/modules/connect-four/commands/connect-four.js b/modules/connect-four/commands/connect-four.js index 79fdc43c..b302fbe1 100644 --- a/modules/connect-four/commands/connect-four.js +++ b/modules/connect-four/commands/connect-four.js @@ -229,7 +229,8 @@ module.exports.run = async function (interaction) { const collector = msg.createMessageComponentCollector({ componentType: ComponentType.Button, - filter: i => i.user.id === interaction.user.id || i.user.id === member.id + filter: i => i.user.id === interaction.user.id || i.user.id === member.id, + time: 600000 }); collector.on('collect', i => { if ((color === 'blue' && i.user.id !== interaction.user.id) || (color === 'red' && i.user.id !== member.id)) return i.reply({ @@ -264,6 +265,10 @@ module.exports.run = async function (interaction) { } } }); + collector.on('end', (_, reason) => { + if (reason === 'time') msg.edit({components: []}).catch(() => { + }); + }); }; diff --git a/modules/connect-four/module.json b/modules/connect-four/module.json index cbfbf594..80ae47f4 100644 --- a/modules/connect-four/module.json +++ b/modules/connect-four/module.json @@ -1,18 +1,13 @@ { "name": "connect-four", - "humanReadableName": { - "en": "Connect Four", - "de": "Vier gewinnt" - }, + "humanReadableName": "Connect Four", + "fa-icon": "fa-solid fa-table-cells", "author": { "scnxOrgID": "60", "name": "TomatoCake", "link": "https://github.com/DEVTomatoCake" }, - "description": { - "en": "Let your users play Connect Four against each other!", - "de": "Lasse Nutzer auf deinem Server Vier gewinnt gegeneinander spielen" - }, + "description": "Let your users play Connect Four against each other!", "commands-dir": "/commands", "noConfig": true, "releaseDate": "0", @@ -20,4 +15,4 @@ "fun" ], "openSourceURL": "https://github.com/DEVTomatoCake/ScootKit-CustomBot/tree/main/modules/connect-four" -} \ No newline at end of file +} diff --git a/modules/counter/config.json b/modules/counter/config.json index d70829d3..524bdd97 100644 --- a/modules/counter/config.json +++ b/modules/counter/config.json @@ -1,324 +1,173 @@ { - "description": {}, - "humanName": { - "en": "Configuration", - "de": "Konfiguration" - }, + "description": "Configure counting channels, rules and moderation settings here", + "humanName": "Configuration", "filename": "config.json", "content": [ { "name": "channels", - "humanName": { - "de": "Kanäle", - "en": "Channels" - }, - "default": { - "en": [], - "de": [] - }, - "description": { - "en": "Channels in which users can participate in the counting game", - "de": "Kanäle, in welchem Nutzer am Zählspiel teilnehmen können." - }, + "humanName": "Channels", + "default": [], + "description": "Channels in which users can participate in the counting game", "type": "array", "content": "channelID" }, { "name": "channelDescription", - "humanName": { - "de": "Kanalbeschreibung", - "en": "Channel-Description" - }, - "default": { - "en": "Next number %x%", - "de": "Nächste Zahl: %x%" - }, - "description": { - "en": "Text which should be set after someone counted (leave blank to disable)", - "de": "Text, welcher gesetzt werden soll, nachdem jemand gezählt hat (leer lassen zum deaktivieren)" - }, + "humanName": "Channel-Description", + "default": "Next number %x%", + "description": "Text which should be set after someone counted (leave blank to disable)", "type": "string", "allowNull": true, "params": [ { "name": "x", - "description": { - "en": "Next number users should count", - "de": "Nächste Zahl, welche die Nutzer zählen sollen" - } + "description": "Next number users should count" } ] }, { "name": "success-reaction", - "humanName": { - "de": "Erfolgsreaktion", - "en": "Success-Reaction" - }, - "default": { - "en": "✅", - "de": "✅" - }, - "description": { - "en": "Reaction which the bot should give when someone counts successfully", - "de": "Reaktion welche der Bot geben soll, wenn jemand erfolgreich gezählt hat" - }, + "humanName": "Success-Reaction", + "default": "✅", + "description": "Reaction which the bot should give when someone counts successfully", "type": "emoji" }, { "name": "restartOnWrongCount", - "default": { - "en": false - }, - "humanName": { - "de": "Spiel neustarten, wenn sich jemand verzählt", - "en": "Restart game, if user miscounts" - }, - "description": { - "en": "If enabled, the game will restarts if a user sends a number that is not in order", - "de": "Wenn aktiviert, wird das Spiel neustarten, wenn ein Nutzer eine Zahl sendet, die nicht in die Reihenfolge passt" - }, + "default": false, + "humanName": "Restart game, if user miscounts", + "description": "If enabled, the game will restarts if a user sends a number that is not in order", "type": "boolean" }, { "name": "restartOnWrongCountMessage", "dependsOn": "restartOnWrongCount", - "default": { - "de": "Aufgrund der Inkompetenz von %mention% muss das Spiel neugestartet werden - die nächste Zahl ist **%i%**.", - "en": "Due to the incompetence of %mention%, the game had to restart - the next number is **%i%**." - }, - "humanName": { - "en": "Message when game gets restarted", - "de": "Nachricht, wenn das Spiel neugestartet werden" - }, + "default": "Due to the incompetence of %mention%, the game had to restart - the next number is **%i%**.", + "humanName": "Message when game gets restarted", "type": "string", "allowEmbed": true, - "description": { - "en": "This message will be sent when the game gets restarted due to a miscount.", - "de": "Diese Nachricht wird gesendet, wenn das Spiel aufgrund einer Verzählung neugestartet wird." - }, + "description": "This message will be sent when the game gets restarted due to a miscount.", "params": [ { "name": "mention", - "description": { - "de": "Erwähnung des Nutzers", - "en": "Mention of the users" - } + "description": "Mention of the users" }, { "name": "i", - "description": { - "de": "Nächste Nummer", - "en": "Next number" - } + "description": "Next number" } ] }, { "name": "onlyOneMessagePerUser", - "default": { - "en": true - }, - "humanName": { - "de": "Nutzer müssen abwechselnd zählen", - "en": "Only one continuous message per user" - }, - "description": { - "en": "If enabled, users can not count more than one number continuously", - "de": "Wenn aktiviert, können Nutzer nicht mehr als eine Nummer nacheinander zählen" - }, + "default": true, + "humanName": "Only one continuous message per user", + "description": "If enabled, users can not count more than one number continuously", "type": "boolean" }, { "name": "protectAgainstDeletion", - "default": { - "en": true - }, - "humanName": { - "de": "Verhindern, dass Nutzer die letzte Zählungsnachricht löschen?", - "en": "Protect against users deleting the last counting message?" - }, - "description": { - "en": "If enabled, the bot will send a message when the last correct counting message gets deleted so that other counters can't be fooled into counting an already counted number again.", - "de": "Wenn aktiviert, wird der Bot eine Nachricht in den Kanal schicken, wenn die letzte korrekte Zählnachricht gelöscht wird - das verhindert, dass andere Nutzer nicht dazu gebracht werden können, eine korrekte Nummer erneut zu zählen." - }, + "default": true, + "humanName": "Protect against users deleting the last counting message?", + "description": "If enabled, the bot will send a message when the last correct counting message gets deleted so that other counters can't be fooled into counting an already counted number again.", "type": "boolean" }, { "name": "protectionMessage", "dependsOn": "protectAgainstDeletion", - "humanName": { - "de": "Löschschutznachricht", - "en": "Deletion protection message" - }, - "default": { - "de": "Scheint als hätte %mention% seine letzte Nachricht gelöscht - die zuletzt gezählte Zahl ist **%number%**.", - "en": "It seems like %mention% deleted their last message - the last counted number is **%number%**." - }, - "description": { - "en": "Message that gets send if a user deletes the last correct counting message.", - "de": "Nachricht, welche verschickt wird, wenn die letzte korrekte Zahlnachricht gelöscht wird." - }, + "humanName": "Deletion protection message", + "default": "It seems like %mention% deleted their last message - the last counted number is **%number%**.", + "description": "Message that gets send if a user deletes the last correct counting message.", "type": "string", "allowEmbed": true, "params": [ { "name": "mention", - "description": { - "en": "Mention of the user who's message got removed", - "de": "Erwähnung des Nutzers, dessen Nachricht gelöscht wurde" - } + "description": "Mention of the user who's message got removed" }, { "name": "number", - "description": { - "en": "Last counted number in this the channel", - "de": "Zuletzt gezählte Nummer in diesem Kanal" - } + "description": "Last counted number in this the channel" } ] }, { "name": "removeReactions", - "default": { - "en": true - }, - "humanName": { - "de": "Reaktionen nach 5 Sekunden entfernen?", - "en": "Remove reactions after 5 seconds?" - }, - "description": { - "en": "If enabled, the reactions the bot gives will be removed after 5 seconds. This will free up space in the counting channel", - "de": "Wenn aktiviert, werden die Reaktionen des Bots nach 5 Sekunden entfernt. Das lässt mehr Platz im Kanal." - }, + "default": true, + "humanName": "Remove reactions after 5 seconds?", + "description": "If enabled, the reactions the bot gives will be removed after 5 seconds. This will free up space in the counting channel", "type": "boolean" }, { "name": "wrong-input-message", - "humanName": { - "de": "Nachricht bei falscher Eingabe", - "en": "Message on wrong input" - }, - "default": { - "en": "⚠️ %err%" - }, - "description": { - "en": "Message that gets send if a user provides an invalid input", - "de": "Nachricht, welche gesendet wird, wenn ein Nutzer eine ungültige Nachricht sendet" - }, + "humanName": "Message on wrong input", + "default": "⚠️ %err%", + "description": "Message that gets send if a user provides an invalid input", "type": "string", "allowEmbed": true, "params": [ { "name": "err", - "description": { - "en": "Description of what they did wrong", - "de": "Beschreibung, was der Nutzer falsch gemacht hat" - } + "description": "Description of what they did wrong" } ] }, { "name": "strikeAmount", - "default": { - "en": 5 - }, - "humanName": { - "en": "Amount of wrong messages to trigger action", - "de": "Anzahl von falschen Nachrichten, um eine Aktion auszulösen" - }, - "description": { - "en": "This is the amount of wrong messages a user has to send to trigger action. Once this amount is reached, the bot will either, depending on your configuration, give a role or disable the SEND_MESSAGES permission for a user (set to 0 to disable)", - "de": "Dies ist die Anzahl von falschen Nachrichten, die ein Nutzer senden muss, um eine Aktion auszulösen. Sobald diese Anzahl erreicht ist, wird der Bot, je nach Konfiguration, entweder dem Nutzer eine Rolle geben oder ihm die \"Nachrichten verfassen\"-Berechtigung entfernen (auf 0 setzen zum Deaktivieren)" - }, + "default": 5, + "humanName": "Amount of wrong messages to trigger action", + "description": "This is the amount of wrong messages a user has to send to trigger action. Once this amount is reached, the bot will either, depending on your configuration, give a role or disable the SEND_MESSAGES permission for a user (set to 0 to disable)", "type": "integer" }, { "name": "giveRoleInsteadOfPermissionRemoval", - "default": { - "en": false - }, - "humanName": { - "de": "Rolle bei Sperrung vergeben, anstatt Rechte zu entfernen", - "en": "Give role on action, instead of removing permission" - }, - "description": { - "en": "If enabled, a role will be given to the user (once their reach the configured action amount of wrong messages) instead of the removal of the \"Send Messages\"-permission in the counter channel", - "de": "Wenn aktiviert, wird dem Nutzer (sobald er die benötigte Anzahl von falschen Nachrichten erreicht hat) eine Rolle gegeben, anstatt die \"Nachrichten verfassen\"-Berechtigung im Kanal zu entfernen" - }, + "default": false, + "humanName": "Give role on action, instead of removing permission", + "description": "If enabled, a role will be given to the user (once their reach the configured action amount of wrong messages) instead of the removal of the \"Send Messages\"-permission in the counter channel", "type": "boolean" }, { "name": "strikeRole", "dependsOn": "giveRoleInsteadOfPermissionRemoval", - "default": { - "en": false - }, - "humanName": { - "de": "Rolle, die bei Sperrung vergeben wird", - "en": "Role given when amount is being reached" - }, - "description": { - "en": "This role will be given to users when they reach the configured amount of wrong messages", - "de": "Diese Rolle wird dem Nutzer gegeben, sobald die konfigurierte Anzahl von falschen Nachrichten erreicht wird" - }, + "default": "", + "humanName": "Role given when amount is being reached", + "description": "This role will be given to users when they reach the configured amount of wrong messages", "type": "roleID" }, { "name": "strikeMessage", - "default": { - "de": "%mention%, ich musste dir den Zugriff auf diesen Kanal verbieten, da du ihn mehrmals falsch verwendet hast.", - "en": "%mention%, I had to restrict your access to this channel because you repeatedly used it improperly." - }, - "humanName": { - "en": "Message when user gets actioned", - "de": "Nachricht, wenn ein Nutzer gesperrt wird" - }, + "default": "%mention%, I had to restrict your access to this channel because you repeatedly used it improperly.", + "humanName": "Message when user gets actioned", "type": "string", "allowEmbed": true, - "description": { - "en": "This message will be sent when a user reach the configured amount of wrong messages and gets actioned", - "de": "Diese Nachricht wird versendet, sobald die konfigurierte Anzahl von falschen Nachrichten erreicht wird und ein Nutzer bestraft wird" - }, + "description": "This message will be sent when a user reach the configured amount of wrong messages and gets actioned", "params": [ { "name": "mention", - "description": { - "de": "Erwähnung des Nutzers", - "en": "Mention of the users" - } + "description": "Mention of the users" } ] }, { "name": "allowCharactersInMessage", - "default": { - "en": false - }, + "default": false, "type": "boolean", - "humanName": { - "en": "Allow text characters in messages?", - "de": "Textcharaktere in der Nachricht erlauben?" - }, - "description": { - "en": "If enabled, users may write additional content into their messages instead of forcing them to just write a number. Messages without a number will still lead to an error.", - "de": "Wenn aktiviert, können Nutzer weitere Inhalte in ihre Nachrichten schreiben, statt sie zu zwingen, nur eine Nachricht zu posten. Nachrichten ohne Zahlen werden weiterhin zu einem Fehler führen." - } + "humanName": "Allow text characters in messages?", + "description": "If enabled, users may write additional content into their messages instead of forcing them to just write a number. Messages without a number will still lead to an error." }, { "name": "allowMaths", - "default": { - "en": true - }, + "default": true, "type": "boolean", - "humanName": { - "en": "Allow users to use maths in their messages?", - "de": "Nutzern erlauben, Mathematik in ihren Nachrichten zu verwenden?" - }, - "description": { - "en": "If enabled, users can use maths in messages, as long as the result of their formula is the correct next number.", - "de": "If enabled, können Nutzer Mathematik in ihren Nachrichten verwenden, solange das Ergebnis des Termes der korrekten nächsten Zahl entspricht." - } + "humanName": "Allow users to use maths in their messages?", + "description": "If enabled, users can use maths in messages, as long as the result of their formula is the correct next number." + }, + { + "name": "enableEasterEggs", + "default": false, + "type": "boolean", + "humanName": "Enable number easter eggs?", + "description": "If enabled, the bot will react with special emojis on certain numbers (e.g. 42, 67, 69, 100, 420)" } ] } \ No newline at end of file diff --git a/modules/counter/events/messageCreate.js b/modules/counter/events/messageCreate.js index f2915962..89c992d8 100644 --- a/modules/counter/events/messageCreate.js +++ b/modules/counter/events/messageCreate.js @@ -2,7 +2,7 @@ const {localize} = require('../../../src/functions/localize'); const {embedType} = require('../../../src/functions/helpers'); let Formula; -const invalidMessages = {}; +const invalidMessages = new Map(); module.exports.run = async function (client, msg) { if (!client.botReadyAt) return; @@ -29,7 +29,7 @@ module.exports.run = async function (client, msg) { object.lastCountedUser = null; object.userCounts = {}; await object.save(); - invalidMessages[msg.author.id]++; + invalidMessages.set(msg.author.id, (invalidMessages.get(msg.author.id) || 0) + 1); return msg.reply(embedType(moduleConfig.restartOnWrongCountMessage, { '%i%': 1, '%mention%': msg.author.toString() @@ -61,13 +61,18 @@ module.exports.run = async function (client, msg) { } let reactions; - if (msg.content === '42') reactions = [await msg.react('❓')]; - else if (msg.content === '420') reactions = [await msg.react('🚬')]; - else if (msg.content === '100') reactions = [await msg.react('💯')]; - else if (msg.content === '110') reactions = [await msg.react('🚓')]; - else if (msg.content === '112' || msg.content === '911') reactions = [await msg.react('🚑'), await msg.react('🚒')]; - else if (msg.content === '69') reactions = [await msg.react('🇳'), await msg.react('🇮'), await msg.react('🇨'), await msg.react('🇪')]; - else reactions = [await msg.react(moduleConfig['success-reaction'])]; + if (moduleConfig.enableEasterEggs) { + if (parsedNumber === 67) reactions = [await msg.react('🤲')]; + else if (parsedNumber === 42) reactions = [await msg.react('❓')]; + else if (parsedNumber === 420) reactions = [await msg.react('🚬')]; + else if (parsedNumber === 100) reactions = [await msg.react('💯')]; + else if (parsedNumber === 110) reactions = [await msg.react('🚓')]; + else if (parsedNumber === 112 || parsedNumber === 911) reactions = [await msg.react('🚑'), await msg.react('🚒')]; + else if (parsedNumber === 69) reactions = [await msg.react('🇳'), await msg.react('🇮'), await msg.react('🇨'), await msg.react('🇪')]; + else reactions = [await msg.react(moduleConfig['success-reaction'])]; + } else { + reactions = [await msg.react(moduleConfig['success-reaction'])]; + } if (moduleConfig.removeReactions) setTimeout(async () => { for (const reaction of reactions) await reaction.remove(); @@ -88,10 +93,8 @@ module.exports.run = async function (client, msg) { await msg.delete(); }, 8000); if (!skipStrike || parseInt(moduleConfig.strikeAmount) === 0) return; - console.log(invalidMessages); - if (!invalidMessages[msg.author.id]) invalidMessages[msg.author.id] = 0; - invalidMessages[msg.author.id]++; - if (invalidMessages[msg.author.id] >= parseInt(moduleConfig.strikeAmount)) { + invalidMessages.set(msg.author.id, (invalidMessages.get(msg.author.id) || 0) + 1); + if (invalidMessages.get(msg.author.id) >= parseInt(moduleConfig.strikeAmount)) { if (moduleConfig.giveRoleInsteadOfPermissionRemoval) await msg.member.roles.add(moduleConfig.strikeRole, '[counter] ' + localize('counter', 'restriction-audit-log')); else await msg.channel.permissionOverwrites.create(msg.author, { SEND_MESSAGES: false diff --git a/modules/counter/milestones.json b/modules/counter/milestones.json index 2ca1f83e..9ddfaaed 100644 --- a/modules/counter/milestones.json +++ b/modules/counter/milestones.json @@ -1,87 +1,43 @@ { - "description": { - "en": "Reward your users, when they reach certain goals", - "de": "Belohne deine Nutzer, wenn diese bestimmte Ziele erreichen" - }, - "humanName": { - "en": "Milestones", - "de": "Ziele" - }, + "description": "Reward your users, when they reach certain goals", + "humanName": "Milestones", "configElementName": { - "de": { - "one": "Ziel", - "more": "Ziele" - }, - "en": { - "one": "Milestone", - "more": "Milestones" - } + "one": "Milestone", + "more": "Milestones" }, "filename": "milestones.json", "configElements": true, "content": [ { "name": "userMessageCount", - "humanName": { - "de": "Nachrichtenzahl", - "en": "Message count" - }, - "default": { - "en": "" - }, - "description": { - "en": "Count of valid counter-messages the users has to achieve this goal", - "de": "Anzahl der gültigen Zähl-Nachrichten, die der Nutzer schreiben muss, um dieses Ziel zu erreichen" - }, + "humanName": "Message count", + "default": "", + "description": "Count of valid counter-messages the users has to achieve this goal", "type": "integer" }, { "name": "giveRoles", - "humanName": { - "de": "Rollen", - "en": "Roles" - }, - "default": { - "en": [], - "de": [] - }, - "description": { - "en": "These roles are given to the user if they achieve this goal (optional)", - "de": "Diese Rollen werden an den Nutzer vergeben, wenn er dieses Ziel erreicht (optional)" - }, + "humanName": "Roles", + "default": [], + "description": "These roles are given to the user if they achieve this goal (optional)", "type": "array", "content": "roleID" }, { "name": "sendMessage", - "humanName": { - "de": "Nachricht", - "en": "Message" - }, - "default": { - "en": "Congrats %mention% for counting %milestone% times!", - "de": "Herzlichen Glückwunsch, %mention%, für %milestone%-mal zählen!!" - }, + "humanName": "Message", + "default": "Congrats %mention% for counting %milestone% times!", "params": [ { "name": "mention", - "description": { - "en": "Mention the user who achieved the milestone", - "de": "Erwähnt den Nutzer, der das Ziel erreicht hat" - } + "description": "Mention the user who achieved the milestone" }, { "name": "milestone", - "description": { - "en": "The milestone (the number of message) that was reached", - "de": "Das Ziel (also die Zahl der Nachrichten, die verschickt), das erreicht wurde" - } + "description": "The milestone (the number of message) that was reached" } ], - "description": { - "en": "This message gets send when they achieve this goal", - "de": "Diese Nachricht wird gesendet, wenn er dieses Ziel erreicht" - }, + "description": "This message gets send when they achieve this goal", "type": "string", "allowNull": true, "allowEmbed": true diff --git a/modules/counter/module.json b/modules/counter/module.json index 9d9b7f0e..0ebed82d 100644 --- a/modules/counter/module.json +++ b/modules/counter/module.json @@ -1,5 +1,6 @@ { "name": "counter", + "fa-icon": "fas fa-arrow-up-1-9", "author": { "scnxOrgID": "1", "name": "SCDerox (SC Network Team)", @@ -15,12 +16,6 @@ "fun" ], "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/counter", - "humanReadableName": { - "en": "Count-Game", - "de": "Zähl-Spiel" - }, - "description": { - "en": "Allow your users to count together", - "de": "Erlaubt es deinen Nutzern, zusammen zu zählen!" - } -} \ No newline at end of file + "humanReadableName": "Count-Game", + "description": "Allow your users to count together" +} diff --git a/modules/duel/commands/duel.js b/modules/duel/commands/duel.js index d7af202d..1d133d40 100644 --- a/modules/duel/commands/duel.js +++ b/modules/duel/commands/duel.js @@ -1,5 +1,6 @@ const {localize} = require('../../../src/functions/localize'); const {ComponentType, MessageEmbed} = require('discord.js'); +const {safeSetFooter} = require('../../../src/functions/helpers'); module.exports.run = async function (interaction) { const member = interaction.options.getMember('user', true); @@ -46,7 +47,7 @@ module.exports.run = async function (interaction) { bullets[member.user.id] = 0; guardAfterEachOther[interaction.user.id] = 0; guardAfterEachOther[member.user.id] = 0; - const a = rep.createMessageComponentCollector({componentType: ComponentType.Button}); + const a = rep.createMessageComponentCollector({componentType: ComponentType.Button, time: 600000}); setTimeout(() => { if (started || a.ended) return; endReason = localize('duel', 'invite-expired', {u: interaction.user.toString(), i: member.toString()}); @@ -128,8 +129,8 @@ module.exports.run = async function (interaction) { const embed = new MessageEmbed() .setTitle(localize('duel', ended ? 'game-ended' : 'game-running-header')) .setColor(ended ? 0x2ECC71 : (!mentions ? 0xD35400 : 0xE67E22)) - .setDescription(lastRoundString + (!ended ? stateString : '\n\n' + localize('duel', 'ended-state')) + '\n*' + localize('duel', 'how-does-this-game-work') + '*') - .setFooter({text: interaction.client.strings.footer, iconURL: interaction.client.strings.footerImgUrl}); + .setDescription(lastRoundString + (!ended ? stateString : '\n\n' + localize('duel', 'ended-state')) + '\n*' + localize('duel', 'how-does-this-game-work') + '*'); + safeSetFooter(embed, interaction.client); i.update({ content: ended ? 'GGs!' : `<@${member.user.id}> vs <@${interaction.user.id}>`, @@ -170,10 +171,11 @@ module.exports.run = async function (interaction) { }); }); a.on('end', () => { - rep.edit({ + if (!ended) rep.edit({ content: endReason, components: [] - }); + }).catch(() => { + }); } ); }; diff --git a/modules/duel/module.json b/modules/duel/module.json index 09eaf569..994f6318 100644 --- a/modules/duel/module.json +++ b/modules/duel/module.json @@ -1,23 +1,19 @@ { "name": "duel", - "humanReadableName": { - "en": "Duel" - }, + "humanReadableName": "Duel", "author": { "scnxOrgID": "1", "name": "SCDerox (SC Network Team)", "link": "https://github.com/SCDerox" }, - "description": { - "en": "Let users play the game \"Duel\" on your discord", - "de": "Erlaubt es deinen Nutzern, das Spiel \"Duel\" auf deinem Discord zu spielen" - }, + "description": "Let users play the game \"Duel\" on your discord", "commands-dir": "/commands", "noConfig": true, "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/duel", "tags": [ "fun" ], + "fa-icon": "fas fa-gun", "earlyAccess": false, "holidayGift": true -} \ No newline at end of file +} diff --git a/modules/economy-system/commands/shop.js b/modules/economy-system/commands/shop.js index 7dd5e64e..00120ec0 100644 --- a/modules/economy-system/commands/shop.js +++ b/modules/economy-system/commands/shop.js @@ -1,4 +1,11 @@ -const {createShopItem, createShopMsg, deleteShopItem, shopMsg, buyShopItem, updateShopItem} = require('../economy-system'); +const { + createShopItem, + createShopMsg, + deleteShopItem, + shopMsg, + buyShopItem, + updateShopItem +} = require('../economy-system'); const {localize} = require('../../../src/functions/localize'); /** @@ -8,9 +15,9 @@ const {localize} = require('../../../src/functions/localize'); async function checkPermsAndSendReplyOnFail(interaction) { const result = interaction.client.configurations['economy-system']['config']['shopManagers'].includes(interaction.user.id) || interaction.client.config['botOperators'].includes(interaction.user.id); if (!result) await interaction.reply({ - content: interaction.client.strings['not_enough_permissions'], - ephemeral: !interaction.client.configurations['economy-system']['config']['publicCommandReplies'] - }); + content: interaction.client.strings['not_enough_permissions'], + ephemeral: !interaction.client.configurations['economy-system']['config']['publicCommandReplies'] + }); return result; } @@ -154,6 +161,6 @@ module.exports.config = { description: localize('economy-system', 'shop-option-description-role') } ] - }, + } ] }; \ No newline at end of file diff --git a/modules/economy-system/configs/config.json b/modules/economy-system/configs/config.json index e37366e1..4165c8d0 100644 --- a/modules/economy-system/configs/config.json +++ b/modules/economy-system/configs/config.json @@ -1,365 +1,187 @@ { - "description": { - "en": "Configure here, how the module should behave", - "de": "Stelle hier ein, wie sich das Modul verhalten soll" - }, - "humanName": { - "en": "Configuration", - "de": "Konfiguration" - }, + "description": "Configure here, how the module should behave", + "humanName": "Configuration", "filename": "config.json", "content": [ { "name": "admins", - "humanName": { - "en": "Administrators", - "de": "Administratoren" - }, - "default": { - "en": [] - }, - "description": { - "en": "Users who can perform admin only actions e.g. manage the balance of users (Bot Operators always have this permission)", - "de": "Benutzer*innen die Admin only Aktionen ausführen können, wie z.B. die Balance von Benutzern ändern (Bot-Operatoren haben immer diese Berechtigung)" - }, + "humanName": "Administrators", + "default": [], + "description": "Users who can perform admin only actions e.g. manage the balance of users (Bot Operators always have this permission)", "type": "array", "content": "integer" }, { "name": "allowCheats", - "humanName": {}, - "default": { - "en": false - }, - "description": { - "en": "Allow admins to edit the balance of users (for a fair system not recommended!)" - }, + "humanName": "Allow Cheats", + "default": false, + "description": "Allow admins to edit the balance of users (for a fair system not recommended!)", "type": "boolean" }, { "name": "selfBalance", - "humanName": {}, - "default": { - "en": false - }, - "description": { - "en": "Allow admins to edit their own balance (for a fair system not recommended! DON'T DO THIS!!!!!)" - }, + "humanName": "Allow Self-Balance Editing", + "default": false, + "description": "Allow admins to edit their own balance (for a fair system not recommended! DON'T DO THIS!!!!!)", "type": "boolean" }, { "name": "shopManagers", - "humanName": { - "en": "shop-managers", - "de": "Shop-Verwaltung" - }, - "default": { - "en": [] - }, - "description": { - "en": "The Ids of the shop managers (Bot Operators have this permission always)", - "de": "Die Benutzer-IDs der Shopverwaltung (Bot-Operatoren haben immer diese Berechtigung)" - }, + "humanName": "shop-managers", + "default": [], + "description": "The Ids of the shop managers (Bot Operators have this permission always)", "type": "array", "content": "integer" }, { "name": "startMoney", - "humanName": { - "en": "Start Money", - "de": "Start Geld" - }, - "default": { - "en": 100 - }, - "description": { - "en": "The amount of money that is given to a new user", - "de": "Das Geld, welches einem neuen Benutzer gegeben wird" - }, + "humanName": "Start Money", + "default": 100, + "description": "The amount of money that is given to a new user", "type": "integer" }, { "name": "currencyName", - "humanName": { - "en": "currency name", - "de": "Währungsbezeichnung" - }, - "default": { - "en": "" - }, - "description": { - "en": "The name of the currency", - "de": "Der Name der Währung" - }, + "humanName": "currency name", + "default": "", + "description": "The name of the currency", "type": "string" }, { "name": "currencySymbol", - "humanName": { - "en": "Symbol of the currency", - "de": "Symbol der Währung" - }, - "default": { - "en": "💰" - }, - "description": { - "en": "The symbol of the currency", - "de": "Das Symbol der Währung" - }, + "humanName": "Symbol of the currency", + "default": "💰", + "description": "The symbol of the currency", "type": "string" }, { "name": "maxWorkMoney", - "humanName": { - "en": "max work money", - "de": "Maximaler Arbeits Lohn" - }, - "default": { - "en": 100 - }, - "description": { - "en": "The highest amount of money you can get for working", - "de": "Der höchte Betrag, den man fürs Arbeiten bekommen kann" - }, + "humanName": "max work money", + "default": 100, + "description": "The highest amount of money you can get for working", "type": "integer" }, { "name": "minWorkMoney", - "humanName": { - "en": "min work money", - "de": "Mainimaler Arbeits Lohn" - }, - "default": { - "en": 20 - }, - "description": { - "en": "The lowest amount of money you can get for working", - "de": "Der niedrigste Betrag, den man fürs Arbeiten bekommen kann" - }, + "humanName": "min work money", + "default": 20, + "description": "The lowest amount of money you can get for working", "type": "integer" }, { "name": "workCooldown", - "humanName": { - "en": "work cooldown", - "de": "Arbeits Cooldown" - }, - "default": { - "en": 20 - }, - "description": { - "en": "The amount of time a user needs to wait util they can use the work command again (in minutes)", - "de": "Die Dauer, die Benutzer*innen warten müssen, bevor der Arbeits-Command wieder ausgeführt werden kann (in Minuten)" - }, + "humanName": "work cooldown", + "default": 20, + "description": "The amount of time a user needs to wait util they can use the work command again (in minutes)", "type": "integer" }, { "name": "maxCrimeMoney", - "humanName": { - "en": "max crime money", - "de": "Maximales Verbrechens Geld" - }, - "default": { - "en": 1000 - }, - "description": { - "en": "The highest amount of money you can get for crime", - "de": "Das maximale Geld, was man dafür bekommen kann, ein Verbrechen zu begehen" - }, + "humanName": "max crime money", + "default": 1000, + "description": "The highest amount of money you can get for crime", "type": "integer" }, { "name": "minCrimeMoney", - "humanName": { - "en": "min crime money", - "de": "Minimales Verbrechens Geld" - }, - "default": { - "en": 100 - }, - "description": { - "en": "The lowest amount of money you can get for crime", - "de": "Das minimale Geld, was man dafür bekommen kann, ein Verbrechen zu begehen" - }, + "humanName": "min crime money", + "default": 100, + "description": "The lowest amount of money you can get for crime", "type": "integer" }, { "name": "crimeCooldown", - "humanName": { - "en": "crime cooldown", - "de": "Verbrechens Cooldown" - }, - "default": { - "en": 30 - }, - "description": { - "en": "The amount of time a user needs to wait util they can use the crime command again (in minutes)", - "de": "Die Dauer, die Benutzer*innen warten müssen, bevor der Verbrechens-Command wieder ausgeführt werden kann (in Minuten)" - }, + "humanName": "crime cooldown", + "default": 30, + "description": "The amount of time a user needs to wait util they can use the crime command again (in minutes)", "type": "integer" }, { "name": "maxRobAmount", - "humanName": { - "en": "max rob amount", - "de": "Maximale Raub Beute" - }, - "default": { - "en": 400 - }, - "description": { - "en": "The highest amount of money that a user can rob", - "de": "Das maximale Geld, was man durch Rauben bekommen kann" - }, + "humanName": "max rob amount", + "default": 400, + "description": "The highest amount of money that a user can rob", "type": "integer" }, { "name": "robPercent", - "humanName": { - "en": "rob percent", - "de": "Raub Prozent" - }, - "default": { - "en": 10 - }, - "description": { - "en": "The amount that can get robed in percent", - "de": "Das maximale Geld, was bei einem Raub erbeutet werden kann, in Prozent" - }, + "humanName": "rob percent", + "default": 10, + "description": "The amount that can get robed in percent", "type": "integer" }, { "name": "robCooldown", - "humanName": { - "en": "rob cooldown", - "de": "Raub Cooldown" - }, - "default": { - "en": 60 - }, - "description": { - "en": "The amount of time a user needs to wait util they can use the rob command again (in minutes)", - "de": "Die Zeit die Benutzer warten müssen, bis sie den Raub-Command nochmal ausführen können (in Minuten)" - }, + "humanName": "rob cooldown", + "default": 60, + "description": "The amount of time a user needs to wait util they can use the rob command again (in minutes)", "type": "integer" }, { "name": "leaderboardChannel", - "humanName": { - "en": "leaderboard-channel", - "de": "Leaderboard-Kanal" - }, - "default": { - "en": "" - }, + "humanName": "leaderboard-channel", + "default": "", "allowNull": true, - "description": { - "en": "The channel for the leaderboard. On this leaderboard everyone can see who has the most money.", - "de": "Der Kanals für das Leaderboard. Hier kann jeder sehen, wer das meiste Geld hat" - }, + "description": "The channel for the leaderboard. On this leaderboard everyone can see who has the most money.", "type": "channelID" }, { "name": "shopChannel", - "humanName": { - "en": "shop channel", - "de": "Shop Kanal" - }, - "default": { - "en": "" - }, - "description": { - "en": "The id of the channel for the shop-Message. This message shows the items of the shop", - "de": "Die ID des Kanals für die Shop-Nachricht. Diese Nachricht zeigt alle Items des Shops" - }, + "humanName": "shop channel", + "default": "", + "description": "The id of the channel for the shop-Message. This message shows the items of the shop", "type": "channelID", "allowNull": true }, { "name": "msgDropsIgnoredChannels", - "humanName": { - "en": "message-drops ignored channels", - "de": "Ignorierte Message-Drop Kanäle" - }, - "default": { - "en": [] - }, - "description": { - "en": "List of Channels where Users can't get message-drops", - "de": "Liste an Kanälen, in denen Benutzer keine Message-Drops bekommen können" - }, + "humanName": "message-drops ignored channels", + "default": [], + "description": "List of Channels where Users can't get message-drops", "type": "array", "content": "string" }, { "name": "messageDrops", - "humanName": {}, - "default": { - "en": 25 - }, - "description": { - "en": "Chance to get money for a message (Chance: 1/ This value). Set to 0 to disable message drops" - }, + "humanName": "Message Drop Chance", + "default": 25, + "description": "Chance to get money for a message (Chance: 1/ This value). Set to 0 to disable message drops", "type": "integer" }, { "name": "messageDropsMax", - "humanName": {}, - "default": { - "en": 50 - }, - "description": { - "en": "The max amount of money in a message Drop" - }, + "humanName": "Max Message Drop Amount", + "default": 50, + "description": "The max amount of money in a message Drop", "type": "integer" }, { "name": "messageDropsMin", - "humanName": {}, - "default": { - "en": 5 - }, - "description": { - "en": "The min amount of money in a message Drop" - }, + "humanName": "Min Message Drop Amount", + "default": 5, + "description": "The min amount of money in a message Drop", "type": "integer" }, { "name": "dailyReward", - "humanName": {}, - "default": { - "en": 25 - }, - "description": { - "en": "The daily reward" - }, + "humanName": "Daily Reward Amount", + "default": 25, + "description": "The daily reward", "type": "integer" }, { "name": "weeklyReward", - "humanName": {}, - "default": { - "en": 100 - }, - "description": { - "en": "The weekly reward" - }, + "humanName": "Weekly Reward Amount", + "default": 100, + "description": "The weekly reward", "type": "integer" }, { "name": "publicCommandReplies", - "humanName": { - "en": "Public Command-Replies", - "de": "Öffentliche Command-Antworten" - }, - "default": { - "en": false - }, - "description": { - "en": "Should the Command-replies be displayed for everyone?", - "de": "Sollen die Command-Antworten für alle angezeigt werden?" - }, + "humanName": "Public Command-Replies", + "default": false, + "description": "Should the Command-replies be displayed for everyone?", "type": "boolean" } ] -} \ No newline at end of file +} diff --git a/modules/economy-system/configs/strings.json b/modules/economy-system/configs/strings.json index f4ff4528..4b3bae90 100644 --- a/modules/economy-system/configs/strings.json +++ b/modules/economy-system/configs/strings.json @@ -1,248 +1,155 @@ { - "description": { - "en": "Configure messages of this module here", - "de": "Passe hier die Nachrichten des Modules an" - }, - "humanName": { - "en": "Messages", - "de": "Nachrichten" - }, + "description": "Configure messages of this module here", + "humanName": "Messages", "filename": "strings.json", "content": [ { "name": "notFound", - "humanName": { - "en": "not found message", - "de": "Nicht gefunden Nachricht" - }, - "default": { - "en": "This item could not be found", - "de": "Dieses Item konnte nicht gefunden werden" - }, - "description": { - "en": "The message that is send if the item wasn't found", - "de": "Die Nachricht, die gesendet wird, wenn das Item nicht gefunden wird" - }, + "humanName": "not found message", + "default": "This item could not be found", + "description": "The message that is send if the item wasn't found", "type": "string", "allowEmbed": true }, { "name": "notEnoughMoney", - "humanName": { - "en": "not enough money", - "de": "Nicht genug Geld" - }, - "default": { - "en": "You haven't enough money to buy this Item", - "de": "Du hast nicht genug Geld, um dieses Item zu kaufen" - }, - "description": { - "en": "The message that is send if the user haven't enough money to buy an item", - "de": "Die Nachricht, die gesendet wird, wenn ein Benutzer nicht genug geld hat, um ein Item zu kaufen" - }, + "humanName": "not enough money", + "default": "You haven't enough money to buy this Item", + "description": "The message that is send if the user haven't enough money to buy an item", "type": "string", "allowEmbed": true }, { "name": "shopMsg", - "humanName": { - "en": "shop message", - "de": "Shop-Nachricht" - }, + "humanName": "shop message", "default": { - "en": { - "title": "Shop", - "description": "%shopItems%" - } - }, - "description": { - "en": "Message for the shop. The Items gets added at the end", - "de": "Die Nachricht, die den aktuellen Shop anzeigt" + "title": "Shop", + "description": "%shopItems%" }, + "description": "Message for the shop. The Items gets added at the end", "type": "string", "allowEmbed": true, "params": [ { "name": "shopItems", - "description": { - "en": "All items of the shop (format specified below)", - "de": "Alle Items des Shops (Format wird unten angegeben)" - } + "description": "All items of the shop (format specified below)" } ] }, { "name": "itemString", - "humanName": { - "en": "item string", - "de": "Item Text" - }, - "default": { - "en": "**%id%** %itemName%, **price**: %price%, **sellcount**: %sellcount%", - "de": "**%id%** %itemName%: **Preis**: %price%, **Verkäufe**: %sellcount%" - }, - "description": { - "en": "String for the items for the shop message", - "de": "Text für die Items für die Shop-Nachricht" - }, + "humanName": "item string", + "default": "**%id%** %itemName%, **price**: %price%, **sellcount**: %sellcount%", + "description": "String for the items for the shop message", "type": "string", "allowEmbed": false, "params": [ { "name": "id", - "description": { - "en": "Id of the item", - "de": "ID des Items" - } + "description": "Id of the item" }, { "name": "itemName", - "description": { - "en": "Name of the item", - "de": "Name des Items" - } + "description": "Name of the item" }, { "name": "price", - "description": { - "en": "Price of the item", - "de": "Preis des Items" - } + "description": "Price of the item" }, { "name": "sellcount", - "description": { - "en": "Count of the sales of the item", - "de": "Anzahl, wie häufig das Item verkauft wurde" - } + "description": "Count of the sales of the item" } ] }, { "name": "cooldown", - "humanName": { - "en": "cooldown", - "de": "Cooldown" - }, - "default": { - "en": "Please wait before using this command again" - }, - "description": { - "en": "This message gets send when a user is currently in cooldown" - }, + "humanName": "cooldown", + "default": "Please wait before using this command again", + "description": "This message gets send when a user is currently in cooldown", "type": "string", "allowEmbed": true }, { "name": "workSuccess", - "humanName": {}, - "default": { - "en": [ - "You worked and earned **%earned%**" - ] - }, - "description": { - "en": "Array of messages from which one random gets send when a user works successfully" - }, + "humanName": "Work Success Messages", + "default": [ + "You worked and earned **%earned%**" + ], + "description": "Array of messages from which one random gets send when a user works successfully", "type": "array", "content": "string", "allowEmbed": true, "params": [ { "name": "earned", - "description": { - "en": "Money that the user had earned" - } + "description": "Money that the user had earned" } ] }, { "name": "crimeSuccess", - "humanName": {}, - "default": { - "en": [ - "You stole a wallet and earned **%earned%**" - ] - }, - "description": { - "en": "Array of messages from which one random gets send when a user commits a crime successfully" - }, + "humanName": "Crime Success Messages", + "default": [ + "You stole a wallet and earned **%earned%**" + ], + "description": "Array of messages from which one random gets send when a user commits a crime successfully", "type": "array", "content": "string", "allowEmbed": true, "params": [ { "name": "earned", - "description": { - "en": "Money that the user had earned" - } + "description": "Money that the user had earned" } ] }, { "name": "crimeFail", - "humanName": {}, - "default": { - "en": [ - "You've stolen a wallet and get caught.You loose **%loose%**" - ] - }, - "description": { - "en": "Array of messages from which one random gets send when a user fails to do some crime" - }, + "humanName": "Crime Fail Messages", + "default": [ + "You've stolen a wallet and get caught.You loose **%loose%**" + ], + "description": "Array of messages from which one random gets send when a user fails to do some crime", "type": "array", "content": "string", "allowEmbed": true, "params": [ { "name": "loose", - "description": { - "en": "Money that the user looses" - } + "description": "Money that the user looses" } ] }, { "name": "robSuccess", - "humanName": {}, - "default": { - "en": "You robed %user% earned **%earned%**" - }, - "description": { - "en": "This message gets send when a user robs another user successfully" - }, + "humanName": "Rob Success Message", + "default": "You robed %user% earned **%earned%**", + "description": "This message gets send when a user robs another user successfully", "type": "string", "allowEmbed": true, "params": [ { "name": "earned", - "description": { - "en": "Money that the user had earned" - } + "description": "Money that the user had earned" }, { "name": "user", - "description": { - "en": "The user that gets robed by you" - } + "description": "The user that gets robed by you" } ] }, { "name": "leaderboardEmbed", - "humanName": {}, + "humanName": "Leaderboard Embed", "default": { - "en": { - "title": "Leaderboard", - "color": "GREEN", - "thumbnail": " ", - "image": " ", - "description": "Here you can see who has the most money" - } - }, - "description": { - "en": "Configure the leaderboard embed here" + "title": "Leaderboard", + "color": "GREEN", + "thumbnail": " ", + "image": " ", + "description": "Here you can see who has the most money" }, + "description": "Configure the leaderboard embed here", "type": "keyed", "content": { "key": "string", @@ -253,462 +160,298 @@ }, { "name": "dailyReward", - "humanName": {}, - "default": { - "en": "You earned **%earned%** by collecting your daily reward" - }, - "description": { - "en": "Message that gets send after the user has claimed the daily reward" - }, + "humanName": "Daily Reward Message", + "default": "You earned **%earned%** by collecting your daily reward", + "description": "Message that gets send after the user has claimed the daily reward", "type": "string", "allowEmbed": true, "params": [ { "name": "earned", - "description": { - "en": "Money that the user had earned" - } + "description": "Money that the user had earned" } ] }, { "name": "weeklyReward", - "humanName": {}, - "default": { - "en": "You earned **%earned%** by collecting your weekly reward" - }, - "description": { - "en": "Message that gets send after the user has claimed the weekly reward" - }, + "humanName": "Weekly Reward Message", + "default": "You earned **%earned%** by collecting your weekly reward", + "description": "Message that gets send after the user has claimed the weekly reward", "type": "string", "allowEmbed": true, "params": [ { "name": "earned", - "description": { - "en": "Money that the user had earned" - } + "description": "Money that the user had earned" } ] }, { "name": "balanceReply", - "humanName": {}, + "humanName": "Balance Reply", "default": { - "en": { - "title": "Balance of %user%", - "fields": [ - { - "name": "Balance:", - "value": "%balance%" - }, - { - "name": "Bank:", - "value": "%bank%" - }, - { - "name": "Total:", - "value": "%total%" - } - ] - } - }, - "description": { - "en": "Reply for the balance command" + "title": "Balance of %user%", + "fields": [ + { + "name": "Balance:", + "value": "%balance%" + }, + { + "name": "Bank:", + "value": "%bank%" + }, + { + "name": "Total:", + "value": "%total%" + } + ] }, + "description": "Reply for the balance command", "type": "string", "allowEmbed": true, "params": [ { "name": "balance", - "description": { - "en": "Current balance of the user" - } + "description": "Current balance of the user" }, { "name": "bank", - "description": { - "en": "Current value that the user has on the bank" - } + "description": "Current value that the user has on the bank" }, { "name": "total", - "description": { - "en": "Total balance of the user" - } + "description": "Total balance of the user" }, { "name": "user", - "description": { - "en": "Username and discriminator of the User" - } + "description": "Username and discriminator of the User" } ] }, { "name": "userNotFound", - "humanName": {}, - "default": { - "en": "I can't find the user **%user%**" - }, - "description": { - "en": "The message that gets sent when the bot can't find a user" - }, + "humanName": "User Not Found", + "default": "I can't find the user **%user%**", + "description": "The message that gets sent when the bot can't find a user", "type": "string", "allowEmbed": true, "params": [ { "name": "user", - "description": { - "en": "User that can't been found" - } + "description": "User that can't been found" } ] }, { "name": "buyMsg", - "humanName": {}, - "default": { - "en": "You got the item **%item%**" - }, - "description": { - "en": "Message that gets send when a user buys something in the shop" - }, + "humanName": "Purchase Message", + "default": "You got the item **%item%**", + "description": "Message that gets send when a user buys something in the shop", "type": "string", "allowEmbed": true, "params": [ { "name": "item", - "description": { - "en": "Name of the item" - } + "description": "Name of the item" } ] }, { "name": "itemCreate", - "humanName": {}, - "default": { - "en": "Successfully created the item %name% with the id %id%. It costs %price% and you get the role %role%" - }, - "description": { - "en": "Message that gets send when a new shop item gets created" - }, + "humanName": "Item Created Message", + "default": "Successfully created the item %name% with the id %id%. It costs %price% and you get the role %role%", + "description": "Message that gets send when a new shop item gets created", "type": "string", "allowEmbed": true, "params": [ { "name": "name", - "description": { - "en": "Name of the created item" - } + "description": "Name of the created item" }, { "name": "id", - "description": { - "en": "Id of the created item" - } + "description": "Id of the created item" }, { "name": "price", - "description": { - "en": "Price of the created item" - } + "description": "Price of the created item" }, { "name": "role", - "description": { - "en": "Role that everyone gets who buys the item" - } + "description": "Role that everyone gets who buys the item" } ] }, { "name": "itemDelete", - "humanName": {}, - "default": { - "en": "Successfully deleted the item %name%." - }, - "description": { - "en": "Message that gets send when a new shop item gets deleted" - }, + "humanName": "Item Deleted Message", + "default": "Successfully deleted the item %name%.", + "description": "Message that gets send when a new shop item gets deleted", "type": "string", "allowEmbed": true, "params": [ { "name": "name", - "description": { - "en": "Name of the deleted item", - "de": "Name des gelöschten Items" - } + "description": "Name of the deleted item" }, { "name": "id", - "description": { - "en": "Id of the deleted item", - "de": "ID des gelöschten Items" - } + "description": "Id of the deleted item" } ] }, { "name": "itemEdit", - "humanName": {}, - "default": { - "en": "Successfully edited the item %name%. Check it out using `/shop list`" - }, - "description": { - "en": "Message that gets sent when a shop item gets edited" - }, + "humanName": "Item Edited Message", + "default": "Successfully edited the item %name%. Check it out using `/shop list`", + "description": "Message that gets sent when a shop item gets edited", "type": "string", "allowEmbed": true, "params": [ { "name": "name", - "description": { - "en": "Name of the edited item", - "de": "Name des bearbeiteten Items" - } + "description": "Name of the edited item" }, { "name": "id", - "description": { - "en": "Id of the edited item", - "de": "ID des bearbeiteten Items" - } + "description": "Id of the edited item" } ] }, { "name": "depositMsg", - "humanName": { - "en": "deposit message" - }, - "default": { - "en": "Successfully deposited **%amount%** to your bank" - }, - "description": { - "en": "The reply when a user deposits money to the bank" - }, + "humanName": "deposit message", + "default": "Successfully deposited **%amount%** to your bank", + "description": "The reply when a user deposits money to the bank", "type": "string", "params": [ { "name": "amount", - "description": {} + "description": "Amount deposited" } ] }, { "name": "withdrawMsg", - "humanName": { - "en": "withdraw message" - }, - "default": { - "en": "Successfully withdrew **%amount%** from your bank" - }, - "description": { - "en": "The reply when a user withdraws money from the bank" - }, + "humanName": "withdraw message", + "default": "Successfully withdrew **%amount%** from your bank", + "description": "The reply when a user withdraws money from the bank", "type": "string", "params": [ { "name": "amount", - "description": {} + "description": "Amount withdrawn" } ] }, { "name": "msgDropMsg", - "humanName": { - "en": "message drop message" - }, - "default": { - "en": [ - "Message-Drop: You earned %earned% simply by chatting!" - ] - }, - "description": { - "en": "The message that gets sent on a message-drop" - }, + "humanName": "message drop message", + "default": [ + "Message-Drop: You earned %earned% simply by chatting!" + ], + "description": "The message that gets sent on a message-drop", "type": "array", "content": "string", "params": [ { "name": "earned", - "description": {} + "description": "Money earned from the drop" } ] }, { "name": "NaN", - "humanName": { - "en": "not a number" - }, - "default": { - "en": "**%input%** isn't a number" - }, - "description": { - "en": "Message that gets send if the bot needs a number but gets something different" - }, + "humanName": "not a number", + "default": "**%input%** isn't a number", + "description": "Message that gets send if the bot needs a number but gets something different", "type": "string", "params": [ { "name": "input", - "description": {} + "description": "The invalid input" } ] }, { "name": "msgDropAlreadyEnabled", - "humanName": { - "en": "message-drop already enabled" - }, - "default": { - "en": "The Mesage-Drop message is already enabled!" - }, - "description": { - "en": "Message that gets send if a User trys to enable the Message-Drop message, but it's already enabled" - }, + "humanName": "message-drop already enabled", + "default": "The Mesage-Drop message is already enabled!", + "description": "Message that gets send if a User trys to enable the Message-Drop message, but it's already enabled", "type": "string" }, { "name": "msgDropEnabled", - "humanName": { - "en": "message-drop enabled" - }, - "default": { - "en": "Successfully enabled the Message-Drop message" - }, - "description": { - "en": "Message that gets send when a User enables the Message-Drop message" - }, + "humanName": "message-drop enabled", + "default": "Successfully enabled the Message-Drop message", + "description": "Message that gets send when a User enables the Message-Drop message", "type": "string" }, { "name": "msgDropAlreadyDisabled", - "humanName": { - "en": "message-drop already disabled" - }, - "default": { - "en": "The Mesage-Drop message is already disabled!" - }, - "description": { - "en": "Message that gets send if a User trys to disable the Message-Drop message, but it's already disabled" - }, + "humanName": "message-drop already disabled", + "default": "The Mesage-Drop message is already disabled!", + "description": "Message that gets send if a User trys to disable the Message-Drop message, but it's already disabled", "type": "string" }, { "name": "msgDropDisabled", - "humanName": { - "en": "message-drop disabled" - }, - "default": { - "en": "Successfully disabled the Message-Drop message" - }, - "description": { - "en": "Message that gets send when a User disables the Message-Drop message" - }, + "humanName": "message-drop disabled", + "default": "Successfully disabled the Message-Drop message", + "description": "Message that gets send when a User disables the Message-Drop message", "type": "string" }, { "name": "rebuyItem", - "humanName": { - "en": "rebuy message", - "de": "Erneutkaufen Nachricht" - }, - "default": { - "en": "You already own this Item", - "de": "Du hast dieses Item bereits gekauft" - }, - "description": { - "en": "The message that is send when the user trys to buy an Item that he already own", - "de": "Die Nachricht, die gesendet wird, wenn der Nutzer das Item bereits besitzt" - }, + "humanName": "rebuy message", + "default": "You already own this Item", + "description": "The message that is send when the user trys to buy an Item that he already own", "type": "string", "allowEmbed": true }, { "name": "multipleMatches", - "humanName": { - "en": "multiple matches", - "de": "mehrere Treffer" - }, - "default": { - "en": "Multiple items match the query", - "de": "Mehrere Items entsprechen der Suche" - }, - "description": { - "en": "The message that gets send when multiple items match the query", - "de": "Die Nachricht, die gesendet wird, wenn mehrere Items der Suche entsprechen" - }, + "humanName": "multiple matches", + "default": "Multiple items match the query", + "description": "The message that gets send when multiple items match the query", "type": "string", "allowEmbed": true }, { "name": "noMatches", - "humanName": { - "en": "no matches", - "de": "keine Treffer" - }, - "default": { - "en": "The item with the id %id%/ the name %name% doesn't exists", - "de": "Das Item mit der ID %id%/ dem Namen %name% wurde nicht gefunden" - }, - "description": { - "en": "The message that gets send when the item can't be found", - "de": "Die Nachricht, die gesendet wird, wenn das Item nicht gefunden wird" - }, + "humanName": "no matches", + "default": "The item with the id %id%/ the name %name% doesn't exists", + "description": "The message that gets send when the item can't be found", "type": "string", "allowEmbed": true, "params": [ { "name": "id", - "description": { - "en": "The specified ID", - "de": "Die angegebene ID" - } + "description": "The specified ID" }, { "name": "name", - "description": { - "en": "The specified name", - "de": "Der angegebene Name" - } + "description": "The specified name" } ] }, { "name": "itemDuplicate", - "humanName": { - "en": "item duplicate", - "de": "Item Duplikat" - }, - "default": { - "en": "There's already an item with the id %id% or the name %name%", - "de": "Es gibt schon ein Item mit der ID %id% oder dem Namen %name%" - }, - "description": { - "en": "The message that gets send when an item with the specified id or name already exists", - "de": "Die Nachricht, die gesendet wird, wenn ein Item mit dem angegebenen Namen oder der angegebenen ID schon existiert" - }, + "humanName": "item duplicate", + "default": "There's already an item with the id %id% or the name %name%", + "description": "The message that gets send when an item with the specified id or name already exists", "type": "string", "allowEmbed": true, "params": [ { "name": "id", - "description": { - "en": "The specified ID", - "de": "Die angegebene ID" - } + "description": "The specified ID" }, { "name": "name", - "description": { - "en": "The specified name", - "de": "Der angegebene Name" - } + "description": "The specified name" } ] } ] -} \ No newline at end of file +} diff --git a/modules/economy-system/economy-system.js b/modules/economy-system/economy-system.js index 0167b685..cd86b4ee 100644 --- a/modules/economy-system/economy-system.js +++ b/modules/economy-system/economy-system.js @@ -188,7 +188,7 @@ async function createShopItem(interaction) { return resolve(localize('economy-system', 'role-to-high')); } - if(price<=0) { + if (price <= 0) { await interaction.editReply(localize('economy-system', 'price-less-than-zero')); return resolve(localize('economy-system', 'price-less-than-zero')); } @@ -392,10 +392,10 @@ async function deleteShopItem(interaction) { } /** -* Function to update a shop-item -* @param {*} interaction Interaction -* @returns {Promise} -*/ + * Function to update a shop-item + * @param {*} interaction Interaction + * @returns {Promise} + */ async function updateShopItem(interaction) { return new Promise(async (resolve) => { const id = interaction.options.get('item-id')['value']; @@ -427,7 +427,7 @@ async function updateShopItem(interaction) { return resolve(localize('economy-system', 'role-to-high')); } - if(newPrice !== null && newPrice<=0) { + if (newPrice !== null && newPrice <= 0) { await interaction.editReply(localize('economy-system', 'price-less-than-zero')); return resolve(localize('economy-system', 'price-less-than-zero')); } @@ -441,7 +441,7 @@ async function updateShopItem(interaction) { if (collidingItem && collidingItem['id'] !== id) { await interaction.editReply(embedType(interaction.client.configurations['economy-system']['strings']['itemDuplicate'], { '%id%': id, - '%name%': "-" + '%name%': '-' })); return resolve(localize('economy-system', 'item-duplicate')); } @@ -466,16 +466,16 @@ async function updateShopItem(interaction) { interaction.client.logger.info(`[economy-system] ` + localize('economy-system', 'edit-item', { u: interaction.user.tag, i: id, - n: newNameOption ? newNameOption['value'] : "-", - p: newPrice ? newPrice : "-", - r: newRole ? newRole['name'] : "-", + n: newNameOption ? newNameOption['value'] : '-', + p: newPrice ? newPrice : '-', + r: newRole ? newRole['name'] : '-' })); if (interaction.client.logChannel) await interaction.client.logChannel.send(`[economy-system] ` + localize('economy-system', 'edit-item', { u: interaction.user.tag, i: id, - n: newNameOption ? newNameOption['value'] : "-", - p: newPrice ? newPrice : "-", - r: newRole ? newRole['name'] : "-", + n: newNameOption ? newNameOption['value'] : '-', + p: newPrice ? newPrice : '-', + r: newRole ? newRole['name'] : '-' })); resolve(`Edited the item ${item.name} successfully`); }); @@ -589,10 +589,10 @@ async function leaderboard(client) { name: client.user.username, iconURL: client.user.avatarURL() }) - .setFooter({ + .setFooter(client.strings.footer ? { text: client.strings.footer, iconURL: client.strings.footerImgUrl - }); + } : null); if (model.length !== 0) embed.addFields({ name: 'Leaderboard:', diff --git a/modules/economy-system/events/interactionCreate.js b/modules/economy-system/events/interactionCreate.js index 7b40565b..127b1200 100644 --- a/modules/economy-system/events/interactionCreate.js +++ b/modules/economy-system/events/interactionCreate.js @@ -6,5 +6,6 @@ module.exports.run = async function (client, interaction) { if (!interaction.isSelectMenu()) return; if (interaction.customId !== 'economy-system_shop-select') return; await interaction.deferReply({ephemeral: true}); + console.log(interaction.values); buyShopItem(interaction, interaction.values[0], null); }; \ No newline at end of file diff --git a/modules/economy-system/module.json b/modules/economy-system/module.json index 9be79097..c0763718 100644 --- a/modules/economy-system/module.json +++ b/modules/economy-system/module.json @@ -15,14 +15,10 @@ "configs/config.json", "configs/strings.json" ], + "fa-icon": "fa-solid fa-bank", "tags": [ "community" ], - "humanReadableName": { - "en": "Economy" - }, - "description": { - "en": "A simple economy-system, containing a shop system, message-drops and commands to earn money", - "de": "Ein einfaches economy-system mit einem Shop, Nachrichten-Drops und Befehlen, um Geld zu verdienen" - } -} \ No newline at end of file + "humanReadableName": "Economy", + "description": "A simple economy-system, containing a shop system, message-drops and commands to earn money" +} diff --git a/modules/fun/config.json b/modules/fun/config.json index ef0f1bd7..dae88fa9 100644 --- a/modules/fun/config.json +++ b/modules/fun/config.json @@ -1,395 +1,219 @@ { - "description": {}, - "humanName": { - "en": "Configuration", - "de": "Konfiguration" - }, + "description": "Customize the messages and images for fun commands here", + "humanName": "Configuration", "filename": "config.json", "content": [ { "name": "ikeaMessage", - "humanName": { - "de": "IKEA-Nachricht", - "en": "IKEA Message" - }, - "default": { - "en": "Here's a ikea-product-name: %name%", - "de": "Hier ist ein IKEA-Produkt-Name: %name%" - }, - "description": { - "en": "Message that gets send when someone uses /random ikea-name", - "de": "Nachricht welche gesendet wird, wenn jemand /random ikea-name benutzt" - }, + "humanName": "IKEA Message", + "default": "Here's a ikea-product-name: %name%", + "description": "Message that gets send when someone uses /random ikea-name", "type": "string", "allowEmbed": true, "params": [ { "name": "name", - "description": { - "en": "Randomly generated name of an ikea product (probably not real)", - "de": "Zufällig generierter Name eines IKEA-Produkts (wahrscheinlich nicht real)" - } + "description": "Randomly generated name of an ikea product (probably not real)" } ] }, { "name": "randomNumberMessage", - "humanName": { - "de": "Zufallszahl-Nachricht", - "en": "Random numer message" - }, - "default": { - "en": "Here your random number between %min% and %max%: %number%", - "de": "Hier ist deine Zufallszahl zwischen %min% und %max%: %number%" - }, - "description": { - "en": "Message that gets send when someone uses /random number", - "de": "Nachricht, welche gesendet wird, wenn jemand /random number benutzt" - }, + "humanName": "Random numer message", + "default": "Here your random number between %min% and %max%: %number%", + "description": "Message that gets send when someone uses /random number", "type": "string", "allowEmbed": true, "params": [ { "name": "min", - "description": { - "en": "Minimal value", - "de": "Niedrigster Wert" - } + "description": "Minimal value" }, { "name": "max", - "description": { - "en": "Maximal value", - "de": "Höchster Wert" - } + "description": "Maximal value" }, { "name": "number", - "description": { - "en": "Generated number", - "de": "Generierte Zahl" - } + "description": "Generated number" } ] }, { "name": "diceRollMessage", - "humanName": { - "de": "Würfel-Nachricht", - "en": "Dice Roll message" - }, - "default": { - "en": "🎲 %number%", - "de": "🎲 %number%" - }, - "description": { - "en": "Message that gets send when someone uses /random dice", - "de": "Nachricht, welche gesendet wird, wenn jemand /random dice benutzt" - }, + "humanName": "Dice Roll message", + "default": "🎲 %number%", + "description": "Message that gets send when someone uses /random dice", "type": "string", "allowEmbed": true, "params": [ { "name": "number", - "description": { - "en": "Generated number", - "de": "Generierte Zahl" - } + "description": "Generated number" } ] }, { "name": "coinFlipMessage", - "humanName": { - "de": "Münzwurf-Nachricht", - "en": "Coin toss message" - }, - "default": { - "en": "\uD83E\uDE99 %site%", - "de": "\uD83E\uDE99 %site%" - }, - "description": { - "en": "Message that gets send when someone uses /random coinfilp", - "de": "Nachricht, welche gesendet wird, wenn jemand /random coinfilp benutzt" - }, + "humanName": "Coin toss message", + "default": "🪙 %site%", + "description": "Message that gets send when someone uses /random coinfilp", "type": "string", "allowEmbed": true, "params": [ { "name": "site", - "description": { - "de": "Seite, auf den die Münze gefallen ist", - "en": "Site on which the coin landed" - } + "description": "Site on which the coin landed" } ] }, { "name": "hugMessage", - "humanName": { - "de": "Umarmungsnachricht", - "en": "Hug message" - }, - "default": { - "en": "<@%authorID%> hugs <@%userID%>", - "de": "<@%authorID%> umarmt <@%userID%>" - }, - "description": { - "de": "Nachricht, welche gesendet wird, wenn jemand /hug benutzt", - "en": "Message that gets send when someone uses /hug" - }, + "humanName": "Hug message", + "default": "<@%authorID%> hugs <@%userID%>", + "description": "Message that gets send when someone uses /hug", "type": "string", "allowEmbed": true, "params": [ { "name": "authorID", - "description": { - "en": "ID of the user who ran this command", - "de": "ID des Nutzers, welcher den Befehl aus" - } + "description": "ID of the user who ran this command" }, { "name": "userID", - "description": { - "en": "ID of the user that gets hugged", - "de": "ID des umarmten Nutzers" - } + "description": "ID of the user that gets hugged" } ] }, { "name": "hugImages", - "humanName": { - "de": "Umarmungsbilder", - "en": "Hug images" - }, - "default": { - "en": [ - "https://scnx-cdn.scootkit.net/1723477011519-tjCfeHPcYYzFe3jRnoUVI7dn.gif", - "https://scnx-cdn.scootkit.net/1723477171157-3wGistN45zd9kwrP67YKfRgU.gif", - "https://scnx-cdn.scootkit.net/1753891037940-pdaiqed4ffL4XHbLe2N0j6fbW6zRvPDzy0ZCwKIRwmOz85yX.gif" - ] - }, - "description": { - "de": "Bilder aus welchen, wenn jemand /hug ausführt, zufällig ausgewählt wird", - "en": "Images that one will be randomly selected from when someone uses /hug." - }, + "humanName": "Hug images", + "default": [ + "https://scnx-cdn.scootkit.net/1723477011519-tjCfeHPcYYzFe3jRnoUVI7dn.gif", + "https://scnx-cdn.scootkit.net/1723477171157-3wGistN45zd9kwrP67YKfRgU.gif", + "https://scnx-cdn.scootkit.net/1753891037940-pdaiqed4ffL4XHbLe2N0j6fbW6zRvPDzy0ZCwKIRwmOz85yX.gif" + ], + "description": "Images that one will be randomly selected from when someone uses /hug.", "type": "array", "content": "imgURL" }, { "name": "kissMessage", - "humanName": { - "en": "Kiss message", - "de": "Kuss-Nachrichten" - }, - "default": { - "en": "<@%authorID%> kissed <@%userID%>", - "de": "<@%authorID%> küsst <@%userID%>" - }, - "description": { - "en": "Message that gets send when someone uses /kiss", - "de": "Nachricht, welche gesendet wird, wenn jemand /kiss benutzt" - }, + "humanName": "Kiss message", + "default": "<@%authorID%> kissed <@%userID%>", + "description": "Message that gets send when someone uses /kiss", "type": "string", "allowEmbed": true, "params": [ { "name": "authorID", - "description": { - "en": "ID of the user who ran this command", - "de": "ID des Nutzers, welcher den Befehl aus" - } + "description": "ID of the user who ran this command" }, { "name": "userID", - "description": { - "en": "ID of the user that gets kissed", - "de": "ID des geküssten Nutzers" - } + "description": "ID of the user that gets kissed" } ] }, { "name": "kissImages", - "humanName": { - "de": "Kussbilder", - "en": "Kiss images" - }, - "default": { - "en": [ - "https://scnx-cdn.scootkit.net/1743549285215-t9x4Fm9ZqE0f4vxyKfrTNo7JlGLO2hFHae8R8arRQHjQeylk.gif", - "https://scnx-cdn.scootkit.net/1695864480892-EVwr6ighEdpxY22G8jUweAPt.gif", - "https://scnx-cdn.scootkit.net/1743549267626-cSru5Kn1Dg2zv5KAefHMtRL5XuWqCW84hegW40aty4b8iFH7.gif" - ] - }, - "description": { - "en": "Images that one will be randomly selected from when someone uses /kiss.", - "de": "Bilder aus welchen, wenn jemand /kiss ausführt, zufällig ausgewählt wird" - }, + "humanName": "Kiss images", + "default": [ + "https://scnx-cdn.scootkit.net/1743549285215-t9x4Fm9ZqE0f4vxyKfrTNo7JlGLO2hFHae8R8arRQHjQeylk.gif", + "https://scnx-cdn.scootkit.net/1695864480892-EVwr6ighEdpxY22G8jUweAPt.gif", + "https://scnx-cdn.scootkit.net/1743549267626-cSru5Kn1Dg2zv5KAefHMtRL5XuWqCW84hegW40aty4b8iFH7.gif" + ], + "description": "Images that one will be randomly selected from when someone uses /kiss.", "type": "array", "content": "imgURL" }, { "name": "slapMessage", - "humanName": { - "de": "Schlag-Nachricht", - "en": "Slap message" - }, - "default": { - "en": "<@%authorID%> slapped <@%userID%>", - "de": "<@%authorID%> schlägt <@%userID%>" - }, - "description": { - "de": "Nachricht, welche gesendet wird, wenn jemand /slap benutzt", - "en": "Message that gets send when someone uses /slap" - }, + "humanName": "Slap message", + "default": "<@%authorID%> slapped <@%userID%>", + "description": "Message that gets send when someone uses /slap", "type": "string", "allowEmbed": true, "params": [ { "name": "authorID", - "description": { - "en": "ID of the user who ran this command", - "de": "ID des Nutzers, welcher den Befehl aus" - } + "description": "ID of the user who ran this command" }, { "name": "userID", - "description": { - "en": "ID of the user that gets slapped", - "de": "ID des geschlagenen Nutzers" - } + "description": "ID of the user that gets slapped" } ] }, { "name": "slapImages", - "humanName": { - "de": "Schlag-Bilder", - "en": "Slap images" - }, - "default": { - "en": [ - "https://scnx-cdn.scootkit.net/1744620013783-xEkcviAsrCZulbhoVoPPWtTUWlJbQda6kk43eQb58CMLFvDU.gif", - "https://scnx-cdn.scootkit.net/1744620140479-qz6nc8xzCSW2TB6Yy40vj6WzCBi31ezRZVElFrKuKCIfc6vZ.gif", - "https://scnx-cdn.scootkit.net/1744620083811-RYado8KTb7E8AzCVfncyNgUxD2GyQFdhjH4YxzVc5aLkGvN4.gif", - "https://scnx-cdn.scootkit.net/1744620244031-0JO1dEMxvKBAz12dj08BIVw8njCxgj8CJ89SnUihMZxnzyDE.gif" - ] - }, - "description": { - "de": "Bilder aus welchen, wenn jemand /slap ausführt, zufällig ausgewählt wird", - "en": "Images that one will be randomly selected from when someone uses /slap." - }, + "humanName": "Slap images", + "default": [ + "https://scnx-cdn.scootkit.net/1744620013783-xEkcviAsrCZulbhoVoPPWtTUWlJbQda6kk43eQb58CMLFvDU.gif", + "https://scnx-cdn.scootkit.net/1744620140479-qz6nc8xzCSW2TB6Yy40vj6WzCBi31ezRZVElFrKuKCIfc6vZ.gif", + "https://scnx-cdn.scootkit.net/1744620083811-RYado8KTb7E8AzCVfncyNgUxD2GyQFdhjH4YxzVc5aLkGvN4.gif", + "https://scnx-cdn.scootkit.net/1744620244031-0JO1dEMxvKBAz12dj08BIVw8njCxgj8CJ89SnUihMZxnzyDE.gif" + ], + "description": "Images that one will be randomly selected from when someone uses /slap.", "type": "array", "content": "imgURL" }, { "name": "patMessage", - "humanName": { - "de": "Tätschel-Nachricht", - "en": "Pat message" - }, - "default": { - "en": "<@%authorID%> patted <@%userID%>", - "de": "<@%authorID%> tätschelt <@%userID%>" - }, - "description": { - "de": "Nachricht, welche gesendet wird, wenn jemand /pat benutzt", - "en": "Message that gets send when someone uses /pat" - }, + "humanName": "Pat message", + "default": "<@%authorID%> patted <@%userID%>", + "description": "Message that gets send when someone uses /pat", "type": "string", "allowEmbed": true, "params": [ { "name": "authorID", - "description": { - "en": "ID of the user who ran this command", - "de": "ID des Nutzers, welcher den Befehl aus" - } + "description": "ID of the user who ran this command" }, { "name": "userID", - "description": { - "en": "ID of the user that gets patted", - "de": "ID des getätschelten Nutzers" - } + "description": "ID of the user that gets patted" } ] }, { "name": "patImages", - "humanName": { - "de": "Tätschel-Bilder", - "en": "Pat images" - }, - "default": { - "en": [ - "https://scnx-cdn.scootkit.net/1744619869697-AYVUENwLWjusxCOKvJLOnpdSiiiQZJC2dmSwnHMSOLr7eLbH.gif", - "https://scnx-cdn.scootkit.net/1744619643063-Iw3QdOJ9LsQLKv3Moe3zvMfakKu0NVfqlrmmd2ssrBqLEJai.gif", - "https://scnx-cdn.scootkit.net/1671631825485-6eaH1p3ngebQigoVjBicgaRy.gif", - "https://scnx-cdn.scootkit.net/1744619413990-auYiCEqSxZnp2QldAOgav77oVb2EiXnPS83icTlX7AkV1JzV.gif" - ] - }, - "description": { - "de": "Bilder aus welchen, wenn jemand /pat ausführt, zufällig ausgewählt wird", - "en": "Images that one will be randomly selected from when someone uses /pat." - }, + "humanName": "Pat images", + "default": [ + "https://scnx-cdn.scootkit.net/1744619869697-AYVUENwLWjusxCOKvJLOnpdSiiiQZJC2dmSwnHMSOLr7eLbH.gif", + "https://scnx-cdn.scootkit.net/1744619643063-Iw3QdOJ9LsQLKv3Moe3zvMfakKu0NVfqlrmmd2ssrBqLEJai.gif", + "https://scnx-cdn.scootkit.net/1671631825485-6eaH1p3ngebQigoVjBicgaRy.gif", + "https://scnx-cdn.scootkit.net/1744619413990-auYiCEqSxZnp2QldAOgav77oVb2EiXnPS83icTlX7AkV1JzV.gif" + ], + "description": "Images that one will be randomly selected from when someone uses /pat.", "type": "array", "content": "imgURL" }, { "name": "8ballMessage", - "humanName": { - "de": "8ball-Nachricht", - "en": "8ball Message" - }, - "default": { - "en": "The oracle has spoken... %answer%", - "de": "Das Orakel hat gesprochen... %answer%" - }, - "description": { - "de": "Nachricht, welche gesendet wird, wenn jemand /random 8ball benutzt", - "en": "Message that gets send when someone uses /random 8ball" - }, + "humanName": "8ball Message", + "default": "The oracle has spoken... %answer%", + "description": "Message that gets send when someone uses /random 8ball", "type": "string", "allowEmbed": true, "params": [ { - "name": "%answer", - "description": { - "en": "Answer to the question", - "de": "Antwort auf die Frage" - } + "name": "answer", + "description": "Answer to the question" } ] }, { "name": "8BallMessages", - "humanName": { - "de": "8ball-Antworten", - "en": "8ball responses" - }, - "default": { - "en": [ - "", - "No", - "Maybe", - "Try again", - "42 is the answer" - ], - "de": [ - "Ja", - "Nein", - "Vielleicht", - "Bitte versuche es erneut", - "42 ist die Antwort" - ] - }, - "description": { - "de": "Mögliche Antworten für /random 8ball", - "en": "Possible answers for /random 8ball" - }, + "humanName": "8ball responses", + "default": [ + "", + "No", + "Maybe", + "Try again", + "42 is the answer" + ], + "description": "Possible answers for /random 8ball", "type": "array", "content": "string" } diff --git a/modules/fun/module.json b/modules/fun/module.json index 36fe7e39..683f1e6c 100644 --- a/modules/fun/module.json +++ b/modules/fun/module.json @@ -1,5 +1,6 @@ { "name": "fun", + "fa-icon": "fas fa-laugh-squint", "author": { "scnxOrgID": "1", "name": "SCDerox (SC Network Team)", @@ -13,12 +14,6 @@ "fun" ], "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/fun", - "humanReadableName": { - "en": "Fun-Commands", - "de": "Fun-Befehle" - }, - "description": { - "en": "Some random fun commands like /hug or /random", - "de": "Einige Spaß-Commands, wie /hug oder /random" - } -} \ No newline at end of file + "humanReadableName": "Fun-Commands", + "description": "Some random fun commands like /hug or /random" +} diff --git a/modules/guess-the-number/config.json b/modules/guess-the-number/config.json deleted file mode 100644 index 2522419b..00000000 --- a/modules/guess-the-number/config.json +++ /dev/null @@ -1,152 +0,0 @@ -{ - "description": {}, - "humanName": { - "en": "Configuration", - "de": "Konfiguration" - }, - "filename": "config.json", - "commandsWarnings": { - "special": [ - { - "name": "/guess-the-number", - "info": { - "en": "You need to first set the permissions in your server settings for this command and after that add them under \"adminRoles\" here.", - "de": "Du musst zuerst die Rechte in deinen Server-Einstellungen einstellen und danach diese unter \"AdminRollen\" hinzufügen." - } - } - ] - }, - "content": [ - { - "name": "adminRoles", - "humanName": { - "de": "Adminrollen", - "en": "Admin-Roles" - }, - "default": { - "en": [], - "de": [] - }, - "description": { - "en": "Every role that can manage game sessions", - "de": "Jede Rolle, welche Spielrunden verwalten kann" - }, - "type": "array", - "content": "roleID" - }, - { - "name": "startMessage", - "humanName": { - "de": "Startnachricht", - "en": "Start-Message" - }, - "default": { - "en": { - "title": "Guess the Number - Game started", - "description": "Guess a number between %min% and %max%. Good look!" - }, - "de": { - "title": "Errate die Zahl - Das Spiel beginnt", - "description": "Errate eine Zahl zwischen %min% und %max%. Viel Glück!" - } - }, - "description": { - "de": "Nachricht, die am Anfang einer Runde gesendet wird", - "en": "Message that gets send when a new round gets started" - }, - "type": "string", - "allowEmbed": true, - "params": [ - { - "name": "min", - "description": { - "en": "Minimal value to guess", - "de": "Niedrigester möglichster Wert" - } - }, - { - "name": "max", - "description": { - "en": "Maximal value to guess", - "de": "Höchster möglichster Wert" - } - } - ] - }, - { - "name": "endMessage", - "humanName": { - "de": "Endnachricht", - "en": "End-Message" - }, - "default": { - "en": { - "title": "Guess the Number - Game ended", - "description": "Good game everyone!\nThe winner is %winner%.\nThe number was **%number%**.\nThere were around **%guessCount% guesses** in total." - }, - "de": { - "title": "Errate die Zahl - Das Spiel ist beendet", - "description": "Gutes Spiel!\nDer Gewinner ist %winner%.\nDie Zahl war **%number%**.\nInsgesamt wurde **%guessCount% mal** geraten." - } - }, - "description": { - "de": "Nachricht, die am Ende einer Runde gesendet wird", - "en": "Message that gets send when a round ends" - }, - "type": "string", - "allowEmbed": true, - "params": [ - { - "name": "min", - "description": { - "en": "Minimal value to guess", - "de": "Niedrigester möglichster Wert" - } - }, - { - "name": "max", - "description": { - "en": "Maximal value to guess", - "de": "Höchster möglichster Wert" - } - }, - { - "name": "winner", - "description": { - "en": "@-mention of the winner", - "de": "@-Erwähnung des Gewinners" - } - }, - { - "name": "guessCount", - "description": { - "en": "Count of guesses in this game session", - "de": "Anzahl der Versuche in dieser Runde" - } - }, - { - "name": "number", - "description": { - "en": "Winning number", - "de": "Nummer, die gesucht wurde" - } - } - ] - }, - { - "name": "higherLowerReactions", - "type": "boolean", - "humanName": { - "de": "Reagiere mit Höher / Geringer Emojis", - "en": "React with Lower / Higher reactions" - }, - "default": { - "en": false - }, - "description": { - "de": "Wenn aktiviert, reagiert der Bot bei falschen Versuchen mit ⬇ (wenn die gesuchte Zahl unter der gesendeten Zahl ist) oder mit ⬆ (wenn die gesuchte Zahl größer als die gesendete Zahl ist). Falls deaktiviert, wird der Bot nur mit ❌ bei falschen Versuchen reagieren.", - "en": "If enabled, the bot will react with ⬇ (if the guess is higher than the correct number) or with ⬆ (if the guess is lower than the correct number) on wrong guesses. If disabled, the bot will just react with ❌ on wrong guesses." - } - } - ] -} \ No newline at end of file diff --git a/modules/guess-the-number/configs/channel.json b/modules/guess-the-number/configs/channel.json index 58ecea0f..a9065498 100644 --- a/modules/guess-the-number/configs/channel.json +++ b/modules/guess-the-number/configs/channel.json @@ -1,42 +1,20 @@ { - "description": { - "en": "Enable the Gamechannel mode to automatically re-start games", - "de": "Aktiviere den Spielkanalmodus, um das Spiel automatisch neuzustarten" - }, - "humanName": { - "en": "Gamechannel Mode", - "de": "Spielkanal-Modus" - }, + "description": "Enable the Gamechannel mode to automatically re-start games", + "humanName": "Gamechannel Mode", "filename": "channel.json", "content": [ { - "default": { - "en": false - }, + "default": false, "name": "enabled", - "description": { - "en": "If enabled, you can configure a game channel, in which a new guess the number game will be started if a number got guessed correctly. You still will be able to manually start games in other channels. Everyone, including admins, can guess in game channels.", - "de": "Wenn aktiviert, kannst du einen Spielkanal konfigurieren, in welchem neue Nummer-Erraten-Spiele gestartet werden, sobald eine Zahl korrekt erraten wurde. Du kannst auch weiterhin manuell Spiele in anderen Kanälen starten. In Spielkanälen kann jeder, also auch Admins, raten." - }, - "humanName": { - "en": "Enable Gamechannel mode?", - "de": "Spielkanalmodus aktivieren?" - }, + "description": "If enabled, you can configure a game channel, in which a new guess the number game will be started if a number got guessed correctly. You still will be able to manually start games in other channels. Everyone, including admins, can guess in game channels.", + "humanName": "Enable Gamechannel mode?", "type": "boolean" }, { - "default": { - "en": "" - }, + "default": "", "dependsOn": "enabled", - "description": { - "en": "In this channel, games will be automatically started if a game ends or no game is currently running", - "de": "In diesem Kanal werden Spiele automatisch gestartet, wenn ein Spiel endet oder gerade kein Spiel läuft." - }, - "humanName": { - "en": "Gamechannel", - "de": "Spielkanal" - }, + "description": "In this channel, games will be automatically started if a game ends or no game is currently running", + "humanName": "Gamechannel", "content": [ "GUILD_TEXT" ], @@ -46,34 +24,18 @@ { "type": "integer", "dependsOn": "enabled", - "default": { - "en": 1 - }, + "default": 1, "name": "minInt", - "humanName": { - "en": "Minimum number", - "de": "Kleinste Nummer" - }, - "description": { - "en": "A number between this and the highest number will be selected at random when a game starts.", - "de": "Eine Nummer zwischen dieser under der höchsten Nummer wird automatisch ausgewählt, wenn das Spiel startet." - } + "humanName": "Minimum number", + "description": "A number between this and the highest number will be selected at random when a game starts." }, { "type": "integer", "dependsOn": "enabled", - "default": { - "en": 1000 - }, + "default": 1000, "name": "maxInt", - "humanName": { - "en": "Highest number", - "de": "Höchste Nummer" - }, - "description": { - "en": "A number between this and the minimum number will be selected at random when a game starts.", - "de": "Eine Nummer zwischen dieser under der kleinsten Nummer wird automatisch ausgewählt, wenn das Spiel startet." - } + "humanName": "Highest number", + "description": "A number between this and the minimum number will be selected at random when a game starts." } ] } \ No newline at end of file diff --git a/modules/guess-the-number/configs/config.json b/modules/guess-the-number/configs/config.json index 3c583908..68a6ae7d 100644 --- a/modules/guess-the-number/configs/config.json +++ b/modules/guess-the-number/configs/config.json @@ -1,155 +1,91 @@ { - "description": { - "en": "Adjust messages and permissions here", - "de": "Passe Nachrichten und Rechte hier an" - }, - "humanName": { - "en": "Configuration", - "de": "Konfiguration" - }, + "description": "Adjust messages and permissions here", + "humanName": "Configuration", "filename": "config.json", "commandsWarnings": { "special": [ { "name": "/guess-the-number", - "info": { - "en": "You need to first set the permissions in your server settings for this command and after that add them under \"adminRoles\" here.", - "de": "Du musst zuerst die Rechte in deinen Server-Einstellungen einstellen und danach diese unter \"AdminRollen\" hinzufügen." - } + "info": "You need to first set the permissions in your server settings for this command and after that add them under \"adminRoles\" here." } ] }, "content": [ { "name": "adminRoles", - "humanName": { - "de": "Adminrollen", - "en": "Admin-Roles" - }, - "default": { - "en": [], - "de": [] - }, - "description": { - "en": "Every role that can manage game sessions.", - "de": "Jede Rolle, welche Spielrunden verwalten kann" - }, + "humanName": "Admin-Roles", + "default": [], + "description": "Every role that can manage game sessions.", "type": "array", "content": "roleID" }, { "name": "startMessage", - "humanName": { - "de": "Startnachricht", - "en": "Start-Message" - }, + "humanName": "Start-Message", "default": { - "en": { - "title": "Guess the Number - Game started", - "description": "Guess a number between %min% and %max%. Good luck!" - }, - "de": { - "title": "Errate die Zahl - Das Spiel beginnt", - "description": "Errate eine Zahl zwischen %min% und %max%. Viel Glück!" - } - }, - "description": { - "de": "Nachricht, die am Anfang einer Runde gesendet wird", - "en": "Message that gets send when a new round gets started" + "title": "Guess the Number - Game started", + "description": "Guess a number between %min% and %max%. Good luck!" }, + "description": "Message that gets send when a new round gets started", "type": "string", "allowEmbed": true, "params": [ { "name": "min", - "description": { - "en": "Minimal value to guess", - "de": "Niedrigester möglichster Wert" - } + "description": "Minimal value to guess" }, { "name": "max", - "description": { - "en": "Maximal value to guess", - "de": "Höchster möglichster Wert" - } + "description": "Maximal value to guess" } ] }, { "name": "endMessage", - "humanName": { - "de": "Endnachricht", - "en": "End-Message" - }, + "humanName": "End-Message", "default": { - "en": { - "title": "Guess the Number - Game ended", - "description": "Good game everyone!\nThe winner is %winner%.\nThe number was **%number%**.\nThere were around **%guessCount% guesses** in total." - }, - "de": { - "title": "Errate die Zahl - Das Spiel ist beendet", - "description": "Gutes Spiel!\nDer Gewinner ist %winner%.\nDie Zahl war **%number%**.\nInsgesamt wurde **%guessCount% mal** geraten." - } - }, - "description": { - "de": "Nachricht, die am Ende einer Runde gesendet wird", - "en": "Message that gets send when a round ends" + "title": "Guess the Number - Game ended", + "description": "Good game everyone!\nThe winner is %winner%.\nThe number was **%number%**.\nThere were around **%guessCount% guesses** in total." }, + "description": "Message that gets send when a round ends", "type": "string", "allowEmbed": true, "params": [ { "name": "min", - "description": { - "en": "Minimal value to guess", - "de": "Niedrigester möglichster Wert" - } + "description": "Minimal value to guess" }, { "name": "max", - "description": { - "en": "Maximal value to guess", - "de": "Höchster möglichster Wert" - } + "description": "Maximal value to guess" }, { "name": "winner", - "description": { - "en": "@-mention of the winner", - "de": "@-Erwähnung des Gewinners" - } + "description": "@-mention of the winner" }, { "name": "guessCount", - "description": { - "en": "Count of guesses in this game session", - "de": "Anzahl der Versuche in dieser Runde" - } + "description": "Count of guesses in this game session" }, { "name": "number", - "description": { - "en": "Winning number", - "de": "Nummer, die gesucht wurde" - } + "description": "Winning number" } ] }, { "name": "higherLowerReactions", "type": "boolean", - "humanName": { - "de": "Reagiere mit Höher / Geringer Emojis", - "en": "React with Lower / Higher reactions" - }, - "default": { - "en": false - }, - "description": { - "de": "Wenn aktiviert, reagiert der Bot bei falschen Versuchen mit ⬇ (wenn die gesuchte Zahl unter der gesendeten Zahl ist) oder mit ⬆ (wenn die gesuchte Zahl größer als die gesendete Zahl ist). Falls deaktiviert, wird der Bot nur mit ❌ bei falschen Versuchen reagieren.", - "en": "If enabled, the bot will react with ⬇ (if the guess is higher than the correct number) or with ⬆ (if the guess is lower than the correct number) on wrong guesses. If disabled, the bot will just react with ❌ on wrong guesses." - } + "humanName": "React with Lower / Higher reactions", + "default": false, + "description": "If enabled, the bot will react with ⬇ (if the guess is higher than the correct number) or with ⬆ (if the guess is lower than the correct number) on wrong guesses. If disabled, the bot will just react with ❌ on wrong guesses." + }, + { + "name": "enableLeaderboard", + "type": "boolean", + "humanName": "Enable leaderboard?", + "default": false, + "description": "If enabled, a leaderboard button is shown on new game messages and user statistics (wins, guesses) are tracked." } ] } \ No newline at end of file diff --git a/modules/guess-the-number/events/interactionCreate.js b/modules/guess-the-number/events/interactionCreate.js index 14419e60..edbfaed1 100644 --- a/modules/guess-the-number/events/interactionCreate.js +++ b/modules/guess-the-number/events/interactionCreate.js @@ -1,5 +1,35 @@ const {localize} = require('../../../src/functions/localize'); module.exports.run = async function (client, interaction) { + if (interaction.customId === 'gtn-leaderboard') { + const users = await client.models['guess-the-number']['User'].findAll({ + order: [['wins', 'DESC'], ['totalGuesses', 'ASC']], + limit: 20 + }); + + if (users.length === 0) return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('guess-the-number', 'leaderboard-empty') + }); + + let description = ''; + for (let i = 0; i < users.length; i++) { + const u = users[i]; + const name = `<@${u.userID}>`; + description += `**${i + 1}.** ${name} — 🏆 ${u.wins} ${localize('guess-the-number', 'wins')} | ${u.totalGuesses} ${localize('guess-the-number', 'guesses')}\n`; + } + + const {MessageEmbed} = require('discord.js'); + const {parseEmbedColor} = require('../../../src/functions/helpers'); + const embed = new MessageEmbed() + .setTitle('🏆 ' + localize('guess-the-number', 'leaderboard-title')) + .setDescription(description) + .setColor(parseEmbedColor('GOLD')); + + return interaction.reply({ + ephemeral: true, + embeds: [embed] + }); + } if (interaction.customId === 'gtn-reaction-meaning') return interaction.reply({ ephemeral: true, content: `## ${localize('guess-the-number', 'emoji-guide-button')}\n* :x:: ${localize('guess-the-number', 'guide-wrong-guess')}\n* :white_check_mark:: ${localize('guess-the-number', 'guide-win')}\n* :no_entry_sign:: ${localize('guess-the-number', 'guide-invalid-guess')}\n* :no_entry:: ${localize('guess-the-number', 'guide-admin-guess')}` diff --git a/modules/guess-the-number/events/messageCreate.js b/modules/guess-the-number/events/messageCreate.js index a7b7f4f7..81f7ca5a 100644 --- a/modules/guess-the-number/events/messageCreate.js +++ b/modules/guess-the-number/events/messageCreate.js @@ -11,6 +11,7 @@ module.exports.run = async (client, msg) => { if (msg.author.bot) return; if (!msg.guild) return; if (msg.guild.id !== client.guildID) return; + if (!msg.member) return; const game = await client.models['guess-the-number']['Channel'].findOne({ where: { channelID: msg.channel.id, @@ -24,6 +25,18 @@ module.exports.run = async (client, msg) => { if (parsedInt < game.min || parsedInt > game.max) return msg.react('🚫'); game.guessCount++; await game.save(); + if (client.configurations['guess-the-number']['config'].enableLeaderboard) { + const [userStats] = await client.models['guess-the-number']['User'].findOrCreate({ + where: {userID: msg.author.id}, + defaults: { + userID: msg.author.id, + wins: 0, + totalGuesses: 0 + } + }); + userStats.totalGuesses++; + await userStats.save(); + } if (parsedInt !== game.number) { if (client.configurations['guess-the-number']['config']['higherLowerReactions']) { if (game.number < parsedInt) await msg.react('⬇'); else await msg.react('⬆'); @@ -33,7 +46,20 @@ module.exports.run = async (client, msg) => { } await msg.react('✅'); game.ended = true; + game.winnerID = msg.author.id; await game.save(); + if (client.configurations['guess-the-number']['config'].enableLeaderboard) { + const [userStats] = await client.models['guess-the-number']['User'].findOrCreate({ + where: {userID: msg.author.id}, + defaults: { + userID: msg.author.id, + wins: 0, + totalGuesses: 0 + } + }); + userStats.wins++; + await userStats.save(); + } const isGamechannel = client.configurations['guess-the-number']['channel'].enabled && client.configurations['guess-the-number']['channel'].channel === msg.channel.id; if (!isGamechannel) await lockChannel(msg.channel, client.configurations['guess-the-number']['config'].adminRoles, '[guess-the-number] ' + localize('guess-the-number', 'game-ended')); await msg.reply(embedType(client.configurations['guess-the-number']['config']['endMessage'], { diff --git a/modules/guess-the-number/guessTheNumber.js b/modules/guess-the-number/guessTheNumber.js index 748a2a53..de7db2ed 100644 --- a/modules/guess-the-number/guessTheNumber.js +++ b/modules/guess-the-number/guessTheNumber.js @@ -18,18 +18,30 @@ module.exports.startGame = async function (channel, number, min, max, ownerID = if (pin.author.id !== channel.client.user.id) continue; await pin.unpin(); } + const buttonComponents = [ + { + type: 'BUTTON', + label: localize('guess-the-number', 'emoji-guide-button'), + style: 'SECONDARY', + customId: 'gtn-reaction-meaning' + } + ]; + if (channel.client.configurations['guess-the-number']['config'].enableLeaderboard) { + buttonComponents.push({ + type: 'BUTTON', + label: localize('guess-the-number', 'leaderboard-button'), + style: 'PRIMARY', + customId: 'gtn-leaderboard', + emoji: '🏆' + }); + } const m = await channel.send(embedType(channel.client.configurations['guess-the-number']['config'].startMessage, { '%min%': min, '%max%': max }, { components: [{ type: 'ACTION_ROW', - components: [{ - type: 'BUTTON', - label: localize('guess-the-number', 'emoji-guide-button'), - style: 'SECONDARY', - customId: 'gtn-reaction-meaning' - }] + components: buttonComponents }] })); await m.pin(); diff --git a/modules/guess-the-number/models/User.js b/modules/guess-the-number/models/User.js new file mode 100644 index 00000000..8a88e30a --- /dev/null +++ b/modules/guess-the-number/models/User.js @@ -0,0 +1,32 @@ +const { + DataTypes, + Model +} = require('sequelize'); + +module.exports = class GuessTheNumberUser extends Model { + static init(sequelize) { + return super.init({ + userID: { + type: DataTypes.STRING, + primaryKey: true + }, + wins: { + type: DataTypes.INTEGER, + defaultValue: 0 + }, + totalGuesses: { + type: DataTypes.INTEGER, + defaultValue: 0 + } + }, { + tableName: 'guess_the_number_Users', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'User', + 'module': 'guess-the-number' +}; \ No newline at end of file diff --git a/modules/guess-the-number/module.json b/modules/guess-the-number/module.json index 0bc9ae44..67556dbf 100644 --- a/modules/guess-the-number/module.json +++ b/modules/guess-the-number/module.json @@ -17,12 +17,6 @@ "fun" ], "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/guess-the-number", - "humanReadableName": { - "en": "Guess the number", - "de": "Errate die Nummer" - }, - "description": { - "en": "Select a number and let your users guess", - "de": "Wähle eine Nummer und lass deine Mitglieder diese erraten" - } -} \ No newline at end of file + "humanReadableName": "Guess the number", + "description": "Select a number and let your users guess" +} diff --git a/modules/info-commands/commands/info.js b/modules/info-commands/commands/info.js index 1c063dd7..f5bbd7a2 100644 --- a/modules/info-commands/commands/info.js +++ b/modules/info-commands/commands/info.js @@ -5,10 +5,13 @@ const { dateToDiscordTimestamp, formatDiscordUserName, formatNumber, - parseEmbedColor + parseEmbedColor, + safeSetFooter, + moduleEnabled } = require('../../../src/functions/helpers'); const {ChannelType, MessageEmbed} = require('discord.js'); const {AgeFromDate} = require('age-calculator'); +const {stringNames} = require('../../invite-tracking/events/guildMemberJoin'); const {calculateLevelXP, isMaxLevel, displayLevel} = require('../../levels/events/messageCreate'); const legacyChannelType = (type) => { @@ -40,8 +43,8 @@ module.exports.subcommands = { .setTitle(localize('info-commands', 'information-about-server', {s: interaction.guild.name})) .setColor(parseEmbedColor('GOLD')) .setThumbnail(interaction.guild.iconURL()) - .setImage(interaction.guild.bannerURL()) - .setFooter({text: interaction.client.strings.footer, iconURL: interaction.client.strings.footerImgUrl}); + .setImage(interaction.guild.bannerURL()); + safeSetFooter(embed, interaction.client); if (!interaction.client.strings.disableFooterTimestamp) embed.setTimestamp(); if (interaction.guild.afkChannel) embed.addField(moduleStrings.serverinfo.afkChannel, `<#${interaction.guild.afkChannelID}> (${interaction.guild.afkTimeout}s)`, true); if (interaction.guild.description) embed.setDescription(interaction.guild.description); @@ -79,8 +82,8 @@ module.exports.subcommands = { .addField(moduleStrings.channelInfo.id, channel.id, true) .addField(moduleStrings.channelInfo.createdAt, ``, true) .addField(moduleStrings.channelInfo.name, channel.name, true) - .setFooter({text: interaction.client.strings.footer, iconURL: interaction.client.strings.footerImgUrl}) .setColor(parseEmbedColor('GREEN')); + safeSetFooter(embed, interaction.client); if (!interaction.client.strings.disableFooterTimestamp) embed.setTimestamp(); if (channel.parent) embed.addField(moduleStrings.channelInfo.parent, channel.parent.name, true); if (channel.position) embed.addField(moduleStrings.channelInfo.position, (channel.position + 1).toString(), true); @@ -111,12 +114,12 @@ module.exports.subcommands = { const role = interaction.options.getRole('role', true); const embed = new MessageEmbed() .setTitle(localize('info-commands', 'information-about-role', {r: role.name})) - .setFooter({text: interaction.client.strings.footer, iconURL: interaction.client.strings.footerImgUrl}) .addField(moduleStrings.roleInfo.createdAt, ``, true) .addField(moduleStrings.roleInfo.position, role.position.toString(), true) .addField(moduleStrings.roleInfo.id, role.id, true) .addField(moduleStrings.roleInfo.name, role.name, true) .setColor(role.color || parseEmbedColor('GREEN')); + safeSetFooter(embed, interaction.client); if (!interaction.client.strings.disableFooterTimestamp) embed.setTimestamp(); if (role.color) embed.addField(moduleStrings.roleInfo.color, role.hexColor, true); if (role.members) { @@ -152,14 +155,14 @@ module.exports.subcommands = { if (!member) return interaction.reply(embedType(moduleStrings['user_not_found'], {}, {ephemeral: true})); let birthday = null; let levelUserData = null; - if (interaction.client.models['birthday']) { + if (moduleEnabled(interaction.client, 'birthday')) { birthday = await interaction.client.models['birthday']['User'].findOne({ where: { id: member.user.id } }); } - if (interaction.client.models['levels']) { + if (moduleEnabled(interaction.client, 'levels')) { levelUserData = await interaction.client.models['levels']['User'].findOne({ where: { userID: member.user.id @@ -171,11 +174,11 @@ module.exports.subcommands = { .setTitle(localize('info-commands', 'information-about-user', {u: formatDiscordUserName(member.user)})) .setColor(member.displayColor || parseEmbedColor('GREEN')) .setThumbnail(member.user.avatarURL({forceStatic: false})) - .setFooter({text: interaction.client.strings.footer, iconURL: interaction.client.strings.footerImgUrl}) .addField(moduleStrings.userinfo.tag, formatDiscordUserName(member.user), true) .addField(moduleStrings.userinfo.id, member.user.id, true) .addField(moduleStrings.userinfo.createdAt, ` ()`, true) .addField(moduleStrings.userinfo.joinedAt, ` ()`, true); + safeSetFooter(embed, interaction.client); if (!interaction.client.strings.disableFooterTimestamp) embed.setTimestamp(); if (member.user.presence) embed.addField(moduleStrings.userinfo.currentStatus, member.user.presence.status, true); if (member.nickname) embed.addField(moduleStrings.userinfo.nickname, member.nickname, true); @@ -197,6 +200,22 @@ module.exports.subcommands = { embed.addField(moduleStrings.userinfo.level, displayLevel(levelUserData.level, interaction.client), true); embed.addField(moduleStrings.userinfo.messages, levelUserData.messages.toString(), true); } + if (moduleEnabled(interaction.client, 'invite-tracking')) { + const invitedUsers = await interaction.client.models['invite-tracking']['UserInvite'].findAll({ + where: { + inviter: member.user.id + } + }); + const userInvites = await interaction.client.models['invite-tracking']['UserInvite'].findAll({ + where: { + userID: member.user.id, + left: false + }, + order: [['createdAt', 'DESC']] + }); + if (userInvites[0]) embed.addField(moduleStrings.userinfo['invited-by'], `${localize('invite-tracking', stringNames[userInvites[0].inviteType])}${userInvites[0].inviter ? ` by <@${userInvites[0].inviter}>` : ''}`, true); + embed.addField(moduleStrings.userinfo.invites, `\`\`\`| ${localize('info-commands', 'total-invites')} | ${localize('info-commands', 'active-invites')} | ${localize('info-commands', 'left-invites')} |\n| ${pufferStringToSize(invitedUsers.length.toString(), localize('info-commands', 'total-invites').length)} | ${pufferStringToSize(invitedUsers.filter(i => !i.left).length.toString(), localize('info-commands', 'active-invites').length)} | ${pufferStringToSize(invitedUsers.filter(i => i.left).length.toString(), localize('info-commands', 'left-invites').length)} |\`\`\``); + } let permstring = ''; member.permissions.toArray().forEach(p => { if (!member.permissions.toArray().includes('ADMINISTRATOR')) permstring = permstring + `${p}, `; diff --git a/modules/info-commands/module.json b/modules/info-commands/module.json index 93d917f4..54c89806 100644 --- a/modules/info-commands/module.json +++ b/modules/info-commands/module.json @@ -14,12 +14,6 @@ "moderation" ], "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/info-commands", - "humanReadableName": { - "en": "Info-Commands", - "de": "Info-Befehle" - }, - "description": { - "en": "Adds info-commands with information about specific parts of your server", - "de": "Fügt viele Info-Commands zu deinen Server hinzu" - } + "humanReadableName": "Info-Commands", + "description": "Adds info-commands with information about specific parts of your server" } diff --git a/modules/info-commands/strings.json b/modules/info-commands/strings.json index 3065a8c4..b263b497 100644 --- a/modules/info-commands/strings.json +++ b/modules/info-commands/strings.json @@ -1,57 +1,31 @@ { - "description": {}, - "humanName": {}, + "description": "Edit the messages and strings of the module here", + "humanName": "Messages", "filename": "strings.json", "content": [ { "name": "serverinfo", - "humanName": { - "de": "Serverinfo" - }, + "humanName": "Server Info", "default": { - "en": { - "id": "ID", - "owner": "Owner", - "boosts": "Boosts", - "emojiCount": "Emoji-Count", - "region": "Region", - "roleCount": "Role-Count", - "rulesChannel": "Rules-Channel", - "dcSystemChannel": "Discord-System-Channel", - "verificationLevel": "Verification-Level", - "banCount": "Bans", - "createdAt": "Created at", - "members": "Members", - "channels": "Channels", - "features": "Features", - "noFeaturesEnabled": "No features enabled", - "afkChannel": "AFK-Channel", - "stickerCount": "Sticker-Count" - }, - "de": { - "id": "ID", - "owner": "Eigentümer", - "boosts": "Boosts", - "emojiCount": "Emoji-Anzahl", - "region": "Region", - "roleCount": "Rollen-Anzahl", - "rulesChannel": "Regelkanal", - "dcSystemChannel": "Discord-Systemkanal", - "verificationLevel": "Verifizierungsstufe", - "banCount": "Banns", - "createdAt": "Erstellt am", - "members": "Mitglieder", - "channels": "Kanäle", - "features": "Funktionen", - "noFeaturesEnabled": "Keine Funktionen aktiviert", - "afkChannel": "AFK-Kanal", - "stickerCount": "Sticker-Anzahl" - } - }, - "description": { - "en": "You can change the parts of the serverinfo-command here", - "de": "Hier kannst du die Teile des serverinfo-Befehls anpassen" - }, + "id": "ID", + "owner": "Owner", + "boosts": "Boosts", + "emojiCount": "Emoji-Count", + "region": "Region", + "roleCount": "Role-Count", + "rulesChannel": "Rules-Channel", + "dcSystemChannel": "Discord-System-Channel", + "verificationLevel": "Verification-Level", + "banCount": "Bans", + "createdAt": "Created at", + "members": "Members", + "channels": "Channels", + "features": "Features", + "noFeaturesEnabled": "No features enabled", + "afkChannel": "AFK-Channel", + "stickerCount": "Sticker-Count" + }, + "description": "You can change the parts of the serverinfo-command here", "type": "keyed", "content": { "key": "string", @@ -61,57 +35,29 @@ }, { "name": "userinfo", - "humanName": { - "de": "Userinfo" - }, + "humanName": "User Info", "default": { - "en": { - "id": "ID", - "tag": "Tag", - "currentStatus": "Current status", - "createdAt": "Account created at", - "joinedAt": "Joined Server at", - "nickname": "Nickname", - "boosterSince": "Server-Booster since", - "displayColor": "Display-Color", - "currentVoiceChannel": "Current Voice-Channel", - "highestRole": "Highest role", - "hoistRole": "Hoisted role", - "birthday": "Birthday", - "permissions": "Permissions", - "xp": "XP", - "invited-by": "Invited by", - "invites": "Invites", - "level": "Level", - "messages": "Messages", - "noPermissions": "This user does not have any permissions ):" - }, - "de": { - "id": "ID", - "tag": "Tag", - "currentStatus": "Aktueller Status", - "createdAt": "Account erstellt am", - "joinedAt": "Server beigetreten am", - "nickname": "Nickname", - "boosterSince": "Server-Booster seit", - "displayColor": "Anzeigefarbe", - "currentVoiceChannel": "Aktueller Sprachkanal", - "highestRole": "Höchste Rolle", - "hoistRole": "Gelistete Rolle", - "birthday": "Geburtstag", - "permissions": "Berechtigungen", - "xp": "XP", - "level": "Level", - "messages": "Nachrichten", - "noPermissions": "Dieser Nutzer hat keine Berechtigungen ):", - "invited-by": "Invited by", - "invites": "Invites" - } - }, - "description": { - "en": "You can change the parts of the userinfo-command here", - "de": "Hier kannst du die Teile des userinfo-Befehls anpassen" - }, + "id": "ID", + "tag": "Tag", + "currentStatus": "Current status", + "createdAt": "Account created at", + "joinedAt": "Joined Server at", + "nickname": "Nickname", + "boosterSince": "Server-Booster since", + "displayColor": "Display-Color", + "currentVoiceChannel": "Current Voice-Channel", + "highestRole": "Highest role", + "hoistRole": "Hoisted role", + "birthday": "Birthday", + "permissions": "Permissions", + "xp": "XP", + "invited-by": "Invited by", + "invites": "Invites", + "level": "Level", + "messages": "Messages", + "noPermissions": "This user does not have any permissions ):" + }, + "description": "You can change the parts of the userinfo-command here", "type": "keyed", "content": { "key": "string", @@ -121,49 +67,25 @@ }, { "name": "channelInfo", - "humanName": { - "de": "Channelinfo" - }, + "humanName": "Channel Info", "default": { - "en": { - "id": "ID", - "createdAt": "Created at", - "type": "Type", - "name": "Name", - "parent": "Category", - "topic": "Topic", - "position": "Current position in category", - "stageInstanceName": "Stage topic", - "stageInstancePrivacy": "Stage Privacy", - "threadArchivedAt": "Thread archived at", - "threadAutoArchiveDuration": "Thread auto Archive Duration", - "threadOwner": "Thread-Owner", - "threadMessages": "Messages in thread", - "threadMemberCount": "Members in this thread", - "membersInChannel": "Members currently in this channel" - }, - "de": { - "id": "ID", - "createdAt": "Erstellt am", - "type": "Typ", - "name": "Name", - "parent": "Kategorie", - "topic": "Kanalbeschreibung", - "position": "Aktuelle Position in der Kategorie", - "stageInstanceName": "Stage Thema", - "stageInstancePrivacy": "Stage Privacy", - "threadArchivedAt": "Thread archiviert am", - "threadAutoArchiveDuration": "Automatische Threadarchivierungsdauer", - "threadOwner": "Thread-Besitzer", - "threadMessages": "Nachrichten im Thread", - "threadMemberCount": "Mitglieder in diesem Thread", - "membersInChannel": "Mitglieder, die sich aktuell in diesem Kanal befinden" - } - }, - "description": { - "en": "You can change the parts of the channelinfo-command here", - "de": "Hier kannst du die Teile des channelinfo-Befehls anpassen" - }, + "id": "ID", + "createdAt": "Created at", + "type": "Type", + "name": "Name", + "parent": "Category", + "topic": "Topic", + "position": "Current position in category", + "stageInstanceName": "Stage topic", + "stageInstancePrivacy": "Stage Privacy", + "threadArchivedAt": "Thread archived at", + "threadAutoArchiveDuration": "Thread auto Archive Duration", + "threadOwner": "Thread-Owner", + "threadMessages": "Messages in thread", + "threadMemberCount": "Members in this thread", + "membersInChannel": "Members currently in this channel" + }, + "description": "You can change the parts of the channelinfo-command here", "type": "keyed", "content": { "key": "string", @@ -173,35 +95,18 @@ }, { "name": "roleInfo", - "humanName": { - "de": "Roleinfo" - }, + "humanName": "Role Info", "default": { - "en": { - "id": "ID", - "createdAt": "Created at", - "color": "Color", - "name": "Name", - "position": "Current position", - "memberWithThisRoleCount": "Count of members with this role", - "memberWithThisRole": "Members with this role", - "permissions": "Permissions" - }, - "de": { - "id": "ID", - "createdAt": "Erstellt am", - "color": "Farbe", - "name": "Name", - "position": "Aktuelle Position", - "memberWithThisRoleCount": "Anzahl der Mitglieder mit dieser Rolle", - "memberWithThisRole": "Mitglieder mit dieser Rolle", - "permissions": "Berechtigungen" - } - }, - "description": { - "en": "You can change the parts of the roleinfo-command here", - "de": "Hier kannst du die Teile des serverinfo-Befehls anpassen" - }, + "id": "ID", + "createdAt": "Created at", + "color": "Color", + "name": "Name", + "position": "Current position", + "memberWithThisRoleCount": "Count of members with this role", + "memberWithThisRole": "Members with this role", + "permissions": "Permissions" + }, + "description": "You can change the parts of the roleinfo-command here", "type": "keyed", "content": { "key": "string", @@ -211,83 +116,45 @@ }, { "name": "user_not_found", - "humanName": { - "de": "Nutzer nicht gefunden" - }, - "default": { - "en": "I could not find this user - try using an ID or a mention", - "de": "Dieser Nutzer konnte nicht gefunden werden - versuche eine ID oder eine Erwähnung zu verwenden" - }, - "description": { - "en": "Message that gets send if the user provided an invalid userid", - "de": "Nachricht, die gesendet wird, wenn der Nutzer eine Ungültige NutzerID angibt" - }, + "humanName": "User Not Found", + "default": "I could not find this user - try using an ID or a mention", + "description": "Message that gets send if the user provided an invalid userid", "type": "string", "allowEmbed": true }, { "name": "channel_not_found", - "humanName": { - "de": "Kanal nicht gefunden" - }, - "default": { - "en": "I could not find this channel - try using an ID or a mention", - "de": "Dieser Kanal konnte nicht gefunden werden - versuche eine ID oder eine Erwähnung zu verwenden" - }, - "description": { - "en": "Message that gets send if the user provided an invalid userid", - "de": "Nachricht, die gesendet wird, wenn der Nutzer eine Ungültige KanalID angibt" - }, + "humanName": "Channel Not Found", + "default": "I could not find this channel - try using an ID or a mention", + "description": "Message that gets send if the user provided an invalid userid", "type": "string", "allowEmbed": true }, { "name": "role_not_found", - "humanName": { - "de": "Rolle nicht gefunden" - }, - "default": { - "en": "I could not find this role - try using an ID or a mention", - "de": "Diese Rolle konnte nicht gefunden werden - versuche eine ID oder eine Erwähnung zu verwenden" - }, - "description": { - "en": "Message that gets send if the user provided an invalid roleid", - "de": "Nachricht, die gesendet wird, wenn der Nutzer eine Ungültige RollenID angibt" - }, + "humanName": "Role Not Found", + "default": "I could not find this role - try using an ID or a mention", + "description": "Message that gets send if the user provided an invalid roleid", "type": "string", "allowEmbed": true }, { "name": "avatarMsg", - "humanName": { - "de": "Avatar-Nachricht" - }, - "default": { - "en": "Here is the avatar: (Please reminder that the image may be protected under copyright-law)", - "de": "Hier ist der Avatar: (Bitte beachte, dass das Bild eventuell urheberrechtlich geschützt ist)" - }, - "description": { - "en": "Message that gets send if the user requested an avatar", - "de": "Nachricht, die gesendet wird, wenn ein Nutzer einen Avatar anfragt" - }, + "humanName": "Avatar Message", + "default": "Here is the avatar: (Please reminder that the image may be protected under copyright-law)", + "description": "Message that gets send if the user requested an avatar", "type": "string", "allowEmbed": true, "params": [ { "name": "avatarUrl", - "description": { - "en": "URL to the avatar", - "de": "URL zum Avatar" - } + "description": "URL to the avatar" }, { "name": "tag", - "description": { - "en": "Tag of the requested user", - "de": "Tag des gewünschten Nutzers" - } + "description": "Tag of the requested user" } ] } ] -} \ No newline at end of file +} diff --git a/modules/levels/commands/leaderboard.js b/modules/levels/commands/leaderboard.js index 71c7ba27..9aa104f7 100644 --- a/modules/levels/commands/leaderboard.js +++ b/modules/levels/commands/leaderboard.js @@ -3,7 +3,8 @@ const { truncate, formatNumber, formatDiscordUserName, - parseEmbedColor + parseEmbedColor, + safeSetFooter } = require('../../../src/functions/helpers'); const {MessageEmbed} = require('discord.js'); const {localize} = require('../../../src/functions/localize'); @@ -34,13 +35,13 @@ module.exports.run = async function (interaction) { */ function addSite(fields) { const embed = new MessageEmbed() - .setFooter({text: interaction.client.strings.footer, iconURL: interaction.client.strings.footerImgUrl}) .setColor(parseEmbedColor(moduleStrings.leaderboardEmbed.color || 'GREEN')) .setThumbnail(interaction.guild.iconURL()) .setTitle(moduleStrings.leaderboardEmbed.title) .setDescription(moduleStrings.leaderboardEmbed.description) .addField('\u200b', '\u200b') .addFields(fields); + safeSetFooter(embed, interaction.client); if (thisUser) embed.addField('\u200b', '\u200b').addField(moduleStrings.leaderboardEmbed.your_level, moduleStrings.leaderboardEmbed.you_are_level_x_with_x_xp.split('%level%').join(displayLevel(thisUser['level'], client)).split('%xp%').join(formatNumber(thisUser['xp']))); sites.push(embed); } diff --git a/modules/levels/commands/manage-levels.js b/modules/levels/commands/manage-levels.js index 7e2c703c..bfede050 100644 --- a/modules/levels/commands/manage-levels.js +++ b/modules/levels/commands/manage-levels.js @@ -25,18 +25,18 @@ async function runXPAction(interaction, newXP) { if (user.xp < 0) return interaction.editReply({ content: '⚠️ ' + localize('levels', 'negative-xp') }); + if (!Number.isFinite(user.xp) || user.xp > 1e12) return interaction.editReply({ + content: '⚠️ ' + localize('levels', 'xp-out-of-range') + }); - function runXPCheck() { + let guard = 0; + while (guard++ < 1_000_000) { const nextLevelXp = calculateLevelXP(interaction.client, user.level + 1); - if (nextLevelXp <= user.xp) { - user.level = user.level + 1; - fixLevelRoles(interaction, member, user.level); - runXPCheck(); - } + if (!Number.isFinite(nextLevelXp) || nextLevelXp > user.xp) break; + user.level = user.level + 1; + await fixLevelRoles(interaction, member, user.level); } - runXPCheck(); - await user.save(); interaction.client.logger.info(localize('levels', 'manipulated', { @@ -84,12 +84,19 @@ async function runLevelAction(interaction, newLevel) { if (!user) return interaction.editReply({ content: '⚠️ ' + localize('levels', 'cheat-no-profile') }); + const isZero = newLevel(user.level) === user.level; user.level = newLevel(user.level); - if (interaction.client.configurations['levels']['config'].startFromZero) user.level = user.level + 1; + if (interaction.client.configurations['levels']['config'].startFromZero && !isZero) user.level = user.level + 1; if (user.level < 1) return interaction.editReply({ content: '⚠️ ' + localize('levels', 'negative-level') }); + if (!Number.isFinite(user.level) || user.level > 1e6) return interaction.editReply({ + content: '⚠️ ' + localize('levels', 'level-out-of-range') + }); user.xp = calculateLevelXP(interaction.client, user.level); + if (!Number.isFinite(user.xp) || user.xp > 1e12) return interaction.editReply({ + content: '⚠️ ' + localize('levels', 'xp-out-of-range') + }); await fixLevelRoles(interaction, member, user.level); diff --git a/modules/levels/commands/profile.js b/modules/levels/commands/profile.js index ba6dc03f..576ff9fd 100644 --- a/modules/levels/commands/profile.js +++ b/modules/levels/commands/profile.js @@ -2,7 +2,8 @@ const { embedType, formatDate, formatNumber, - parseEmbedColor + parseEmbedColor, + safeSetFooter } = require('../../../src/functions/helpers'); const {MessageEmbed} = require('discord.js'); const {localize} = require('../../../src/functions/localize'); @@ -31,10 +32,6 @@ module.exports.run = async function (interaction) { const nextLevelXp = calculateLevelXP(interaction.client, user.level + 1); const embed = new MessageEmbed() - .setFooter({ - text: interaction.client.strings.footer, - iconURL: interaction.client.strings.footerImgUrl - }) .setColor(parseEmbedColor(moduleStrings.embed.color || 'GREEN')) .setThumbnail(member.user.avatarURL({forceStatic: false})) .setTitle(moduleStrings.embed.title.replaceAll('%username%', member.user.username)) @@ -43,13 +40,15 @@ module.exports.run = async function (interaction) { .addField(moduleStrings.embed.xp, `${formatNumber(isMaxLevel(user.level, interaction.client) ? calculateLevelXP(interaction.client, interaction.client.configurations['levels']['config'].maximumLevel) : user.xp)}/${isMaxLevel(user.level, interaction.client) ? '∞' : formatNumber(nextLevelXp)}`, true) .addField(moduleStrings.embed.level, displayLevel(user.level, interaction.client), true); + safeSetFooter(embed, interaction.client); + const roleFactor = getMemberRoleFactor(member); if (roleFactor !== 1) { let roleString = ''; for (const role of member.roles.cache.filter(f => moduleConfig['multiplication_roles'][f.id]).values()) { roleString = roleString + `\n* <@&${role.id}>: ${moduleConfig['multiplication_roles'][role.id]}x`; } - embed.addField(moduleStrings.embed.roleFactor, `${roleString}\n${localize('levels', 'role-factors-total', {f: roleFactor})}`, true); + embed.addField(moduleStrings.embed.roleFactor, `${roleString}\n${localize('levels', 'role-factors-total', {f: formatNumber(roleFactor, {maximumFractionDigits: 2})})}`, true); } embed.addField(moduleStrings.embed.joinedAt, formatDate(member.joinedAt), true); interaction.reply({ diff --git a/modules/levels/configs/config.json b/modules/levels/configs/config.json index ddd03df4..5369b6e2 100644 --- a/modules/levels/configs/config.json +++ b/modules/levels/configs/config.json @@ -1,12 +1,6 @@ { - "description": { - "en": "Configure the function of the module here", - "de": "Stelle hier die Funktionen des Modules ein" - }, - "humanName": { - "en": "Configuration", - "de": "Konfiguration" - }, + "description": "Configure the function of the module here", + "humanName": "Configuration", "filename": "config.json", "commandsWarnings": { "normal": [ @@ -16,144 +10,82 @@ "content": [ { "name": "min-xp", - "humanName": { - "en": "XP given at least for messages", - "de": "Für Nachrichten mindestens gegebenes XP" - }, - "default": { - "en": 25, - "de": 25 - }, - "description": { - "en": "How much XP the user gets at least for each message", - "de": "So viel XP bekommt ein Benutzer mindestens pro Nachricht" - }, - "type": "integer" + "humanName": "XP given at least for messages", + "default": 25, + "description": "How much XP the user gets at least for each message", + "type": "integer", + "category": "xp" }, { "name": "max-xp", - "humanName": { - "en": "XP given at most for messages", - "de": "Für Nachrichten maximal gegebenes XP" - }, - "default": { - "en": 65, - "de": 65 - }, - "description": { - "en": "How much XP the user gets at most for each messages", - "de": "So viel XP bekommt ein Benutzer maximal pro Nachricht" - }, - "type": "integer" + "humanName": "XP given at most for messages", + "default": 65, + "description": "How much XP the user gets at most for each messages", + "type": "integer", + "category": "xp" }, { "name": "voiceXPPerMinute", "type": "float", - "default": { - "en": 0.5 - }, - "humanName": { - "en": "XP given per Voice Minute", - "de": "Pro Sprachminute vergebenes XP" - }, - "description": { - "en": "How many XP will be given to users per minute when they are in a voice channel with other members. No XP will be given if they are alone in their channel or are muted or deafened. Numbers will be rounded and XP will be given every 15 minutes or when the user leaves the channel.", - "de": "Wie viel XP Nutzer pro Minute erhalten, wenn sie sich in einem Sprachkanal mit anderen Nutzern befinden. Es wird kein XP vergeben, wenn sie alleine in einem Kanal sind oder stummgeschaltet sind oder den Ton deaktiviert haben. Zahlen werden gerundet und XP wird alle 15 Minuten vergeben, oder wenn der Nutzer den Kanal verlässt." - } + "default": 0.5, + "humanName": "XP given per Voice Minute", + "description": "How many XP will be given to users per minute when they are in a voice channel with other members. No XP will be given if they are alone in their channel or are muted or deafened. Numbers will be rounded and XP will be given every 15 minutes or when the user leaves the channel.", + "category": "xp" }, { "name": "cooldown", - "humanName": { - "en": "Cooldown" - }, - "default": { - "en": 1500, - "de": 1500 - }, - "description": { - "en": "In ms. How much cooldown there is between each XP getting", - "de": "In Millisekunden! So viel Zeit muss ein Nutzer zwischen jeder Nachricht mit XP warten" - }, - "type": "integer" + "humanName": "Cooldown", + "default": 1500, + "description": "In ms. How much cooldown there is between each XP getting", + "type": "integer", + "category": "xp" }, { "name": "curveType", "type": "select", "content": [ { - "displayName": { - "en": "Easy Linear", - "de": "Einfacherer Linearfunktion" - }, + "displayName": "Easy Linear", "value": "EXPONENTIAL" }, { - "displayName": { - "en": "Default Linear", - "de": "Standardmässige Linearfunktion" - }, + "displayName": "Default Linear", "value": "LINEAR" }, { - "displayName": { - "en": "Exponentiation (softer start, harder leveling after level 14)", - "de": "Potenzfunktion (leichter start, ab Level 14 härter)" - }, + "displayName": "Exponentiation (softer start, harder leveling after level 14)", "value": "EXPONENTIATION" }, { "value": "CUSTOM", - "displayName": { - "en": "Custom formula (dangerous!)", - "de": "Eigene Formel (gefährlich!)" - } + "displayName": "Custom formula (dangerous!)" } ], - "humanName": { - "en": "Type of the leveling curve", - "de": "Art der Levelingkurve" - }, - "default": { - "en": "LINEAR" - }, - "description": { - "en": "Type of the leveling curve. The exponential curve is recommended, as archiving new levels gets harder the higher your level is. Leveling is always the same if you use the linear curve.", - "de": "Art der Levelingkurve. Die exponentielle Kurve wird empfohlen, da mit dieser das Aufsteigen von Leveln schwerer wird je höher das eigene Level ist. Mit der linearen Kurve ist das Aufsteigen zum nächsten Level für alle gleich schwer." - }, + "humanName": "Type of the leveling curve", + "default": "LINEAR", + "description": "Type of the leveling curve. The exponential curve is recommended, as archiving new levels gets harder the higher your level is. Leveling is always the same if you use the linear curve.", "links": [ { - "label": { - "en": "Calculate how much XP is needed to level up", - "de": "Berechne, wie viel XP zum Aufsteigen notwendig ist" - }, + "label": "Calculate how much XP is needed to level up", "url": "https://scootk.it/level-calculator" } - ] + ], + "category": "xp" }, { "name": "customLevelCurve", - "default": { - "en": "" - }, + "default": "", "allowNull": true, - "humanName": { - "en": "Custom Level Formula (if enabled)", - "de": "Eigene Levelformel (wenn aktiviert)" - }, + "humanName": "Custom Level Formula (if enabled)", "type": "string", "links": [ { - "label": { - "en": "Calculate how much XP is needed to level up", - "de": "Berechne, wie viel XP zum Aufsteigen notwendig ist" - }, + "label": "Calculate how much XP is needed to level up", "url": "https://scootk.it/level-calculator" } ], - "description": { - "en": "Your custom leveling formula. Use the x variable (and no other variables). The result of the formula should be the required XP to reach level x (your variable). Example: \"x*750+((x-1)*500)\" (our default level curve)", - "de": "Deine eigene Levelformel. Nutze nur die x Variabel (und keine andere Variablen). Das Ergebnis deiner Formel sollte die XP-Anzahl sein, die notwendig ist, um Level x zu erreichen (deine Variabel). Beispiel: \"x*750+((x-1)*500)\" (unsere Standartkurve)" - } + "description": "Your custom leveling formula. Use the x variable (and no other variables). The result of the formula should be the required XP to reach level x (your variable). Example: \"x*750+((x-1)*500)\" (our default level curve)", + "category": "xp" }, { "name": "levelUpMessagesConditions", @@ -163,306 +95,204 @@ "only-role-rewards", "none" ], - "humanName": { - "de": "Welche Level-Up-Nachrichten sollen gesendet werden?", - "en": "Which Level-Up-Messages should get sent?" - }, - "default": { - "en": "all" - }, - "description": { - "en": "This settings changes in which cases a level up message should be sent. With the setting \"all\", level up messages will be sent at every level up. With the setting \"only-role-rewards\", level up messages will only be sent if the new level has a role reward. With the \"none\" setting, no level up messages will be sent.", - "de": "Diese Einstellung verändert, welche Art von Level-Up-Nachrichten gesendet werden. Mit der Einstellung \"all\", werden Level-Up-Nachrichten bei jedem Level-Up versendet. Mit der Einstellung \"only-role-rewards\" werden Level-Up-Nachrichten nur gesandt, wenn das neue Level eine Rollenbelohnung hat. Wenn die Einstellung \"none\" gewählt ist, werden keine Level-Up-Nachrichten verschickt." - } + "humanName": "Which Level-Up-Messages should get sent?", + "default": "all", + "description": "This settings changes in which cases a level up message should be sent. With the setting \"all\", level up messages will be sent at every level up. With the setting \"only-role-rewards\", level up messages will only be sent if the new level has a role reward. With the \"none\" setting, no level up messages will be sent.", + "category": "messages" }, { "name": "level_up_channel_id", - "humanName": { - "en": "Level-Up-Channel" - }, - "default": { - "en": "" - }, - "description": { - "en": "Channel in which Level-Up-Messages should get send. (Leave empty to disable)", - "de": "Channel, in den die Level-Up-Nachrichten gesendet werden. (Leerlassen, um sie einfach in den aktuellen Channel zu schicken))" - }, + "humanName": "Level-Up-Channel", + "default": "", + "description": "Channel in which Level-Up-Messages should get send. (Leave empty to disable)", "type": "channelID", - "allowNull": true + "allowNull": true, + "category": "messages" }, { "name": "sortLeaderboardBy", - "humanName": { - "en": "Leaderboard-Sort-Category", - "de": "Ranglisten-Sortierung" - }, - "default": { - "en": "levels", - "de": "levels" - }, - "description": { - "en": "How the leaderboard should be sorted", - "de": "Wähle aus, wie der /leaderboard-command aussehen werden soll" - }, + "humanName": "Leaderboard-Sort-Category", + "default": "levels", + "description": "How the leaderboard should be sorted", "type": "select", "content": [ "levels", "xp" - ] + ], + "category": "leaderboard" }, { "name": "blacklisted_channels", - "humanName": { - "en": "Blacklisted Channels", - "de": "Channel ohne XP" - }, + "humanName": "Blacklisted Channels", "channelTypes": [ "GUILD_TEXT", "GUILD_NEWS", "GUILD_VOICE", "GUILD_FORUM" ], - "default": { - "en": [], - "de": [] - }, - "description": { - "en": "Blacklisted-Channels in which users can not earn XP", - "de": "Channel, in denen kein XP gesammelt werden kann" - }, + "default": [], + "description": "Blacklisted-Channels in which users can not earn XP", "type": "array", - "content": "channelID" + "content": "channelID", + "category": "xp" }, { "name": "blacklistedRoles", - "humanName": { - "en": "Blacklisted roles", - "de": "Rollen, die kein XP sammeln" - }, + "humanName": "Blacklisted roles", "type": "array", "content": "roleID", - "default": { - "en": [] - }, - "description": { - "de": "Diese Rollen werden kein XP für ihre Nachrichten erhalten", - "en": "These roles won't receive XP when writing messages" - } + "default": [], + "description": "These roles won't receive XP when writing messages", + "category": "xp" }, { "name": "reward_roles", - "humanName": { - "en": "Level Reward roles", - "de": "Level-Belohnung-Rollen" - }, - "default": { - "en": {}, - "de": {} - }, - "description": { - "de": "Level, bei denen der Nutzer eine Rolle bekommt. Parameter 1: Level, Parameter 2: Rollen-ID", - "en": "Level at which users should get roles. Parameter 1: Level, Parameter 2: Role-ID" - }, + "humanName": "Level Reward roles", + "default": {}, + "description": "Level at which users should get roles. Parameter 1: Level, Parameter 2: Role-ID", "type": "keyed", "content": { "key": "integer", "value": "roleID" - } + }, + "category": "roles" }, { "name": "multiplication_roles", - "humanName": { - "en": "XP Multiplication Roles", - "de": "XP-Multiplikator Rollen" - }, - "default": { - "en": {}, - "de": {} - }, - "description": { - "en": "Allows you to configure roles that have a higher multiplication factor than normal (default value is 1). If a user has more than one of the configured roles, the multiplication factors get multiplied together before multiplying the result with the amount of XP the user receives for their message.", - "de": "Erlaubt es dir, den Multiplikationsfaktor von bestimmten Rollen anzupassen. Standardmäßig haben Rollen einen Wert von 1. Bevor der XP Wert für eine Nachricht an den Nutzer gegeben wird, werden alle Faktoren von Rollen miteinander multipliziert und das Ergebnis dann mal den XP-Wert genommen." - }, + "humanName": "XP Multiplication Roles", + "default": {}, + "description": "Allows you to configure roles that have a higher multiplication factor than normal (default value is 1). If a user has more than one of the configured roles, the multiplication factors get multiplied together before multiplying the result with the amount of XP the user receives for their message.", "type": "keyed", "content": { "key": "roleID", "value": "float" - } + }, + "category": "xp" }, { "name": "multiplication_channels", - "humanName": { - "en": "XP Multiplication Channels", - "de": "XP-Multiplikator Kanäle" - }, - "default": { - "en": {}, - "de": {} - }, - "description": { - "en": "Allows you to configure channels that have a higher multiplication factor than normal (default value is 1). Messages sent in these channels will have their XP value multiplied by the multiplier configured here.", - "de": "Erlaubt es dir, den Multiplikationsfaktor von bestimmten Kanälen anzupassen. Standardmäßig haben Rollen einen Wert von 1. Die XP-Werte von Nachrichten, die in hier konfigurierten Kanälen gesendet werden, werden mit den hier eingestellten Multiplikator multipliziert." - }, + "humanName": "XP Multiplication Channels", + "default": {}, + "description": "Allows you to configure channels that have a higher multiplication factor than normal (default value is 1). Messages sent in these channels will have their XP value multiplied by the multiplier configured here.", "type": "keyed", "content": { "key": "channelID", "value": "float" - } + }, + "category": "xp" }, { "name": "onlyTopLevelRole", - "humanName": { - "en": "Only keep highest Level-Role", - "de": "Nur die höchste Level-Rolle behalten" - }, - "default": { - "en": false - }, - "description": { - "en": "If enabled, all previous level roles a user had will get removed, when they advance to a new level.", - "de": "Wenn aktiviert, werden alle vorherigen Level-Rollen, die ein Nutzer hatte, entfernt, wenn dieser ein neues Level erreicht." - }, - "type": "boolean" + "humanName": "Only keep highest Level-Role", + "default": false, + "description": "If enabled, all previous level roles a user had will get removed, when they advance to a new level.", + "type": "boolean", + "category": "roles" }, { "name": "reset-on-leave", - "humanName": { - "en": "Rest Level on leave", - "de": "Level beim Verlassen zurücksetzen" - }, - "default": { - "en": false - }, - "description": { - "en": "If enabled, all levels and the XP of a user will be deleted, when they leave your server.", - "de": "Wenn aktiviert, werden alle Level und das XP eines Nutzers gelöscht, wenn er den Server verlässt." - }, - "type": "boolean" + "humanName": "Rest Level on leave", + "default": false, + "description": "If enabled, all levels and the XP of a user will be deleted, when they leave your server.", + "type": "boolean", + "category": "general" }, { "name": "randomMessages", - "humanName": { - "en": "Random messages", - "de": "Zufällige Nachrichten" - }, - "default": { - "en": false - }, - "description": { - "de": "Wenn aktiviert wird das Modul die Level-Up-Nachricht zufällig auswählen und nicht die in Nachrichten angegebene verwenden", - "en": "If enabled the module will randomly select a messages from random-levelup-messages and ignore the one set in strings" - }, - "type": "boolean" + "humanName": "Random messages", + "default": false, + "description": "If enabled the module will randomly select a messages from random-levelup-messages and ignore the one set in strings", + "type": "boolean", + "category": "messages" }, { "name": "leaderboard-channel", - "humanName": { - "en": "Live Leaderboard-Channel", - "de": "Live Ranglisten-Channel" - }, - "default": { - "en": "" - }, - "description": { - "de": "Wenn gesetzt wird der Bot in diesen Channel eine Nachricht senden, welche die aktuellen Level der Nutzern enthält", - "en": "If set, the bot will send a messages in this channel with the current leaderboard and edit it every five minutes" - }, + "humanName": "Live Leaderboard-Channel", + "default": "", + "description": "If set, the bot will send a messages in this channel with the current leaderboard and edit it every five minutes", "type": "channelID", "content": [ "GUILD_TEXT" ], - "allowNull": true + "allowNull": true, + "category": "leaderboard" }, { "name": "leaderboard-channel-max-amount", - "humanName": { - "en": "Maximum amount of users displayed in live leaderboard Channel", - "de": "Maximale Anzahl von Nutzern im Live Ranglistenkanal" - }, - "default": { - "en": 15 - }, + "humanName": "Maximum amount of users displayed in live leaderboard Channel", + "default": 15, "maxValue": 25, - "description": { - "de": "Dies ist die Anzahl von Nutzern, die in der Live Rangliste angezeigt werden sollen. /leaderboard zeigt weiterhin die vollständige Rangliste.", - "en": "This is the maximum amount of users displayed in the Live Leaderboard channel. /leaderboard will still show the full leaderboard." - }, - "type": "integer" + "description": "This is the maximum amount of users displayed in the Live Leaderboard channel. /leaderboard will still show the full leaderboard.", + "type": "integer", + "category": "leaderboard" }, { "name": "maximumLevelEnabled", - "humanName": { - "en": "Enable maximum level?", - "de": "Maximales Level aktivieren?" - }, - "default": { - "en": false - }, - "description": { - "en": "If enabled, users can only level until they reach the configured maximum level. After that, they can't level up and can't earn XP. Can be enabled retroactively.", - "de": "Wenn aktiviert können Nutzer nur ein bestimmtes Level erreichen. Sobald sie dieses Level erreicht haben, können sie nicht weiter aufsteigen oder weiter XP verdienen. Kann rückwirkend aktiviert werden." - }, - "type": "boolean" + "humanName": "Enable maximum level?", + "default": false, + "description": "If enabled, users can only level until they reach the configured maximum level. After that, they can't level up and can't earn XP. Can be enabled retroactively.", + "type": "boolean", + "category": "general" }, { "dependsOn": "maximumLevelEnabled", "name": "maximumLevel", - "humanName": { - "en": "Maximum level", - "de": "Maximales Level" - }, - "default": { - "en": 200 - }, - "description": { - "en": "Once a user reaches this level, they neither earn more XP nor level up anymore.", - "de": "Sobald ein Nutzer dieses Level erreicht hat, kann dieser weder mehr XP verdienen noch weiter Level aufsteigen." - }, - "type": "integer" + "humanName": "Maximum level", + "default": 200, + "description": "Once a user reaches this level, they neither earn more XP nor level up anymore.", + "type": "integer", + "category": "general" }, { "name": "startFromZero", - "humanName": { - "en": "Start with Level 0?", - "de": "Von Level 0 starten?" - }, - "default": { - "en": false - }, - "description": { - "en": "If enabled, the initial level of users will be displayed as zero. This doesn't affect leveling, this is a cosmetic setting and can be applied retroactively.", - "de": "Wenn aktiviert werden die Anfangslevel von Nutzern als null angezeigt. Das hat keinen Einfluss auf das Leveling, das ist eine kosmetische Einstellung und kann rückwirkend angewandt werden." - }, - "type": "boolean" + "humanName": "Start with Level 0?", + "default": false, + "description": "If enabled, the initial level of users will be displayed as zero. This doesn't affect leveling, this is a cosmetic setting and can be applied retroactively.", + "type": "boolean", + "category": "general" }, { "name": "useTags", - "humanName": { - "en": "Use User's Tags instead of their Mention in the Leaderboard-Channel-Embed", - "de": "Nutze den Tag der Nutzer, anstatt eine Erwähnung im Ranglisten-Channel-Embed" - }, - "default": { - "en": false - }, - "description": { - "en": "If enabled, the bot will use the tag of users in the Leaderboard-Channel-Embed instead of their mention.", - "de": "Wenn aktiviert, wird im Ranglisten-Channel-Embed der Tag des Nutzers angezeigt und nicht eine Erwähnung (bei großen Servern empfohlen)" - }, - "type": "boolean" + "humanName": "Use User's Tags instead of their Mention in the Leaderboard-Channel-Embed", + "default": false, + "description": "If enabled, the bot will use the tag of users in the Leaderboard-Channel-Embed instead of their mention.", + "type": "boolean", + "category": "general" }, { "name": "allowCheats", - "humanName": { - "en": "Cheats" - }, - "default": { - "en": false - }, - "description": { - "en": "If enabled admins can change the XP of other users (not recommended (please leave it off if you want to have a fair levelling system!!!))", - "de": "Wenn aktiviert können Administratoren die XP von anderen Nutzern editieren (nicht empfohlen, wenn du einen coolen, fairen Server haben willst (wirklich nicht!!!)))" - }, - "type": "boolean" + "humanName": "Cheats", + "default": false, + "description": "If enabled admins can change the XP of other users (not recommended (please leave it off if you want to have a fair levelling system!!!))", + "type": "boolean", + "category": "general" + } + ], + "categories": [ + { + "id": "general", + "icon": "fas fa-gears", + "displayName": "General Settings" + }, + { + "id": "xp", + "icon": "fas fa-arrow-up-1-9", + "displayName": "XP Settings" + }, + { + "id": "leaderboard", + "icon": "fas fa-ranking-stars", + "displayName": "Leaderboard" + }, + { + "id": "roles", + "icon": "fa-solid fa-users", + "displayName": "Level Roles" + }, + { + "id": "messages", + "icon": "fas fa-comment-dots", + "displayName": "Level-up Messages" } ] } \ No newline at end of file diff --git a/modules/levels/configs/random-levelup-messages.json b/modules/levels/configs/random-levelup-messages.json index 4f632475..04c02a6d 100644 --- a/modules/levels/configs/random-levelup-messages.json +++ b/modules/levels/configs/random-levelup-messages.json @@ -1,28 +1,14 @@ { - "description": { - "en": "If enabled, the bot will randomly select a message from here", - "de": "Wenn aktiviert, wird der Bot zufällige eine Nachricht von hier auswählen" - }, - "humanName": { - "en": "Random-Level-Up-Messages", - "de": "Zufällige Level-Up-Nachrichten" - }, + "description": "If enabled, the bot will randomly select a message from here", + "humanName": "Random-Level-Up-Messages", "filename": "random-levelup-messages.json", "configElements": true, "content": [ { "name": "type", - "humanName": { - "de": "Nachrichtentyp" - }, - "default": { - "en": "normal", - "de": "normal" - }, - "description": { - "en": "Type of this message", - "de": "Typ dieser Nachricht" - }, + "humanName": "Message Type", + "default": "normal", + "description": "Type of this message", "type": "select", "content": [ "normal", @@ -31,64 +17,39 @@ }, { "name": "message", - "humanName": { - "de": "Nachrichten" - }, + "humanName": "Messages", "allowGeneratedImage": true, - "default": { - "en": "" - }, - "description": { - "en": "Messages which should be send", - "de": "Nachrichten, die gesendet werden sollen" - }, + "default": "", + "description": "Messages which should be send", "type": "string", "allowEmbed": true, "params": [ { "name": "mention", - "description": { - "en": "Mention of the user", - "de": "Erwähnung des Nutzers" - } + "description": "Mention of the user" }, { "name": "avatarURL", "isImage": true, - "description": { - "en": "Avatar of the user", - "de": "Profilbild des Nutzers" - } + "description": "Avatar of the user" }, { "name": "username", - "description": { - "en": "Username of the user", - "de": "Nutzername des Nutzers" - } + "description": "Username of the user" }, { "name": "tag", - "description": { - "en": "Tag of the user", - "de": "Tag des Nutzers" - } + "description": "Tag of the user" }, { "name": "newLevel", - "description": { - "en": "New level of the user", - "de": "Neues Level des Nutzers" - } + "description": "New level of the user" }, { "name": "role", - "description": { - "en": "Mention of the role (No ping, only if type = with-reward)", - "de": "Erwähnung der Rolle (Kein \"Ping\", nur, wenn Nachrichtentyp = with-reward)" - } + "description": "Mention of the role (No ping, only if type = with-reward)" } ] } ] -} \ No newline at end of file +} diff --git a/modules/levels/configs/special-levelup-messages.json b/modules/levels/configs/special-levelup-messages.json index 0808788f..b6523439 100644 --- a/modules/levels/configs/special-levelup-messages.json +++ b/modules/levels/configs/special-levelup-messages.json @@ -1,89 +1,51 @@ { - "description": { - "en": "If enabled, the bot will randomly select a message from here", - "de": "Wenn aktiviert, wird der Bot zufällige eine Nachricht von hier auswählen" - }, - "humanName": { - "en": "Selected messages", - "de": "Bestimmte Nachrichten" - }, + "description": "If enabled, the bot will randomly select a message from here", + "humanName": "Selected messages", "filename": "special-levelup-messages.json", "configElements": true, "content": [ { "name": "level", - "humanName": { - "de": "Level" - }, - "default": { - "en": "" - }, - "description": { - "en": "Level at which this messages should get send", - "de": "Level, bei welchem diese Nachricht gesendet werden soll" - }, + "humanName": "Level", + "default": "", + "description": "Level at which this messages should get send", "type": "integer" }, { "name": "message", "allowGeneratedImage": true, - "humanName": { - "de": "Nachricht" - }, - "default": { - "en": "" - }, - "description": { - "en": "Messages which should be send", - "de": "Nachricht, die gesendet werden soll" - }, + "humanName": "Message", + "default": "", + "description": "Messages which should be send", "type": "string", "allowEmbed": true, "params": [ { "name": "mention", - "description": { - "en": "Mention of the user", - "de": "Erwähnung des Nutzers" - } + "description": "Mention of the user" }, { "name": "avatarURL", "isImage": true, - "description": { - "en": "Avatar of the user", - "de": "Profilbild des Nutzers" - } + "description": "Avatar of the user" }, { "name": "username", - "description": { - "en": "Username of the user", - "de": "Nutzername des Nutzers" - } + "description": "Username of the user" }, { "name": "tag", - "description": { - "en": "Tag of the user", - "de": "Tag des Nutzers" - } + "description": "Tag of the user" }, { "name": "newLevel", - "description": { - "en": "New level of the user", - "de": "Neues Level des Nutzers" - } + "description": "New level of the user" }, { "name": "role", - "description": { - "en": "Mention of the role (No ping, only if level has reward)", - "de": "Erwähnung der Rolle (Kein \"Ping\", nur, wenn das Level eine Belohnung hat)" - } + "description": "Mention of the role (No ping, only if level has reward)" } ] } ] -} \ No newline at end of file +} diff --git a/modules/levels/configs/strings.json b/modules/levels/configs/strings.json index 2cfcd899..e6456b64 100644 --- a/modules/levels/configs/strings.json +++ b/modules/levels/configs/strings.json @@ -1,301 +1,188 @@ { - "description": { - "en": "Edit the messages and strings of the module here", - "de": "Stelle hier die Nachrichten des Modules ein" - }, - "humanName": { - "en": "Messages", - "de": "Nachrichten" - }, + "description": "Edit the messages and strings of the module here", + "humanName": "Messages", "filename": "strings.json", "content": [ { "name": "user_not_found", - "humanName": { - "en": "User not found", - "de": "Nutzer nicht gefunden" - }, - "default": { - "en": "⚠️ We do not have any records of this user", - "de": "⚠️ Dieser Nutzer hat anscheinend noch keine Nachricht verschickt" - }, - "description": { - "en": "This messages gets send if someone checks a profile of a user when the user never send a message", - "de": "Diese Nachricht wird verschickt, wenn ein eine Person sein Profil sehen will, aber noch kein XP hat" - }, + "humanName": "User not found", + "default": "⚠️ We do not have any records of this user", + "description": "This messages gets send if someone checks a profile of a user when the user never send a message", "type": "string", - "allowEmbed": true + "allowEmbed": true, + "category": "general" }, { "name": "embed", - "humanName": { - "de": "Profilembed" - }, + "humanName": "Profile Embed", "default": { - "en": { - "title": "%username%'s Profile", - "description": "You can find %username%'s profile here.", - "messages": "Message-Count", - "xp": "XP", - "level": "Level", - "joinedAt": "Joined server", - "roleFactor": "Role Factor(s)", - "color": "GREEN" - }, - "de": { - "title": "%username%'s Profil", - "description": "Das Profil von %username% findest du hier.", - "messages": "Nachrichten-Anzahl", - "roleFactor": "Rollen-Faktor(en)", - "xp": "XP", - "level": "Level", - "joinedAt": "Server beigetreten", - "color": "GREEN" - } - }, - "description": { - "en": "Embed which gets send if !profile gets executed", - "de": "Embed das gesendet wird, wenn /profile ausgeführt wird" - }, + "title": "%username%'s Profile", + "description": "You can find %username%'s profile here.", + "messages": "Message-Count", + "xp": "XP", + "level": "Level", + "joinedAt": "Joined server", + "roleFactor": "Role Factor(s)", + "color": "GREEN" + }, + "description": "Embed which gets send if !profile gets executed", "type": "keyed", "content": { "key": "string", "value": "string" }, - "disableKeyEdits": true + "disableKeyEdits": true, + "category": "general" }, { "name": "leaderboardEmbed", - "humanName": { - "de": "Ranglisten-Embed" - }, + "humanName": "Leaderboard Embed", "default": { - "en": { - "title": "Leaderboard", - "description": "You can find the level of every user here", - "and_x_more_people": "And %count% other members", - "more_level": "More Levels", - "x_levels_are_not_shown": "And **%count% Level** are not being displayed", - "your_level": "Your Level", - "you_are_level_x_with_x_xp": "You are currently on **Level %level%** with **%xp% XP**. See more with `/profile`.", - "joinedAt": "Joined server", - "color": "GREEN" - }, - "de": { - "title": "Rangliste", - "description": "Hier findest du das Level von jedem Nutzer", - "and_x_more_people": "Und %count% andere Mitglieder", - "more_level": "Mehr Level", - "x_levels_are_not_shown": "Und **%count% Level** werden nicht angezeigt", - "your_level": "Dein Level", - "you_are_level_x_with_x_xp": "Du bist aktuell auf **Level %level%** mit **%xp% XP**. Siehe mehr mit `/profile`.", - "joinedAt": "Server beigetreten", - "color": "GREEN" - } - }, - "description": { - "en": "This embed gets send if !leaderboard (!lb) gets executed", - "de": "Dieses Embed wird gesendet, wenn /leaderboard (/lb) ausgeführt wird" - }, + "title": "Leaderboard", + "description": "You can find the level of every user here", + "and_x_more_people": "And %count% other members", + "more_level": "More Levels", + "x_levels_are_not_shown": "And **%count% Level** are not being displayed", + "your_level": "Your Level", + "you_are_level_x_with_x_xp": "You are currently on **Level %level%** with **%xp% XP**. See more with `/profile`.", + "joinedAt": "Joined server", + "color": "GREEN" + }, + "description": "This embed gets send if !leaderboard (!lb) gets executed", "type": "keyed", "content": { "key": "string", "value": "string" }, - "disableKeyEdits": true + "disableKeyEdits": true, + "category": "leaderboard" }, { "name": "level_up_message", "allowGeneratedImage": true, - "humanName": { - "de": "Level-Up-Nachricht" - }, - "default": { - "en": "Level Up! Your new level is **%newLevel%**!", - "de": "Level Up! Dein neues Level ist jetzt %newLevel%" - }, - "description": { - "en": "This messages gets send if a user levels up (gets overwritten if randomMessages is enabled)", - "de": "Diese Nachricht wird gesendet, wenn ein Nutzer ein Level aufsteigt (Wird überschrieben, wenn \"Zufällige Nachrichten\" aktiviert ist)" - }, + "humanName": "Level Up Message", + "default": "Level Up! Your new level is **%newLevel%**!", + "description": "This messages gets send if a user levels up (gets overwritten if randomMessages is enabled)", "type": "string", "allowEmbed": true, "params": [ { "name": "mention", - "description": { - "en": "Mention of the user", - "de": "Erwähnung des Nutzers" - } + "description": "Mention of the user" }, { "name": "avatarURL", "isImage": true, - "description": { - "en": "Avatar of the user", - "de": "Profilbild des Nutzers" - } + "description": "Avatar of the user" }, { "name": "username", - "description": { - "en": "Username of the user", - "de": "Nutzername des Nutzers" - } + "description": "Username of the user" }, { "name": "tag", - "description": { - "en": "Tag of the user", - "de": "Tag des Nutzers" - } + "description": "Tag of the user" }, { "name": "newLevel", - "description": { - "en": "New level of the user", - "de": "Neues Level des Nutzers" - } + "description": "New level of the user" } - ] + ], + "category": "general" }, { "name": "level_up_message_with_reward", "allowGeneratedImage": true, - "humanName": { - "de": "Level-Up-Nachricht mit Belohnung" - }, - "default": { - "en": "Level Up! Your new level is **%newLevel%**! You received %role%.", - "de": "Level Up! Dein neues Level ist **%newLevel%**! Du erhältst %role%." - }, - "description": { - "en": "This messages gets send if a user levels up and gets a role (gets overwritten if randomMessages is enabled)", - "de": "Diese Nachricht wird gesendet, wenn ein Nutzer ein Level aufsteigt und eine Rolle erhält (Wird überschrieben, wenn \"Zufällige Nachrichten\" aktiviert ist)" - }, + "humanName": "Level Up Message with Reward", + "default": "Level Up! Your new level is **%newLevel%**! You received %role%.", + "description": "This messages gets send if a user levels up and gets a role (gets overwritten if randomMessages is enabled)", "type": "string", "allowEmbed": true, "params": [ { "name": "mention", - "description": { - "en": "Mention of the user", - "de": "Erwähnung des Nutzers" - } + "description": "Mention of the user" }, { "name": "avatarURL", "isImage": true, - "description": { - "en": "Avatar of the user", - "de": "Profilbild des Nutzers" - } + "description": "Avatar of the user" }, { "name": "username", - "description": { - "en": "Username of the user", - "de": "Nutzername des Nutzers" - } + "description": "Username of the user" }, { "name": "tag", - "description": { - "en": "Tag of the user", - "de": "Tag des Nutzers" - } + "description": "Tag of the user" }, { "name": "newLevel", - "description": { - "en": "New level of the user", - "de": "Neues Level des Nutzers" - } + "description": "New level of the user" }, { "name": "role", - "description": { - "en": "Mention of the role (No ping)", - "de": "Erwähnung der Rolle (Ohne \"Ping\")" - } + "description": "Mention of the role (No ping)" } - ] + ], + "category": "general" }, { "name": "liveLeaderBoardEmbed", - "humanName": { - "de": "Echtzeit-Rangliste" - }, + "humanName": "Live Leaderboard", "default": { - "en": { - "title": "Live Leaderboard", - "description": "Find all the users levels here. Updated every five minutes.", - "color": "GREEN", - "button": "👤 Show my level" - }, - "de": { - "title": "Echtzeit-Rangliste", - "description": "Hier findest du das Level von jedem Nutzer. Diese Liste wird alle fünf Minuten aktualisiert.", - "color": "GREEN", - "button": "👤 Mein Level anzeigen" - } - }, - "description": { - "en": "Embed which gets send to the leaderboard-channel and gets updated", - "de": "Embed, welches in den Ranglisten-Kanal gesendet und danach geupdated wird" + "title": "Live Leaderboard", + "description": "Find all the users levels here. Updated every five minutes.", + "color": "GREEN", + "button": "👤 Show my level" }, + "description": "Embed which gets send to the leaderboard-channel and gets updated", "type": "keyed", "content": { "key": "string", "value": "string" }, - "disableKeyEdits": true + "disableKeyEdits": true, + "category": "leaderboard" }, { "name": "leaderboard-button-answer", - "humanName": { - "de": "Nachricht bei Klick auf den Knopf unter dem Live-Leaderboard" - }, - "default": { - "en": "Hi, %name%, you are currently on **level %level%** with **%userXP%**/%nextLevelXP% **XP**. Learn more with `/profile`.", - "de": "Hi, %name%, du bist aktuell auf **Level %level%** mit **%userXP%**/%nextLevelXP% **Erfahrungspunkten**. Erfahre mehr mit `/profile`." - }, - "description": { - "en": "This messages gets send if a user clicks on the button below the live-leaderboard", - "de": "Diese Nachricht wird gesendet, wenn ein Nutzer den Knopf unter dem Live-Embed drückt" - }, + "humanName": "Leaderboard Button Response", + "default": "Hi, %name%, you are currently on **level %level%** with **%userXP%**/%nextLevelXP% **XP**. Learn more with `/profile`.", + "description": "This messages gets send if a user clicks on the button below the live-leaderboard", "type": "string", "allowEmbed": true, "params": [ { "name": "name", - "description": { - "en": "Username of the user", - "de": "Username des Nutzers" - } + "description": "Username of the user" }, { "name": "level", - "description": { - "en": "Level of the user", - "de": "Level des Nutzers" - } + "description": "Level of the user" }, { "name": "userXP", - "description": { - "en": "XP of the user", - "de": "XP des Nutzers" - } + "description": "XP of the user" }, { "name": "nextLevelXP", - "description": { - "en": "XP of the next level", - "de": "Benötigtes XP zum nächsten Level" - } + "description": "XP of the next level" } - ] + ], + "category": "leaderboard" + } + ], + "categories": [ + { + "id": "leaderboard", + "icon": "fas fa-ranking-stars", + "displayName": "Leaderboard Messages" + }, + { + "id": "general", + "icon": "fas fa-comment-dots", + "displayName": "General Messages" } ] -} \ No newline at end of file +} diff --git a/modules/levels/events/messageCreate.js b/modules/levels/events/messageCreate.js index 8020fdc6..b721788d 100644 --- a/modules/levels/events/messageCreate.js +++ b/modules/levels/events/messageCreate.js @@ -61,6 +61,7 @@ module.exports.getMemberRoleFactor = getMemberRoleFactor; async function grantXPAndLevelUP(client, member, xp, xpType, channel, msg = null) { const moduleConfig = client.configurations['levels']['config']; + if (member.roles.cache.some(r => moduleConfig.blacklistedRoles.some(br => String(br) === r.id))) return; const moduleStrings = client.configurations['levels']['strings']; let user = await client.models['levels']['User'].findOne({ @@ -88,8 +89,35 @@ async function grantXPAndLevelUP(client, member, xp, xpType, channel, msg = null await user.save(); if (nextLevelXp <= user.xp && !currentlyLevelingUp.has(member.user.id)) { + const cachedXp = user.xp; + const cachedLevel = user.level; + // Sanity-check the stored values before entering the loop. Out-of-range values + // (NaN, Infinity, absurdly large XP, negative level) indicate a corrupted row + // and can make the level-up loop run effectively forever. + if ( + !Number.isFinite(cachedXp) || !Number.isFinite(cachedLevel) || + cachedXp < 0 || cachedLevel < 0 || + cachedXp > 1e12 || cachedLevel > 1e6 + ) { + client.logger.error(`[levels] skipping level-up for user ${member.user.id}: corrupted values (xp=${cachedXp}, level=${cachedLevel})`); + return; + } let i = 1; - while (user.xp >= calculateLevelXP(client, user.level + i)) i++; + let lastRequired = -Infinity; + while (i <= 1000) { + const required = calculateLevelXP(client, cachedLevel + i); + if (!Number.isFinite(required) || required <= lastRequired) { + client.logger.error(`[levels] level curve returned non-monotonic or non-finite value at level ${cachedLevel + i} (got ${required}); aborting level-up for user ${member.user.id}`); + return; + } + if (cachedXp < required) break; + lastRequired = required; + i++; + } + if (i > 1000) { + client.logger.error(`[levels] level-up loop exceeded 1000 iterations for user ${member.user.id} (xp=${cachedXp}, level=${cachedLevel}); skipping`); + return; + } currentlyLevelingUp.add(member.user.id); user.level = user.level + (i - 1); const levelUpChannel = client.channels.cache.find(c => c.id === moduleConfig.level_up_channel_id && c.type === ChannelType.GuildText); @@ -151,13 +179,14 @@ module.exports.run = async (client, msg) => { if (msg.author.bot || msg.system) return; if (!msg.guild) return; if (msg.guild.id !== client.guildID) return; + if (!msg.member) return; if (cooldown.has(msg.author.id)) return; const moduleConfig = client.configurations['levels']['config']; if (msg.content.includes(client.config.prefix)) return; if (moduleConfig.blacklisted_channels.includes(msg.channel.id) || moduleConfig.blacklisted_channels.includes(msg.channel.parentId) || moduleConfig.blacklisted_channels.includes(msg.channel.parent?.parentId)) return; - if (msg.member.roles.cache.filter(r => moduleConfig.blacklistedRoles.includes(r.id)).size !== 0) return; + if (msg.member.roles.cache.some(r => moduleConfig.blacklistedRoles.some(br => String(br) === r.id))) return; let xp = randomIntFromInterval(moduleConfig['min-xp'], moduleConfig['max-xp']); await grantXPAndLevelUP(client, msg.member, xp, 'message', msg.channel, msg); diff --git a/modules/levels/events/voiceStateUpdate.js b/modules/levels/events/voiceStateUpdate.js index 4d684812..7641a1f3 100644 --- a/modules/levels/events/voiceStateUpdate.js +++ b/modules/levels/events/voiceStateUpdate.js @@ -2,35 +2,67 @@ const {ChannelType} = require('discord.js'); const {grantXPAndLevelUP} = require('./messageCreate'); const states = new Map(); -async function startVoiceSession(client, currentState) { - const moduleConfig = client.configurations['levels']['config']; - if (moduleConfig.blacklisted_channels.includes(currentState.channel.id) || moduleConfig.blacklisted_channels.includes(currentState.channel.parentId)) return; +function isChannelBlacklisted(client, channel) { + if (!channel) return true; + const blacklist = client.configurations['levels']['config'].blacklisted_channels; + return blacklist.includes(channel.id) || blacklist.includes(channel.parentId) || blacklist.includes(channel.parent?.parentId); +} + +function isRoleBlacklisted(client, member) { + return member.roles.cache.some(r => client.configurations['levels']['config'].blacklistedRoles.some(br => String(br) === r.id)); +} + +function hasHumanCompany(channel) { + if (!channel) return false; + return channel.members.filter(m => !m.user.bot).size >= 2; +} + +function isEligible(client, voiceState) { + if (!voiceState || !voiceState.channel) return false; + if (!voiceState.member || voiceState.member.user.bot) return false; + if (voiceState.deaf || voiceState.mute) return false; + if (voiceState.channel.type === ChannelType.GuildStageVoice) return false; + if (isChannelBlacklisted(client, voiceState.channel)) return false; + if (isRoleBlacklisted(client, voiceState.member)) return false; + if (!hasHumanCompany(voiceState.channel)) return false; + return true; +} + +async function startVoiceSession(client, voiceState) { + if (states.has(voiceState.member.id)) return; const int = setInterval(() => { - grantXP(client, currentState?.member).then(() => { + grantXP(client, voiceState?.member).then(() => { }); }, 1000 * 60 * 15); - states.set(currentState.member.id, { + states.set(voiceState.member.id, { start: new Date(), - channel: currentState.channel, + channel: voiceState.channel, lastXPTime: new Date(), end: null, interval: int }); } -async function endVoiceSession(client, currentState) { - if (!states.has(currentState.member.id)) return; - const oldState = states.get(currentState.member.id); +async function endVoiceSession(client, member) { + if (!states.has(member.id)) return; + const oldState = states.get(member.id); clearInterval(oldState.interval); - states.delete(currentState.member.id); - await grantXP(client, currentState.member); + states.delete(member.id); + await grantXP(client, member, oldState); } -async function grantXP(client, member) { - const stateData = states.get(member?.id); +async function grantXP(client, member, overrideStateData) { + const stateData = overrideStateData || states.get(member?.id); if (!stateData) return; + if (isRoleBlacklisted(client, member)) { + if (states.has(member.id)) { + clearInterval(states.get(member.id).interval); + states.delete(member.id); + } + return; + } const diff = new Date().getTime() - stateData.lastXPTime.getTime(); stateData.lastXPTime = new Date(); const moduleConfig = client.configurations['levels']['config']; @@ -39,15 +71,30 @@ async function grantXP(client, member) { await grantXPAndLevelUP(client, member, xp, 'voice', stateData.channel); } +async function updateChannelSessions(client, channel) { + if (!channel) return; + for (const member of channel.members.values()) { + if (member.user.bot) continue; + const voiceState = member.voice; + if (isEligible(client, voiceState)) { + if (!states.has(member.id)) await startVoiceSession(client, voiceState); + } else if (states.has(member.id)) { + await endVoiceSession(client, member); + } + } +} + module.exports.run = async function (client, oldState, newState) { if (!client.botReadyAt) return; if (!newState.guild || newState.member.user.bot) return; if (newState.guild.id !== client.guildID || client.configurations['levels']['config']['voiceXPPerMinute'] === 0) return; - if (newState.channel && (client.configurations['levels']['config'].blacklisted_channels.includes(newState.channel.id) || client.configurations['levels']['config'].blacklisted_channels.includes(newState.channel.parentId) || client.configurations['levels']['config'].blacklisted_channels.includes(newState.channel.parent?.parentId))) return; - if (newState.member.roles.cache.filter(r => client.configurations['levels']['config'].blacklistedRoles.includes(r.id)).size !== 0) return; + const channelChanged = oldState.channel !== newState.channel; + const muteOrDeafChanged = oldState.deaf !== newState.deaf || oldState.mute !== newState.mute; + if (!channelChanged && !muteOrDeafChanged) return; - if (oldState.channel !== newState.channel || oldState.deaf !== newState.deaf || oldState.mute !== newState.mute) await endVoiceSession(client, newState); + if (states.has(newState.member.id)) await endVoiceSession(client, newState.member); - if (newState.channel && !newState.deaf && !newState.mute && newState.channel.type !== ChannelType.GuildStageVoice) await startVoiceSession(client, newState); -}; \ No newline at end of file + if (oldState.channel && oldState.channel !== newState.channel) await updateChannelSessions(client, oldState.channel); + if (newState.channel) await updateChannelSessions(client, newState.channel); +}; diff --git a/modules/levels/leaderboardChannel.js b/modules/levels/leaderboardChannel.js index 7f611806..bea25520 100644 --- a/modules/levels/leaderboardChannel.js +++ b/modules/levels/leaderboardChannel.js @@ -8,7 +8,8 @@ const {localize} = require('../../src/functions/localize'); const { formatDiscordUserName, formatNumber, - parseEmbedColor + parseEmbedColor, + safeSetFooter } = require('../../src/functions/helpers'); const {displayLevel, isMaxLevel, calculateLevelXP} = require('./events/messageCreate'); const {client} = require('../../main'); @@ -66,10 +67,11 @@ module.exports.updateLeaderBoard = async function (client, force = false) { .setTitle(moduleStrings.liveLeaderBoardEmbed.title) .setDescription(moduleStrings.liveLeaderBoardEmbed.description) .setColor(parseEmbedColor(moduleStrings.liveLeaderBoardEmbed.color)) - .setFooter({text: client.strings.footer, iconURL: client.strings.footerImgUrl}) .setThumbnail(channel.guild.iconURL()) .addField(localize('levels', 'leaderboard'), leaderboardString); + safeSetFooter(embed, client); + if (!client.strings.disableFooterTimestamp) embed.setTimestamp(); const components = [{ diff --git a/modules/levels/module.json b/modules/levels/module.json index 4fbb00c6..fe94d622 100644 --- a/modules/levels/module.json +++ b/modules/levels/module.json @@ -1,8 +1,6 @@ { "name": "levels", - "humanReadableName": { - "en": "Level-System" - }, + "humanReadableName": "Level-System", "author": { "scnxOrgID": "1", "name": "SCDerox (SC Network Team)", @@ -18,11 +16,9 @@ "configs/random-levelup-messages.json", "configs/special-levelup-messages.json" ], + "fa-icon": "fas fa-comments", "tags": [ "community" ], - "description": { - "en": "Easy to use levelsystem with a lot of customization!", - "de": "Einfaches Level-System mit vielen Anpassungsmöglichkeiten!" - } -} \ No newline at end of file + "description": "Easy to use levelsystem with a lot of customization!" +} diff --git a/modules/massrole/configs/config.json b/modules/massrole/configs/config.json index 374c73cf..9147781d 100644 --- a/modules/massrole/configs/config.json +++ b/modules/massrole/configs/config.json @@ -1,40 +1,23 @@ { - "description": { - "en": "Configure the function of the module here", - "de": "Stelle hier die Funktionen des Modules ein" - }, - "humanName": { - "en": "Configuration", - "de": "Konfiguration" - }, + "description": "Configure the function of the module here", + "humanName": "Configuration", "filename": "config.json", "commandsWarnings": { "special": [ { "name": "/massrole", - "info": { - "en": "You need to first set the permissions in your server settings for this command and after that add them under \"adminRoles\" here.", - "de": "Du musst zuerst die Rechte in deinen Server-Einstellungen einstellen und danach diese unter \"AdminRollen\" hinzufügen." - } + "info": "You need to first set the permissions in your server settings for this command and after that add them under \"adminRoles\" here." } ] }, "content": [ { "name": "adminRoles", - "humanName": { - "de": "Adminrollen" - }, - "default": { - "en": [], - "de": [] - }, - "description": { - "en": "Every role that can use the massrole command", - "de": "Jede Rolle, welche den Massrole command verwenden kann" - }, + "humanName": "Admin Roles", + "default": [], + "description": "Every role that can use the massrole command", "type": "array", "content": "roleID" } ] -} \ No newline at end of file +} diff --git a/modules/massrole/configs/strings.json b/modules/massrole/configs/strings.json index 7cfd2da8..11e6b224 100644 --- a/modules/massrole/configs/strings.json +++ b/modules/massrole/configs/strings.json @@ -1,12 +1,6 @@ { - "description": { - "en": "Edit the messages and strings of the module here", - "de": "Stelle hier die Nachrichten des Modules ein" - }, - "humanName": { - "en": "Messages", - "de": "Nachrichten" - }, + "description": "Edit the messages and strings of the module here", + "humanName": "Messages", "commandsWarnings": { "normal": [ "/massrole" @@ -16,35 +10,17 @@ "content": [ { "name": "done", - "humanName": { - "en": "Action executed", - "de": "Aktion ausgeführt" - }, - "default": { - "en": "The action was executed successfully.", - "de": "Die Aktion wurde erfolgreich ausgeführt." - }, - "description": { - "en": "This messages gets send when a action was executed successfully", - "de": "Diese Nachricht wird verschickt, wenn eine Akton erfolgreich ausgeführt wurde" - }, + "humanName": "Action executed", + "default": "The action was executed successfully.", + "description": "This messages gets send when a action was executed successfully", "type": "string", "allowEmbed": true }, { "name": "notDone", - "humanName": { - "en": "Action not executed", - "de": "Aktion nicht ausgeführt" - }, - "default": { - "en": "The Action couldn't be executed because the bot has not enough permissions.", - "de": "Die Aktion konnte nicht vollständig ausgeführt werden, da der Bot nicht genug Rechte hat." - }, - "description": { - "en": "This messages gets send when a action was not executed successfully", - "de": "Diese Nachricht wird verschickt, wenn eine Aktion nicht erfolgreich ausgeführt wurde" - }, + "humanName": "Action not executed", + "default": "The Action couldn't be executed because the bot has not enough permissions.", + "description": "This messages gets send when a action was not executed successfully", "type": "string", "allowEmbed": true } diff --git a/modules/massrole/module.json b/modules/massrole/module.json index a9849d7b..0fc85335 100644 --- a/modules/massrole/module.json +++ b/modules/massrole/module.json @@ -1,8 +1,6 @@ { "name": "massrole", - "humanReadableName": { - "en": "Massrole" - }, + "humanReadableName": "Massrole", "author": { "name": "hfgd", "link": "https://github.com/hfgd123", @@ -10,6 +8,7 @@ }, "openSourceURL": "https://github.com/hfgd123/CustomDCBot/tree/main/modules/massrole", "commands-dir": "/commands", + "fa-icon": "fa-solid fa-users-viewfinder", "config-example-files": [ "configs/config.json", "configs/strings.json" @@ -17,8 +16,5 @@ "tags": [ "tools" ], - "description": { - "en": "Simple module to manage the roles of many members at once!", - "de": "Einfaches Modul, um die Rollen vieler Nutzer gleichzeitig zu verwalten!" - } -} \ No newline at end of file + "description": "Simple module to manage the roles of many members at once!" +} diff --git a/modules/moderation/commands/moderate.js b/modules/moderation/commands/moderate.js index c2ff96c5..2b884c8a 100644 --- a/modules/moderation/commands/moderate.js +++ b/modules/moderation/commands/moderate.js @@ -416,11 +416,11 @@ module.exports.subcommands = { fieldCount++; fieldCache.push({ name: `#${action.actionID}: ${action.type}`, - value: localize('moderation', 'action-description-format', { + value: truncate(localize('moderation', 'action-description-format', { reason: action.reason, u: action.memberID, t: dateToDiscordTimestamp(new Date(action.createdAt)) - }) + }), 1024) }); if (fieldCount % 3 === 0) { addSite(fieldCache); diff --git a/modules/moderation/configs/antiGrief.json b/modules/moderation/configs/antiGrief.json index ea9e16eb..dae4abf3 100644 --- a/modules/moderation/configs/antiGrief.json +++ b/modules/moderation/configs/antiGrief.json @@ -1,116 +1,56 @@ { - "description": { - "en": "This system can prevent moderation-tool-abuse by staff-members", - "de": "Dieses System kann Moderation-Tool-Missbrauch von Teammitgliedern verhindern" - }, - "humanName": { - "en": "Anti-Grief-Configuration", - "de": "Anti-Grief-Konfiguration" - }, - "informationBanner": { - "en": "This feature can automatically quarantine moderators that abuse their permissions (banning / warning / kicking more people than you set up). For this to work, place your bot above all other roles and make sure that the quarantine-role is right below it. This ensures that moderators / admins can not just give permissions to the quarantine-role or remove permissions from the bot.", - "de": "Diese Funktion kann automatisch Moderatoren in Quarantäne versetzen, wenn sie ihre Berechtigungen (wenn sie mehr Leute Bannen / Warnen / Kicken als du einstellst). Damit das fehlerfrei funktioniert, musst dein Bot über alle anderen Rollen platziert sein und direkt darunter muss die Quarantäne-Rolle sein. Das stellt sicher, dass Moderatoren / Administratoren nicht einfach der Quarantäne-Rolle Rechte geben können oder dem Bot Rechte entfernen können." - }, - "warningBanner": { - "en": "This feature is currently limited to actions run by the moderation-module. If you've given your moderators native discord-permissions, they can bypass this. We plan to support native actions (+ channel-deletes and other griefing actions) in future.", - "de": "Diese Funktion ist aktuell nur für Aktionen, die mit dem Moderations-Modul durchgeführt wurden, verfügbar. Wenn du deinen Moderatoren native Discord-Berechtigungen gegeben hast, können sie das ganz einfach umgehen. Wir planen, native Aktionen (und Channel-Löschungen oder andere Grief-Aktionen) in der Zukunft zu unterstützen." - }, + "description": "This system can prevent moderation-tool-abuse by staff-members", + "humanName": "Anti-Grief-Configuration", + "informationBanner": "This feature can automatically quarantine moderators that abuse their permissions (banning / warning / kicking more people than you set up). For this to work, place your bot above all other roles and make sure that the quarantine-role is right below it. This ensures that moderators / admins can not just give permissions to the quarantine-role or remove permissions from the bot.", + "warningBanner": "This feature is currently limited to actions run by the moderation-module. If you've given your moderators native discord-permissions, they can bypass this. We plan to support native actions (+ channel-deletes and other griefing actions) in future.", "filename": "antiGrief.json", "content": [ { "name": "enabled", - "humanName": { - "de": "Aktiviert", - "en": "Enabled?" - }, - "default": { - "en": false - }, - "description": { - "en": "Enables or disables the anti-join-grief-system", - "de": "Aktiviert oder deaktiviert das Anti-Join-Grief-System" - }, + "humanName": "Enabled?", + "default": false, + "description": "Enables or disables the anti-join-grief-system", "type": "boolean", "elementToggle": true, "category": "settings" }, { "name": "timeframe", - "humanName": { - "de": "Zeitfenster (in Stunden)", - "en": "Timeframe (in hours)" - }, - "default": { - "en": 3 - }, - "description": { - "en": "Timeframe in hours in which the limits can not be overstepped", - "de": "Zeitfenster in Stunden, in welchem die Limits nicht überschritten werden dürfen" - }, + "humanName": "Timeframe (in hours)", + "default": 3, + "description": "Timeframe in hours in which the limits can not be overstepped", "type": "integer", "category": "settings" }, { "name": "max_warn", - "humanName": { - "de": "Maximale Anzahl von Verwarnungen in dem Zeitfenster", - "en": "Maximal amount of warns in the timeframe" - }, - "default": { - "en": 15 - }, - "description": { - "en": "Maximal amount of warns a moderator can give in the timeframe until they get quarantined", - "de": "Maximale Anzahl von Verwarnungen, die ein Moderator in dem Zeitfenster vergeben kann, bis sie in Quarantäne gesteckt werden" - }, + "humanName": "Maximal amount of warns in the timeframe", + "default": 15, + "description": "Maximal amount of warns a moderator can give in the timeframe until they get quarantined", "type": "integer", "category": "actions" }, { "name": "max_mute", - "humanName": { - "de": "Maximale Anzahl von Mutes in dem Zeitfenster", - "en": "Maximal amount of mutes in the timeframe" - }, - "default": { - "en": 20 - }, - "description": { - "en": "Maximal amount of mutes a moderator can give in the timeframe until they get quarantined", - "de": "Maximale Anzahl von Mutes, die ein Moderator in dem Zeitfenster vergeben kann, bis sie in Quarantäne gesteckt werden" - }, + "humanName": "Maximal amount of mutes in the timeframe", + "default": 20, + "description": "Maximal amount of mutes a moderator can give in the timeframe until they get quarantined", "type": "integer", "category": "actions" }, { "name": "max_kick", - "humanName": { - "de": "Maximale Anzahl von Kicks in dem Zeitfenster", - "en": "Maximal amount of kicks in the timeframe" - }, - "default": { - "en": 10 - }, - "description": { - "en": "Maximal amount of kicks a moderator can give in the timeframe until they get quarantined", - "de": "Maximale Anzahl von Kicks, die ein Moderator in dem Zeitfenster vergeben kann, bis sie in Quarantäne gesteckt werden" - }, + "humanName": "Maximal amount of kicks in the timeframe", + "default": 10, + "description": "Maximal amount of kicks a moderator can give in the timeframe until they get quarantined", "type": "integer", "category": "actions" }, { "name": "max_ban", - "humanName": { - "de": "Maximale Anzahl von Bans in dem Zeitfenster", - "en": "Maximal amount of bans in the timeframe" - }, - "default": { - "en": 5 - }, - "description": { - "en": "Maximal amount of bans a moderator can give in the timeframe until they get quarantined", - "de": "Maximale Anzahl von Bans, die ein Moderator in dem Zeitfenster vergeben kann, bis sie in Quarantäne gesteckt werden" - }, + "humanName": "Maximal amount of bans in the timeframe", + "default": 5, + "description": "Maximal amount of bans a moderator can give in the timeframe until they get quarantined", "type": "integer", "category": "actions" } @@ -119,18 +59,12 @@ { "id": "settings", "icon": "fas fa-gears", - "displayName": { - "en": "Detection Settings", - "de": "Erkennungseinstellungen" - } + "displayName": "Detection Settings" }, { "id": "actions", "icon": "fas fa-hammer", - "displayName": { - "en": "Actions", - "de": "Aktionen" - } + "displayName": "Actions" } ] } \ No newline at end of file diff --git a/modules/moderation/configs/antiJoinRaid.json b/modules/moderation/configs/antiJoinRaid.json index aa7c6852..db2f11f4 100644 --- a/modules/moderation/configs/antiJoinRaid.json +++ b/modules/moderation/configs/antiJoinRaid.json @@ -1,80 +1,38 @@ { - "description": { - "en": "This system can prevent spammers from raiding your server", - "de": "Dieses System kann es Spammern verhindern, deinen Server zu raiden" - }, - "humanName": { - "en": "Anti-Join-Raid-Configuration", - "de": "Anti-Join-Raid-Konfiguration" - }, + "description": "This system can prevent spammers from raiding your server", + "humanName": "Anti-Join-Raid-Configuration", "filename": "antiJoinRaid.json", "content": [ { "name": "enabled", - "humanName": { - "de": "Aktiviert", - "en": "Enabled?" - }, - "default": { - "en": true, - "de": true - }, - "description": { - "en": "Enables or disables the anti-join-raid-system", - "de": "Aktiviert oder deaktiviert das Anti-Join-Raid-System" - }, + "humanName": "Enabled?", + "default": true, + "description": "Enables or disables the anti-join-raid-system", "type": "boolean", "elementToggle": true, "category": "settings" }, { "name": "timeframe", - "humanName": { - "de": "Zeitfenster (in Minuten)", - "en": "Timeframe (in minutes)" - }, - "default": { - "en": 5, - "de": 5 - }, - "description": { - "en": "Timeframe in which join actions should be recorded (in minutes)", - "de": "Zeitfenster, in welchem Serverbeitritte gezählt werden sollen (in Minuten)" - }, + "humanName": "Timeframe (in minutes)", + "default": 5, + "description": "Timeframe in which join actions should be recorded (in minutes)", "type": "integer", "category": "settings" }, { "name": "maxJoinsInTimeframe", - "humanName": { - "de": "Maximale Beitrittsanzahl", - "en": "Maximal count of new users" - }, - "default": { - "en": 3, - "de": 3 - }, - "description": { - "en": "Count of joins that are allowed to happen in the selected timeframe", - "de": "Anzahl an Serverbeitritten, die im ausgewählten Zeitfenster zugelassen werden" - }, + "humanName": "Maximal count of new users", + "default": 3, + "description": "Count of joins that are allowed to happen in the selected timeframe", "type": "integer", "category": "settings" }, { "name": "action", - "humanName": { - "de": "Aktion", - "en": "Action" - }, - "default": { - "en": "quarantine", - "de": "quarantine" - }, - "description": { - "en": "Select the action here that should get performed if the anti-join-system gets triggered", - "de": "Wähle hier die Aktion aus, die ausgeführt werden soll, wenn das Anti-Join-Raid-System ausgelöst wird" - }, + "humanName": "Action", + "default": "quarantine", + "description": "Select the action here that should get performed if the anti-join-system gets triggered", "type": "select", "content": [ "mute", @@ -87,34 +45,17 @@ }, { "name": "roleID", - "humanName": { - "de": "Rolle", - "en": "Role" - }, - "default": { - "en": "" - }, - "description": { - "en": "Only if action = give-role. Role that gets given to users who trigger the antiJoinRaid-System", - "de": "Nur verfügbar, wenn Aktion = give-role. Rolle, die Nutzern gegeben wird, die das Anti-Join-Raid-System auslösen" - }, + "humanName": "Role", + "default": "", + "description": "Only if action = give-role. Role that gets given to users who trigger the antiJoinRaid-System", "type": "roleID", "category": "actions" }, { "name": "removeOtherRoles", - "humanName": { - "de": "Andere Rollen entfernen", - "en": "Remove other roles" - }, - "default": { - "en": true, - "de": true - }, - "description": { - "en": "Only if action = give-role. If enabled other roles that have been give to the user get removed after a short interval (and the giving of the role from \"roleID\" will be delayed)", - "de": "Nur verfügbar, wenn Aktion = give-role. Wenn aktiviert, werden andere Rollen die der Nutzer hat nach einem kurzen Zeitraum entfernt (und das Vergeben der Rolle von \"Rolle\" wird verzögert)" - }, + "humanName": "Remove other roles", + "default": true, + "description": "Only if action = give-role. If enabled other roles that have been give to the user get removed after a short interval (and the giving of the role from \"roleID\" will be delayed)", "type": "boolean", "category": "actions" } @@ -123,18 +64,12 @@ { "id": "settings", "icon": "fas fa-gears", - "displayName": { - "en": "Detection Settings", - "de": "Erkennungseinstellungen" - } + "displayName": "Detection Settings" }, { "id": "actions", "icon": "fas fa-hammer", - "displayName": { - "en": "Actions", - "de": "Aktionen" - } + "displayName": "Actions" } ] } \ No newline at end of file diff --git a/modules/moderation/configs/antiSpam.json b/modules/moderation/configs/antiSpam.json index 3542a71e..637a97b9 100644 --- a/modules/moderation/configs/antiSpam.json +++ b/modules/moderation/configs/antiSpam.json @@ -1,131 +1,62 @@ { - "description": { - "en": "You can configure here, how your bot should react to spam", - "de": "Du kannst hier einstellen, wie dein Bot auf Spam reagieren soll" - }, - "humanName": { - "en": "Anti-Spam-Configuration", - "de": "Anti-Spam-Konfiguration" - }, + "description": "You can configure here, how your bot should react to spam", + "humanName": "Anti-Spam-Configuration", "filename": "antiSpam.json", "content": [ { "name": "enabled", - "humanName": { - "de": "Aktiviert", - "en": "Enabled?" - }, - "default": { - "en": true, - "de": true - }, - "description": { - "en": "Enable or disable the anti spam system", - "de": "Aktiviert oder deaktiviert das Anti-Spam-System" - }, + "humanName": "Enabled?", + "default": true, + "description": "Enable or disable the anti spam system", "type": "boolean", "elementToggle": true, "category": "settings" }, { "name": "timeframe", - "humanName": { - "de": "Zeitfenster (in Sekunden)", - "en": "Timeframe (in seconds)" - }, - "default": { - "en": 5, - "de": 5 - }, - "description": { - "en": "Timeframe in seconds after which message objects get deleted (and can not longer be used to detect spam)", - "de": "Zeitfenster in Sekunden, in dem Nachrichten gelöscht werden (und nicht länger zur Erkennung von Spam verwendet werden können)" - }, + "humanName": "Timeframe (in seconds)", + "default": 5, + "description": "Timeframe in seconds after which message objects get deleted (and can not longer be used to detect spam)", "type": "integer", "category": "settings" }, { "name": "maxMessagesInTimeframe", - "humanName": { - "de": "Maximale Nachrichten im Zeitfenster", - "en": "Maximal count of messages in timeframe" - }, - "default": { - "en": 10, - "de": 10 - }, - "description": { - "en": "Count of messages that are allowed to be sent in the selected timeframe", - "de": "Anzahl an Nachrichten, die im ausgewählten Zeitfenster erlaubt sind" - }, + "humanName": "Maximal count of messages in timeframe", + "default": 10, + "description": "Count of messages that are allowed to be sent in the selected timeframe", "type": "integer", "category": "settings" }, { "name": "maxDuplicatedMessagesInTimeframe", - "humanName": { - "de": "Maximale gleiche Nachrichten im Zeitfenster", - "en": "Maximal count of duplicated messages in timeframe" - }, - "default": { - "en": 5, - "de": 5 - }, - "description": { - "en": "Count of identical messages that are allowed to be sent in the selected timeframe", - "de": "Anzahl an gleichen Nachrichten, die im ausgewählten Zeitfenster erlaubt sind" - }, + "humanName": "Maximal count of duplicated messages in timeframe", + "default": 5, + "description": "Count of identical messages that are allowed to be sent in the selected timeframe", "type": "integer", "category": "settings" }, { "name": "maxPingsInTimeframe", - "humanName": { - "de": "Maximale Pings im Zeitfenster", - "en": "Maximal count of pings in timeframe" - }, - "default": { - "en": 4, - "de": 4 - }, - "description": { - "en": "Count of pings (also counts replies) that are allowed to be sent in the selected timeframe", - "de": "Anzahl an Erwähnungen (zählt auch Antworten), die im ausgewählten Zeitfenster erlaubt sind" - }, + "humanName": "Maximal count of pings in timeframe", + "default": 4, + "description": "Count of pings (also counts replies) that are allowed to be sent in the selected timeframe", "type": "integer", "category": "settings" }, { "name": "maxMassPings", - "humanName": { - "de": "Maximale Massenpings im Zeitfenster", - "en": "Maximal count of mass-pings in timeframe" - }, - "default": { - "en": 3, - "de": 3 - }, - "description": { - "en": "Count of mass pings (= @everyone, @here and roles) that are allowed to be sent in the selected timeframe", - "de": "Anzahl an Massenerwähnungen (= @everyone, @here und Rollen), die im ausgewählten Zeitfenster erlaubt sind" - }, + "humanName": "Maximal count of mass-pings in timeframe", + "default": 3, + "description": "Count of mass pings (= @everyone, @here and roles) that are allowed to be sent in the selected timeframe", "type": "integer", "category": "settings" }, { "name": "action", - "humanName": { - "de": "Aktion", - "en": "Action" - }, - "default": { - "en": "mute", - "de": "mute" - }, - "description": { - "en": "Select what should happen if someone spams", - "de": "Wähle hier die Aktion aus, die ausgeführt werden soll, wenn jemand spammt" - }, + "humanName": "Action", + "default": "mute", + "description": "Select what should happen if someone spams", "type": "select", "content": [ "mute", @@ -138,88 +69,46 @@ }, { "name": "sendChatMessage", - "humanName": { - "de": "Chatnachricht senden", - "en": "Send Chat-Message" - }, - "default": { - "en": true, - "de": true - }, - "description": { - "en": "If enabled the bot will send a chat message if it has to take action agains a bot", - "de": "Wenn aktiviert, wird der Bot eine Nachricht in den Chat senden, wenn er eine Aktion gegen einen Bot ausführen musste" - }, + "humanName": "Send Chat-Message", + "default": true, + "description": "If enabled the bot will send a chat message if it has to take action agains a bot", "type": "boolean", "category": "actions" }, { "name": "message", "dependsOn": "sendChatMessage", - "humanName": { - "de": "Nachricht", - "en": "Message" - }, - "default": { - "en": "Anti-Spam: I took action against <@%userid%> because of **%reason%**", - "de": "Anti-Spam: Ich habe wegen **%reason%** eine Aktion gegen <@%userid%> ausgeführt" - }, - "description": { - "en": "This will get send in the channel the spam is occurring in when anti-spam gets triggered", - "de": "Das wird in den Kanal gesendet, wenn das Anti-Spam-System ausgelöst wird" - }, + "humanName": "Message", + "default": "Anti-Spam: I took action against <@%userid%> because of **%reason%**", + "description": "This will get send in the channel the spam is occurring in when anti-spam gets triggered", "type": "string", "allowEmbed": true, "params": [ { "name": "userid", - "description": { - "en": "ID of the user", - "de": "ID des Nutzers" - } + "description": "ID of the user" }, { "name": "reason", - "description": { - "en": "Reason of the action", - "de": "Grund der Aktion" - } + "description": "Reason of the action" } ], "category": "actions" }, { "name": "ignoredChannels", - "humanName": { - "de": "Ignorierte Kanäle", - "en": "Whitelisted Channels" - }, - "default": { - "en": [], - "de": [] - }, - "description": { - "en": "You can set channels that get ignored here", - "de": "Du kannst hier Kanäle einstellen, die ignoriert werden sollen" - }, + "humanName": "Whitelisted Channels", + "default": [], + "description": "You can set channels that get ignored here", "type": "array", "content": "channelID", "category": "exemptions" }, { "name": "ignoredRoles", - "humanName": { - "de": "Ignorierte Rollen", - "en": "Whitelisted roles" - }, - "default": { - "en": [], - "de": [] - }, - "description": { - "en": "You can set roles that get ignored here", - "de": "Du kannst hier Rollen einstellen, die ignoriert werden sollen" - }, + "humanName": "Whitelisted roles", + "default": [], + "description": "You can set roles that get ignored here", "type": "array", "content": "roleID", "category": "exemptions" @@ -229,26 +118,17 @@ { "id": "settings", "icon": "fas fa-gears", - "displayName": { - "en": "Detection Settings", - "de": "Erkennungseinstellungen" - } + "displayName": "Detection Settings" }, { "id": "actions", "icon": "fas fa-hammer", - "displayName": { - "en": "Actions", - "de": "Aktionen" - } + "displayName": "Actions" }, { "id": "exemptions", "icon": "fa-solid fa-shield", - "displayName": { - "en": "Exemptions", - "de": "Ausnahmen" - } + "displayName": "Exemptions" } ] } \ No newline at end of file diff --git a/modules/moderation/configs/config.json b/modules/moderation/configs/config.json index ef76531f..4ab12fd3 100644 --- a/modules/moderation/configs/config.json +++ b/modules/moderation/configs/config.json @@ -1,226 +1,116 @@ { - "description": { - "en": "You can set up permissions and features of this module here", - "de": "Du kannst hier die Rechte dieses Modules einstellen" - }, - "humanName": { - "en": "Configuration", - "de": "Konfiguration" - }, + "description": "You can set up permissions and features of this module here", + "humanName": "Configuration", "filename": "config.json", "commandsWarnings": { "special": [ { "name": "/moderate", - "info": { - "en": "Each moderator needs to be able to execute the /moderate command, so set your permissions in your server-settings accordingly. Additionally, moderator need to be entered into their level below.", - "de": "Jeder Modator muss den /moderate Befehl ausführen können, bitte konfiguriere das in deinen Server-Einstellungen. Zusätzlich muss jede Moderator-Rolle zu ihrem Level unten manuell eingetragen werden." - } + "info": "Each moderator needs to be able to execute the /moderate command, so set your permissions in your server-settings accordingly. Additionally, moderator need to be entered into their level below." } ] }, "content": [ { "name": "logchannel-id", - "humanName": { - "de": "Log-Kanal", - "en": "Log-Channel" - }, - "default": { - "en": "" - }, - "description": { - "en": "Moderative actions will get logged in this channel", - "de": "Moderative Aktionen werden in diesem Kanal geloggt" - }, + "humanName": "Log-Channel", + "default": "", + "description": "Moderative actions will get logged in this channel", "type": "channelID", "category": "general" }, { "name": "quarantine-role-id", - "humanName": { - "de": "Quarantäne-Rolle", - "en": "Quarantine-Role" - }, - "default": { - "en": "" - }, - "description": { - "en": "When a user gets quarantined, all of their roles will get removed and this quarantine-role wil get assigned", - "de": "Wenn ein Nutzer in Quarantäne gesteckt wird, werden alle Rollen von diesem entfernt und nur diese hinzugefügt" - }, + "humanName": "Quarantine-Role", + "default": "", + "description": "When a user gets quarantined, all of their roles will get removed and this quarantine-role wil get assigned", "type": "roleID", "category": "roles" }, { "name": "report-channel-id", - "default": { - "en": "" - }, - "humanName": { - "en": "Report-Channel", - "de": "Report-Kanal" - }, - "description": { - "en": "Channel in which user-reports should get send. (optional, default: Log-Channel)", - "de": "Kanal, in welchem Nutzer-Reports should get send. (optional, default: Log-Kanal)" - }, + "default": "", + "humanName": "Report-Channel", + "description": "Channel in which user-reports should get send. (optional, default: Log-Channel)", "type": "channelID", "allowNull": true, "category": "reports" }, { "name": "remove-all-roles-on-quarantine", - "humanName": { - "de": "Bei Quarantäne alle Rollen entfernen", - "en": "Remove all roles on quarantine" - }, - "default": { - "en": true, - "de": true - }, - "description": { - "en": "If enabled all roles from a user get removed if they get quarantined (they get saved an can be restored with /unquarantine)", - "de": "Wenn diese Option aktiviert ist, werden alle Rollen eines Nutzers entfernt, wenn er in Quarantäne gesetzt wird (sie werden gespeichert und mit /unquarantine wiederhergestellt)" - }, + "humanName": "Remove all roles on quarantine", + "default": true, + "description": "If enabled all roles from a user get removed if they get quarantined (they get saved an can be restored with /unquarantine)", "type": "boolean", "category": "roles" }, { "name": "moderator-roles_level1", - "humanName": { - "en": "Moderator-Level 1" - }, - "default": { - "en": [], - "de": [] - }, - "description": { - "en": "Moderator roles that can perform the following actions: Warn", - "de": "Rollen, die folgende Aktionen ausführen können: Warn" - }, + "humanName": "Moderator-Level 1", + "default": [], + "description": "Moderator roles that can perform the following actions: Warn", "type": "array", "content": "roleID", "category": "roles" }, { "name": "moderator-roles_level2", - "humanName": { - "en": "Moderator-Level 2" - }, - "default": { - "en": [], - "de": [] - }, - "description": { - "en": "Moderator roles that can perform the following actions: Warn, Mute, Unmute, Lock, Unlock, Channelmute, Remove-Channel-Mute", - "de": "Rollen, die folgende Aktionen ausführen können: Warn, Mute, Unmute, Channelmute, Channel-Mute entfernen" - }, + "humanName": "Moderator-Level 2", + "default": [], + "description": "Moderator roles that can perform the following actions: Warn, Mute, Unmute, Lock, Unlock, Channelmute, Remove-Channel-Mute", "type": "array", "content": "roleID", "category": "roles" }, { "name": "moderator-roles_level3", - "humanName": { - "en": "Moderator-Level 3" - }, - "default": { - "en": [], - "de": [] - }, - "description": { - "en": "Moderator roles that can perform the following actions: Warn, Mute, Unmute, Kick, Clear", - "de": "Rollen, die folgende Aktionen ausführen können: Warn, Mute, Unmute, Kick, Clear" - }, + "humanName": "Moderator-Level 3", + "default": [], + "description": "Moderator roles that can perform the following actions: Warn, Mute, Unmute, Kick, Clear", "type": "array", "content": "roleID", "category": "roles" }, { "name": "moderator-roles_level4", - "humanName": { - "en": "Moderator-Level 4" - }, - "default": { - "en": [], - "de": [] - }, - "description": { - "en": "Moderator roles that can perform the following actions: Warn, Mute, Unmute, Kick, Clear, Ban, Unban", - "de": "Rollen, die folgende Aktionen ausführen können: Warn, Mute, Unmute, Kick, Clear, Ban, Unban" - }, + "humanName": "Moderator-Level 4", + "default": [], + "description": "Moderator roles that can perform the following actions: Warn, Mute, Unmute, Kick, Clear, Ban, Unban", "type": "array", "content": "roleID", "category": "roles" }, { "name": "roles-to-ping-on-report", - "humanName": { - "de": "Rollenpings bei Report", - "en": "Roles to ping on reports" - }, - "default": { - "en": [], - "de": [] - }, - "description": { - "en": "Roles that should get pinged in the log-channel when a user reports someone", - "de": "Rollen, die im log-Kanal gepingt werden sollen, wenn ein Nutzer jemanden Reportet" - }, + "humanName": "Roles to ping on reports", + "default": [], + "description": "Roles that should get pinged in the log-channel when a user reports someone", "type": "array", "content": "roleID", "category": "reports" }, { "name": "require_reason", - "humanName": { - "de": "Begründung erzwingen", - "en": "Force moderators to set a reason" - }, - "default": { - "en": true, - "de": true - }, - "description": { - "en": "Should moderators be required to set a reason?", - "de": "Sollen Moderatoren verpflichtet werden, eine Begründung anzugeben?" - }, + "humanName": "Force moderators to set a reason", + "default": true, + "description": "Should moderators be required to set a reason?", "type": "boolean", "category": "reports" }, { "name": "require_proof", - "humanName": { - "de": "Beweis-Bild erzwingen", - "en": "Force moderators to upload proof" - }, + "humanName": "Force moderators to upload proof", "dependsOn": "require_reason", - "default": { - "en": false, - "de": false - }, - "description": { - "en": "Should moderators be required to upload proof for their actions?", - "de": "Sollen Moderatoren verpflichtet werden, einen Beweis hochzuladen?" - }, + "default": false, + "description": "Should moderators be required to upload proof for their actions?", "type": "boolean", "category": "reports" }, { "name": "action_on_invite", - "humanName": { - "de": "Aktion bei Invite", - "en": "Action on invite" - }, - "default": { - "en": "mute", - "de": "mute" - }, - "description": { - "en": "What should the bot do if someone posts an invite link?", - "de": "Was soll der Bot tun, wenn jemand einen Invite sendet?" - }, + "humanName": "Action on invite", + "default": "mute", + "description": "What should the bot do if someone posts an invite link?", "type": "select", "content": [ "none", @@ -232,20 +122,21 @@ ], "category": "automod" }, + { + "name": "allowed_invite_guild_ids", + "humanName": "Allowed invite guild IDs", + "default": [], + "description": "Guild IDs whose invites should be allowed (in addition to this server's invites which are always allowed).", + "type": "array", + "content": "string", + "dependsOn": "action_on_invite", + "category": "automod" + }, { "name": "action_on_scam_link", - "humanName": { - "de": "Aktion bei Scam-Link", - "en": "Action on Scam-Link" - }, - "default": { - "en": "none", - "de": "mute" - }, - "description": { - "en": "What should the bot do if someone posts an suspicious or confirmed scam link?", - "de": "Was soll der Bot tun, wenn jemand einen Link zu einer verdächtigen oder bestätigten Scam-Seite sendet?" - }, + "humanName": "Action on Scam-Link", + "default": "none", + "description": "What should the bot do if someone posts an suspicious or confirmed scam link?", "type": "select", "content": [ "none", @@ -259,18 +150,9 @@ }, { "name": "scam_link_level", - "humanName": { - "de": "Level der Scam-Link-Erkennung", - "en": "Level of Scam-Link-Detection" - }, - "default": { - "en": "confirmed", - "de": "confirmed" - }, - "description": { - "en": "Select the Level of Scam-Link-Filter. \"confirmed\" only contains verified Scam-Domains, while \"suspicious\" may contain not-harmful domains.", - "de": "\"confirmed\" enthält nur Scam-Domains, die wirklich als solche verifiziert wurden, während \"suspicious\" auch nicht-gefährdende Domains beinhalten kann" - }, + "humanName": "Level of Scam-Link-Detection", + "default": "confirmed", + "description": "Select the Level of Scam-Link-Filter. \"confirmed\" only contains verified Scam-Domains, while \"suspicious\" may contain not-harmful domains.", "type": "select", "content": [ "confirmed", @@ -280,72 +162,36 @@ }, { "name": "whitelisted_channels_for_invite_blocking", - "humanName": { - "de": "Erlaubte Kanäle für Invitesperre", - "en": "Whitelisted channels for invite-ban" - }, - "default": { - "en": [], - "de": [] - }, - "description": { - "en": "Channels or categories where invite blocking is disabled", - "de": "Kanäle oder Kategorien, in welchen die Invitesperre deaktiviert ist" - }, + "humanName": "Whitelisted channels for invite-ban", + "default": [], + "description": "Channels or categories where invite blocking is disabled", "type": "array", "content": "channelID", "category": "automod" }, { "name": "whitelisted_roles_for_invite_blocking", - "humanName": { - "de": "Erlaubte Rollen für Invitesperre", - "en": "Whitelisted roles for invite-ban" - }, - "default": { - "en": [], - "de": [] - }, - "description": { - "en": "ID of Roles which are allowed to bypass invite blocking", - "de": "Rollen, welche die Invitesperre umgehen dürfen" - }, + "humanName": "Whitelisted roles for invite-ban", + "default": [], + "description": "ID of Roles which are allowed to bypass invite blocking", "type": "array", "content": "roleID", "category": "automod" }, { "name": "blacklisted_words", - "humanName": { - "de": "Gesperrte Wörter", - "en": "Blacklisted words" - }, - "default": { - "en": [], - "de": [] - }, - "description": { - "en": "Words that are blacklisted", - "de": "Wörter, die blockiert sind" - }, + "humanName": "Blacklisted words", + "default": [], + "description": "Words that are blacklisted", "type": "array", "content": "string", "category": "automod" }, { "name": "action_on_posting_blacklisted_word", - "humanName": { - "de": "Aktion bei gesperrtem Wort", - "en": "Action on blacklisted Word" - }, - "default": { - "en": "mute", - "de": "mute" - }, - "description": { - "en": "What should the bot do if someone posts a blacklisted word?", - "de": "Was soll der Bot tun, wenn jemand ein gesperrtes Wort sagt?" - }, + "humanName": "Action on blacklisted Word", + "default": "mute", + "description": "What should the bot do if someone posts a blacklisted word?", "type": "select", "content": [ "none", @@ -359,102 +205,55 @@ }, { "name": "defaultMuteDuration", - "humanName": { - "de": "Standardmäßige Mute-Länge", - "en": "Default Mute-Duration" - }, + "humanName": "Default Mute-Duration", "type": "string", - "default": { - "en": "14d" - }, - "description": { - "en": "Default mute duration when none was configured. Will also be used for automod features (e.g. when someone posts a blacklisted word). Maximum value of 28 days.", - "de": "Standardmäßige Mute-Länge, wenn keine eingestellt wurde. Wird auch für Automod-Funktionen verwendet (also wenn z.B. jemand ein gesperrtes Wort postet). Höchstlänge von 28 Tagen." - }, + "default": "14d", + "description": "Default mute duration when none was configured. Will also be used for automod features (e.g. when someone posts a blacklisted word). Maximum value of 28 days.", "category": "actions" }, { "name": "changeNicknames", - "humanName": { - "de": "Nicknamen bei Mute- / Quarantäne ändern", - "en": "Change nicknames on Mute- / Quarantine" - }, - "default": { - "en": false, - "de": false - }, - "description": { - "en": "If enabled, the user will get renamed when they get muted or quarantined", - "de": "Wenn aktiviert, wird der Nutzer umbenannt, wenn er gemutet oder in Quarantäne gesteckt wird" - }, + "humanName": "Change nicknames on Mute- / Quarantine", + "default": false, + "description": "If enabled, the user will get renamed when they get muted or quarantined", "type": "boolean", "category": "nicknames" }, { "name": "changeNicknameOnMute", "dependsOn": "changeNicknames", - "humanName": { - "de": "Neuer Nickname bei Mute", - "en": "New nickname on mute" - }, - "default": { - "en": "%nickname%", - "de": "%nickname%" - }, - "description": { - "en": "The nickname in which the user should be renamed when they get muted", - "de": "Der Nickname, in welchen der Nutzer umbenannt werden soll, wenn er gemuted wird" - }, + "humanName": "New nickname on mute", + "default": "%nickname%", + "description": "The nickname in which the user should be renamed when they get muted", "type": "string", "params": [ { "name": "nickname", - "description": { - "en": "Original nickname of the user" - } + "description": "Original nickname of the user" } ], "category": "nicknames" }, { "name": "changeNicknameOnQuarantine", - "humanName": { - "de": "Nickname während der Quarantäne", - "en": "Nickname during quarantine" - }, + "humanName": "Nickname during quarantine", "dependsOn": "changeNicknames", - "default": { - "en": "%nickname%", - "de": "%nickname%" - }, - "description": { - "en": "The nickname in which the user should be renamed when they get quarantined" - }, + "default": "%nickname%", + "description": "The nickname in which the user should be renamed when they get quarantined", "type": "string", "params": [ { "name": "nickname", - "description": { - "en": "Original nickname of the user" - } + "description": "Original nickname of the user" } ], "category": "nicknames" }, { "name": "automod", - "humanName": { - "de": "Automod", - "en": "Automod" - }, - "default": { - "en": {}, - "de": {} - }, - "description": { - "en": "You can define here what should happen (options: mute, kick, ban, quarantine) when someone gets x warns. Specify duration by writing : after the action.", - "de": "Du kannst hier festlegen, was passieren soll (optionen: mute, kick, ban), wenn jemand x Verwarnungen bekommt. Länge festlegen, indem : hinter die Aktion geschrieben wird." - }, + "humanName": "Automod", + "default": {}, + "description": "You can define here what should happen (options: mute, kick, ban, quarantine) when someone gets x warns. Specify duration by writing : after the action.", "type": "keyed", "content": { "key": "integer", @@ -464,34 +263,18 @@ }, { "name": "warnsExpire", - "humanName": { - "de": "Sollen Warns automatisch gelöscht werden?", - "en": "Should warns be deleted automatically?" - }, - "default": { - "en": false - }, - "description": { - "en": "If enabled, warns will be deleted automatically after a certain period of time. Warns expired this way will completely disappear and can not be viewed again after they expired.", - "de": "Wenn aktiviert, werden Warns automatisch nach einer bestimmten Zeitspanne gelöscht. Auf diese Weiße abgelaufene Warns werden komplett verschwinden und können nie erneut gesehen werden." - }, + "humanName": "Should warns be deleted automatically?", + "default": false, + "description": "If enabled, warns will be deleted automatically after a certain period of time. Warns expired this way will completely disappear and can not be viewed again after they expired.", "type": "boolean", "category": "actions" }, { "name": "warnExpiration", - "humanName": { - "de": "Zeit, nach der Warns automatisch ablaufen", - "en": "Time after which warns will be automatically removed" - }, - "default": { - "en": "3 months" - }, + "humanName": "Time after which warns will be automatically removed", + "default": "3 months", "dependsOn": "warnsExpire", - "description": { - "en": "Warns will be automatically deleted after this value after it's creation. Please note that this action will delete existing warns if they expired. Enter an english value, such as \"1y\" (= 1 year), \"3 Months\" (= 3 Months) oder \"2w\" (= 2 Weeks).", - "de": "Warnungen werden automatisch gelöscht, wenn sie diese Zeitspanne nach Erstellung erreicht haben. Trage einen englischen Wert, wie \"1y\" (= 1 Jahr), \"3 Months\" (= 3 Monate) oder \"2w\" (= 2 Woche) ein." - }, + "description": "Warns will be automatically deleted after this value after it's creation. Please note that this action will delete existing warns if they expired. Enter an english value, such as \"1y\" (= 1 year), \"3 Months\" (= 3 Months) oder \"2w\" (= 2 Weeks).", "type": "string", "category": "actions" } @@ -500,50 +283,32 @@ { "id": "general", "icon": "fas fa-gears", - "displayName": { - "en": "General Settings", - "de": "Allgemeine Einstellungen" - } + "displayName": "General Settings" }, { "id": "roles", "icon": "fa-solid fa-users", - "displayName": { - "en": "Roles & Permissions", - "de": "Rollen & Berechtigungen" - } + "displayName": "Roles & Permissions" }, { "id": "reports", "icon": "fa-solid fa-flag", - "displayName": { - "en": "Reports", - "de": "Meldungen" - } + "displayName": "Reports" }, { "id": "automod", "icon": "far fa-robot", - "displayName": { - "en": "Auto-Moderation", - "de": "Auto-Moderation" - } + "displayName": "Auto-Moderation" }, { "id": "actions", "icon": "fas fa-hammer", - "displayName": { - "en": "Actions & Punishments", - "de": "Aktionen & Bestrafungen" - } + "displayName": "Actions & Punishments" }, { "id": "nicknames", "icon": "fa-solid fa-user-pen", - "displayName": { - "en": "Nickname Management", - "de": "Nicknamen-Verwaltung" - } + "displayName": "Nickname Management" } ] } \ No newline at end of file diff --git a/modules/moderation/configs/joinGate.json b/modules/moderation/configs/joinGate.json index 95194423..e77776a5 100644 --- a/modules/moderation/configs/joinGate.json +++ b/modules/moderation/configs/joinGate.json @@ -1,62 +1,30 @@ { - "description": { - "en": "This system can prevent suspicious accounts from getting access to your server", - "de": "Dieses System kann verhindern, dass verdächtige Accounts Zugriff erhalten" - }, - "humanName": { - "de": "Join-Gate-Konfiguration", - "en": "Join-Gate-Configuration" - }, + "description": "This system can prevent suspicious accounts from getting access to your server", + "humanName": "Join-Gate-Configuration", "filename": "joinGate.json", "content": [ { "name": "enabled", - "humanName": { - "de": "Aktiviert?", - "en": "Enabled?" - }, - "default": { - "en": true, - "de": true - }, - "description": { - "en": "Enable or disable the join gate", - "de": "Aktiviere oder deaktiviere das Join-Gate" - }, + "humanName": "Enabled?", + "default": true, + "description": "Enable or disable the join gate", "type": "boolean", "elementToggle": true, "category": "general" }, { "name": "allUsers", - "humanName": { - "de": "Alle Nutzer filtern", - "en": "Filter all users" - }, - "default": { - "en": false - }, - "description": { - "en": "If enabled all users action against all new users will be taken", - "de": "Wenn aktiviert, werden Aktionen gegen alle neuen Nutzer ausgefüht" - }, + "humanName": "Filter all users", + "default": false, + "description": "If enabled all users action against all new users will be taken", "type": "boolean", "category": "general" }, { "name": "action", - "humanName": { - "de": "Aktion", - "en": "Action" - }, - "default": { - "en": "quarantine", - "de": "quarantine" - }, - "description": { - "en": "Select the action here that should get performed if the join gate gets triggered", - "de": "Wähle hier die Aktion, die ausgeführt werden soll, wenn das Join-Gate ausgelöst wird" - }, + "humanName": "Action", + "default": "quarantine", + "description": "Select the action here that should get performed if the join gate gets triggered", "type": "select", "content": [ "mute", @@ -69,85 +37,41 @@ }, { "name": "roleID", - "humanName": { - "de": "Rolle", - "en": "Role" - }, - "default": { - "en": "" - }, - "description": { - "en": "Only if action = give-role. Role that gets given to users who fail the join gate", - "de": "Nur verfügbar, wenn Aktion = give-role. Rolle, die Nutzern gegeben wird, die das Join-Gate nicht bestehen" - }, + "humanName": "Role", + "default": "", + "description": "Only if action = give-role. Role that gets given to users who fail the join gate", "type": "roleID", "category": "roles" }, { "name": "removeOtherRoles", - "humanName": { - "de": "Andere Rollen entfernen", - "en": "Remove other roles" - }, - "default": { - "en": true, - "de": true - }, - "description": { - "en": "Only if action = give-role. If enabled other roles that have been give to the user get removed after a short interval (and the giving of the role from \"roleID\" will be delayed)", - "de": "Nur verfügbar, wenn Aktion = give-role. Wenn aktiviert, werden andere Rollen die der Nutzer hat nach einem kurzen Zeitraum entfernt (und das Vergeben der Rolle von \"Rolle\" wird verzögert)" - }, + "humanName": "Remove other roles", + "default": true, + "description": "Only if action = give-role. If enabled other roles that have been give to the user get removed after a short interval (and the giving of the role from \"roleID\" will be delayed)", "type": "boolean", "category": "roles" }, { "name": "minAccountAge", - "humanName": { - "de": "Minimales Accountalter", - "en": "Minimum account age" - }, - "default": { - "en": "3", - "de": 3 - }, - "description": { - "en": "Age of the account of a new user that is required to be set to pass the join gate (in days)", - "de": "Alter des Accounts eines neuen Nutzers, der beitritt, welches benötigt wird um das Join-Gate zu bestehen (in Tagen)" - }, + "humanName": "Minimum account age", + "default": 3, + "description": "Age of the account of a new user that is required to be set to pass the join gate (in days)", "type": "integer", "category": "general" }, { "name": "requireProfilePicture", - "humanName": { - "de": "Benötige Profilbild", - "en": "Require profile picture" - }, - "default": { - "en": true, - "de": true - }, - "description": { - "en": "If enabled users are required to have a profile picture set to pass the join gate", - "de": "Wenn aktiviert, brauchen Nutzer ein Profilbild um das Join-Gate zu bestehen" - }, + "humanName": "Require profile picture", + "default": true, + "description": "If enabled users are required to have a profile picture set to pass the join gate", "type": "boolean", "category": "general" }, { "name": "ignoreBots", - "humanName": { - "de": "Ignoriere Bots", - "en": "Ignore bots" - }, - "default": { - "en": true, - "de": true - }, - "description": { - "en": "If enabled bots are allowed to pass the join gate without any restrictions", - "de": "Wenn aktiviert, bestehen Bots das Join-Gate ohne Beschränkungen" - }, + "humanName": "Ignore bots", + "default": true, + "description": "If enabled bots are allowed to pass the join gate without any restrictions", "type": "boolean", "category": "general" } @@ -156,18 +80,12 @@ { "id": "general", "icon": "fas fa-door-open", - "displayName": { - "en": "General Settings", - "de": "Allgemeine Einstellungen" - } + "displayName": "General Settings" }, { "id": "roles", "icon": "fa-solid fa-users", - "displayName": { - "en": "Roles", - "de": "Rollen" - } + "displayName": "Roles" } ] } \ No newline at end of file diff --git a/modules/moderation/configs/lockdown.json b/modules/moderation/configs/lockdown.json index 51b15db9..d0eded22 100644 --- a/modules/moderation/configs/lockdown.json +++ b/modules/moderation/configs/lockdown.json @@ -1,27 +1,13 @@ { - "description": { - "en": "Configure the server-wide lockdown system. This is separate from per-channel lock/unlock commands.", - "de": "Konfiguriere das serverweite Lockdown-System. Dies ist getrennt von den kanalweisen Sperr-/Entsperr-Befehlen." - }, - "humanName": { - "en": "Lockdown Configuration", - "de": "Lockdown-Konfiguration" - }, + "description": "Configure the server-wide lockdown system. This is separate from per-channel lock/unlock commands.", + "humanName": "Lockdown Configuration", "filename": "lockdown.json", "content": [ { "name": "enabled", - "humanName": { - "en": "Enable lockdown system?", - "de": "Lockdown-System aktivieren?" - }, - "default": { - "en": false - }, - "description": { - "en": "Enables the /moderate lockdown command and automatic lockdown triggers", - "de": "Aktiviert den /moderate lockdown Befehl und automatische Lockdown-Auslöser" - }, + "humanName": "Enable lockdown system?", + "default": false, + "description": "Enables the /moderate lockdown command and automatic lockdown triggers", "type": "boolean", "elementToggle": true, "category": "general" @@ -30,34 +16,28 @@ "name": "logChannel", "type": "channelID", "dependsOn": "enabled", - "humanName": { - "en": "Lockdown log channel", - "de": "Lockdown-Log-Kanal" - }, - "default": { - "en": "" - }, - "description": { - "en": "Channel where detailed lockdown log entries are posted. Falls back to the moderation log channel if not set.", - "de": "Kanal, in dem detaillierte Lockdown-Logeinträge gepostet werden. Fällt auf den Moderations-Logkanal zurück, wenn nicht gesetzt." - }, + "humanName": "Lockdown log channel", + "default": "", + "description": "Channel where detailed lockdown log entries are posted. Falls back to the moderation log channel if not set.", "category": "general" }, { "name": "sendMessageInAffectedChannels", "type": "boolean", "dependsOn": "enabled", - "humanName": { - "en": "Send message in affected channels?", - "de": "Nachricht in betroffenen Kanälen senden?" - }, - "default": { - "en": true - }, - "description": { - "en": "If enabled, the lockdown/lift message will be sent in every affected channel", - "de": "Wenn aktiviert, wird die Lockdown-/Aufhebungsnachricht in jedem betroffenen Kanal gesendet" - }, + "humanName": "Send message in affected channels?", + "default": true, + "description": "If enabled, the lockdown/lift message will be sent in every affected channel", + "category": "messages" + }, + { + "name": "lockdownMessageChannels", + "type": "array", + "content": "channelID", + "dependsOn": "sendMessageInAffectedChannels", + "humanName": "Channels for lockdown messages", + "default": [], + "description": "If set, lockdown/lift messages will only be sent in these channels instead of all affected channels. Leave empty to send in all affected channels.", "category": "messages" }, { @@ -65,32 +45,17 @@ "type": "string", "allowEmbed": true, "dependsOn": "sendMessageInAffectedChannels", - "humanName": { - "en": "Lockdown activation message", - "de": "Lockdown-Aktivierungsnachricht" - }, - "description": { - "en": "Message sent in affected channels when lockdown is activated", - "de": "Nachricht, die in betroffenen Kanälen gesendet wird, wenn der Lockdown aktiviert wird" - }, - "default": { - "en": "🔒 **Server Lockdown** - This server is currently in lockdown mode. Reason: %reason%", - "de": "🔒 **Server-Lockdown** - Dieser Server befindet sich im Lockdown-Modus. Grund: %reason%" - }, + "humanName": "Lockdown activation message", + "description": "Message sent in affected channels when lockdown is activated", + "default": "🔒 **Server Lockdown** - This server is currently in lockdown mode. Reason: %reason%", "params": [ { "name": "reason", - "description": { - "en": "Reason for the lockdown", - "de": "Grund für den Lockdown" - } + "description": "Reason for the lockdown" }, { "name": "user", - "description": { - "en": "User who activated the lockdown (or 'System' for automatic)", - "de": "Nutzer, der den Lockdown aktiviert hat (oder 'System' bei automatisch)" - } + "description": "User who activated the lockdown (or 'System' for automatic)" } ], "category": "messages" @@ -100,25 +65,13 @@ "type": "string", "allowEmbed": true, "dependsOn": "sendMessageInAffectedChannels", - "humanName": { - "en": "Lockdown lifted message", - "de": "Lockdown-Aufhebungsnachricht" - }, - "description": { - "en": "Message sent in affected channels when lockdown is lifted", - "de": "Nachricht, die in betroffenen Kanälen gesendet wird, wenn der Lockdown aufgehoben wird" - }, - "default": { - "en": "🔓 **Lockdown Lifted** - The server lockdown has been lifted. You can chat again.", - "de": "🔓 **Lockdown aufgehoben** - Der Server-Lockdown wurde aufgehoben. Ihr könnt wieder schreiben." - }, + "humanName": "Lockdown lifted message", + "description": "Message sent in affected channels when lockdown is lifted", + "default": "🔓 **Lockdown Lifted** - The server lockdown has been lifted. You can chat again.", "params": [ { "name": "user", - "description": { - "en": "User who lifted the lockdown", - "de": "Nutzer, der den Lockdown aufgehoben hat" - } + "description": "User who lifted the lockdown" } ], "category": "messages" @@ -127,68 +80,36 @@ "name": "autoLiftAfter", "type": "integer", "dependsOn": "enabled", - "humanName": { - "en": "Auto-lift lockdown after (minutes, 0 = manual only)", - "de": "Lockdown automatisch aufheben nach (Minuten, 0 = nur manuell)" - }, - "default": { - "en": 0 - }, - "description": { - "en": "Automatically lift the lockdown after this many minutes. Set to 0 to require manual lifting.", - "de": "Den Lockdown nach dieser Anzahl Minuten automatisch aufheben. Auf 0 setzen für nur manuelle Aufhebung." - }, + "humanName": "Auto-lift lockdown after (minutes, 0 = manual only)", + "default": 0, + "description": "Automatically lift the lockdown after this many minutes. Set to 0 to require manual lifting.", "category": "automation" }, { "name": "autoTriggerOnJoinRaid", "type": "boolean", "dependsOn": "enabled", - "humanName": { - "en": "Auto-lockdown on join raid?", - "de": "Automatischer Lockdown bei Join-Raid?" - }, - "default": { - "en": false - }, - "description": { - "en": "Automatically activate lockdown when the anti-join-raid system is triggered", - "de": "Lockdown automatisch aktivieren, wenn das Anti-Join-Raid-System ausgelöst wird" - }, + "humanName": "Auto-lockdown on join raid?", + "default": false, + "description": "Automatically activate lockdown when the anti-join-raid system is triggered", "category": "automation" }, { "name": "autoTriggerOnJoinGate", "type": "boolean", "dependsOn": "enabled", - "humanName": { - "en": "Auto-lockdown on join-gate violations?", - "de": "Automatischer Lockdown bei Join-Gate-Verletzungen?" - }, - "default": { - "en": false - }, - "description": { - "en": "Automatically activate lockdown when the join-gate system is triggered. Thresholds are configured in the Join-Gate configuration.", - "de": "Lockdown automatisch aktivieren, wenn das Join-Gate-System ausgelöst wird. Schwellwerte werden in der Join-Gate-Konfiguration konfiguriert." - }, + "humanName": "Auto-lockdown on join-gate violations?", + "default": false, + "description": "Automatically activate lockdown when the join-gate system is triggered. Thresholds are configured in the Join-Gate configuration.", "category": "automation" }, { "name": "autoTriggerOnSpam", "type": "boolean", "dependsOn": "enabled", - "humanName": { - "en": "Auto-lockdown on spam detection?", - "de": "Automatischer Lockdown bei Spam-Erkennung?" - }, - "default": { - "en": false - }, - "description": { - "en": "Automatically activate lockdown when the anti-spam system is triggered. Thresholds are configured in the Anti-Spam configuration.", - "de": "Lockdown automatisch aktivieren, wenn das Anti-Spam-System ausgelöst wird. Schwellwerte werden in der Anti-Spam-Konfiguration konfiguriert." - }, + "humanName": "Auto-lockdown on spam detection?", + "default": false, + "description": "Automatically activate lockdown when the anti-spam system is triggered. Thresholds are configured in the Anti-Spam configuration.", "category": "automation" } ], @@ -196,26 +117,17 @@ { "id": "general", "icon": "fas fa-gears", - "displayName": { - "en": "General Settings", - "de": "Allgemeine Einstellungen" - } + "displayName": "General Settings" }, { "id": "messages", "icon": "fas fa-comment-dots", - "displayName": { - "en": "Messages", - "de": "Nachrichten" - } + "displayName": "Messages" }, { "id": "automation", "icon": "far fa-robot", - "displayName": { - "en": "Automation", - "de": "Automatisierung" - } + "displayName": "Automation" } ] } \ No newline at end of file diff --git a/modules/moderation/configs/strings.json b/modules/moderation/configs/strings.json index 392255a3..b5841570 100644 --- a/modules/moderation/configs/strings.json +++ b/modules/moderation/configs/strings.json @@ -1,516 +1,347 @@ { - "description": { - "en": "Set up which messages your bot should send", - "de": "Stelle hier ein, welche Nachrichten dein Bot schicken soll" - }, - "humanName": { - "en": "Messages", - "de": "Nachrichten" - }, + "description": "Set up which messages your bot should send", + "humanName": "Messages", "filename": "strings.json", "content": [ { "name": "no_permissions", - "humanName": {}, - "default": { - "en": "You can not do that. You need at least moderator level %required_level% to do this", - "de": "You can not do that. You need at least moderator level %required_level% to do this" - }, - "description": { - "en": "Message that gets send if the user doesn't has the required role and/or has not the required mod-level" - }, + "humanName": "No Permissions", + "default": "You can not do that. You need at least moderator level %required_level% to do this", + "description": "Message that gets send if the user doesn't has the required role and/or has not the required mod-level", "type": "string", "allowEmbed": true, "params": [ { "name": "required_level", - "description": { - "en": "Required mod-level to do this." - } + "description": "Required mod-level to do this." } ], "category": "actions" }, { "name": "user_not_found", - "humanName": {}, - "default": { - "en": "I could not find this user - try using an ID or a mention", - "de": "I could not find this user - try using an ID or a mention" - }, - "description": { - "en": "Message that gets send if the user provided an invalid userid" - }, + "humanName": "User Not Found", + "default": "I could not find this user - try using an ID or a mention", + "description": "Message that gets send if the user provided an invalid userid", "type": "string", "allowEmbed": true, "category": "actions" }, { "name": "missing_reason", - "humanName": {}, - "default": { - "en": "Please specify an reason", - "de": "Please specify an reason" - }, - "description": { - "en": "Message that gets send if the user does not provide a reason and 'require reason' is activated" - }, + "humanName": "Missing Reason", + "default": "Please specify an reason", + "description": "Message that gets send if the user does not provide a reason and 'require reason' is activated", "type": "string", "allowEmbed": true, "category": "errors" }, { "name": "this_is_a_mod", - "humanName": {}, - "default": { - "en": "You can not perform this action on your college.", - "de": "You can not perform this action on your college." - }, - "description": { - "en": "Message that gets send if the user tries to mute another moderator" - }, + "humanName": "Target Is a Moderator", + "default": "You can not perform this action on your college.", + "description": "Message that gets send if the user tries to mute another moderator", "type": "string", "allowEmbed": true, "category": "actions" }, { "name": "submitted-report-message", - "humanName": {}, - "default": { - "en": "Thanks for reporting %user%. I notified our server team and transmitted them an [encrypted snapshot](<%mURL%>) of the current messages in this channel, so they can see what really happened. Please make sure that our bots and staff can message you, so we can ask you follow-up-questions, if needed.", - "de": "Thanks for reporting %user%. I notified our server team and transmitted them an [encrypted snapshot](<%mURL%>) of the current messages in this channel, so they can see what really happened. Please make sure that our bots and staff can message you, so we can ask you follow-up-questions, if needed." - }, - "description": { - "en": "Message that gets send, if someone reports somebody." - }, + "humanName": "Report Submitted", + "default": "Thanks for reporting %user%. I notified our server team and transmitted them an [encrypted snapshot](<%mURL%>) of the current messages in this channel, so they can see what really happened. Please make sure that our bots and staff can message you, so we can ask you follow-up-questions, if needed.", + "description": "Message that gets send, if someone reports somebody.", "type": "string", "allowEmbed": true, "params": [ { "name": "user", - "description": { - "en": "Tag of the user they reported" - } + "description": "Tag of the user they reported" }, { "name": "mURL", - "description": { - "en": "URL to the message log" - } + "description": "URL to the message log" } ], "category": "actions" }, { "name": "mute_message", - "humanName": {}, - "default": { - "en": "You got muted for **%reason%** by %user%!", - "de": "You got muted for **%reason%** by %user%!" - }, - "description": { - "en": "Message that gets send to a user when they got muted" - }, + "humanName": "Mute Message", + "default": "You got muted for **%reason%** by %user%!", + "description": "Message that gets send to a user when they got muted", "type": "string", "allowEmbed": true, "params": [ { "name": "user", - "description": { - "en": "Tag of the moderator" - } + "description": "Tag of the moderator" }, { "name": "reason", - "description": { - "en": "Reason of the mute" - } + "description": "Reason of the mute" } ], "category": "actions" }, { "name": "channel_mute", - "humanName": {}, - "default": { - "en": "You got channel-muted from %channel% for **%reason%** by %user%!" - }, - "description": { - "en": "Message that gets send to a user when they got muted" - }, + "humanName": "Channel Mute Message", + "default": "You got channel-muted from %channel% for **%reason%** by %user%!", + "description": "Message that gets send to a user when they got muted", "type": "string", "allowEmbed": true, "params": [ { "name": "user", - "description": { - "en": "Tag of the moderator" - } + "description": "Tag of the moderator" }, { "name": "reason", - "description": { - "en": "Reason of the mute" - } + "description": "Reason of the mute" }, { "name": "channel", - "description": { - "en": "Channel from which the user got muted" - } + "description": "Channel from which the user got muted" } ], "category": "actions" }, { "name": "remove-channel_mute", - "humanName": {}, - "default": { - "en": "Your channel-mute from %channel% got removed because of **%reason%** by %user%!" - }, - "description": { - "en": "Message that gets send to a user when they got muted" - }, + "humanName": "Channel Unmute Message", + "default": "Your channel-mute from %channel% got removed because of **%reason%** by %user%!", + "description": "Message that gets send to a user when they got muted", "type": "string", "allowEmbed": true, "params": [ { "name": "user", - "description": { - "en": "Tag of the moderator" - } + "description": "Tag of the moderator" }, { "name": "reason", - "description": { - "en": "Reason of the mute" - } + "description": "Reason of the mute" }, { "name": "channel", - "description": { - "en": "Channel from which the user got unmuted" - } + "description": "Channel from which the user got unmuted" } ], "category": "actions" }, { "name": "tmpmute_message", - "humanName": {}, - "default": { - "en": "You got temporarily muted for **%reason%** by %user%! This action is going to expire on %date%.", - "de": "You got temporarily muted for **%reason%** by %user%! This action is going to expire on %date%." - }, - "description": { - "en": "Message that gets send to a user when they got temporarily muted" - }, + "humanName": "Temporary Mute Message", + "default": "You got temporarily muted for **%reason%** by %user%! This action is going to expire on %date%.", + "description": "Message that gets send to a user when they got temporarily muted", "type": "string", "allowEmbed": true, "params": [ { "name": "user", - "description": { - "en": "Tag of the moderator" - } + "description": "Tag of the moderator" }, { "name": "reason", - "description": { - "en": "Reason of the mute" - } + "description": "Reason of the mute" }, { "name": "date", - "description": { - "en": "Timestamp when this action expires" - } + "description": "Timestamp when this action expires" } ], "category": "actions" }, { "name": "quarantine_message", - "humanName": {}, - "default": { - "en": "You got quarantined for **%reason%** by %user%!", - "de": "You got quarantined for **%reason%** by %user%!" - }, - "description": { - "en": "Message that gets send to a user when they get quarantined" - }, + "humanName": "Quarantine Message", + "default": "You got quarantined for **%reason%** by %user%!", + "description": "Message that gets send to a user when they get quarantined", "type": "string", "allowEmbed": true, "params": [ { "name": "user", - "description": { - "en": "Tag of the moderator" - } + "description": "Tag of the moderator" }, { "name": "reason", - "description": { - "en": "Reason of the mute" - } + "description": "Reason of the mute" } ], "category": "actions" }, { "name": "tmpquarantine_message", - "humanName": {}, - "default": { - "en": "You got quarantined temporarily for **%reason%** by %user%! This action is going to expire on %date%", - "de": "You got quarantined temporarily for **%reason%** by %user%! This action is going to expire on %date%" - }, - "description": { - "en": "Message that gets send to a user when they get quarantined" - }, + "humanName": "Temporary Quarantine Message", + "default": "You got quarantined temporarily for **%reason%** by %user%! This action is going to expire on %date%", + "description": "Message that gets send to a user when they get quarantined", "type": "string", "allowEmbed": true, "params": [ { "name": "user", - "description": { - "en": "Tag of the moderator" - } + "description": "Tag of the moderator" }, { "name": "reason", - "description": { - "en": "Reason of the mute" - } + "description": "Reason of the mute" }, { "name": "date", - "description": { - "en": "Date when the quarantine is going to be removed automatically" - } + "description": "Date when the quarantine is going to be removed automatically" } ], "category": "actions" }, { "name": "unquarantine_message", - "humanName": {}, - "default": { - "en": "You got unquarantined for **%reason%** by %user%!", - "de": "You got unquarantined for **%reason%** by %user%!" - }, - "description": { - "en": "Message that gets send to a user when they get unquarantined" - }, + "humanName": "Unquarantine Message", + "default": "You got unquarantined for **%reason%** by %user%!", + "description": "Message that gets send to a user when they get unquarantined", "type": "string", "allowEmbed": true, "params": [ { "name": "user", - "description": { - "en": "Tag of the moderator" - } + "description": "Tag of the moderator" }, { "name": "reason", - "description": { - "en": "Reason of the mute" - } + "description": "Reason of the mute" } ], "category": "actions" }, { "name": "unmute_message", - "humanName": {}, - "default": { - "en": "You got unmuted for **%reason%** by %user%!", - "de": "You got unmuted for **%reason%** by %user%!" - }, - "description": { - "en": "Message that gets send to a user when they got unmuted" - }, + "humanName": "Unmute Message", + "default": "You got unmuted for **%reason%** by %user%!", + "description": "Message that gets send to a user when they got unmuted", "type": "string", "allowEmbed": true, "params": [ { "name": "user", - "description": { - "en": "Tag of the moderator" - } + "description": "Tag of the moderator" }, { "name": "reason", - "description": { - "en": "Reason of the unmute" - } + "description": "Reason of the unmute" } ], "category": "actions" }, { "name": "kick_message", - "humanName": {}, - "default": { - "en": "You got kicked for **%reason%** by %user%!", - "de": "You got kicked for **%reason%** by %user%!" - }, - "description": { - "en": "Message that gets send to a user when they got kicked" - }, + "humanName": "Kick Message", + "default": "You got kicked for **%reason%** by %user%!", + "description": "Message that gets send to a user when they got kicked", "type": "string", "allowEmbed": true, "params": [ { "name": "user", - "description": { - "en": "Tag of the moderator" - } + "description": "Tag of the moderator" }, { "name": "reason", - "description": { - "en": "Reason of the kick" - } + "description": "Reason of the kick" } ], "category": "actions" }, { "name": "ban_message", - "humanName": {}, - "default": { - "en": "You got banned for **%reason%** by %user%!", - "de": "You got banned for **%reason%** by %user%!" - }, - "description": { - "en": "Message that gets send to a user when they got banned" - }, + "humanName": "Ban Message", + "default": "You got banned for **%reason%** by %user%!", + "description": "Message that gets send to a user when they got banned", "type": "string", "allowEmbed": true, "params": [ { "name": "user", - "description": { - "en": "Tag of the moderator" - } + "description": "Tag of the moderator" }, { "name": "reason", - "description": { - "en": "Reason of the ban" - } + "description": "Reason of the ban" } ], "category": "actions" }, { "name": "tmpban_message", - "humanName": {}, - "default": { - "en": "You got temporarily banned for **%reason%** by %user%! This action is going to expire on %date%", - "de": "You got temporarily banned for **%reason%** by %user%! This action is going to expire on %date%" - }, - "description": { - "en": "Message that gets send to a user when they got banned temporarily" - }, + "humanName": "Temporary Ban Message", + "default": "You got temporarily banned for **%reason%** by %user%! This action is going to expire on %date%", + "description": "Message that gets send to a user when they got banned temporarily", "type": "string", "allowEmbed": true, "params": [ { "name": "user", - "description": { - "en": "Tag of the moderator" - } + "description": "Tag of the moderator" }, { "name": "reason", - "description": { - "en": "Reason of the ban" - } + "description": "Reason of the ban" }, { "name": "date", - "description": { - "en": "Date on which the ban expires" - } + "description": "Date on which the ban expires" } ], "category": "actions" }, { "name": "warn_message", - "humanName": {}, - "default": { - "en": "You got warned for **%reason%** by %user%!", - "de": "You got warned for **%reason%** by %user%!" - }, - "description": { - "en": "Message that gets send to a user when they got warned" - }, + "humanName": "Warn Message", + "default": "You got warned for **%reason%** by %user%!", + "description": "Message that gets send to a user when they got warned", "type": "string", "allowEmbed": true, "params": [ { "name": "user", - "description": { - "en": "Tag of the moderator" - } + "description": "Tag of the moderator" }, { "name": "reason", - "description": { - "en": "Reason of the warn" - } + "description": "Reason of the warn" } ], "category": "actions" }, { "name": "lock_channel_message", - "humanName": {}, - "default": { - "en": "This channel got locked because %reason% by %user%", - "de": "This channel got locked because %reason% by %user%" - }, - "description": { - "en": "Message that gets send in a channel if it gets locked" - }, + "humanName": "Channel Lock Message", + "default": "This channel got locked because %reason% by %user%", + "description": "Message that gets send in a channel if it gets locked", "type": "string", "allowEmbed": true, "params": [ { "name": "user", - "description": { - "en": "Tag of the moderator" - } + "description": "Tag of the moderator" }, { "name": "reason", - "description": { - "en": "Reason of the lock" - } + "description": "Reason of the lock" } ], "category": "actions" }, { "name": "unlock_channel_message", - "humanName": {}, - "default": { - "en": "This channel got unlocked by %user%", - "de": "This channel got unlocked by %user%" - }, - "description": { - "en": "Message that gets send in a channel if it gets unlocked" - }, + "humanName": "Channel Unlock Message", + "default": "This channel got unlocked by %user%", + "description": "Message that gets send in a channel if it gets unlocked", "type": "string", "allowEmbed": true, "params": [ { "name": "user", - "description": { - "en": "Tag of the moderator" - } + "description": "Tag of the moderator" } ], "category": "actions" @@ -520,18 +351,12 @@ { "id": "actions", "icon": "fas fa-hammer", - "displayName": { - "en": "Action Messages", - "de": "Aktionsnachrichten" - } + "displayName": "Action Messages" }, { "id": "errors", "icon": "fa-duotone fa-regular fa-triangle-exclamation", - "displayName": { - "en": "Error Messages", - "de": "Fehlermeldungen" - } + "displayName": "Error Messages" } ] -} \ No newline at end of file +} diff --git a/modules/moderation/configs/verification.json b/modules/moderation/configs/verification.json index bd97f94f..f5a7652b 100644 --- a/modules/moderation/configs/verification.json +++ b/modules/moderation/configs/verification.json @@ -1,137 +1,105 @@ { - "description": { - "en": "Require accounts to verify that they are not a robot before accessing your server", - "de": "Zwinge neue Nutzer zu verifizieren, dass sie kein Roboter sind" - }, - "humanName": { - "de": "Verifikation-Konfiguration", - "en": "Verification-Configuration" - }, + "description": "Require accounts to verify that they are not a robot before accessing your server", + "humanName": "Verification-Configuration", "filename": "verification.json", "content": [ { "name": "enabled", - "humanName": { - "de": "Aktiviert?", - "en": "Enabled?" - }, - "default": { - "en": false - }, - "description": { - "en": "If checked, verification on your server will be enabled", - "de": "Wenn aktiviert, wird Verifikation auf deinem Server aktiviert" - }, + "humanName": "Enabled?", + "default": false, + "description": "If checked, verification on your server will be enabled", "type": "boolean", "elementToggle": true, "category": "general" }, { "name": "verification-needed-role", - "humanName": { - "de": "Rolle für Nutzer, die sich noch verifizieren müssen", - "en": "Role for users with pending verification" - }, - "default": { - "en": "" - }, - "description": { - "en": "Role, which members should be given before they verify themselves", - "de": "Rolle, die Nutzer erhalten, bevor sie sich verifiziert haben" - }, + "humanName": "Role for users with pending verification", + "default": "", + "description": "Role, which members should be given before they verify themselves", "type": "roleID", "allowNull": true, "category": "roles" }, { "name": "verification-passed-role", - "humanName": { - "de": "Rolle für Nutzer mit bestandener Verifikation", - "en": "Role for users that passed verification" - }, - "default": { - "en": "" - }, - "description": { - "en": "Role, which members should be given after they got verified successfully", - "de": "Rolle, die Nutzern gegeben werden soll, wenn sie sich erfolgreich verifiziert haben" - }, + "humanName": "Role for users that passed verification", + "default": "", + "description": "Role, which members should be given after they got verified successfully", "type": "roleID", "allowNull": true, "category": "roles" }, { "name": "verification-log", - "humanName": {}, - "default": { - "en": "Verification-Log", - "de": "Verifikation-Log" - }, - "description": { - "en": "Channel where all verification-actions should get logged", - "de": "Kanal, in welchem alle Verifikation-Aktionen dokumentiert werden sollen" - }, + "humanName": "Verification Log Channel", + "default": "Verification-Log", + "description": "Channel where all verification-actions should get logged", "type": "channelID", "allowNull": true, "category": "general" }, { "name": "type", - "humanName": { - "en": "Type of verification", - "de": "Art der Verifikation" - }, - "default": { - "en": "captcha", - "de": "captcha" - }, - "description": { - "en": "How should the verification process be performed on your server?", - "de": "Wie sollen sich Nutzer verifizieren müssen, wenn sie den Server beitreten?" - }, + "humanName": "Type of verification", + "default": "captcha", + "description": "How should new members verify themselves on your server?", "type": "select", "content": [ - "manual", - "captcha" + { + "displayName": "Image Captcha: distorted image, solved in-channel", + "value": "captcha" + }, + { + "displayName": "Image Captcha (DM): legacy, sent via direct message", + "value": "captcha-dm" + }, + { + "displayName": "Word challenge: retype a displayed word", + "value": "word" + }, + { + "displayName": "Math challenge: solve an arithmetic problem", + "value": "math" + }, + { + "displayName": "Manual: a moderator approves each new member", + "value": "manual" + }, + { + "displayName": "Button click: one click, no challenge", + "value": "button" + } ], "category": "general" }, { "name": "captchaLevel", - "humanName": { - "en": "Difficulty of captcha", - "de": "Schwäre des Captcha" - }, - "default": { - "en": "medium", - "de": "medium" - }, - "description": { - "en": "How difficult should the captcha sent to users be? (only if \"Type of verification\" = \"captcha\")", - "de": "Wie schwer soll das Captcha sein, dass an Nutzer gesendet wird? (Nur wenn \"Art der Verifikation\" = \"captcha\")" - }, + "humanName": "Challenge difficulty", + "default": "medium", + "description": "Difficulty of the verification challenge. Applies to Image Captcha, Image Captcha (DM), Word and Math. Not used for Manual or Button.", "type": "select", "content": [ - "easy", - "medium", - "hard" + { + "displayName": "Easy: short words / small numbers", + "value": "easy" + }, + { + "displayName": "Medium (default)", + "value": "medium" + }, + { + "displayName": "Hard: longer words / larger numbers & multiplication", + "value": "hard" + } ], - "category": "messages" + "category": "general" }, { "name": "actionOnFail", - "humanName": { - "de": "Aktion bei Fehlschlagen der Verifikation", - "en": "Action on failure of verification" - }, - "default": { - "en": "kick", - "de": "kick" - }, - "description": { - "en": "What should happen if someone fails the verification?", - "de": "Was soll passieren, wenn die Verifikation fehlschlägt?" - }, + "humanName": "Action on failure of verification", + "default": "kick", + "description": "What should happen if someone fails the verification?", "type": "select", "content": [ "kick", @@ -142,108 +110,94 @@ "category": "general" }, { - "name": "restart-verification-channel", - "humanName": { - "de": "Verifikation-Neustarten-Kanal", - "en": "Restart Verification-Channel" - }, - "default": { - "en": "" - }, - "description": { - "en": "(optional) Add support for a channel where users can easily restart their verification process (for example if they had DMs disabled when they tried) and get notified if we couldn't reach them", - "de": "(optional) Kanal in welchem Nutzer ganz einfach den Verifikationsprozess neustarten können (zum Beispiel, wenn der Nutzer PNs deaktiviert hat) und benachrichtigt werden, wenn wir sie nicht erreichen konnten" - }, + "name": "verification-channel", + "humanName": "Verification Channel", + "default": "", + "description": "Channel where users can verify themselves by clicking the Verify Me button. For the legacy DM type, this serves as a fallback channel for users with DMs disabled.", "type": "channelID", "allowNull": true, "category": "general" }, + { + "name": "maxRetries", + "humanName": "Maximum verification attempts", + "default": 3, + "description": "How many attempts a user gets before the failure action is applied. Applies to Image Captcha, Image Captcha (DM), Word and Math types.", + "type": "integer", + "category": "general" + }, + { + "name": "retryCooldown", + "humanName": "Cooldown between retries", + "default": "5m", + "description": "How long a user must wait between verification attempts (e.g. 5m, 10m, 1h).", + "type": "string", + "category": "general" + }, + { + "name": "actionOnFailDuration", + "humanName": "Punishment duration", + "default": "1h", + "description": "Duration for mute or quarantine punishment when a user exhausts all verification attempts (e.g. 1h, 1d). Only applies when action on fail is mute or quarantine.", + "type": "string", + "category": "general" + }, + { + "name": "cooldown-message", + "humanName": "Cooldown message", + "default": "⏳ Please wait %t% before trying again.", + "description": "Shown when a user needs to wait before verifying again.", + "type": "string", + "allowEmbed": true, + "category": "messages", + "params": [ + { + "name": "t", + "description": "Discord timestamp showing when the user can try again" + } + ] + }, { "name": "captcha-message", - "humanName": { - "de": "Captcha-Nachricht", - "en": "Captcha-Message" - }, - "default": { - "en": "Welcome! Please verify that you are a human. You have two minutes to complete this.", - "de": "Willkommen! Bitte verifiziere, dass du kein Bot bist. Du hast zwei Minuten, um dies zu tun." - }, - "description": { - "en": "This message gets sent to users who need to complete a captcha", - "de": "Diese Nachricht wird an den Nutzer gesendet, der ein Captcha durchführen muss" - }, + "humanName": "Captcha-Message", + "default": "Welcome! Please verify that you are a human. You have two minutes to complete this.", + "description": "This message gets sent to users who need to complete a captcha", "type": "string", "allowEmbed": true, "category": "messages" }, { "name": "manual-verification-message", - "humanName": { - "en": "Manual-Verification-Message", - "de": "Manuelle-Verifikation-Nachricht" - }, - "default": { - "en": "Welcome! A human will be verifying your account shortly. I will update you if I have any news.", - "de": "Willkommen! Ein Mensch wird deinen Account bald überprüfen und dir Zugriff auf den Server geben, bitte gedulde dich. Ich informiere dich bei Neuigkeiten." - }, - "description": { - "en": "This message gets sent to users who need to get verified manually.", - "de": "Diese Nachricht wird an Nutzer geschickt, die manuell verifiziert werden müssen" - }, + "humanName": "Manual-Verification-Message", + "default": "Welcome! A human will be verifying your account shortly. I will update you if I have any news.", + "description": "This message gets sent to users who need to get verified manually.", "type": "string", "allowEmbed": true, "category": "messages" }, { "name": "captcha-failed-message", - "humanName": { - "de": "Captcha fehlgeschlagen-Nachricht", - "en": "Captcha failed-Message" - }, - "default": { - "en": "It seems like you failed the verification. This is bad, I will have to take moderative actions against you - sorry fellow bot.", - "de": "Es scheint, als hättest du die Verifikation nicht bestanden. Schade, ich werde moderative Maßnahmen gegen dich ergreifen - entschuldige, Roboter." - }, - "description": { - "en": "This message gets sent when a user fails the verification", - "de": "Diese Nachricht wird an Nutzer gesendet, bei denen die Verifikation fehlgeschlagen ist" - }, + "humanName": "Captcha failed-Message", + "default": "It seems like you failed the verification. This is bad, I will have to take moderative actions against you - sorry fellow bot.", + "description": "This message gets sent when a user fails the verification", "type": "string", "allowEmbed": true, "category": "messages" }, { "name": "captcha-succeeded-message", - "humanName": { - "de": "Captcha abgeschlossen-Nachricht", - "en": "Captcha completed-Message" - }, - "default": { - "en": "Thanks! We have verified that you are indeed not a bot, so I granted you access to the whole server! Have fun <3", - "de": "Danke dir! Wir konnten verifizieren, dass du tatsächlich kein Bot bist, also haben wir dir auf den gesamten Server Zugriff gegeben! Viel Spaß <3" - }, - "description": { - "en": "This message gets sent to users when they complete the verification", - "de": "Diese Nachricht wird gesendet, wenn ein Nutzer die Verifikation erfolgreich abgeschlossen hat" - }, + "humanName": "Captcha completed-Message", + "default": "Thanks! We have verified that you are indeed not a bot, so I granted you access to the whole server! Have fun <3", + "description": "This message gets sent to users when they complete the verification", "type": "string", "allowEmbed": true, "category": "messages" }, { "name": "verify-channel-first-message", - "humanName": { - "de": "Verifkations-Kanal-Info-Nachricht", - "en": "Verification-Channel-Info-Message" - }, - "default": { - "en": "Welcome! I have send you a DM about your verification-process. Please read it carefully. If you have DMs disabled, please activate them and click the button below. This step is required to join this server.", - "de": "Willkommen! Ich habe dir eine PN über den Verifikationsprozess. Bitte lese sie dir genau durch. Wenn du PNs deaktiviert hast, aktiviere sie bitte und klicke den Knopf unten. Dieser Schritt ist notwendig, um dem Server beizutreten." - }, - "description": { - "en": "This message is the introduction message in the verify-channel.", - "de": "Das ist die Informations-Nachricht im Verfikationskanal." - }, + "humanName": "Verification-Channel-Info-Message", + "default": "Welcome! Please verify yourself by clicking the button below. This step is required to access this server.", + "description": "This message is the introduction message in the verify-channel.", "type": "string", "allowEmbed": true, "category": "messages" @@ -253,26 +207,17 @@ { "id": "general", "icon": "fa-solid fa-badge-check", - "displayName": { - "en": "General Settings", - "de": "Allgemeine Einstellungen" - } + "displayName": "General Settings" }, { "id": "messages", "icon": "fas fa-comment-dots", - "displayName": { - "en": "Messages", - "de": "Nachrichten" - } + "displayName": "Messages" }, { "id": "roles", "icon": "fa-solid fa-users", - "displayName": { - "en": "Roles", - "de": "Rollen" - } + "displayName": "Roles" } ] -} \ No newline at end of file +} diff --git a/modules/moderation/events/botReady.js b/modules/moderation/events/botReady.js index cff01d6c..3d6d1c78 100644 --- a/modules/moderation/events/botReady.js +++ b/modules/moderation/events/botReady.js @@ -38,8 +38,13 @@ exports.run = async (client) => { await restoreLockdownState(client); const verificationConfig = client.configurations['moderation']['verification']; - if (!verificationConfig.enabled || !verificationConfig['restart-verification-channel']) return; - const channel = await client.channels.fetch(verificationConfig['restart-verification-channel']).catch(() => { + if (!verificationConfig.enabled) return; + + // Support both new and legacy config field name + const channelId = verificationConfig['verification-channel'] || verificationConfig['restart-verification-channel']; + if (!channelId) return; + + const channel = await client.channels.fetch(channelId).catch(() => { }); if (!channel || (channel || {}).type !== ChannelType.GuildText) return client.logger.error('[moderation] ' + localize('moderation', 'verify-channel-set-but-not-found-or-wrong-type')); let message = (await channel.messages.fetch()).filter(msg => msg.author.id === client.user.id).last(); @@ -47,6 +52,8 @@ exports.run = async (client) => { message = await channel.send(localize('moderation', 'generating-message')); await message.pin(); } + + const isLegacyDM = verificationConfig.type === 'captcha-dm'; await message.edit(embedType(verificationConfig['verify-channel-first-message'], {}, { components: [ { @@ -54,8 +61,8 @@ exports.run = async (client) => { components: [ { type: 'BUTTON', - label: '📨 ' + localize('moderation', 'restart-verification-button'), - customId: `mod-rvp`, + label: isLegacyDM ? ('📨 ' + localize('moderation', 'restart-verification-button')) : ('✅ ' + localize('moderation', 'verify-me-button')), + customId: isLegacyDM ? 'mod-rvp' : 'mod-verify', style: 'PRIMARY' } ] diff --git a/modules/moderation/events/guildMemberAdd.js b/modules/moderation/events/guildMemberAdd.js index 44172322..cc16286f 100644 --- a/modules/moderation/events/guildMemberAdd.js +++ b/modules/moderation/events/guildMemberAdd.js @@ -3,10 +3,11 @@ const {moderationAction} = require('../moderationActions'); const {activateLockdown, isLockdownActive} = require('../lockdown'); const {localize} = require('../../../src/functions/localize'); const {embedType} = require('../../../src/functions/helpers'); -const {ChannelType, MessageAttachment} = require('discord.js'); +const {ChannelType, AttachmentBuilder} = require('discord.js'); const {client} = require('../../../main'); let joinCache = []; +let raidActionInProgress = false; module.exports.run = async (client, guildMember) => { if (guildMember.guild.id !== client.config.guildID) return; @@ -30,7 +31,7 @@ module.exports.run = async (client, guildMember) => { joinCache = joinCache.filter(e => e.id !== guildMember.user.id && e.timestamp !== timestamp); }, antiJoinRaidConfig.timeframe * 60000); - if (joinCache.length >= antiJoinRaidConfig.maxJoinsInTimeframe) await performJoinRaidAction(); + if (joinCache.length >= antiJoinRaidConfig.maxJoinsInTimeframe && !raidActionInProgress) await performJoinRaidAction(); /** * Performs anti-join-raid actions @@ -38,6 +39,7 @@ module.exports.run = async (client, guildMember) => { * @return {Promise} */ async function performJoinRaidAction() { + raidActionInProgress = true; for (const join of joinCache.filter(j => j.id !== guildMember.user.id)) { const member = await guildMember.guild.members.fetch(join.id).catch(() => { }); @@ -67,6 +69,10 @@ module.exports.run = async (client, guildMember) => { if (lockdownConfig && lockdownConfig.enabled && lockdownConfig.autoTriggerOnJoinRaid && !await isLockdownActive(client)) { await activateLockdown(client, localize('moderation', 'lockdown-joinraid-trigger'), localize('moderation', 'lockdown-system'), true); } + joinCache = []; + setTimeout(() => { + raidActionInProgress = false; + }, 30000); } } @@ -79,68 +85,38 @@ module.exports.run = async (client, guildMember) => { if (verificationConfig.enabled) { if (guildMember.user.bot) return; if (verificationConfig['verification-needed-role'].length !== 0) await guildMember.roles.add(verificationConfig['verification-needed-role'], '[moderation] ' + localize('moderation', 'verification-started')); - await sendDMPart(verificationConfig, guildMember).catch(() => dmFail()); - /** - * Sends a backup message for users who have their dms disabled - * @private - * @returns {Promise} - */ - async function dmFail() { - const channel = await client.channels.fetch(verificationConfig['restart-verification-channel'] || '').catch(() => { - }); - if (!channel || (channel || {}).type !== ChannelType.GuildText) return client.logger.error('[moderation] ' + localize('moderation', 'verify-channel-set-but-not-found-or-wrong-type')); - const m = await channel.send({ - content: localize('moderation', 'dms-not-enabled-ping', {p: guildMember.toString()}), + // Only send DMs for legacy captcha-dm type + if (verificationConfig.type === 'captcha-dm') { + await sendDMPart(verificationConfig, guildMember).catch(() => dmFail()); - components: [ - { - type: 'ACTION_ROW', - components: [ - { - type: 'BUTTON', - label: '📨 ' + localize('moderation', 'restart-verification-button'), - customId: `mod-rvp`, - style: 'PRIMARY' - } - ] - } - ] - } - ); - setTimeout(() => { - m.delete().then(() => { + async function dmFail() { + const channel = await client.channels.fetch(verificationConfig['verification-channel'] || verificationConfig['restart-verification-channel'] || '').catch(() => { }); - }, 300000); - } - - if (guildMember.guild.channels.cache.get(verificationConfig['verification-log']) && verificationConfig.type === 'manual') { - await guildMember.guild.channels.cache.get(verificationConfig['verification-log']).send({ - embeds: [{ - title: localize('moderation', 'verification'), - color: 'GREEN', - description: `${localize('moderation', 'user')}: ${guildMember.toString()} (\`${guildMember.user.id}\`)\n${localize('moderation', 'manual-verification-needed')}` - }], - components: [ - { - type: 'ACTION_ROW', + if (!channel || (channel || {}).type !== ChannelType.GuildText) return client.logger.error('[moderation] ' + localize('moderation', 'verify-channel-set-but-not-found-or-wrong-type')); + const m = await channel.send({ + content: localize('moderation', 'dms-not-enabled-ping', {p: guildMember.toString()}), components: [ { - type: 'BUTTON', - label: '❌ ' + localize('moderation', 'verification-deny'), - customId: `mod-ver-d-${guildMember.user.id}`, - style: 'DANGER' - }, - { - type: 'BUTTON', - label: '✅ ' + localize('moderation', 'verification-approve'), - customId: `mod-ver-p-${guildMember.user.id}`, - style: 'SUCCESS' + type: 'ACTION_ROW', + components: [ + { + type: 'BUTTON', + label: '📨 ' + localize('moderation', 'restart-verification-button'), + customId: `mod-rvp`, + style: 'PRIMARY' + } + ] } ] } - ] - }); + ); + setTimeout(() => { + m.delete().then(() => { + }); + }, 300000); + } + } } @@ -155,7 +131,7 @@ async function runJoinGate(guildMember) { const joinGateConfig = client.configurations['moderation']['joinGate']; if (guildMember.user.bot && joinGateConfig.ignoreBots) return; if (joinGateConfig.allUsers) return performJoinGateAction(localize('moderation', 'joingate-for-everyone')); - const daysSinceCreation = (new Date().getTime() / 86400000).toFixed(0) - (guildMember.user.createdTimestamp / 86400000).toFixed(0); + const daysSinceCreation = Math.floor((Date.now() - guildMember.user.createdTimestamp) / 86400000); if (daysSinceCreation <= joinGateConfig.minAccountAge) return performJoinGateAction(localize('moderation', 'account-age-to-low', { a: daysSinceCreation, c: joinGateConfig.minAccountAge @@ -200,66 +176,74 @@ module.exports.runJoinGate = runJoinGate; async function sendDMPart(verificationConfig, guildMember) { return new Promise(async (resolve, reject) => { try { - if (verificationConfig.type === 'manual') await guildMember.user.send(embedType(verificationConfig['manual-verification-message'], {})); - else { - if (!guildMember.client.scnxSetup) return guildMember.client.logger.error('[moderation] Captcha Generation is only available if your bot has an SCNX Integration set up.'); - const captcha = await require('../../../src/functions/scnx-integration').generateCaptcha(verificationConfig.captchaLevel); - await guildMember.user.send(embedType(verificationConfig['captcha-message'], {}, { - files: [new MessageAttachment(captcha.buffer, {name: 'you-call-it-captcha-we-call-it-ai-training.png'})] - })); - const c = await guildMember.user.createDM(); - const col = c.createMessageCollector({time: 120000}); - let p = false; - let d = null; - if (guildMember.guild.channels.cache.get(verificationConfig['verification-log'])) { - d = await guildMember.guild.channels.cache.get(verificationConfig['verification-log']).send({ - embeds: [{ - title: localize('moderation', 'verification'), - color: 'GREEN', - description: `${localize('moderation', 'user')}: ${guildMember.toString()} (\`${guildMember.user.id}\`)\n${localize('moderation', 'captcha-verification-pending')}` - }], - components: [ - { - type: 'ACTION_ROW', - components: [ - { - type: 'BUTTON', - label: '⏭️ ' + localize('moderation', 'verification-skip'), - customId: `mod-ver-p-${guildMember.user.id}`, - style: 'SECONDARY' - } - ] - } - ] - }); - const coli = d.createMessageComponentCollector({time: 120000}); - coli.on('collect', () => { - p = true; - }); - coli.on('end', () => { - d.delete(); - }); - } - col.on('collect', (m) => { - if (m.author.id === guildMember.user.id && !p) { - p = true; - if (m.content.toUpperCase() === captcha.solution.toUpperCase()) verificationPassed(guildMember); - else { - client.logger.log(`${guildMember.user.id} failed verification. Entered: "${m.content.toUpperCase()}", expected: "${captcha.solution.toUpperCase()}"`); - verificationFail(guildMember); + if (!guildMember.client.scnxSetup) return guildMember.client.logger.error('[moderation] Captcha Generation is only available if your bot has an SCNX Integration set up.'); + const captcha = await require('../../../src/functions/scnx-integration').generateCaptcha(verificationConfig.captchaLevel); + await guildMember.user.send(embedType(verificationConfig['captcha-message'], {}, { + files: [new AttachmentBuilder(captcha.buffer, {name: 'you-call-it-captcha-we-call-it-ai-training.png'})] + })); + const c = await guildMember.user.createDM(); + const col = c.createMessageCollector({time: 120000}); + let p = false; + let d = null; + let dDeleted = false; + if (guildMember.guild.channels.cache.get(verificationConfig['verification-log'])) { + d = await guildMember.guild.channels.cache.get(verificationConfig['verification-log']).send({ + embeds: [{ + title: localize('moderation', 'verification'), + color: 'GREEN', + description: `${localize('moderation', 'user')}: ${guildMember.toString()} (\`${guildMember.user.id}\`)\n${localize('moderation', 'captcha-verification-pending')}` + }], + components: [ + { + type: 'ACTION_ROW', + components: [ + { + type: 'BUTTON', + label: '⏭️ ' + localize('moderation', 'verification-skip'), + customId: `mod-ver-skip-${guildMember.user.id}`, + style: 'SECONDARY' + } + ] } - if (d && !d.deleted) d.delete().catch(() => { + ] + }); + const coli = d.createMessageComponentCollector({time: 120000}); + coli.on('collect', () => { + p = true; + }); + coli.on('end', () => { + if (!dDeleted) { + dDeleted = true; + d.delete().catch(() => { }); } }); - col.on('end', () => { - if (!p) { + } + col.on('collect', (m) => { + if (m.author.id === guildMember.user.id && !p) { + p = true; + if (m.content.toUpperCase() === captcha.solution.toUpperCase()) verificationPassed(guildMember); + else { + client.logger.log(`${guildMember.user.id} failed verification. Entered: "${m.content.toUpperCase()}", expected: "${captcha.solution.toUpperCase()}"`); verificationFail(guildMember); - if (d && !d.deleted) d.delete().catch(() => { + } + if (d && !dDeleted) { + dDeleted = true; + d.delete().catch(() => { }); } - }); - } + } + }); + col.on('end', () => { + if (!p) { + verificationFail(guildMember); + if (d && !dDeleted) { + dDeleted = true; + d.delete().catch(() => { + }); + } + } + }); resolve(); } catch (e) { reject(e); @@ -275,12 +259,20 @@ module.exports.sendDMPart = sendDMPart; * @param {GuildMember} guildMember Member who passed the verification * @returns {Promise} */ -async function verificationPassed(guildMember) { +async function verificationPassed(guildMember, interaction = null) { const verificationConfig = guildMember.client.configurations['moderation']['verification']; if (verificationConfig['verification-needed-role'].length !== 0) await guildMember.roles.remove(verificationConfig['verification-needed-role'], '[' + localize('moderation', 'verification') + '] ' + localize('moderation', 'verification-completed')); if (verificationConfig['verification-passed-role'].length !== 0) await guildMember.roles.add(verificationConfig['verification-passed-role'], '[' + localize('moderation', 'verification') + '] ' + localize('moderation', 'verification-completed')); - await guildMember.user.send(embedType(verificationConfig['captcha-succeeded-message'])).catch(() => { - }); + if (interaction) { + await interaction.followUp({ + ...embedType(verificationConfig['captcha-succeeded-message']), + ephemeral: true + }).catch(() => { + }); + } else { + await guildMember.user.send(embedType(verificationConfig['captcha-succeeded-message'])).catch(() => { + }); + } if (guildMember.guild.channels.cache.get(verificationConfig['verification-log'])) await guildMember.guild.channels.cache.get(verificationConfig['verification-log']).send({ embeds: [{ title: localize('moderation', 'verification'), @@ -298,17 +290,31 @@ module.exports.verificationPassed = verificationPassed; * @param {GuildMember} guildMember Member who failed verification * @returns {Promise} */ -async function verificationFail(guildMember) { +async function verificationFail(guildMember, interaction = null) { const verificationConfig = guildMember.client.configurations['moderation']['verification']; - await guildMember.user.send(embedType(verificationConfig['captcha-failed-message'])); - await moderationAction(guildMember.client, verificationConfig.actionOnFail, guildMember.guild.me, guildMember, '[' + localize('moderation', 'verification') + '] ' + localize('moderation', 'verification-failed')); + if (interaction) { + await interaction.followUp({ + ...embedType(verificationConfig['captcha-failed-message']), + ephemeral: true + }).catch(() => { + }); + } else { + await guildMember.user.send(embedType(verificationConfig['captcha-failed-message'])).catch(() => { + }); + } + const durationParser = require('parse-duration'); + let expiresAt = null; + if (['mute', 'quarantine'].includes(verificationConfig.actionOnFail) && verificationConfig.actionOnFailDuration) { + expiresAt = new Date(new Date().getTime() + durationParser(verificationConfig.actionOnFailDuration)); + } + await moderationAction(guildMember.client, verificationConfig.actionOnFail, guildMember.guild.members.me, guildMember, '[' + localize('moderation', 'verification') + '] ' + localize('moderation', 'verification-failed'), {}, expiresAt); if (guildMember.guild.channels.cache.get(verificationConfig['verification-log'])) await guildMember.guild.channels.cache.get(verificationConfig['verification-log']).send({ embeds: [{ title: localize('moderation', 'verification'), - color: 'GREEN', + color: 'RED', description: `${localize('moderation', 'user')}: ${guildMember.toString()} (\`${guildMember.user.id}\`)\n${localize('moderation', 'verification-failed')}` }] }); } -module.exports.verificationFail = verificationFail; +module.exports.verificationFail = verificationFail; \ No newline at end of file diff --git a/modules/moderation/events/guildMemberUpdate.js b/modules/moderation/events/guildMemberUpdate.js index 78aaa529..f2a30123 100644 --- a/modules/moderation/events/guildMemberUpdate.js +++ b/modules/moderation/events/guildMemberUpdate.js @@ -2,9 +2,8 @@ const {runJoinGate} = require('./guildMemberAdd'); module.exports.run = async function (client, oldGuildMember, newGuildMember) { if (!client.botReadyAt) return; const joinGateConfig = client.configurations['moderation']['joinGate']; - const verificationConfig = client.configurations['moderation']['verification']; if (oldGuildMember.pending && !newGuildMember.pending && joinGateConfig.enabled && !['kick', 'ban'].includes(joinGateConfig.action)) { await runJoinGate(newGuildMember); } -}; \ No newline at end of file +}; diff --git a/modules/moderation/events/interactionCreate.js b/modules/moderation/events/interactionCreate.js index 2604fc38..9a6cb4f4 100644 --- a/modules/moderation/events/interactionCreate.js +++ b/modules/moderation/events/interactionCreate.js @@ -1,10 +1,82 @@ const {verificationPassed, verificationFail, sendDMPart} = require('./guildMemberAdd'); const {localize} = require('../../../src/functions/localize'); +const {ModalBuilder, ActionRowBuilder, TextInputBuilder, TextInputStyle, AttachmentBuilder} = require('discord.js'); +const {embedType} = require('../../../src/functions/helpers'); +const durationParser = require('parse-duration'); + +// In-memory captcha solutions: userId -> { solution, expiresAt } +const pendingCaptchas = new Map(); + +// Cooldown for captcha image generation: userId -> timestamp of last generation +const captchaGenerationCooldowns = new Map(); +const CAPTCHA_GENERATION_COOLDOWN_MS = 60000; // 1 minute + +// Clean up expired captchas and cooldowns every 5 minutes +setInterval(() => { + const now = Date.now(); + for (const [userId, data] of pendingCaptchas) { + if (now > data.expiresAt) pendingCaptchas.delete(userId); + } + for (const [userId, timestamp] of captchaGenerationCooldowns) { + if (now - timestamp > 600000) captchaGenerationCooldowns.delete(userId); // cleanup after 10 min max + } +}, 300000); + +const WORD_LIST_EASY = ['RAIN', 'MOON', 'STAR', 'WOLF', 'TREE', 'FIRE', 'GOLD', 'SNOW', 'LAKE', 'ROCK', + 'LEAF', 'BIRD', 'BOOK', 'DOOR', 'RING', 'BLUE', 'CAKE', 'CORN', 'DUST', 'WAVE']; + +const WORD_LIST_MEDIUM = ['BRIDGE', 'CASTLE', 'FLOWER', 'GUITAR', 'HARBOR', 'ISLAND', 'JUNGLE', 'KNIGHT', 'LEMON', 'MARBLE', + 'NEEDLE', 'ORANGE', 'PENCIL', 'QUARTZ', 'RABBIT', 'SILVER', 'TURTLE', 'VELVET', 'WALNUT', 'ZENITH', + 'ANCHOR', 'BREEZE', 'CANDLE', 'DESERT', 'EAGLE', 'FOREST', 'GLOBAL', 'HAMMER', 'IVORY', 'JACKET', + 'KITTEN', 'MIRROR', 'NECTAR', 'OYSTER', 'PLANET', 'RAVEN', 'SUNSET', 'THRONE', 'PEARL', 'COMET', + 'TIGER', 'CLOUD', 'PRISM', 'BLAZE', 'FROST', 'DELTA', 'OCEAN', 'STONE', 'VAPOR', 'CEDAR']; + +const WORD_LIST_HARD = ['THUNDER', 'HORIZON', 'MYSTERY', 'JOURNEY', 'PROPHET', 'VOYAGER', 'PYRAMID', 'ECLIPSE', + 'COMPASS', 'LAGOON', 'ARCHERY', 'TWILIGHT', 'PARADISE', 'MONARCHY', 'LABYRINTH', 'ALCHEMY', + 'CHEMISTRY', 'OCTOBER', 'CATHEDRAL', 'ORCHESTRA']; + +function generateSimpleChallenge(type, difficulty) { + const level = ['easy', 'medium', 'hard'].includes(difficulty) ? difficulty : 'medium'; + if (type === 'math') { + let a, b, op, answer; + if (level === 'easy') { + a = Math.floor(Math.random() * 10) + 1; + b = Math.floor(Math.random() * 10) + 1; + op = Math.random() < 0.5 ? '+' : '-'; + answer = op === '+' ? a + b : a - b; + } else if (level === 'hard') { + const ops = ['+', '-', '×']; + op = ops[Math.floor(Math.random() * ops.length)]; + if (op === '×') { + a = Math.floor(Math.random() * 12) + 1; + b = Math.floor(Math.random() * 12) + 1; + answer = a * b; + } else { + a = Math.floor(Math.random() * 100) + 1; + b = Math.floor(Math.random() * 100) + 1; + answer = op === '+' ? a + b : a - b; + } + } else { + // medium — current behaviour + a = Math.floor(Math.random() * 50) + 1; + b = Math.floor(Math.random() * 50) + 1; + op = Math.random() < 0.5 ? '+' : '-'; + answer = op === '+' ? a + b : a - b; + } + return {question: localize('moderation', 'simple-math-challenge', {a, op, b}), answer: String(answer)}; + } + // word + const list = level === 'easy' ? WORD_LIST_EASY : level === 'hard' ? WORD_LIST_HARD : WORD_LIST_MEDIUM; + const word = list[Math.floor(Math.random() * list.length)]; + return {question: localize('moderation', 'simple-word-challenge', {w: word}), answer: word}; +} module.exports.run = async (client, interaction) => { - if (!interaction.isMessageComponent()) return; + if (!interaction.isMessageComponent() && !interaction.isModalSubmit()) return; + const verificationConfig = client.configurations['moderation']['verification']; + + // === Legacy DM restart button (captcha-dm type) === if (interaction.customId === 'mod-rvp') { - const verificationConfig = client.configurations['moderation']['verification']; if (interaction.member.roles.cache.filter(r => verificationConfig['verification-passed-role'].includes(r.id)).size !== 0) return interaction.reply({ ephemeral: true, content: '⚠️ ' + localize('moderation', 'already-verified') @@ -20,18 +92,300 @@ module.exports.run = async (client, interaction) => { content: '⚠️ ' + localize('moderation', 'dms-still-disabled', {g: interaction.member.guild.name}) }); }); + return; + } + + // === New "Verify Me" button === + if (interaction.customId === 'mod-verify') { + // Already verified? + if (verificationConfig['verification-passed-role'] && interaction.member.roles.cache.filter(r => verificationConfig['verification-passed-role'].includes(r.id)).size !== 0) { + return interaction.reply({ephemeral: true, content: '⚠️ ' + localize('moderation', 'already-verified')}); + } + + const VerificationRequest = client.models['moderation']['VerificationRequest']; + let request = await VerificationRequest.findOne({ + where: {userID: interaction.user.id}, + order: [['createdAt', 'DESC']] + }); + + // Check cooldown and retries (for captcha / captcha-dm / word / math) + if (['captcha', 'captcha-dm', 'word', 'math'].includes(verificationConfig.type)) { + if (!request || request.status === 'approved') { + request = await VerificationRequest.create({ + userID: interaction.user.id, + type: verificationConfig.type + }); + } + + // Check max retries — re-execute punishment if somehow missed + const maxRetries = verificationConfig.maxRetries || 3; + if (request.attempts >= maxRetries) { + if (request.status !== 'denied') { + await request.update({status: 'denied'}); + await interaction.deferReply({ephemeral: true}); + await verificationFail(interaction.member, interaction); + return; + } + return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('moderation', 'retries-exhausted') + }); + } + + // Check cooldown + if (request.lastAttemptAt) { + const cooldown = durationParser(verificationConfig.retryCooldown || '5m'); + const lastAttemptTime = new Date(request.lastAttemptAt).getTime(); + const elapsed = Date.now() - lastAttemptTime; + if (elapsed < cooldown) { + const readyAt = Math.ceil((lastAttemptTime + cooldown) / 1000); + return interaction.reply(embedType(verificationConfig['cooldown-message'] || localize('moderation', 'cooldown-message'), {'%t%': ``}, {ephemeral: true})); + } + } + } + + // === Captcha type: send ephemeral with image === + if (verificationConfig.type === 'captcha') { + // Cooldown to prevent captcha image generation spam + const lastGeneration = captchaGenerationCooldowns.get(interaction.user.id); + if (lastGeneration) { + const elapsed = Date.now() - lastGeneration; + if (elapsed < CAPTCHA_GENERATION_COOLDOWN_MS) { + const readyAt = Math.ceil((lastGeneration + CAPTCHA_GENERATION_COOLDOWN_MS) / 1000); + return interaction.reply(embedType(verificationConfig['cooldown-message'] || localize('moderation', 'cooldown-message'), {'%t%': ``}, {ephemeral: true})); + } + } + + await interaction.deferReply({ephemeral: true}); + if (!client.scnxSetup) return interaction.editReply({content: '⚠️ Captcha generation is not available.'}); + const captcha = await require('../../../src/functions/scnx-integration').generateCaptcha(verificationConfig.captchaLevel); + captchaGenerationCooldowns.set(interaction.user.id, Date.now()); + + pendingCaptchas.set(interaction.user.id, { + solution: captcha.solution, + expiresAt: Date.now() + 300000 // 5 minutes + }); + + await interaction.editReply({ + ...embedType(verificationConfig['captcha-message'] || localize('moderation', 'captcha-verification-pending')), + files: [new AttachmentBuilder(captcha.buffer, {name: 'captcha.png'})], + components: [ + { + type: 1, // ACTION_ROW + components: [ + { + type: 2, // BUTTON + label: '🔑 ' + localize('moderation', 'enter-solution-button'), + customId: 'mod-captcha-solve', + style: 1 // PRIMARY + } + ] + } + ] + }); + return; + } + + // === Word / Math type: open modal directly === + if (verificationConfig.type === 'word' || verificationConfig.type === 'math') { + const challenge = generateSimpleChallenge(verificationConfig.type, verificationConfig.captchaLevel); + + pendingCaptchas.set(interaction.user.id, { + solution: challenge.answer, + expiresAt: Date.now() + 300000 + }); + + const modal = new ModalBuilder() + .setCustomId('mod-simple-modal') + .setTitle(localize('moderation', 'verification-modal-title')) + .addComponents( + new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId('answer') + .setLabel(challenge.question) + .setStyle(TextInputStyle.Short) + .setRequired(true) + .setPlaceholder(localize('moderation', 'simple-solution-label')) + ) + ); + await interaction.showModal(modal); + return; + } + + // === Manual type: submit for review === + if (verificationConfig.type === 'manual') { + if (request && request.type === 'manual' && request.status === 'pending') { + return interaction.reply({ + ephemeral: true, + content: '⏳ ' + localize('moderation', 'already-pending-review') + }); + } + + if (!request || request.status === 'denied') { + request = await VerificationRequest.create({userID: interaction.user.id, type: 'manual'}); + } + + await interaction.reply({ephemeral: true, content: localize('moderation', 'verification-submitted')}); + + // Post approve/deny in log channel + const logChannel = interaction.guild.channels.cache.get(verificationConfig['verification-log']); + if (logChannel) { + const logMsg = await logChannel.send({ + embeds: [{ + title: localize('moderation', 'verification'), + color: 0x57F287, // GREEN + description: `${localize('moderation', 'user')}: ${interaction.member.toString()} (\`${interaction.user.id}\`)\n${localize('moderation', 'manual-verification-needed')}` + }], + components: [ + { + type: 1, + components: [ + { + type: 2, + label: '❌ ' + localize('moderation', 'verification-deny'), + customId: `mod-ver-d-${interaction.user.id}`, + style: 4 // DANGER + }, + { + type: 2, + label: '✅ ' + localize('moderation', 'verification-approve'), + customId: `mod-ver-p-${interaction.user.id}`, + style: 3 // SUCCESS + } + ] + } + ] + }); + await request.update({logMessageID: logMsg.id}); + } + return; + } + + // === Button type: one click, no challenge === + if (verificationConfig.type === 'button') { + await verificationPassed(interaction.member, interaction); + return; + } + + return; } + + // === "Enter Solution" button for captcha type === + if (interaction.customId === 'mod-captcha-solve') { + const pending = pendingCaptchas.get(interaction.user.id); + if (!pending || Date.now() > pending.expiresAt) { + pendingCaptchas.delete(interaction.user.id); + return interaction.reply({ephemeral: true, content: '⚠️ ' + localize('moderation', 'captcha-expired')}); + } + + const modal = new ModalBuilder() + .setCustomId('mod-captcha-modal') + .setTitle(localize('moderation', 'verification-modal-title')) + .addComponents( + new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId('answer') + .setLabel(localize('moderation', 'captcha-solution-label')) + .setStyle(TextInputStyle.Short) + .setRequired(true) + ) + ); + await interaction.showModal(modal); + return; + } + + // === Modal submit for captcha === + if (interaction.customId === 'mod-captcha-modal') { + await handleVerificationModalSubmit(client, interaction, verificationConfig); + return; + } + + // === Modal submit for simple === + if (interaction.customId === 'mod-simple-modal') { + await handleVerificationModalSubmit(client, interaction, verificationConfig); + return; + } + + // === Manual approve/deny buttons === if (!interaction.customId.startsWith('mod-ver-')) return; - interaction.customId = interaction.customId.replaceAll('mod-ver-', ''); - const a = interaction.customId.split('-')[0]; - const id = interaction.customId.split('-')[1]; - const member = await interaction.guild.members.fetch(id).catch(() => {}); + const parsedId = interaction.customId.replace('mod-ver-', ''); + const action = parsedId.split('-')[0]; + const userId = parsedId.split('-')[1]; + const member = await interaction.guild.members.fetch(userId).catch(() => { + }); if (!member) return interaction.reply({ ephemeral: true, content: '⚠️ ' + localize('moderation', 'member-not-found') }); - if (a === 'p') await verificationPassed(member); + + // Update VerificationRequest record + const VerificationRequest = client.models['moderation']['VerificationRequest']; + const request = await VerificationRequest.findOne({where: {userID: userId, status: 'pending'}}); + if (request) await request.update({status: action === 'p' ? 'approved' : 'denied'}); + + if (action === 'p') await verificationPassed(member); else await verificationFail(member); await interaction.message.edit({embeds: interaction.message.embeds, components: []}); - interaction.reply({ephemeral: true, content: localize('moderation', 'verification-update-proceeded')}); -}; \ No newline at end of file + await interaction.reply({ephemeral: true, content: localize('moderation', 'verification-update-proceeded')}); +}; + +async function handleVerificationModalSubmit(client, interaction, verificationConfig) { + const answer = interaction.fields.getTextInputValue('answer').trim(); + const pending = pendingCaptchas.get(interaction.user.id); + + if (!pending || Date.now() > pending.expiresAt) { + pendingCaptchas.delete(interaction.user.id); + return interaction.reply({ephemeral: true, content: '⚠️ ' + localize('moderation', 'captcha-expired')}); + } + + const VerificationRequest = client.models['moderation']['VerificationRequest']; + let request = await VerificationRequest.findOne({where: {userID: interaction.user.id, status: 'pending'}}); + if (!request) { + const denied = await VerificationRequest.findOne({ + where: {userID: interaction.user.id, status: 'denied'}, + order: [['createdAt', 'DESC']] + }); + if (denied) { + const maxRetries = verificationConfig.maxRetries || 3; + if (denied.attempts >= maxRetries) { + return interaction.reply({ + ephemeral: true, + content: '⚠️ ' + localize('moderation', 'retries-exhausted') + }); + } + request = denied; + await request.update({status: 'pending'}); + } else { + request = await VerificationRequest.create({userID: interaction.user.id, type: verificationConfig.type}); + } + } + + const isCorrect = answer.toUpperCase() === pending.solution.toUpperCase(); + pendingCaptchas.delete(interaction.user.id); + + if (isCorrect) { + await request.update({status: 'approved'}); + await interaction.deferReply({ephemeral: true}); + await verificationPassed(interaction.member, interaction); + return; + } + + // Wrong answer + const attempts = request.attempts + 1; + await request.update({attempts, lastAttemptAt: new Date()}); + + const maxRetries = verificationConfig.maxRetries || 3; + if (attempts >= maxRetries) { + await request.update({status: 'denied'}); + await interaction.deferReply({ephemeral: true}); + await verificationFail(interaction.member, interaction); + return; + } + + const cooldownMs = durationParser(verificationConfig.retryCooldown || '5m'); + const cooldownMinutes = Math.ceil(cooldownMs / 60000); + await interaction.reply({ + ephemeral: true, + content: '❌ ' + localize('moderation', 'retry-message', {t: cooldownMinutes + 'm', a: attempts, m: maxRetries}) + }); +} \ No newline at end of file diff --git a/modules/moderation/events/messageCreate.js b/modules/moderation/events/messageCreate.js index 1af6bd09..bc85ac70 100644 --- a/modules/moderation/events/messageCreate.js +++ b/modules/moderation/events/messageCreate.js @@ -4,7 +4,23 @@ const {embedType} = require('../../../src/functions/helpers'); const {localize} = require('../../../src/functions/localize'); const stopPhishing = require('stop-discord-phishing'); +// Cache resolved invite codes to guild IDs to avoid repeated API calls +const inviteGuildCache = new Map(); + +const INVITE_PATTERN = /(?:discord\.gg|discordapp\.com\/invite|discord\.com\/invite)\/([a-zA-Z0-9-]+)/g; + +function extractInviteCodes(content) { + const codes = []; + let match; + while ((match = INVITE_PATTERN.exec(content)) !== null) { + codes.push(match[1]); + } + INVITE_PATTERN.lastIndex = 0; + return codes; +} + const messageCache = {}; +const actionInProgress = new Set(); module.exports.run = async (client, msg) => { if (!client.botReadyAt) return; @@ -34,6 +50,7 @@ module.exports.run = async (client, msg) => { * @return {Promise} */ async function antiSpam() { + if (actionInProgress.has(msg.author.id)) return; if (!messageCache[msg.author.id]) messageCache[msg.author.id] = []; messageCache[msg.author.id].push({ id: msg.id, @@ -42,7 +59,9 @@ module.exports.run = async (client, msg) => { massMentions: msg.mentions.everyone || Array.from(msg.mentions.roles.keys()).length !== 0 }); setTimeout(() => { + if (!messageCache[msg.author.id]) return; messageCache[msg.author.id] = messageCache[msg.author.id].filter(m => m.id !== msg.id); + if (messageCache[msg.author.id].length === 0) delete messageCache[msg.author.id]; }, antiSpamConfig.timeframe * 1000); if (messageCache[msg.author.id].length >= antiSpamConfig.maxMessagesInTimeframe) return await performAntiSpamAction(localize('moderation', 'reached-messages-in-timeframe', { m: antiSpamConfig.maxMessagesInTimeframe, @@ -68,6 +87,8 @@ module.exports.run = async (client, msg) => { * @return {Promise} */ async function performAntiSpamAction(reason) { + actionInProgress.add(msg.author.id); + delete messageCache[msg.author.id]; await moderationAction(client, antiSpamConfig.action, {user: client.user}, msg.member, `[${localize('moderation', 'anti-spam')}]: ${reason}`, {roles: roles}); if (antiSpamConfig.sendChatMessage) await msg.channel.send(embedType(antiSpamConfig.message, { '%reason%': reason, @@ -77,6 +98,7 @@ module.exports.run = async (client, msg) => { if (lockdownConfig && lockdownConfig.enabled && lockdownConfig.autoTriggerOnSpam && !await isLockdownActive(client)) { await activateLockdown(client, localize('moderation', 'lockdown-spam-trigger'), localize('moderation', 'lockdown-system'), true); } + setTimeout(() => actionInProgress.delete(msg.author.id), 10000); } } @@ -113,9 +135,29 @@ async function performBadWordAndInviteProtection(msg) { if (moduleConfig['whitelisted_channels_for_invite_blocking'].includes(msg.channel.id) || moduleConfig['whitelisted_channels_for_invite_blocking'].includes(msg.channel.parentId)) return; if (msg.member.roles.cache.find(r => moduleConfig['whitelisted_roles_for_invite_blocking'].includes(r.id))) return; if (moduleConfig['action_on_invite'] !== 'none') { - if (msg.content.includes('discord.gg/') || msg.content.includes('discordapp.com/invite/')) { + const inviteCodes = extractInviteCodes(msg.content); + for (const code of inviteCodes) { + let guildId = inviteGuildCache.get(code); + if (!guildId) { + try { + const invite = await msg.client.fetchInvite(code); + guildId = invite.guild ? invite.guild.id : null; + if (guildId) { + if (inviteGuildCache.size > 500) { + const firstKey = inviteGuildCache.keys().next().value; + inviteGuildCache.delete(firstKey); + } + inviteGuildCache.set(code, guildId); + } + } catch (e) { + guildId = null; + } + } + if (guildId === msg.guild.id) continue; + if (guildId && (moduleConfig['allowed_invite_guild_ids'] || []).includes(guildId)) continue; await msg.delete(); await moderationAction(msg.client, moduleConfig['action_on_invite'], msg.client, msg.member, localize('moderation', 'invite-sent', {c: msg.channel.toString()}), {roles}); + return; } } } diff --git a/modules/moderation/lockdown.js b/modules/moderation/lockdown.js index 3f468172..c49cfb5a 100644 --- a/modules/moderation/lockdown.js +++ b/modules/moderation/lockdown.js @@ -128,10 +128,11 @@ async function activateLockdown(client, reason, triggeredBy, isAutomatic = false if (role.position >= botHighestRole.position) continue; if (moderatorRoles.has(role.id)) continue; + // Safety check before accessing cache if (!channel.permissionOverwrites || !channel.permissionOverwrites.cache) continue; const overwrite = channel.permissionOverwrites.cache.get(role.id); - if (overwrite && overwrite.allow.has(PermissionFlagsBits.SendMessages)) { + if (overwrite && !overwrite.deny.has(PermissionFlagsBits.SendMessages)) { await channel.permissionOverwrites.edit(role, { SendMessages: false, SendMessagesInThreads: false, @@ -171,10 +172,11 @@ async function activateLockdown(client, reason, triggeredBy, isAutomatic = false if (role.position >= botHighestRole.position) continue; if (moderatorRoles.has(role.id)) continue; + // Safety check before accessing cache if (!channel.permissionOverwrites || !channel.permissionOverwrites.cache) continue; const overwrite = channel.permissionOverwrites.cache.get(role.id); - if (overwrite && (overwrite.allow.has(PermissionFlagsBits.Connect) || overwrite.allow.has(PermissionFlagsBits.Speak) || overwrite.allow.has(PermissionFlagsBits.SendMessages))) { + if (overwrite && !(overwrite.deny.has(PermissionFlagsBits.Connect) && overwrite.deny.has(PermissionFlagsBits.Speak) && overwrite.deny.has(PermissionFlagsBits.SendMessages))) { await channel.permissionOverwrites.edit(role, { Connect: false, Speak: false, @@ -222,7 +224,7 @@ async function activateLockdown(client, reason, triggeredBy, isAutomatic = false if (!channel.permissionOverwrites || !channel.permissionOverwrites.cache) continue; const overwrite = channel.permissionOverwrites.cache.get(role.id); - if (overwrite && (overwrite.allow.has(PermissionFlagsBits.Connect) || overwrite.allow.has(PermissionFlagsBits.RequestToSpeak) || overwrite.allow.has(PermissionFlagsBits.SendMessages))) { + if (overwrite && !(overwrite.deny.has(PermissionFlagsBits.Connect) && overwrite.deny.has(PermissionFlagsBits.RequestToSpeak) && overwrite.deny.has(PermissionFlagsBits.SendMessages))) { await channel.permissionOverwrites.edit(role, { Connect: false, RequestToSpeak: false, @@ -249,21 +251,33 @@ async function activateLockdown(client, reason, triggeredBy, isAutomatic = false affectedChannels.push(channel.id); successfullyLockedCount++; - - if (lockdownConfig.sendMessageInAffectedChannels && typeof channel.send === 'function') { - const msgPayload = embedType(lockdownConfig.lockdownMessage, { - '%reason%': reason, - '%user%': triggeredBy - }); - await channel.send(msgPayload).catch(() => {}); - } } catch (error) { client.logger.error(`[moderation] [lockdown] Failed to lock channel ${channel.id}: ${error.message}`); + // Continue with next channel - backup is already saved } } client.logger.info(`[moderation] [lockdown] Successfully locked ${successfullyLockedCount}/${channelsToLockdown.length} channels`); + // PHASE 3b: Send notification messages + if (lockdownConfig.sendMessageInAffectedChannels) { + const msgPayload = embedType(lockdownConfig.lockdownMessage, { + '%reason%': reason, + '%user%': triggeredBy + }); + const targetChannels = (lockdownConfig.lockdownMessageChannels || []).length > 0 + ? lockdownConfig.lockdownMessageChannels + : affectedChannels; + for (const channelId of targetChannels) { + const ch = guild.channels.cache.get(channelId); + if (ch && typeof ch.send === 'function') { + await ch.send(msgPayload).catch(() => { + }); + } + } + } + + // PHASE 4: Kick non-moderator users from voice and stage channels let kickedUsersCount = 0; let totalVoiceUsers = 0; for (const [, channel] of guild.channels.cache) { @@ -272,9 +286,11 @@ async function activateLockdown(client, reason, triggeredBy, isAutomatic = false for (const [, member] of channel.members) { totalVoiceUsers++; + // Skip moderators const isModerator = member.roles.cache.some(role => moderatorRoles.has(role.id)); if (isModerator) continue; + // Kick non-moderator try { await member.voice.disconnect(`[moderation] [lockdown] ${reason}`); kickedUsersCount++; @@ -362,14 +378,27 @@ async function liftLockdown(client, reason, liftedBy) { deny: BigInt(o.deny) })), `[moderation] [lockdown-lift] ${reason}`); restoredCount++; + } catch (e) { + client.logger.warn(localize('moderation', 'lockdown-restore-failed', { + c: backup.channelID, + e: e.toString() + })); + } + } - if (lockdownConfig.sendMessageInAffectedChannels && typeof channel.send === 'function') { - await channel.send(embedType(lockdownConfig.liftMessage, { + // Send lift notification messages + if (lockdownConfig.sendMessageInAffectedChannels) { + const restoredChannelIds = (state.permissionBackup || []).map(b => b.channelID); + const targetChannels = (lockdownConfig.lockdownMessageChannels || []).length > 0 + ? lockdownConfig.lockdownMessageChannels + : restoredChannelIds; + for (const channelId of targetChannels) { + const ch = guild.channels.cache.get(channelId); + if (ch && typeof ch.send === 'function') { + await ch.send(embedType(lockdownConfig.liftMessage, { '%user%': liftedBy })).catch(() => {}); } - } catch (e) { - client.logger.warn(localize('moderation', 'lockdown-restore-failed', {c: backup.channelID, e: e.toString()})); } } diff --git a/modules/moderation/models/VerificationRequest.js b/modules/moderation/models/VerificationRequest.js new file mode 100644 index 00000000..356de851 --- /dev/null +++ b/modules/moderation/models/VerificationRequest.js @@ -0,0 +1,46 @@ +const {DataTypes, Model} = require('sequelize'); + +module.exports = class VerificationRequest extends Model { + static init(sequelize) { + return super.init({ + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + userID: { + type: DataTypes.STRING, + allowNull: false + }, + type: { + type: DataTypes.STRING, + allowNull: false + }, + status: { + type: DataTypes.STRING, + defaultValue: 'pending' + }, + attempts: { + type: DataTypes.INTEGER, + defaultValue: 0 + }, + lastAttemptAt: { + type: DataTypes.DATE, + allowNull: true + }, + logMessageID: { + type: DataTypes.STRING, + allowNull: true + } + }, { + tableName: 'moderation_VerificationRequests', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'VerificationRequest', + 'module': 'moderation' +}; diff --git a/modules/moderation/moderationActions.js b/modules/moderation/moderationActions.js index eadc7154..7715ab65 100644 --- a/modules/moderation/moderationActions.js +++ b/modules/moderation/moderationActions.js @@ -1,5 +1,13 @@ const {scheduleJob} = require('node-schedule'); -const {embedType, formatDate, dateToDiscordTimestamp, formatDiscordUserName, safeSetFooter} = require('../../src/functions/helpers'); +const { + embedType, + formatDate, + dateToDiscordTimestamp, + formatDiscordUserName, + safeSetFooter, + truncate, + tryArchiveDiscordAttachment +} = require('../../src/functions/helpers'); const {MessageEmbed} = require('discord.js'); const {localize} = require('../../src/functions/localize'); const durationParser = require('parse-duration'); @@ -173,7 +181,7 @@ async function moderationAction(client, type, user, victim, reason, additionalDa })).catch(() => { }); if (victim.bannable) await victim.ban({ - days: additionalData.days || 0, + deleteMessageDays: additionalData.days || 0, reason: '[moderation] ' + localize('moderation', 'banned-audit-log-reason', { u: formatDiscordUserName(user.user), r: reason @@ -184,7 +192,7 @@ async function moderationAction(client, type, user, victim, reason, additionalDa victim.user.tag = victim.id; victim.user.id = victim.id; await guild.members.ban(victim.id, { - days: additionalData.days || 0, + deleteMessageDays: additionalData.days || 0, reason: '[moderation] ' + localize('moderation', 'banned-audit-log-reason', { u: formatDiscordUserName(user.user), r: reason @@ -279,6 +287,16 @@ async function moderationAction(client, type, user, victim, reason, additionalDa if (!channel) { client.error('[moderation] ' + localize('moderation', 'missing-logchannel')); } else { + let proofURL = null; + if (proof) { + const victimName = victim?.user ? formatDiscordUserName(victim.user) : 'unknown'; + const archived = await tryArchiveDiscordAttachment(client, proof.url, { + displayName: `Moderation case #${modAction.actionID} (${type}) — evidence against ${victimName}`.slice(0, 100), + tags: ['moderation', 'report-evidence', type], + uploaderDiscordID: user?.user?.id || user?.id + }); + proofURL = archived ? archived.url : (proof.proxyURL || proof.url); + } const fields = []; if (expiringAt) fields.push({ name: localize('moderation', 'expires-at'), @@ -287,7 +305,7 @@ async function moderationAction(client, type, user, victim, reason, additionalDa }); if (proof) fields.push({ name: localize('moderation', 'proof'), - value: `[${localize('moderation', 'file')}](${proof.proxyURL || proof.url})`, + value: `[${localize('moderation', 'file')}](${proofURL})`, inline: true }); if (additionalData.channel) fields.push({ @@ -298,7 +316,7 @@ async function moderationAction(client, type, user, victim, reason, additionalDa const modEmbed = new MessageEmbed() .setColor(expiringAt ? 0xf1c40f : (type.includes('un') ? 0x2ecc71 : 0xe74c3c)) .setTimestamp() - .setImage(proof ? (proof.proxyURL || proof.url) : null) + .setImage(proofURL) .setAuthor({ name: formatDiscordUserName(client.user), iconURL: client.user.avatarURL() @@ -309,7 +327,7 @@ async function moderationAction(client, type, user, victim, reason, additionalDa .addField('User', `${formatDiscordUserName(user.user)}\n\`${user.user.id}\``, true) .addField(localize('moderation', 'action'), expiringAt ? `tmp-${type}` : type, true) .addFields(fields) - .addField(localize('moderation', 'reason'), reason); + .addField(localize('moderation', 'reason'), truncate(reason, 1024)); safeSetFooter(modEmbed, client); await channel.send({ embeds: [modEmbed] diff --git a/modules/moderation/module.json b/modules/moderation/module.json index f656ec6c..51d795b9 100644 --- a/modules/moderation/module.json +++ b/modules/moderation/module.json @@ -15,18 +15,14 @@ "configs/antiSpam.json", "configs/antiGrief.json", "configs/antiJoinRaid.json", - "configs/verification.json" + "configs/verification.json", + "configs/lockdown.json" ], + "fa-icon": "fas fa-hammer", "tags": [ "moderation" ], "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/moderation", - "humanReadableName": { - "en": "Moderation & Security", - "de": "Moderation & Sicherheit" - }, - "description": { - "en": "Advanced security- and moderation-system with tons of features", - "de": "Fortgeschrittenes Moderation- und Sicherheit-System mit vielen Funktionen" - } -} \ No newline at end of file + "humanReadableName": "Moderation & Security", + "description": "Advanced security- and moderation-system with tons of features" +} diff --git a/modules/nicknames/configs/config.json b/modules/nicknames/configs/config.json index 000397da..a087f74b 100644 --- a/modules/nicknames/configs/config.json +++ b/modules/nicknames/configs/config.json @@ -1,27 +1,13 @@ { - "description": { - "en": "Configure the function of the module here", - "de": "Stelle hier die Funktionen des Modules ein" - }, - "humanName": { - "en": "Configuration", - "de": "Konfiguration" - }, + "description": "Configure the function of the module here", + "humanName": "Configuration", "filename": "config.json", "content": [ { "name": "forceDisplayname", - "humanName": { - "en": "Force display name", - "de": "Anzeigenamen erzwingen" - }, - "default": { - "en": false - }, - "description": { - "en": "Use display names of users instead of custom nicknames.", - "de": "Anzeigenamen von Benutzern anstelle von benutzerdefinierten Nicknamen verwenden." - }, + "humanName": "Force display name", + "default": false, + "description": "Use display names of users instead of custom nicknames.", "type": "boolean" } ] diff --git a/modules/nicknames/configs/strings.json b/modules/nicknames/configs/strings.json index 343e8739..6b8ed954 100644 --- a/modules/nicknames/configs/strings.json +++ b/modules/nicknames/configs/strings.json @@ -1,58 +1,28 @@ { - "description": { - "en": "Set a prefixes and/or suffixes for roles.", - "de": "Setze Präfixe und/oder Suffixe für Rollen." - }, - "humanName": { - "en": "Roles", - "de": "Rollen" - }, + "description": "Set a prefixes and/or suffixes for roles.", + "humanName": "Roles", "filename": "strings.json", "configElements": true, "content": [ { "name": "roleID", - "humanName": { - "en": "Role", - "de": "Rolle" - }, - "default": { - "en": "" - }, - "description": { - "en": "The role you want to set a prefix/suffix for.", - "de": "Die Rolle, für die ein Präfix/Suffix vergeben werden soll." - }, + "humanName": "Role", + "default": "", + "description": "The role you want to set a prefix/suffix for.", "type": "roleID" }, { "name": "prefix", - "humanName": { - "en": "Prefix", - "de": "Präfix" - }, - "default": { - "en": "" - }, - "description": { - "en": "The Prefix to be set.", - "de": "Das Präfix." - }, + "humanName": "Prefix", + "default": "", + "description": "The Prefix to be set.", "type": "string" }, { "name": "suffix", - "humanName": { - "en": "Suffix", - "de": "Suffix" - }, - "default": { - "en": "" - }, - "description": { - "en": "The Suffix to be set.", - "de": "Das Suffix." - }, + "humanName": "Suffix", + "default": "", + "description": "The Suffix to be set.", "type": "string" } ] diff --git a/modules/nicknames/module.json b/modules/nicknames/module.json index 39c91b14..6390e005 100644 --- a/modules/nicknames/module.json +++ b/modules/nicknames/module.json @@ -1,15 +1,13 @@ { "name": "nicknames", - "humanReadableName": { - "en": "Role-Nicknames", - "de": "Rollen-Nicknamen" - }, + "humanReadableName": "Role-Nicknames", "author": { "name": "hfgd", "link": "https://github.com/hfgd123", "scnxOrgID": "2" }, "openSourceURL": "https://github.com/hfgd123/CustomDCBot/tree/main/modules/nicknames", + "fa-icon": "fa-solid fa-user-pen", "events-dir": "/events", "models-dir": "/models", "config-example-files": [ @@ -19,8 +17,5 @@ "tags": [ "community" ], - "description": { - "en": "Simple module to edit user nicknames based on roles!", - "de": "Einfaches Modul, um die Nicknames von Nutzern basierend auf ihren Rollen zu bearbeiten!" - } -} \ No newline at end of file + "description": "Simple module to edit user nicknames based on roles!" +} diff --git a/modules/ping-on-vc-join/actual-config.json b/modules/ping-on-vc-join/actual-config.json index 7865aead..c0f75c95 100644 --- a/modules/ping-on-vc-join/actual-config.json +++ b/modules/ping-on-vc-join/actual-config.json @@ -1,46 +1,32 @@ { - "description": { - "en": "Configure messages that should get send when a user joins a Voice-Channel", - "de": "Stelle hier Nachrichten ein, die versendet werden, wenn ein Nutzer einem Sprachkanal beitritt" - }, - "humanName": { - "en": "Configuration", - "de": "Konfiguration" - }, + "description": "Configure messages that should get send when a user joins a Voice-Channel", + "humanName": "Configuration", "filename": "actual-config.json", "content": [ { "name": "assignRoleToUsersInVoiceChannels", - "humanName": { - "en": "Assign roles to members connected to voice channels?", - "de": "Nutzer, die mit Sprachkanälen verbunden sind, Rollen zuweisen?" - }, - "default": { - "en": false - }, - "description": { - "en": "If enabled, users will receive a role when they join a voice channel. This role will be removed when they leave the voice channel (switching voice channels does not trigger a role removal).", - "de": "Wenn aktiviert, werden Nutzer beim Beitritt eines Sprachkanals eine Rolle erhalten. Diese Rolle wird entfernt, wenn sie den Sprachkanal verlassen (Sprachkanäle wechseln zählt nicht)." - }, - "type": "boolean" + "humanName": "Assign roles to members connected to voice channels?", + "default": false, + "description": "If enabled, users will receive a role when they join a voice channel. This role will be removed when they leave the voice channel (switching voice channels does not trigger a role removal).", + "type": "boolean", + "category": "roles" }, { "name": "voiceRoles", "dependsOn": "assignRoleToUsersInVoiceChannels", - "humanName": { - "en": "Roles for users that are connected to voice channels", - "de": "Nutzer, die mit Sprachkanälen verbunden sind, Rollen zuweisen?" - }, - "default": { - "en": [], - "de": [] - }, - "description": { - "en": "Users that are currently connected to a voice channel will be assigned these roles.", - "de": "Nutzer, die aktuell mit einem Sprachkanal verbunden sind, erhalten diese Rolen." - }, + "humanName": "Roles for users that are connected to voice channels", + "default": [], + "description": "Users that are currently connected to a voice channel will be assigned these roles.", "type": "array", - "content": "roleID" + "content": "roleID", + "category": "roles" + } + ], + "categories": [ + { + "id": "roles", + "icon": "fa-solid fa-users", + "displayName": "Voice Roles" } ] } \ No newline at end of file diff --git a/modules/ping-on-vc-join/config.json b/modules/ping-on-vc-join/config.json index cce3e041..c726f453 100644 --- a/modules/ping-on-vc-join/config.json +++ b/modules/ping-on-vc-join/config.json @@ -1,130 +1,109 @@ { - "description": { - "en": "Configure messages that should get send when a user joins a Voice-Channel", - "de": "Stelle hier Nachrichten ein, die versendet werden, wenn ein Nutzer einem Sprachkanal beitritt" - }, - "humanName": { - "en": "Message on Voice Join", - "de": "Nachricht beim Kanalbeitritt" - }, + "description": "Configure messages that should get send when a user joins a Voice-Channel", + "humanName": "Message on Voice Join", "filename": "config.json", "configElements": true, "content": [ { "name": "channels", - "humanName": { - "en": "Channels", - "de": "Auslöserkanäle" - }, - "default": { - "en": [], - "de": [] - }, - "description": { - "en": "Channel-ID in which this messages should get triggered", - "de": "Kanäle, bei denen der Bot reagieren soll, wenn ein Nutzer joint" - }, + "humanName": "Channels", + "default": [], + "description": "Channel-ID in which this messages should get triggered", "type": "array", - "content": "channelID" + "content": "channelID", + "category": "general" }, { "name": "message", - "humanName": { - "de": "Nachricht", - "en": "Message" - }, - "default": { - "en": "The user %tag% joined the voicechat %vc%", - "de": "Der Nutzer %tag% ist dem Voicechat %vc% beigetreten." - }, - "description": { - "en": "Here you can set the message that should be send if someone joins a selected voicechat", - "de": "Hier kannst du die Nachricht einstellen, die gesendet werden soll, wenn jemand dem Sprachkanal beitritt" - }, + "humanName": "Message", + "default": "The user %tag% joined the voicechat %vc%", + "description": "Here you can set the message that should be send if someone joins a selected voicechat", "type": "string", "allowEmbed": true, "params": [ { "name": "tag", - "description": { - "en": "Tag of the user", - "de": "Tag des Nutzers" - } + "description": "Tag of the user" }, { "name": "vc", - "description": { - "en": "Name of the voicechat", - "de": "Name des Sprackkanals" - } + "description": "Name of the voicechat" }, { "name": "mention", - "description": { - "en": "Mention of the user", - "de": "Erwähnung des Nutzers" - } + "description": "Mention of the user" } - ] + ], + "category": "messages" }, { "name": "notify_channel_id", - "humanName": { - "de": "Benachrichtigungskanal", - "en": "Notification-Channel" - }, - "default": { - "en": "" - }, + "humanName": "Notification-Channel", + "default": "", "content": [ "GUILD_TEXT" ], - "description": { - "en": "Channel where the message should be send", - "de": "Kanal, in welchen die Nachricht gesendet werden soll" - }, - "type": "channelID" + "description": "Channel where the message should be send", + "type": "channelID", + "category": "general" + }, + { + "name": "cooldownEnabled", + "humanName": "Enable Cooldown?", + "default": false, + "description": "When enabled, messages will only be sent once per channel within the cooldown period", + "type": "boolean", + "category": "cooldown" + }, + { + "name": "cooldownMinutes", + "humanName": "Cooldown Duration (Minutes)", + "default": 5, + "description": "Duration in minutes to wait before sending another message for the same channel", + "type": "integer", + "dependsOn": "cooldownEnabled", + "category": "cooldown" }, { "name": "send_pn_to_member", - "humanName": { - "en": "Join-DM", - "de": "Join-PN" - }, - "default": { - "en": false - }, - "description": { - "en": "Should the bot send a PN to the member?", - "de": "Soll der Bot eine PN an den Nutzer schicken?" - }, - "type": "boolean" + "humanName": "Join-DM", + "default": false, + "description": "Should the bot send a PN to the member?", + "type": "boolean", + "category": "messages" }, { "name": "pn_message", - "humanName": { - "en": "Join-DM-Message", - "de": "Join-PN-Nachricht" - }, - "default": { - "en": "Hi, I saw you joined the voice chat %vc%. Nice (;", - "de": "Hi, ich habe gesehen, dass du %vc% beigetreten bist. Nice (;" - }, - "description": { - "de": "Diese Nachricht wird an den Nutzer versandt, wenn er einem Voicechat beitritt (wenn \"Join-PN\" aktiviert ist)." - }, + "humanName": "Join-DM-Message", + "default": "Hi, I saw you joined the voice chat %vc%. Nice (;", + "description": "This message is sent to the user when they join a voice chat (if \"Join DM\" is enabled).", "type": "string", "dependsOn": "send_pn_to_member", "allowEmbed": true, "params": [ { "name": "vc", - "description": { - "en": "Name of the voicechat", - "de": "Name des Sprachkanals" - } + "description": "Name of the voicechat" } - ] + ], + "category": "messages" + } + ], + "categories": [ + { + "id": "general", + "icon": "fas fa-gears", + "displayName": "General Settings" + }, + { + "id": "cooldown", + "icon": "fa-regular fa-clock-rotate-left", + "displayName": "Cooldown" + }, + { + "id": "messages", + "icon": "fas fa-comment-dots", + "displayName": "Messages" } ] -} \ No newline at end of file +} diff --git a/modules/ping-on-vc-join/events/voiceStateUpdate.js b/modules/ping-on-vc-join/events/voiceStateUpdate.js index 4703c930..8eb529ce 100644 --- a/modules/ping-on-vc-join/events/voiceStateUpdate.js +++ b/modules/ping-on-vc-join/events/voiceStateUpdate.js @@ -1,15 +1,19 @@ const {embedType, disableModule, formatDiscordUserName} = require('../../../src/functions/helpers'); const {localize} = require('../../../src/functions/localize'); -const cooldown = new Set(); +const userCooldown = new Set(); // Per-user cooldown (legacy) +const channelCooldown = new Map(); // Per-channel cooldown: Map exports.run = async (client, oldState, newState) => { if (!client.botReadyAt) return; const roleConfig = client.configurations['ping-on-vc-join']['actual-config']; - if (roleConfig.assignRoleToUsersInVoiceChannels && roleConfig.voiceRoles.length !== 0) { + + // Ignore bots for role assignment + if (roleConfig.assignRoleToUsersInVoiceChannels && roleConfig.voiceRoles.length !== 0 && !newState.member.user.bot) { if (oldState.channel && !newState.channel) newState.member.roles.remove(roleConfig.voiceRoles); if (!oldState.channel && newState.channel) newState.member.roles.add(roleConfig.voiceRoles); } + if (!newState.channel || newState.channel.id === oldState?.channel?.id) return; const channel = await client.channels.fetch(newState.channelId); if (channel.guild.id !== client.guild.id) return; @@ -21,24 +25,60 @@ exports.run = async (client, oldState, newState) => { const member = await client.guild.members.fetch(newState.id); if (member.user.bot) return; - if (cooldown.has(member.user.id)) return; + // Check cooldown based on configuration + const cooldownEnabled = configElement['cooldownEnabled'] || false; + + if (cooldownEnabled) { + // Per-channel cooldown + const cooldownKey = `${channel.id}`; + const now = Date.now(); + const cooldownEnd = channelCooldown.get(cooldownKey); + + if (cooldownEnd && now < cooldownEnd) { + // Still in cooldown, don't send message + return; + } + } else { + // Legacy per-user cooldown + if (userCooldown.has(member.user.id)) return; + } const notifyChannel = newState.guild.channels.cache.get(configElement['notify_channel_id']); - if (!notifyChannel) return disableModule('partner-list', localize('ping-on-vc-join', 'channel-bot-found', {c: configElement['notify_channel_id']})); + if (!notifyChannel) return disableModule('ping-on-vc-join', localize('ping-on-vc-join', 'channel-not-found', {c: configElement['notify_channel_id']})); setTimeout(async () => { // Wait 3 seconds before pinging a role if (!member.voice) return; if (member.voice.channelId !== channel.id) return; + await notifyChannel.send(embedType(configElement['message'], { '%vc%': channel.name, '%tag%': formatDiscordUserName(member.user), '%mention%': `<@${member.user.id}>` })); - cooldown.add(member.user.id); - setTimeout(() => { - cooldown.delete(member.user.id); - }, 300000); // 5 min + // Set cooldown after sending message + if (cooldownEnabled) { + // Per-channel cooldown + const cooldownMinutes = configElement['cooldownMinutes'] || 5; + const cooldownMs = cooldownMinutes * 60 * 1000; + const cooldownKey = `${channel.id}`; + + channelCooldown.set(cooldownKey, Date.now() + cooldownMs); + + // Clean up expired cooldowns periodically + setTimeout(() => { + const now = Date.now(); + if (channelCooldown.get(cooldownKey) <= now) { + channelCooldown.delete(cooldownKey); + } + }, cooldownMs); + } else { + // Legacy per-user cooldown + userCooldown.add(member.user.id); + setTimeout(() => { + userCooldown.delete(member.user.id); + }, 300000); // 5 min + } if (configElement['send_pn_to_member']) { await member.send(embedType(configElement['pn_message'], { diff --git a/modules/ping-on-vc-join/module.json b/modules/ping-on-vc-join/module.json index 25206a7b..2d84496a 100644 --- a/modules/ping-on-vc-join/module.json +++ b/modules/ping-on-vc-join/module.json @@ -15,12 +15,6 @@ "tags": [ "support" ], - "humanReadableName": { - "en": "Voice-Channel Actions", - "de": "Sprachkanal-Aktionen" - }, - "description": { - "en": "Sends messages when someone joins a voicechat and assign roles to users in Voice-Channels", - "de": "Sende Nachrichten, wenn jemand einem Sprachkanal beitritt und vergebe Rollen an Nutzer in Sprachkanälen" - } -} \ No newline at end of file + "humanReadableName": "Voice-Channel Actions", + "description": "Sends messages when someone joins a voicechat and assign roles to users in Voice-Channels" +} diff --git a/modules/ping-protection/commands/ping-protection.js b/modules/ping-protection/commands/ping-protection.js index d8ac43c7..4d61d83d 100644 --- a/modules/ping-protection/commands/ping-protection.js +++ b/modules/ping-protection/commands/ping-protection.js @@ -1,180 +1,196 @@ -const { - fetchModHistory, - getPingCountInWindow, - generateHistoryResponse, - generateActionsResponse +const { + fetchModHistory, + getPingCountInWindow, + generateHistoryResponse, + generateActionsResponse } = require('../ping-protection'); -const { localize } = require('../../../src/functions/localize'); -const { truncate } = require('../../../src/functions/helpers'); -const { ActionRowBuilder, ButtonBuilder, EmbedBuilder, ButtonStyle, MessageFlags } = require('discord.js'); +const {localize} = require('../../../src/functions/localize'); +const {truncate, safeSetFooter} = require('../../../src/functions/helpers'); +const { + ActionRowBuilder, + ButtonBuilder, + EmbedBuilder, + ButtonStyle, + MessageFlags +} = require('discord.js'); module.exports.run = async function (interaction) { - const group = interaction.options.getSubcommandGroup(false); - const sub = interaction.options.getSubcommand(false); + const group = interaction.options.getSubcommandGroup(false); + const sub = interaction.options.getSubcommand(false); - if (group) { - return module.exports.subcommands[group][sub](interaction); - } - return module.exports.subcommands[sub](interaction); + if (group) { + return module.exports.subcommands[group][sub](interaction); + } + return module.exports.subcommands[sub](interaction); }; // Handles subcommands module.exports.subcommands = { - 'user': { - 'history': async function (interaction) { - const user = interaction.options.getUser('user'); - const payload = await generateHistoryResponse(interaction.client, user.id, 1); - await interaction.reply({ ...payload, flags: MessageFlags.Ephemeral }); - }, - 'actions-history': async function (interaction) { - const user = interaction.options.getUser('user'); - const payload = await generateActionsResponse(interaction.client, user.id, 1); - await interaction.reply({ ...payload, flags: MessageFlags.Ephemeral }); - }, - 'panel': async function (interaction) { - const user = interaction.options.getUser('user'); - const pingerId = user.id; - const storageConfig = interaction.client.configurations['ping-protection']['storage']; - const retentionWeeks = (storageConfig && storageConfig.pingHistoryRetention) - ? storageConfig.pingHistoryRetention - : 12; - const timeframeDays = retentionWeeks * 7; - - const pingCount = await getPingCountInWindow(interaction.client, pingerId, timeframeDays); - const modData = await fetchModHistory(interaction.client, pingerId, 1, 1000); - - const row = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId(`ping-protection_history_${user.id}`) - .setLabel(localize('ping-protection', 'btn-history')) - .setStyle(ButtonStyle.Secondary), - new ButtonBuilder() - .setCustomId(`ping-protection_actions_${user.id}`) - .setLabel(localize('ping-protection', 'btn-actions')) - .setStyle(ButtonStyle.Secondary), - new ButtonBuilder() - .setCustomId(`ping-protection_delete_${user.id}`) - .setLabel(localize('ping-protection', 'btn-delete')) - .setStyle(ButtonStyle.Danger) - ); - - const embed = new EmbedBuilder() - .setTitle(localize('ping-protection', 'panel-title', { u: user.tag })) - .setDescription(localize('ping-protection', 'panel-description', { u: user.toString(), i: user.id })) - .setColor('Blue') - .setThumbnail(user.displayAvatarURL({ dynamic: true })) - .addFields([{ - name: localize('ping-protection', 'field-quick-history', { w: retentionWeeks }), - value: localize('ping-protection', 'field-quick-desc', { p: pingCount, m: modData.total }), - inline: false - }]) - .setFooter({ - text: interaction.client.strings.footer, - iconURL: interaction.client.strings.footerImgUrl - }); - if (!interaction.client.strings.disableFooterTimestamp) embed.setTimestamp(); - - await interaction.reply({ - embeds: [embed.toJSON()], - components: [row.toJSON()], - flags: MessageFlags.Ephemeral - }); - } - }, - 'list': { - 'protected': async function (interaction) { - await listHandler(interaction, 'protected'); + 'user': { + 'history': async function (interaction) { + const user = interaction.options.getUser('user'); + const payload = await generateHistoryResponse(interaction.client, user.id, 1); + await interaction.reply({ + ...payload, + flags: MessageFlags.Ephemeral + }); + }, + 'actions-history': async function (interaction) { + const user = interaction.options.getUser('user'); + const payload = await generateActionsResponse(interaction.client, user.id, 1); + await interaction.reply({ + ...payload, + flags: MessageFlags.Ephemeral + }); + }, + 'panel': async function (interaction) { + const user = interaction.options.getUser('user'); + const pingerId = user.id; + const storageConfig = interaction.client.configurations['ping-protection']['storage']; + const retentionWeeks = (storageConfig && storageConfig.pingHistoryRetention) + ? storageConfig.pingHistoryRetention + : 12; + const timeframeDays = retentionWeeks * 7; + + const pingCount = await getPingCountInWindow(interaction.client, pingerId, timeframeDays); + const modData = await fetchModHistory(interaction.client, pingerId, 1, 1000); + + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(`ping-protection_history_${user.id}`) + .setLabel(localize('ping-protection', 'btn-history')) + .setStyle(ButtonStyle.Secondary), + new ButtonBuilder() + .setCustomId(`ping-protection_actions_${user.id}`) + .setLabel(localize('ping-protection', 'btn-actions')) + .setStyle(ButtonStyle.Secondary), + new ButtonBuilder() + .setCustomId(`ping-protection_delete_${user.id}`) + .setLabel(localize('ping-protection', 'btn-delete')) + .setStyle(ButtonStyle.Danger) + ); + + const embed = new EmbedBuilder() + .setTitle(localize('ping-protection', 'panel-title', {u: user.tag})) + .setDescription(localize('ping-protection', 'panel-description', { + u: user.toString(), + i: user.id + })) + .setColor('Blue') + .setThumbnail(user.displayAvatarURL({dynamic: true})) + .addFields([{ + name: localize('ping-protection', 'field-quick-history', {w: retentionWeeks}), + value: localize('ping-protection', 'field-quick-desc', { + p: pingCount, + m: modData.total + }), + inline: false + }]); + + safeSetFooter(embed, interaction.client); + if (!interaction.client.strings.disableFooterTimestamp) embed.setTimestamp(); + + await interaction.reply({ + embeds: [embed.toJSON()], + components: [row.toJSON()], + flags: MessageFlags.Ephemeral + }); + } }, - 'whitelisted': async function (interaction) { - await listHandler(interaction, 'whitelisted'); + 'list': { + 'protected': async function (interaction) { + await listHandler(interaction, 'protected'); + }, + 'whitelisted': async function (interaction) { + await listHandler(interaction, 'whitelisted'); + } } - } }; // Handles list subcommands async function listHandler(interaction, type) { - const config = interaction.client.configurations['ping-protection']['configuration']; - const embed = new EmbedBuilder() - .setColor('Green') - .setFooter({ - text: interaction.client.strings.footer, - iconURL: interaction.client.strings.footerImgUrl - }); + const config = interaction.client.configurations['ping-protection']['configuration']; + const embed = new EmbedBuilder() + .setColor('Green'); + + safeSetFooter(embed, interaction.client); + + if (!interaction.client.strings.disableFooterTimestamp) embed.setTimestamp(); + + if (type === 'protected') { + embed.setTitle(localize('ping-protection', 'list-protected-title')); + embed.setDescription(localize('ping-protection', 'list-protected-desc')); + + const usersList = config.protectedUsers.length > 0 + ? config.protectedUsers.map(id => `<@${id}>`).join('\n') + : localize('ping-protection', 'list-none'); + + const rolesList = config.protectedRoles.length > 0 + ? config.protectedRoles.map(id => `<@&${id}>`).join('\n') + : localize('ping-protection', 'list-none'); + + embed.addFields([ + { + name: localize('ping-protection', 'field-protected-users'), + value: truncate(usersList, 1024), + inline: true + }, + { + name: localize('ping-protection', 'field-protected-roles'), + value: truncate(rolesList, 1024), + inline: true + } + ]); + + } else if (type === 'whitelisted') { + embed.setTitle(localize('ping-protection', 'list-whitelist-title')); + embed.setDescription(localize('ping-protection', 'list-whitelist-desc')); + + const rolesList = config.ignoredRoles.length > 0 + ? config.ignoredRoles.map(id => `<@&${id}>`).join('\n') + : localize('ping-protection', 'list-none'); + + const channelsList = config.ignoredChannels.length > 0 + ? config.ignoredChannels.map(id => `<#${id}>`).join('\n') + : localize('ping-protection', 'list-none'); + + const usersList = config.ignoredUsers.length > 0 + ? config.ignoredUsers.map(id => `<@${id}>`).join('\n') + : localize('ping-protection', 'list-none'); + + embed.addFields([ + { + name: localize('ping-protection', 'field-wl-roles'), + value: truncate(rolesList, 1024), + inline: true + }, + { + name: localize('ping-protection', 'field-wl-channels'), + value: truncate(channelsList, 1024), + inline: true + }, + { + name: localize('ping-protection', 'field-wl-users'), + value: truncate(usersList, 1024), + inline: true + } + ]); + } - if (!interaction.client.strings.disableFooterTimestamp) embed.setTimestamp(); - - if (type === 'protected') { - embed.setTitle(localize('ping-protection', 'list-protected-title')); - embed.setDescription(localize('ping-protection', 'list-protected-desc')); - - const usersList = config.protectedUsers.length > 0 - ? config.protectedUsers.map(id => `<@${id}>`).join('\n') - : localize('ping-protection', 'list-none'); - - const rolesList = config.protectedRoles.length > 0 - ? config.protectedRoles.map(id => `<@&${id}>`).join('\n') - : localize('ping-protection', 'list-none'); - - embed.addFields([ - { - name: localize('ping-protection', 'field-protected-users'), - value: truncate(usersList, 1024), - inline: true - }, - { - name: localize('ping-protection', 'field-protected-roles'), - value: truncate(rolesList, 1024), - inline: true - } - ]); - - } else if (type === 'whitelisted') { - embed.setTitle(localize('ping-protection', 'list-whitelist-title')); - embed.setDescription(localize('ping-protection', 'list-whitelist-desc')); - - const rolesList = config.ignoredRoles.length > 0 - ? config.ignoredRoles.map(id => `<@&${id}>`).join('\n') - : localize('ping-protection', 'list-none'); - - const channelsList = config.ignoredChannels.length > 0 - ? config.ignoredChannels.map(id => `<#${id}>`).join('\n') - : localize('ping-protection', 'list-none'); - - const usersList = config.ignoredUsers.length > 0 - ? config.ignoredUsers.map(id => `<@${id}>`).join('\n') - : localize('ping-protection', 'list-none'); - - embed.addFields([ - { - name: localize('ping-protection', 'field-wl-roles'), - value: truncate(rolesList, 1024), - inline: true }, - { - name: localize('ping-protection', 'field-wl-channels'), - value: truncate(channelsList, 1024), - inline: true }, - { - name: localize('ping-protection', 'field-wl-users'), - value: truncate(usersList, 1024), - inline: true - } - ]); - } - - await interaction.reply({ - embeds: [embed.toJSON()], - flags: MessageFlags.Ephemeral - }); + await interaction.reply({ + embeds: [embed.toJSON()], + flags: MessageFlags.Ephemeral + }); } module.exports.config = { - name: 'ping-protection', - description: localize('ping-protection', 'cmd-desc-module'), - usage: '/ping-protection', - type: 'slash', - defaultPermission: false, - options: [ - { + name: 'ping-protection', + description: localize('ping-protection', 'cmd-desc-module'), + usage: '/ping-protection', + type: 'slash', + defaultPermission: false, + options: [ + { type: 'SUB_COMMAND_GROUP', name: 'user', description: localize('ping-protection', 'cmd-desc-group-user'), diff --git a/modules/ping-protection/configs/configuration.json b/modules/ping-protection/configs/configuration.json index acd5b7d0..53a7ea0d 100644 --- a/modules/ping-protection/configs/configuration.json +++ b/modules/ping-protection/configs/configuration.json @@ -1,207 +1,132 @@ { "filename": "configuration.json", - "humanName": { - "en": "General Configuration" - }, + "humanName": "General Configuration", "commandsWarnings": { "normal": [ "/ping-protection user history", "/ping-protection user actions-history", - "/ping-protection list roles", - "/ping-protection list users", + "/ping-protection list protected", "/ping-protection list whitelisted" ] }, - "description": { - "en": "Configure protected users/roles, whitelisted roles/members, ignored channels and the notification message." - }, + "description": "Configure protected users/roles, whitelisted roles/members, ignored channels and the notification message.", "categories": [ { "id": "protection", "icon": "fa-solid fa-shield", - "displayName": { - "en": "Protected" - } + "displayName": "Protected" }, { "id": "whitelisted", "icon": "fa-solid fa-badge-check", - "displayName": { - "en": "Whitelists" - } + "displayName": "Whitelists" }, { "id": "rules", "icon": "fas fa-gears", - "displayName": { - "en": "Ping rules" - } + "displayName": "Ping rules" }, { "id": "automod", "icon": "far fa-robot", - "displayName": { - "en": "AutoMod settings" - } + "displayName": "AutoMod settings" }, { "id": "messages", "icon": "fa-duotone fa-regular fa-triangle-exclamation", - "displayName": { - "en": "Warning message" - } + "displayName": "Warning message" } ], "content": [ { "name": "protectedRoles", "category": "protection", - "humanName": { - "en": "Protected Roles" - }, - "description": { - "en": "Specific roles which are protected from pings." - }, + "humanName": "Protected Roles", + "description": "Specific roles which are protected from pings.", "type": "array", "content": "roleID", - "default": { - "en": [] - } + "default": [] }, { "name": "protectAllUsersWithProtectedRole", "category": "protection", - "humanName": { - "en": "Protect all users with a protected role" - }, - "description": { - "en": "if enabled, all users with at least one protected role will be protected from pings, even if they are not specifically listed as protected users." - }, + "humanName": "Protect all users with a protected role", + "description": "if enabled, all users with at least one protected role will be protected from pings, even if they are not specifically listed as protected users.", "type": "boolean", - "default": { - "en": true - } + "default": true }, { "name": "protectedUsers", "category": "protection", - "humanName": { - "en": "Protected Users" - }, - "description": { - "en": "Specific users who are protected from pings." - }, + "humanName": "Protected Users", + "description": "Specific users who are protected from pings.", "type": "array", "content": "userID", - "default": { - "en": [] - } + "default": [] }, { "name": "ignoredRoles", "category": "whitelisted", - "humanName": { - "en": "Whitelisted Roles" - }, - "description": { - "en": "Roles allowed to ping protected members or roles." - }, + "humanName": "Whitelisted Roles", + "description": "Roles allowed to ping protected members or roles.", "type": "array", "content": "roleID", - "default": { - "en": [] - } + "default": [] }, { "name": "ignoredChannels", "category": "whitelisted", - "humanName": { - "en": "Whitelisted Channels" - }, - "description": { - "en": "Pings in these channels are ignored." - }, + "humanName": "Whitelisted Channels", + "description": "Pings in these channels are ignored.", "type": "array", "content": "channelID", - "default": { - "en": [] - } + "default": [] }, { "name": "ignoredUsers", "category": "whitelisted", - "humanName": { - "en": "Whitelisted Users" - }, - "description": { - "en": "Pings from these users are ignored." - }, + "humanName": "Whitelisted Users", + "description": "Pings from these users are ignored.", "type": "array", "content": "userID", - "default": { - "en": [] - } + "default": [] }, { "name": "allowReplyPings", "category": "rules", - "humanName": { - "en": "Allow Reply Pings" - }, - "description": { - "en": "If enabled, replying to a protected user (with mention ON) is allowed." - }, + "humanName": "Allow Reply Pings", + "description": "If enabled, replying to a protected user (with mention ON) is allowed.", "type": "boolean", - "default": { - "en": false - } + "default": false }, { "name": "selfPingConfiguration", "category": "rules", - "humanName": { - "en": "Self-Ping configuration" - }, - "description": { - "en": "Configure what happens when a protected user pings themselves. Note: Automod overrides this setting meaning this setting will not apply if Automod is enabled." - }, + "humanName": "Self-Ping configuration", + "description": "Configure what happens when a protected user pings themselves. Note: Automod overrides this setting meaning this setting will not apply if Automod is enabled.", "type": "select", "content": [ "Get punished like normal members", "Ignored", "Get fun easter eggs when pinging themselves" ], - "default": { - "en": "Ignored" - } + "default": "Ignored" }, { "name": "enableAutomod", "category": "automod", - "humanName": { - "en": "Enable automod" - }, - "description": { - "en": "If enabled, the bot will utilise Discord's native AutoMod to block the message with a ping of a protected user/role." - }, + "humanName": "Enable automod", + "description": "If enabled, the bot will utilise Discord's native AutoMod to block the message with a ping of a protected user/role.", "type": "boolean", - "default": { - "en": true - } + "default": true }, { "name": "autoModLogChannel", "category": "automod", - "humanName": { - "en": "AutoMod Log Channel" - }, - "description": { - "en": "Channel where AutoMod alerts are sent." - }, + "humanName": "AutoMod Log Channel", + "description": "Channel where AutoMod alerts are sent.", "type": "channelID", - "default": { - "en": [] - }, + "default": "", "channelTypes": [ "GUILD_TEXT" ], @@ -210,63 +135,43 @@ { "name": "autoModBlockMessage", "category": "automod", - "humanName": { - "en": "AutoMod custom message for message block" - }, - "description": { - "en": "Custom text shown to the user when blocked (Max 150 characters)." - }, + "humanName": "AutoMod custom message for message block", + "description": "Custom text shown to the user when blocked (Max 150 characters).", "type": "string", "maxLength": 150, - "default": { - "en": "Your message was blocked because you are trying to ping a protected user/role. The message content might be logged depending on the configuration." - }, + "default": "Your message was blocked because you are trying to ping a protected user/role. The message content might be logged depending on the configuration.", "dependsOn": "enableAutomod" }, { "name": "pingWarningMessage", "category": "messages", - "humanName": { - "en": "Warning Message" - }, - "description": { - "en": "The message that gets sent to the user when they ping someone." - }, + "humanName": "Warning Message", + "description": "The message that gets sent to the user when they ping someone.", "type": "string", "allowEmbed": true, "params": [ { "name": "target-name", - "description": { - "en": "Name of the pinged user/role" - } + "description": "Name of the pinged user/role" }, { "name": "target-mention", - "description": { - "en": "Mention of the pinged user/role" - } + "description": "Mention of the pinged user/role" }, { "name": "target-id", - "description": { - "en": "ID of the pinged user/role" - } + "description": "ID of the pinged user/role" }, { "name": "pinger-id", - "description": { - "en": "ID of the user who pinged" - } + "description": "ID of the user who pinged" } ], "default": { - "en": { - "title": "You are not allowed to ping %target-name%!", - "description": "<@%pinger-id%>, You are not allowed to ping %target-mention% due to your role. You can view which roles/members you are not allowed to ping by using the `/ping-protection list protected` command.\n\nIf you were replying, make sure to turn off the mention in the reply.", - "image": "https://scnx-cdn.scootkit.net/1769198862209-rJfCVKzAuo6uQLhPUe9o2P6ArJkDBSVUCEyUQM6bqt5WFKWK.gif", - "color": "#ed4245" - } + "title": "You are not allowed to ping %target-name%!", + "description": "<@%pinger-id%>, You are not allowed to ping %target-mention% due to your role. You can view which roles/members you are not allowed to ping by using the `/ping-protection list protected` command.\n\nIf you were replying, make sure to turn off the mention in the reply.", + "image": "https://scnx-cdn.scootkit.net/1769198862209-rJfCVKzAuo6uQLhPUe9o2P6ArJkDBSVUCEyUQM6bqt5WFKWK.gif", + "color": "#ed4245" } } ] diff --git a/modules/ping-protection/configs/moderation.json b/modules/ping-protection/configs/moderation.json index 1c15ed63..9bf55ec2 100644 --- a/modules/ping-protection/configs/moderation.json +++ b/modules/ping-protection/configs/moderation.json @@ -1,156 +1,96 @@ { "filename": "moderation.json", - "humanName": { - "en": "Moderation Actions" - }, + "humanName": "Moderation Actions", "configElementName": { - "en": { - "one": "punishment", - "more": "punishment" - } - }, - "description": { - "en": "Define triggers for punishments." + "one": "punishment", + "more": "punishment" }, + "description": "Define triggers for punishments.", "configElements": true, "content": [ { "name": "pingsCount", - "humanName": { - "en": "Pings to trigger moderation" - }, - "description": { - "en": "The amount of pings required to trigger a moderation action." - }, + "humanName": "Pings to trigger moderation", + "description": "The amount of pings required to trigger a moderation action.", "type": "integer", - "default": { - "en": 10 - } + "default": 10 }, { "name": "useCustomTimeframe", - "humanName": { - "en": "Use a custom timeframe" - }, - "description": { - "en": "If enabled, you can choose your own custom timeframe of days in which the pings must occur to trigger the moderation action." - }, + "humanName": "Use a custom timeframe", + "description": "If enabled, you can choose your own custom timeframe of days in which the pings must occur to trigger the moderation action.", "type": "boolean", - "default": { - "en": false - } + "default": false }, { "name": "timeframeDays", - "humanName": { - "en": "Timeframe (Days)" - }, - "description": { - "en": "In how many days must these pings occur?" - }, + "humanName": "Timeframe (Days)", + "description": "In how many days must these pings occur?", "type": "integer", - "default": { - "en": 7 - }, + "default": 7, "dependsOn": "useCustomTimeframe" }, { "name": "actionType", - "humanName": { - "en": "Action" - }, - "description": { - "en": "What punishment should be applied?" - }, + "humanName": "Action", + "description": "What punishment should be applied?", "type": "select", "content": [ "MUTE", "KICK" ], - "default": { - "en": "MUTE" - } + "default": "MUTE" }, { "name": "muteDuration", - "humanName": { - "en": "Mute Duration (only if action type is MUTE)" - }, - "description": { - "en": "How long to mute the user? (in minutes)" - }, + "humanName": "Mute Duration (only if action type is MUTE)", + "description": "How long to mute the user? (in minutes)", "type": "integer", - "default": { - "en": 60 - } + "default": 60 }, { "name": "enableActionLogging", - "humanName": { - "en": "Enable action logging" - }, - "description": { - "en": "If enabled, moderation actions will be logged in the channel where a protected user/role got pinged." - }, + "humanName": "Enable action logging", + "description": "If enabled, moderation actions will be logged in the channel where a protected user/role got pinged.", "type": "boolean", - "default": { - "en": true - } + "default": true }, { "name": "actionLogMessage", - "humanName": { - "en": "Action log message" - }, - "description": { - "en": "The message that will be sent when a user is punished for pinging protected users/roles." - }, + "humanName": "Action log message", + "description": "The message that will be sent when a user is punished for pinging protected users/roles.", "type": "string", "allowEmbed": true, "params": [ { "name": "pinger-mention", - "description": { - "en": "Mention of the user who pinged" - } + "description": "Mention of the user who pinged" }, { "name": "pinger-name", - "description": { - "en": "Name of the user who pinged" - } + "description": "Name of the user who pinged" }, { "name": "action", - "description": { - "en": "The action that was taken (muted/kicked)" - } + "description": "The action that was taken (muted/kicked)" }, { "name": "pings", - "description": { - "en": "Number of pings that triggered the action" - } + "description": "Number of pings that triggered the action" }, { "name": "timeframe", - "description": { - "en": "The timeframe in days in which the pings occurred" - } + "description": "The timeframe in days in which the pings occurred" }, { "name": "duration", - "description": { - "en": "Duration of the mute in minutes (only for the mute action)" - } + "description": "Duration of the mute in minutes (only for the mute action)" } ], "default": { - "en": { - "title": "Moderation action taken against %pinger-name%", - "description": "I have taken action against %pinger-mention% for pinging protected users/roles %pings% times within %timeframe% days.\n **Action:** %action%\n**Duration:** %duration% minutes", - "color": "#ed4245" - } + "title": "Moderation action taken against %pinger-name%", + "description": "I have taken action against %pinger-mention% for pinging protected users/roles %pings% times within %timeframe% days.\n **Action:** %action%\n**Duration:** %duration% minutes", + "color": "#ed4245" } } ] diff --git a/modules/ping-protection/configs/storage.json b/modules/ping-protection/configs/storage.json index 995a1ca1..586ba025 100644 --- a/modules/ping-protection/configs/storage.json +++ b/modules/ping-protection/configs/storage.json @@ -1,62 +1,40 @@ { "filename": "storage.json", - "humanName": { - "en": "Data Storage" - }, - "description": { - "en": "Configure how long moderation logs and leaver data are kept." - }, + "humanName": "Data Storage", + "description": "Configure how long moderation logs and leaver data are kept.", "categories": [ { "id": "pings", "icon": "fa-regular fa-clock-rotate-left", - "displayName": { - "en": "Ping History" - } + "displayName": "Ping History" }, { "id": "moderation", "icon": "fas fa-hammer", - "displayName": { - "en": "Moderation Logs" - } + "displayName": "Moderation Logs" }, { "id": "leavers", "icon": "fas fa-right-from-bracket", - "displayName": { - "en": "Leaver Data" - } + "displayName": "Leaver Data" } ], "content": [ { "name": "enablePingHistory", "category": "pings", - "humanName": { - "en": "Enable Ping History" - }, - "description": { - "en": "If enabled, the bot will keep a history of pings to enforce moderation actions." - }, + "humanName": "Enable Ping History", + "description": "If enabled, the bot will keep a history of pings to enforce moderation actions.", "type": "boolean", - "default": { - "en": true - } + "default": true }, { "name": "pingHistoryRetention", "category": "pings", - "humanName": { - "en": "Ping History Retention" - }, - "description": { - "en": "Decides on how long to keep ping logs. Minimum is 4 weeks (1 month) with a maximum of 96 weeks (2 years). This is the length factor of the 'Basic' punishment timeframe." - }, + "humanName": "Ping History Retention", + "description": "Decides on how long to keep ping logs. Minimum is 4 weeks (1 month) with a maximum of 96 weeks (2 years). This is the length factor of the 'Basic' punishment timeframe.", "type": "integer", - "default": { - "en": 12 - }, + "default": 12, "minValue": "4", "maxValue": "96", "dependsOn": "enablePingHistory" @@ -64,60 +42,36 @@ { "name": "deleteAllPingHistoryAfterTimeframe", "category": "pings", - "humanName": { - "en": "Delete all the pings in history after the timeframe?" - }, - "description": { - "en": "If enabled, the bot will delete ALL the pings history of an user after the timeframe instead of only the ping(s) exceeding the timeframe in the history." - }, + "humanName": "Delete all the pings in history after the timeframe?", + "description": "If enabled, the bot will delete ALL the pings history of an user after the timeframe instead of only the ping(s) exceeding the timeframe in the history.", "type": "boolean", - "default": { - "en": false - } + "default": false }, { "name": "modLogRetention", "category": "moderation", - "humanName": { - "en": "Moderation Log Retention (Months)" - }, - "description": { - "en": "How long to keep records of punishments (1 - 24 Months). This is applied when moderation actions are enabled." - }, + "humanName": "Moderation Log Retention (Months)", + "description": "How long to keep records of punishments (1 - 24 Months). This is applied when moderation actions are enabled.", "type": "integer", - "default": { - "en": 12 - }, + "default": 12, "minValue": "1", "maxValue": "24" }, { "name": "enableLeaverDataRetention", "category": "leavers", - "humanName": { - "en": "Keep user logs after they leave" - }, - "description": { - "en": "If enabled, the bot will keep a history of the user after they leave." - }, + "humanName": "Keep user logs after they leave", + "description": "If enabled, the bot will keep a history of the user after they leave.", "type": "boolean", - "default": { - "en": true - } + "default": true }, { "name": "leaverRetention", "category": "leavers", - "humanName": { - "en": "Leaver Data Retention (Days)" - }, - "description": { - "en": "How long to keep data after a user leaves (1-7 Days)." - }, + "humanName": "Leaver Data Retention (Days)", + "description": "How long to keep data after a user leaves (1-7 Days).", "type": "integer", - "default": { - "en": 1 - }, + "default": 1, "minValue": "1", "maxValue": "7", "dependsOn": "enableLeaverDataRetention" diff --git a/modules/ping-protection/events/autoModerationActionExecution.js b/modules/ping-protection/events/autoModerationActionExecution.js index 22f80fae..76d03d21 100644 --- a/modules/ping-protection/events/autoModerationActionExecution.js +++ b/modules/ping-protection/events/autoModerationActionExecution.js @@ -1,15 +1,15 @@ -const { processPing } = require('../ping-protection'); +const {processPing} = require('../ping-protection'); // Handles auto mod actions module.exports.run = async function (client, execution) { - if (execution.ruleTriggerType !== 1) return; + if (execution.ruleTriggerType !== 1) return; const config = client.configurations['ping-protection']['configuration']; if (config.ignoredUsers.includes(execution.userId)) return; - const matchedKeyword = execution.matchedKeyword || ""; + const matchedKeyword = execution.matchedKeyword || ''; const rawId = matchedKeyword.replace(/[^0-9]/g, ''); - + let isProtected = config.protectedRoles.includes(rawId) || config.protectedUsers.includes(rawId); let originChannel = execution.channel; @@ -24,7 +24,8 @@ module.exports.run = async function (client, execution) { if (targetMember && targetMember.roles.cache.some(r => config.protectedRoles.includes(r.id))) { isProtected = true; } - } catch (e) {} + } catch (e) { + } } if (!isProtected) return; diff --git a/modules/ping-protection/events/botReady.js b/modules/ping-protection/events/botReady.js index 6e43412d..1599c573 100644 --- a/modules/ping-protection/events/botReady.js +++ b/modules/ping-protection/events/botReady.js @@ -1,4 +1,7 @@ -const { enforceRetention, syncNativeAutoMod } = require('../ping-protection'); +const { + enforceRetention, + syncNativeAutoMod +} = require('../ping-protection'); const schedule = require('node-schedule'); module.exports.run = async function (client) { diff --git a/modules/ping-protection/events/guildMemberAdd.js b/modules/ping-protection/events/guildMemberAdd.js index 8420f997..1cdb394f 100644 --- a/modules/ping-protection/events/guildMemberAdd.js +++ b/modules/ping-protection/events/guildMemberAdd.js @@ -2,7 +2,7 @@ * Checks when a member rejoins the server and updates their leaver status */ -const { markUserAsRejoined } = require('../ping-protection'); +const {markUserAsRejoined} = require('../ping-protection'); module.exports.run = async function (client, member) { if (!client.botReadyAt) return; diff --git a/modules/ping-protection/events/guildMemberRemove.js b/modules/ping-protection/events/guildMemberRemove.js index 58fa7704..e07fdb3a 100644 --- a/modules/ping-protection/events/guildMemberRemove.js +++ b/modules/ping-protection/events/guildMemberRemove.js @@ -2,7 +2,10 @@ * Checks when a member leaves the server and handles data retention and/or deletion */ -const { markUserAsLeft, deleteAllUserData } = require('../ping-protection'); +const { + markUserAsLeft, + deleteAllUserData +} = require('../ping-protection'); module.exports.run = async function (client, member) { if (!client.botReadyAt) return; diff --git a/modules/ping-protection/events/interactionCreate.js b/modules/ping-protection/events/interactionCreate.js index 042de12a..f6288ae5 100644 --- a/modules/ping-protection/events/interactionCreate.js +++ b/modules/ping-protection/events/interactionCreate.js @@ -1,13 +1,23 @@ -const { ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder, MessageFlags } = require('discord.js'); -const { deleteAllUserData, generateHistoryResponse, generateActionsResponse } = require('../ping-protection'); -const { localize } = require('../../../src/functions/localize'); +const { + ModalBuilder, + TextInputBuilder, + TextInputStyle, + ActionRowBuilder, + MessageFlags +} = require('discord.js'); +const { + deleteAllUserData, + generateHistoryResponse, + generateActionsResponse +} = require('../ping-protection'); +const {localize} = require('../../../src/functions/localize'); // Interaction handler module.exports.run = async function (client, interaction) { if (!client.botReadyAt) return; - + if (interaction.isButton() && interaction.customId.startsWith('ping-protection_')) { - + // Ping history pagination if (interaction.customId.startsWith('ping-protection_hist-page_')) { const parts = interaction.customId.split('_'); @@ -16,14 +26,14 @@ module.exports.run = async function (client, interaction) { const replyOptions = await generateHistoryResponse(client, userId, targetPage); await interaction.update(replyOptions); - return; + return; } if (interaction.customId.startsWith('ping-protection_mod-page_')) { const parts = interaction.customId.split('_'); const userId = parts[2]; const targetPage = parseInt(parts[3]); - + const replyOptions = await generateActionsResponse(client, userId, targetPage); await interaction.update(replyOptions); return; @@ -31,39 +41,37 @@ module.exports.run = async function (client, interaction) { // Panel buttons const [prefix, action, userId] = interaction.customId.split('_'); - - const isAdmin = interaction.member.permissions.has('Administrator') || - (client.config.admins || []).includes(interaction.user.id); + + const isAdmin = interaction.member.permissions.has('Administrator') || + (client.config.admins || []).includes(interaction.user.id); if (['history', 'actions', 'delete'].includes(action)) { - if (!isAdmin) return interaction.reply({ - content: localize('ping-protection', 'no-permission'), - flags: MessageFlags.Ephemeral }); + if (!isAdmin) return interaction.reply({ + content: localize('ping-protection', 'no-permission'), + flags: MessageFlags.Ephemeral + }); } if (action === 'history') { const replyOptions = await generateHistoryResponse(client, userId, 1); - await interaction.reply({ - ...replyOptions, - flags: MessageFlags.Ephemeral + await interaction.reply({ + ...replyOptions, + flags: MessageFlags.Ephemeral }); - } - - else if (action === 'actions') { + } else if (action === 'actions') { const replyOptions = await generateActionsResponse(client, userId, 1); - await interaction.reply({ - ...replyOptions, - flags: MessageFlags.Ephemeral + await interaction.reply({ + ...replyOptions, + flags: MessageFlags.Ephemeral }); - } - else if (action === 'delete') { + } else if (action === 'delete') { const modal = new ModalBuilder() .setCustomId(`ping-protection_confirm-delete_${userId}`) .setTitle(localize('ping-protection', 'modal-title')); const input = new TextInputBuilder() .setCustomId('confirmation_text') - .setLabel(localize('ping-protection', 'modal-label')) + .setLabel(localize('ping-protection', 'modal-label')) .setStyle(TextInputStyle.Paragraph) .setPlaceholder(localize('ping-protection', 'modal-phrase')) .setRequired(true); @@ -78,17 +86,19 @@ module.exports.run = async function (client, interaction) { if (interaction.isModalSubmit() && interaction.customId.startsWith('ping-protection_confirm-delete_')) { const userId = interaction.customId.split('_')[2]; const userInput = interaction.fields.getTextInputValue('confirmation_text'); - const requiredPhrase = localize('ping-protection', 'modal-phrase', { locale: interaction.locale }); + const requiredPhrase = localize('ping-protection', 'modal-phrase', {locale: interaction.locale}); if (userInput === requiredPhrase) { await deleteAllUserData(client, userId); - await interaction.reply({ - content: `✅ ${localize('ping-protection', 'modal-success-data-deletion', {u: userId})}`, - flags: MessageFlags.Ephemeral }); + await interaction.reply({ + content: `✅ ${localize('ping-protection', 'modal-success-data-deletion', {u: userId})}`, + flags: MessageFlags.Ephemeral + }); } else { - await interaction.reply({ - content: `❌ ${localize('ping-protection', 'modal-failed')}`, - flags: MessageFlags.Ephemeral }); + await interaction.reply({ + content: `❌ ${localize('ping-protection', 'modal-failed')}`, + flags: MessageFlags.Ephemeral + }); } } }; \ No newline at end of file diff --git a/modules/ping-protection/events/messageCreate.js b/modules/ping-protection/events/messageCreate.js index e551fb04..6de69cdc 100644 --- a/modules/ping-protection/events/messageCreate.js +++ b/modules/ping-protection/events/messageCreate.js @@ -1,9 +1,9 @@ -const { +const { processPing, sendPingWarning } = require('../ping-protection'); -const { localize } = require('../../../src/functions/localize'); -const { randomElementFromArray } = require('../../../src/functions/helpers'); +const {localize} = require('../../../src/functions/localize'); +const {randomElementFromArray} = require('../../../src/functions/helpers'); // Tracks the last meme for duplicates + counts for grind message const lastMemeMap = new Map(); @@ -32,8 +32,7 @@ module.exports.run = async function (client, message) { mentionedUsers.forEach(user => { if (config.protectedUsers.includes(user.id)) { protectedMentions.add(user.id); - } - else if (config.protectAllUsersWithProtectedRole) { + } else if (config.protectAllUsersWithProtectedRole) { const member = message.mentions.members.get(user.id); if (member && member.roles.cache.some(r => config.protectedRoles.includes(r.id))) { protectedMentions.add(user.id); @@ -45,7 +44,7 @@ module.exports.run = async function (client, message) { // Handles reply pings if (config.allowReplyPings && message.mentions.repliedUser) { const repliedId = message.mentions.repliedUser.id; - + if (protectedMentions.has(repliedId)) { const manualMentionRegex = new RegExp(`<@!?${repliedId}>`); const isManualPing = manualMentionRegex.test(message.content); @@ -60,7 +59,7 @@ module.exports.run = async function (client, message) { const pingedProtectedUser = protectedMentions.size > 0; if (!pingedProtectedRole && !pingedProtectedUser) return; - + let target = null; if (pingedProtectedUser) { const firstId = protectedMentions.values().next().value; @@ -69,48 +68,49 @@ module.exports.run = async function (client, message) { target = message.mentions.roles.find(r => config.protectedRoles.includes(r.id)); } - if (!target) return; + if (!target) return; // Funny easter egg when they ping themselves - if (target.id === message.author.id && config.selfPingConfiguration === "Ignored") return; - if (target.id === message.author.id && config.selfPingConfiguration === "Get fun easter eggs when pinging themselves") { - const secretChance = 0.01; // Secret for a reason.. (1% chance) - const standardMemes = [ - localize('ping-protection', 'meme-why'), - localize('ping-protection', 'meme-played'), - localize('ping-protection', 'meme-spider') - ]; - const secretMeme = localize('ping-protection', 'meme-rick'); - const currentCount = (selfPingCountMap.get(message.author.id) || 0) + 1; - selfPingCountMap.set(message.author.id, currentCount); - - setTimeout(() => { - selfPingCountMap.delete(message.author.id); - }, 300000); - - const roll = Math.random(); - let content = ''; - - if (roll < secretChance) { - content = secretMeme; - lastMemeMap.set(message.author.id, -1); - selfPingCountMap.delete(message.author.id); - } else if (currentCount === 5) { - content = localize('ping-protection', 'meme-grind'); - } else { - const lastIndex = lastMemeMap.get(message.author.id); - - let possibleMemes = standardMemes.map((_, index) => index); - if (lastIndex !== undefined && lastIndex !== -1 && standardMemes.length > 1) { - possibleMemes = possibleMemes.filter(i => i !== lastIndex); - } - - const randomIndex = randomElementFromArray(possibleMemes); - content = standardMemes[randomIndex]; - lastMemeMap.set(message.author.id, randomIndex); + if (target.id === message.author.id && config.selfPingConfiguration === 'Ignored') return; + if (target.id === message.author.id && config.selfPingConfiguration === 'Get fun easter eggs when pinging themselves') { + const secretChance = 0.01; // Secret for a reason.. (1% chance) + const standardMemes = [ + localize('ping-protection', 'meme-why'), + localize('ping-protection', 'meme-played'), + localize('ping-protection', 'meme-spider') + ]; + const secretMeme = localize('ping-protection', 'meme-rick'); + const currentCount = (selfPingCountMap.get(message.author.id) || 0) + 1; + selfPingCountMap.set(message.author.id, currentCount); + + setTimeout(() => { + selfPingCountMap.delete(message.author.id); + }, 300000); + + const roll = Math.random(); + let content = ''; + + if (roll < secretChance) { + content = secretMeme; + lastMemeMap.set(message.author.id, -1); + selfPingCountMap.delete(message.author.id); + } else if (currentCount === 5) { + content = localize('ping-protection', 'meme-grind'); + } else { + const lastIndex = lastMemeMap.get(message.author.id); + + let possibleMemes = standardMemes.map((_, index) => index); + if (lastIndex !== undefined && lastIndex !== -1 && standardMemes.length > 1) { + possibleMemes = possibleMemes.filter(i => i !== lastIndex); } - await message.reply({ content: content }).catch(() => {}); - return; + + const randomIndex = randomElementFromArray(possibleMemes); + content = standardMemes[randomIndex]; + lastMemeMap.set(message.author.id, randomIndex); + } + await message.reply({content: content}).catch(() => { + }); + return; } await sendPingWarning(client, message, target, config); @@ -118,18 +118,20 @@ module.exports.run = async function (client, message) { const isRole = !target.username; let memberToPunish = message.member; if (!memberToPunish) { - try { - memberToPunish = await message.guild.members.fetch(message.author.id); - } catch (e) {return;} + try { + memberToPunish = await message.guild.members.fetch(message.author.id); + } catch (e) { + return; + } } await processPing( - client, - message.author.id, - target.id, - isRole, + client, + message.author.id, + target.id, + isRole, message.url, - message.channel, + message.channel, memberToPunish ); }; \ No newline at end of file diff --git a/modules/ping-protection/models/LeaverData.js b/modules/ping-protection/models/LeaverData.js index 1727dcff..b25e009d 100644 --- a/modules/ping-protection/models/LeaverData.js +++ b/modules/ping-protection/models/LeaverData.js @@ -1,4 +1,7 @@ -const { DataTypes, Model } = require('sequelize'); +const { + DataTypes, + Model +} = require('sequelize'); module.exports = class PingProtectionLeaverData extends Model { static init(sequelize) { diff --git a/modules/ping-protection/models/ModerationLog.js b/modules/ping-protection/models/ModerationLog.js index c90099f8..28691b04 100644 --- a/modules/ping-protection/models/ModerationLog.js +++ b/modules/ping-protection/models/ModerationLog.js @@ -1,9 +1,12 @@ -const { DataTypes, Model } = require('sequelize'); +const { + DataTypes, + Model +} = require('sequelize'); module.exports = class PingProtectionModerationLog extends Model { static init(sequelize) { return super.init({ - id: { + id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true, @@ -17,7 +20,7 @@ module.exports = class PingProtectionModerationLog extends Model { type: DataTypes.STRING, allowNull: false }, - reason: { + reason: { type: DataTypes.STRING, allowNull: true }, diff --git a/modules/ping-protection/models/PingHistory.js b/modules/ping-protection/models/PingHistory.js index 268418a8..709e26e1 100644 --- a/modules/ping-protection/models/PingHistory.js +++ b/modules/ping-protection/models/PingHistory.js @@ -1,4 +1,7 @@ -const { DataTypes, Model } = require('sequelize'); +const { + DataTypes, + Model +} = require('sequelize'); module.exports = class PingProtectionPingHistory extends Model { static init(sequelize) { diff --git a/modules/ping-protection/module.json b/modules/ping-protection/module.json index b945a1c7..f813f948 100644 --- a/modules/ping-protection/module.json +++ b/modules/ping-protection/module.json @@ -17,12 +17,7 @@ "tags": [ "moderation" ], - "humanReadableName": { - "en": "Ping-Protection", - "de": "Ping-Schutz" - }, - "description": { - "en": "Powerful and highly customizable ping-protection module to protect members/roles from unwanted mentions with moderation capabilities.", - "de": "Leistungsstarkes und hochgradig anpassbares Ping-Schutz-Modul zum Schutz von Mitgliedern/Rollen vor unerwünschten Erwähnungen mit Moderationsfunktionen." - } -} \ No newline at end of file + "fa-icon": "fa-duotone fa-clock-alarm", + "humanReadableName": "Ping-Protection", + "description": "Powerful and highly customizable ping-protection module to protect members/roles from unwanted mentions with moderation capabilities." +} diff --git a/modules/ping-protection/ping-protection.js b/modules/ping-protection/ping-protection.js index 012143dd..b0adb378 100644 --- a/modules/ping-protection/ping-protection.js +++ b/modules/ping-protection/ping-protection.js @@ -3,10 +3,20 @@ * @module ping-protection * @author itskevinnn */ -const { Op } = require('sequelize'); -const { ActionRowBuilder, ButtonBuilder, EmbedBuilder, ButtonStyle } = require('discord.js'); -const { embedType, embedTypeV2, formatDate } = require('../../src/functions/helpers'); -const { localize } = require('../../src/functions/localize'); +const {Op} = require('sequelize'); +const { + ActionRowBuilder, + ButtonBuilder, + EmbedBuilder, + ButtonStyle +} = require('discord.js'); +const { + embedType, + embedTypeV2, + formatDate, + safeSetFooter +} = require('../../src/functions/helpers'); +const {localize} = require('../../src/functions/localize'); const recentPings = new Set(); @@ -26,7 +36,7 @@ async function addPing(client, userId, messageUrl, targetId, isRole) { where: { userId: userId, targetId: targetId, - createdAt: { [Op.gt]: new Date(Date.now() - duplicateWindow) } + createdAt: {[Op.gt]: new Date(Date.now() - duplicateWindow)} } }); @@ -46,35 +56,53 @@ async function getPingCountInWindow(client, userId, days) { return await client.models['ping-protection']['PingHistory'].count({ where: { userId: userId, - createdAt: { [Op.gt]: cutoffDate } + createdAt: {[Op.gt]: cutoffDate} } }); } // Fetches ping history -async function fetchPingHistory(client, userId, page = 1, limit = 8) { +async function fetchPingHistory(client, userId, page = 1, limit = 8) { const offset = (page - 1) * limit; - const { count, rows } = await client.models['ping-protection']['PingHistory'].findAndCountAll({ - where: { userId: userId }, - order: [['createdAt', 'DESC']], + const { + count, + rows + } = await client.models['ping-protection']['PingHistory'].findAndCountAll({ + where: {userId: userId}, + order: [['createdAt', 'DESC']], limit: limit, offset: offset }); - return { total: count, history: rows }; + return { + total: count, + history: rows + }; } // Fetches moderation history async function fetchModHistory(client, userId, page = 1, limit = 8) { - if (!client.models['ping-protection'] || !client.models['ping-protection']['ModerationLog']) return { total: 0, history: [] }; + if (!client.models['ping-protection'] || !client.models['ping-protection']['ModerationLog']) return { + total: 0, + history: [] + }; try { const offset = (page - 1) * limit; - const { count, rows } = await client.models['ping-protection']['ModerationLog'].findAndCountAll({ - where: { victimID: userId }, + const { + count, + rows + } = await client.models['ping-protection']['ModerationLog'].findAndCountAll({ + where: {victimID: userId}, order: [['createdAt', 'DESC']], limit: limit, offset: offset }); - return { total: count, history: rows }; + return { + total: count, + history: rows + }; } catch (e) { - return { total: 0, history: [] }; + return { + total: 0, + history: [] + }; } } // Gets leaver status @@ -100,7 +128,7 @@ async function sendPingWarning(client, message, target, moduleConfig) { const warningMsg = moduleConfig.pingWarningMessage; if (!warningMsg) return; - let warnMsg = { ...warningMsg }; + let warnMsg = {...warningMsg}; const placeholders = { '%target-name%': target.name || target.tag || target.username || 'Unknown', '%target-mention%': target.toString(), @@ -111,7 +139,8 @@ async function sendPingWarning(client, message, target, moduleConfig) { try { let messageOptions = await embedTypeV2(warnMsg, placeholders); return message.reply(messageOptions).catch(async () => { - return message.channel.send(messageOptions).catch(() => {}); + return message.channel.send(messageOptions).catch(() => { + }); }); } catch (error) { client.logger.warn(`[Ping Protection] ${error.message}`); @@ -121,7 +150,7 @@ async function sendPingWarning(client, message, target, moduleConfig) { // Syncs the native AutoMod rule based on configuration async function syncNativeAutoMod(client) { const config = client.configurations['ping-protection']['configuration']; - + try { const guild = await client.guilds.fetch(client.guildID); const rules = await guild.autoModerationRules.fetch(); @@ -130,7 +159,8 @@ async function syncNativeAutoMod(client) { // Logic to disable/delete the rule if (!config || !config.enableAutomod) { if (existingRule) { - await existingRule.delete().catch(() => {}); + await existingRule.delete().catch(() => { + }); } return; } @@ -144,13 +174,13 @@ async function syncNativeAutoMod(client) { const protectedIdsSet = new Set(config.protectedUsers || []); if (config.protectAllUsersWithProtectedRole && config.protectedRoles && config.protectedRoles.length > 0) { - guild.members.cache.forEach(member => { + guild.members.cache.forEach(member => { if (member.roles.cache.some(r => config.protectedRoles.includes(r.id))) { protectedIdsSet.add(member.id); } }); } - + protectedIdsSet.forEach(id => { keywords.push(`<@${id}>`); keywords.push(`<@!${id}>`); @@ -158,36 +188,40 @@ async function syncNativeAutoMod(client) { if (keywords.length === 0) { if (existingRule) { - await existingRule.delete().catch(() => {}); + await existingRule.delete().catch(() => { + }); } return; } if (keywords.length > 1000) { client.logger.warn(localize('ping-protection', 'log-automod-keyword-limit')); - keywords.splice(1000); + keywords.splice(1000); } - + // AutoMod rule data const actions = []; const blockMetadata = {}; if (config.autoModBlockMessage) { blockMetadata.customMessage = config.autoModBlockMessage; } - actions.push({ type: 1, metadata: blockMetadata }); + actions.push({ + type: 1, + metadata: blockMetadata + }); const alertChannelId = getSafeChannelId(config.autoModLogChannel); if (alertChannelId) { actions.push({ - type: 2, - metadata: { channel: alertChannelId } + type: 2, + metadata: {channel: alertChannelId} }); } const ruleData = { name: 'Ping Protection System', - eventType: 1, - triggerType: 1, + eventType: 1, + triggerType: 1, triggerMetadata: { keywordFilter: keywords }, @@ -222,20 +256,20 @@ async function generateHistoryResponse(client, userId, page = 1) { totalPages = Math.ceil(total / limit) || 1; } - const user = await client.users.fetch(userId).catch(() => ({ - username: 'Unknown User', - displayAvatarURL: () => null + const user = await client.users.fetch(userId).catch(() => ({ + username: 'Unknown User', + displayAvatarURL: () => null })); - + const leaverData = await getLeaverStatus(client, userId); - let description = ""; - + let description = ''; + if (leaverData) { const dateStr = formatDate(leaverData.leftAt); - const warningKey = history.length > 0 - ? 'leaver-warning-long' - : 'leaver-warning-short'; - description += `⚠️ ${localize('ping-protection', warningKey, { d: dateStr })}\n\n`; + const warningKey = history.length > 0 + ? 'leaver-warning-long' + : 'leaver-warning-short'; + description += `⚠️ ${localize('ping-protection', warningKey, {d: dateStr})}\n\n`; } if (!isEnabled) { @@ -245,15 +279,15 @@ async function generateHistoryResponse(client, userId, page = 1) { } else { const lines = history.map((entry, index) => { const timeString = formatDate(entry.createdAt); - - let targetString = "Detected"; + + let targetString = 'Detected'; if (entry.targetId) { targetString = entry.isRole ? `<@&${entry.targetId}>` : `<@${entry.targetId}>`; } const hasValidLink = entry.messageUrl && entry.messageUrl !== 'Blocked by AutoMod'; const linkText = hasValidLink - ? `[${localize('ping-protection', 'label-jump')}](${entry.messageUrl})` + ? `[${localize('ping-protection', 'label-jump')}](${entry.messageUrl})` : localize('ping-protection', 'no-message-link'); return localize('ping-protection', 'list-entry-text', { @@ -285,23 +319,21 @@ async function generateHistoryResponse(client, userId, page = 1) { ); const embed = new EmbedBuilder() - .setTitle(localize('ping-protection', 'embed-history-title', { - u: user.username + .setTitle(localize('ping-protection', 'embed-history-title', { + u: user.username })) - .setThumbnail(user.displayAvatarURL({ - dynamic: true + .setThumbnail(user.displayAvatarURL({ + dynamic: true })) .setDescription(description) - .setColor('Orange') - .setFooter({ - text: client.strings.footer, - iconURL: client.strings.footerImgUrl - }); + .setColor('Orange'); + + safeSetFooter(embed, client); if (!client.strings.disableFooterTimestamp) embed.setTimestamp(); - return { - embeds: [embed.toJSON()], - components: [row.toJSON()] + return { + embeds: [embed.toJSON()], + components: [row.toJSON()] }; } @@ -318,12 +350,12 @@ async function generateActionsResponse(client, userId, page = 1) { history = data.history; totalPages = Math.ceil(total / limit) || 1; - const user = await client.users.fetch(userId).catch(() => ({ - username: 'Unknown User', - displayAvatarURL: () => null + const user = await client.users.fetch(userId).catch(() => ({ + username: 'Unknown User', + displayAvatarURL: () => null })); - - let description = ""; + + let description = ''; if (history.length === 0) { description += localize('ping-protection', 'no-data-found'); @@ -355,55 +387,53 @@ async function generateActionsResponse(client, userId, page = 1) { ); const embed = new EmbedBuilder() - .setTitle(localize('ping-protection', 'embed-actions-title', { - u: user.username + .setTitle(localize('ping-protection', 'embed-actions-title', { + u: user.username })) - .setThumbnail(user.displayAvatarURL({ - dynamic: true + .setThumbnail(user.displayAvatarURL({ + dynamic: true })) .setDescription(description) - .setColor(isEnabled - ? 'Red' + .setColor(isEnabled + ? 'Red' : 'Grey' - ) - .setFooter({ - text: client.strings.footer, - iconURL: client.strings.footerImgUrl - }); + ); + + safeSetFooter(embed, client); if (!client.strings.disableFooterTimestamp) embed.setTimestamp(); - return { - embeds: [embed.toJSON()], - components: [row.toJSON()] + return { + embeds: [embed.toJSON()], + components: [row.toJSON()] }; } // Handles data deletion async function deleteAllUserData(client, userId) { - await client.models['ping-protection']['PingHistory'].destroy({ - where: { userId: userId } + await client.models['ping-protection']['PingHistory'].destroy({ + where: {userId: userId} }); - await client.models['ping-protection']['ModerationLog'].destroy({ - where: { victimID: userId } + await client.models['ping-protection']['ModerationLog'].destroy({ + where: {victimID: userId} }); - await client.models['ping-protection']['LeaverData'].destroy({ - where: { userId: userId } + await client.models['ping-protection']['LeaverData'].destroy({ + where: {userId: userId} }); - client.logger.info(localize('ping-protection', 'log-data-deletion', { - u: userId + client.logger.info(localize('ping-protection', 'log-data-deletion', { + u: userId })); } async function markUserAsLeft(client, userId) { - await client.models['ping-protection']['LeaverData'].upsert({ - userId: userId, - leftAt: new Date() + await client.models['ping-protection']['LeaverData'].upsert({ + userId: userId, + leftAt: new Date() }); } async function markUserAsRejoined(client, userId) { - await client.models['ping-protection']['LeaverData'].destroy({ - where: { userId: userId } + await client.models['ping-protection']['LeaverData'].destroy({ + where: {userId: userId} }); } @@ -419,8 +449,8 @@ async function enforceRetention(client) { if (storageConfig.DeleteAllPingHistoryAfterTimeframe) { const usersWithExpiredData = await client.models['ping-protection']['PingHistory'].findAll({ - where: { - createdAt: { [Op.lt]: historyCutoff } + where: { + createdAt: {[Op.lt]: historyCutoff} }, attributes: ['userId'], group: ['userId'] @@ -429,32 +459,31 @@ async function enforceRetention(client) { const userIdsToWipe = usersWithExpiredData.map(entry => entry.userId); if (userIdsToWipe.length > 0) { await client.models['ping-protection']['PingHistory'].destroy({ - where: { userId: userIdsToWipe } + where: {userId: userIdsToWipe} }); } - } - else { - await client.models['ping-protection']['PingHistory'].destroy({ - where: { createdAt: { [Op.lt]: historyCutoff } } + } else { + await client.models['ping-protection']['PingHistory'].destroy({ + where: {createdAt: {[Op.lt]: historyCutoff}} }); } } if (storageConfig.modLogRetention) { const modCutoff = new Date(); modCutoff.setMonth(modCutoff.getMonth() - (storageConfig.modLogRetention || 12)); - await client.models['ping-protection']['ModerationLog'].destroy({ - where: { - createdAt: { [Op.lt]: modCutoff } - } + await client.models['ping-protection']['ModerationLog'].destroy({ + where: { + createdAt: {[Op.lt]: modCutoff} + } }); } if (storageConfig.enableLeaverDataRetention) { const leaverCutoff = new Date(); leaverCutoff.setDate(leaverCutoff.getDate() - (storageConfig.leaverRetention || 1)); - const leaversToDelete = await client.models['ping-protection']['LeaverData'].findAll({ - where: { - leftAt: { [Op.lt]: leaverCutoff } - } + const leaversToDelete = await client.models['ping-protection']['LeaverData'].findAll({ + where: { + leftAt: {[Op.lt]: leaverCutoff} + } }); for (const leaver of leaversToDelete) { await deleteAllUserData(client, leaver.userId); @@ -465,15 +494,15 @@ async function enforceRetention(client) { // Executes moderation action async function executeAction(client, member, rule, reason, storageConfig, originChannel = null, stats = {}) { - const actionType = rule.actionType; - + const actionType = rule.actionType; + // Sends action log if enabled const sendActionLog = async () => { if (!rule.enableActionLogging || !originChannel) return; const logMsgConfig = rule.actionLogMessage; if (!logMsgConfig) return; - let safeMsg = { ...logMsgConfig }; + let safeMsg = {...logMsgConfig}; const placeholders = { '%pinger-mention%': member.toString(), @@ -486,10 +515,11 @@ async function executeAction(client, member, rule, reason, storageConfig, origin try { let messageOptions = await embedTypeV2(safeMsg, placeholders); - await originChannel.send(messageOptions).catch(() => {}); + await originChannel.send(messageOptions).catch(() => { + }); } catch (error) { - client.logger.warn(localize('ping-protection', 'log-action-log-failed', { - e: error.message + client.logger.warn(localize('ping-protection', 'log-action-log-failed', { + e: error.message })); } }; @@ -497,29 +527,28 @@ async function executeAction(client, member, rule, reason, storageConfig, origin // Sends error message if action fails const sendErrorLog = async (error) => { if (!originChannel) return; - - const errorEmbed = new EmbedBuilder() - .setTitle(localize('ping-protection', 'punish-log-failed-title', { - u: member.user.tag - })) - .setDescription( - localize('ping-protection', 'punish-log-failed-desc', { - m: member.toString() - }) + - `\n${localize('ping-protection', 'punish-log-error', { - e: error.message - })}` - ) - .setColor("#ed4245") - .setFooter({ - text: client.strings.footer, - iconURL: client.strings.footerImgUrl - }); - if (!client.strings.disableFooterTimestamp) errorEmbed.setTimestamp(); - await originChannel.send({ embeds: [errorEmbed.toJSON()] }).catch(() => {}); + const errorEmbed = new EmbedBuilder() + .setTitle(localize('ping-protection', 'punish-log-failed-title', { + u: member.user.tag + })) + .setDescription( + localize('ping-protection', 'punish-log-failed-desc', { + m: member.toString() + }) + + `\n${localize('ping-protection', 'punish-log-error', { + e: error.message + })}` + ) + .setColor('#ed4245'); + + safeSetFooter(errorEmbed, client); + if (!client.strings.disableFooterTimestamp) errorEmbed.setTimestamp(); + + await originChannel.send({embeds: [errorEmbed.toJSON()]}).catch(() => { + }); }; - + if (!member) { client.logger.debug(localize('ping-protection', 'log-not-a-member')); return false; @@ -527,10 +556,10 @@ async function executeAction(client, member, rule, reason, storageConfig, origin const botMember = await member.guild.members.fetch(client.user.id); if (botMember.roles.highest.position <= member.roles.highest.position) { - await sendErrorLog({ - message: localize('ping-protection', 'punish-role-error', { - tag: member.user.tag - }) + await sendErrorLog({ + message: localize('ping-protection', 'punish-role-error', { + tag: member.user.tag + }) }); client.logger.warn(localize('ping-protection', 'log-punish-role-error', { tag: member.user.tag @@ -543,39 +572,39 @@ async function executeAction(client, member, rule, reason, storageConfig, origin await client.models['ping-protection']['ModerationLog'].create({ victimID: member.id, type, actionDuration: duration, reason }); - } catch (dbError) {} + } catch (dbError) { + } }; if (actionType === 'MUTE') { const durationMs = rule.muteDuration * 60000; await logDb('MUTE', rule.muteDuration); - try { - await member.timeout(durationMs, reason); + try { + await member.timeout(durationMs, reason); await sendActionLog(); - return true; - } catch (error) { + return true; + } catch (error) { await sendErrorLog(error); client.logger.warn(localize('ping-protection', 'log-mute-error', { - tag: member.user.tag, + tag: member.user.tag, e: error.message })); - return false; + return false; } - } - else if (actionType === 'KICK') { + } else if (actionType === 'KICK') { await logDb('KICK'); - try { - await member.kick(reason); + try { + await member.kick(reason); await sendActionLog(); - return true; - } catch (error) { + return true; + } catch (error) { await sendErrorLog(error); client.logger.warn(localize('ping-protection', 'log-kick-error', { - tag: member.user.tag, + tag: member.user.tag, e: error.message })); - return false; + return false; } } return false; @@ -590,7 +619,8 @@ async function processPing(client, userId, targetId, isRole, messageUrl, originC if (storageConfig?.enablePingHistory) { try { await addPing(client, userId, messageUrl, targetId, isRole); - } catch (e) {} + } catch (e) { + } } if (!moderationRules || !Array.isArray(moderationRules) || moderationRules.length === 0) return; @@ -599,16 +629,16 @@ async function processPing(client, userId, targetId, isRole, messageUrl, originC const rule = moderationRules[i]; const retentionWeeks = storageConfig?.pingHistoryRetention || 12; - const timeframeDays = rule.useCustomTimeframe - ? (rule.timeframeDays || 7) - : (retentionWeeks * 7); + const timeframeDays = rule.useCustomTimeframe + ? (rule.timeframeDays || 7) + : (retentionWeeks * 7); const pingCount = await getPingCountInWindow(client, userId, timeframeDays); const requiredCount = rule.pingsCount ?? rule.pingsCountAdvanced ?? rule.pingsCountBasic; - + // Skip this rule if no valid threshold is configured if (typeof requiredCount !== 'number' || !Number.isFinite(requiredCount)) { continue; @@ -618,21 +648,24 @@ async function processPing(client, userId, targetId, isRole, messageUrl, originC const oneMinuteAgo = new Date(Date.now() - 60000); try { const recentLog = await client.models['ping-protection']['ModerationLog'].findOne({ - where: { - victimID: userId, - createdAt: { [Op.gt]: oneMinuteAgo } + where: { + victimID: userId, + createdAt: {[Op.gt]: oneMinuteAgo} } }); if (recentLog) break; - } catch (e) {} + } catch (e) { + } - const generatedReason = rule.useCustomTimeframe - ? localize('ping-protection', 'reason-advanced', { - c: pingCount, - d: timeframeDays }) - : localize('ping-protection', 'reason-basic', { - c: pingCount, - w: retentionWeeks }); + const generatedReason = rule.useCustomTimeframe + ? localize('ping-protection', 'reason-advanced', { + c: pingCount, + d: timeframeDays + }) + : localize('ping-protection', 'reason-basic', { + c: pingCount, + w: retentionWeeks + }); if (memberToPunish) { const success = await executeAction( @@ -642,9 +675,12 @@ async function processPing(client, userId, targetId, isRole, messageUrl, originC generatedReason, storageConfig, originChannel, - { pingCount, timeframeDays } + { + pingCount, + timeframeDays + } ); - + if (success) break; } } diff --git a/modules/polls/configs/config.json b/modules/polls/configs/config.json index 63b26001..b8113d80 100644 --- a/modules/polls/configs/config.json +++ b/modules/polls/configs/config.json @@ -1,12 +1,6 @@ { - "description": { - "en": "Configure the function of the module here", - "de": "Stelle hier die Funktionen des Modules ein" - }, - "humanName": { - "en": "Configuration", - "de": "Konfiguration" - }, + "description": "Configure the function of the module here", + "humanName": "Configuration", "filename": "config.json", "commandsWarnings": { "normal": [ @@ -16,38 +10,19 @@ "content": [ { "name": "reactions", - "humanName": { - "de": "Emojis", - "en": "Emojis" - }, + "humanName": "Emojis", "default": { - "en": { - "1": "1️⃣", - "2": "2️⃣", - "3": "3️⃣", - "4": "4️⃣", - "5": "5️⃣", - "6": "6️⃣", - "7": "7️⃣", - "8": "8️⃣", - "9": "9️⃣" - }, - "de": { - "1": "1️⃣", - "2": "2️⃣", - "3": "3️⃣", - "4": "4️⃣", - "5": "5️⃣", - "6": "6️⃣", - "7": "7️⃣", - "8": "8️⃣", - "9": "9️⃣" - } - }, - "description": { - "en": "You can set the different emojis to use", - "de": "Du kannst die verschiedenen Emojis, die benutzt werden sollen, einstellen" + "1": "1️⃣", + "2": "2️⃣", + "3": "3️⃣", + "4": "4️⃣", + "5": "5️⃣", + "6": "6️⃣", + "7": "7️⃣", + "8": "8️⃣", + "9": "9️⃣" }, + "description": "You can set the different emojis to use", "type": "keyed", "content": { "key": "string", diff --git a/modules/polls/configs/strings.json b/modules/polls/configs/strings.json index ad3d920e..37d73e69 100644 --- a/modules/polls/configs/strings.json +++ b/modules/polls/configs/strings.json @@ -1,47 +1,23 @@ { - "description": { - "en": "Edit the messages and strings of the module here", - "de": "Stelle hier die Nachrichten des Modules ein" - }, - "humanName": { - "en": "Messages", - "de": "Nachrichten" - }, + "description": "Edit the messages and strings of the module here", + "humanName": "Messages", "filename": "strings.json", "content": [ { "name": "embed", - "humanName": { - "de": "Embed" - }, + "humanName": "Embed", "default": { - "en": { - "title": "New Poll", - "color": "BLUE", - "options": "Today's options", - "liveView": "Live-Views of the results", - "expiresOn": "End of this poll", - "thisPollExpiresOn": "This poll expires on %date%.", - "endedPollTitle": "Poll ended", - "visibility": "Visibility of votes", - "endedPollColor": "RED" - }, - "de": { - "title": "Neue Umfrage", - "color": "BLUE", - "options": "Heutige Auswahlmöglichkeiten", - "liveView": "Live-Anzeige der Ergebnisse", - "expiresOn": "Ende dieser Umfrage", - "visibility": "Sichtbarkeit der Stimmen", - "thisPollExpiresOn": "Diese Umfrage endet am %date%.", - "endedPollTitle": "Umfrage beendet", - "endedPollColor": "RED" - } - }, - "description": { - "en": "You can edit the settings of your embed here", - "de": "Du kannst die Einstellungen des Embeds hier bearbeiten" + "title": "New Poll", + "color": "BLUE", + "options": "Today's options", + "liveView": "Live-Views of the results", + "expiresOn": "End of this poll", + "thisPollExpiresOn": "This poll expires on %date%.", + "endedPollTitle": "Poll ended", + "visibility": "Visibility of votes", + "endedPollColor": "RED" }, + "description": "You can edit the settings of your embed here", "type": "keyed", "content": { "key": "string", @@ -50,4 +26,4 @@ "disableKeyEdits": true } ] -} \ No newline at end of file +} diff --git a/modules/polls/module.json b/modules/polls/module.json index 9c55497d..40e924e6 100644 --- a/modules/polls/module.json +++ b/modules/polls/module.json @@ -5,13 +5,11 @@ "name": "SCDerox (SC Network Team)", "link": "https://github.com/SCDerox" }, - "description": { - "en": "Simple module to create fresh polls on your server! Supports anonymous polls and more.", - "de": "Einfaches Modul, um coole Umfragen auf deinem Server zu erstellen! Unterstützt anonyme Umfragen und mehr." - }, + "description": "Simple module to create fresh polls on your server! Supports anonymous polls and more.", "events-dir": "/events", "commands-dir": "/commands", "models-dir": "/models", + "fa-icon": "fas fa-poll", "config-example-files": [ "configs/config.json", "configs/strings.json" @@ -20,8 +18,5 @@ "community" ], "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/polls", - "humanReadableName": { - "en": "Polls", - "de": "Umfragen" - } -} \ No newline at end of file + "humanReadableName": "Polls" +} diff --git a/modules/quiz/commands/quiz.js b/modules/quiz/commands/quiz.js index bc3a32fd..54773b69 100644 --- a/modules/quiz/commands/quiz.js +++ b/modules/quiz/commands/quiz.js @@ -3,7 +3,8 @@ const durationParser = require('parse-duration'); const { formatDate, shuffleArray, - parseEmbedColor + parseEmbedColor, + safeSetFooter } = require('../../../src/functions/helpers'); const {localize} = require('../../../src/functions/localize'); const {createQuiz} = require('../quizUtil'); @@ -158,10 +159,11 @@ module.exports.subcommands = { const embed = new MessageEmbed() .setTitle(moduleStrings.embed.leaderboardTitle) .setColor(parseEmbedColor(moduleStrings.embed.leaderboardColor)) - .setFooter({text: interaction.client.strings.footer, iconURL: interaction.client.strings.footerImgUrl}) .setThumbnail(interaction.guild.iconURL()) .addField(moduleStrings.embed.leaderboardSubtitle, leaderboardString); + safeSetFooter(embed, interaction.client); + if (!interaction.client.strings.disableFooterTimestamp) embed.setTimestamp(); const components = [{ diff --git a/modules/quiz/configs/config.json b/modules/quiz/configs/config.json index 3821745c..df755612 100644 --- a/modules/quiz/configs/config.json +++ b/modules/quiz/configs/config.json @@ -1,12 +1,6 @@ { - "description": { - "en": "Configure the function of the module here", - "de": "Stelle hier die Funktionen des Modules ein" - }, - "humanName": { - "en": "Configuration", - "de": "Konfiguration" - }, + "description": "Configure the function of the module here", + "humanName": "Configuration", "filename": "config.json", "commandsWarnings": { "normal": [ @@ -16,41 +10,21 @@ "content": [ { "name": "emojis", - "humanName": { - "de": "Emojis" - }, + "humanName": "Emojis", "default": { - "en": { - "1": "1️⃣", - "2": "2️⃣", - "3": "3️⃣", - "4": "4️⃣", - "5": "5️⃣", - "6": "6️⃣", - "7": "7️⃣", - "8": "8️⃣", - "9": "9️⃣", - "true": "✅", - "false": "❌" - }, - "de": { - "1": "1️⃣", - "2": "2️⃣", - "3": "3️⃣", - "4": "4️⃣", - "5": "5️⃣", - "6": "6️⃣", - "7": "7️⃣", - "8": "8️⃣", - "9": "9️⃣", - "true": "✅", - "false": "❌" - } - }, - "description": { - "en": "You can set the emojis to use", - "de": "Du kannst die verschiedenen Emojis, die benutzt werden sollen, einstellen" - }, + "1": "1️⃣", + "2": "2️⃣", + "3": "3️⃣", + "4": "4️⃣", + "5": "5️⃣", + "6": "6️⃣", + "7": "7️⃣", + "8": "8️⃣", + "9": "9️⃣", + "true": "✅", + "false": "❌" + }, + "description": "You can set the emojis to use", "type": "keyed", "content": { "key": "string", @@ -60,32 +34,16 @@ }, { "name": "dailyQuizLimit", - "humanName": { - "en": "Daily quiz limit", - "de": "Tägliches Quizlimit" - }, - "default": { - "en": 5 - }, - "description": { - "en": "How many quizzes can be played per day using /quiz play", - "de": "Wie viele Quiz pro Tag mit /quiz play gespielt werden können" - }, + "humanName": "Daily quiz limit", + "default": 5, + "description": "How many quizzes can be played per day using /quiz play", "type": "integer" }, { "name": "leaderboardChannel", - "humanName": { - "en": "Quiz leaderboard channel", - "de": "Quiz-Leaderboard-Kanal" - }, - "default": { - "en": "" - }, - "description": { - "en": "In which channel the quiz leaderboard is displayed", - "de": "In welchem Kanal das Quiz-Leaderboard angezeigt wird" - }, + "humanName": "Quiz leaderboard channel", + "default": "", + "description": "In which channel the quiz leaderboard is displayed", "type": "channelID", "content": [ "GUILD_TEXT", @@ -95,32 +53,16 @@ }, { "name": "createAllowedRole", - "humanName": { - "en": "Role needed to create quizzes", - "de": "Rolle zum Erstellen von Quiz" - }, - "default": { - "en": "" - }, - "description": { - "en": "Which role a user needs to have to be able to create quizzes with /quiz create/create-bool", - "de": "Welche Rolle ein Nutzer haben muss, um Quiz mit /quiz create/create-bool erstellen zu können" - }, + "humanName": "Role needed to create quizzes", + "default": "", + "description": "Which role a user needs to have to be able to create quizzes with /quiz create/create-bool", "type": "roleID" }, { "name": "mode", - "humanName": { - "en": "Mode for quiz selection", - "de": "Modus zur Quizauswahl" - }, - "default": { - "en": "Random" - }, - "description": { - "en": "How a /quiz play quiz is selected for users", - "de": "Wie ein /quiz-play-Quiz für Nutzer ausgewählt wird" - }, + "humanName": "Mode for quiz selection", + "default": "Random", + "description": "How a /quiz play quiz is selected for users", "type": "select", "content": [ "Random", @@ -129,17 +71,9 @@ }, { "name": "livePreview", - "humanName": { - "en": "Live preview of results", - "de": "Live-Vorschau der Ergebnisse" - }, - "default": { - "en": false - }, - "description": { - "en": "Whether the live preview of results is enabled", - "de": "Ob die Live-Vorschau der Ergebnisse aktiviert ist" - }, + "humanName": "Live preview of results", + "default": false, + "description": "Whether the live preview of results is enabled", "type": "boolean" } ] diff --git a/modules/quiz/configs/quizList.json b/modules/quiz/configs/quizList.json index 5b380bd1..f99d71a6 100644 --- a/modules/quiz/configs/quizList.json +++ b/modules/quiz/configs/quizList.json @@ -1,74 +1,36 @@ { - "description": { - "en": "Create and edit the quizzes of the server", - "de": "Erstelle und bearbeite hier die Quiz des Servers" - }, - "humanName": { - "en": "Edit quiz", - "de": "Quiz bearbeiten" - }, + "description": "Create and edit the quizzes of the server", + "humanName": "Edit quiz", "configElements": true, "filename": "quizList.json", "content": [ { "name": "description", - "humanName": { - "en": "Question or statement", - "de": "Frage oder Behauptung" - }, - "default": { - "en": "" - }, - "description": { - "en": "Title/Question of the quiz", - "de": "Titel/Frage des Quiz" - }, + "humanName": "Question or statement", + "default": "", + "description": "Title/Question of the quiz", "type": "string" }, { "name": "duration", - "humanName": { - "en": "Time limit", - "de": "Zeitlimit" - }, - "default": { - "en": "1m" - }, - "description": { - "en": "How much time the user has to answer", - "de": "Wie viel Zeit der Nutzer zum Beantworten hat" - }, + "humanName": "Time limit", + "default": "1m", + "description": "How much time the user has to answer", "type": "string" }, { "name": "correctOptions", - "humanName": { - "en": "Correct answers", - "de": "Richtige Antworten" - }, - "default": { - "en": [] - }, - "description": { - "en": "Correct answers", - "de": "Richtige Antworten" - }, + "humanName": "Correct answers", + "default": [], + "description": "Correct answers", "type": "array", "content": "string" }, { "name": "wrongOptions", - "humanName": { - "en": "Wrong answers", - "de": "Falsche Antworten" - }, - "default": { - "en": [] - }, - "description": { - "en": "Wrong answers", - "de": "Falsche Antworten" - }, + "humanName": "Wrong answers", + "default": [], + "description": "Wrong answers", "type": "array", "content": "string" } diff --git a/modules/quiz/configs/strings.json b/modules/quiz/configs/strings.json index 4d5cc913..1bdc523e 100644 --- a/modules/quiz/configs/strings.json +++ b/modules/quiz/configs/strings.json @@ -1,53 +1,26 @@ { - "description": { - "en": "Edit the messages and strings of the module here", - "de": "Stelle hier die Nachrichten des Modules ein" - }, - "humanName": { - "en": "Messages", - "de": "Nachrichten" - }, + "description": "Edit the messages and strings of the module here", + "humanName": "Messages", "filename": "strings.json", "content": [ { "name": "embed", - "humanName": { - "de": "Embed" - }, + "humanName": "Embed", "default": { - "en": { - "title": "New quiz - What's right?", - "color": "BLUE", - "options": "Today's options", - "liveView": "Live view of the results", - "expiresOn": "End of this quiz", - "thisQuizExpiresOn": "This quiz expires on %date%.", - "endedQuizTitle": "Quiz ended", - "endedQuizColor": "RED", - "leaderboardTitle": "The best quiz players", - "leaderboardSubtitle": "Quiz leaderboard", - "leaderboardColor": "GREEN", - "leaderboardButton": "View my ranking" - }, - "de": { - "title": "Neues Quiz - Was ist richtig?", - "color": "BLUE", - "options": "Mögliche antworten", - "liveView": "Live-Anzeige der Ergebnisse", - "expiresOn": "Ende dieses Quiz", - "thisQuizExpiresOn": "Dieses Quiz endet am %date%.", - "endedQuizTitle": "Quiz beendet", - "endedQuizColor": "RED", - "leaderboardTitle": "Die besten Quiz-Spieler", - "leaderboardSubtitle": "Quiz-Rangliste", - "leaderboardColor": "GREEN", - "leaderboardButton": "Meine Platzierung ansehen" - } - }, - "description": { - "en": "You can edit the settings of your embed here", - "de": "Du kannst die Einstellungen des Embeds hier bearbeiten" + "title": "New quiz - What's right?", + "color": "BLUE", + "options": "Today's options", + "liveView": "Live view of the results", + "expiresOn": "End of this quiz", + "thisQuizExpiresOn": "This quiz expires on %date%.", + "endedQuizTitle": "Quiz ended", + "endedQuizColor": "RED", + "leaderboardTitle": "The best quiz players", + "leaderboardSubtitle": "Quiz leaderboard", + "leaderboardColor": "GREEN", + "leaderboardButton": "View my ranking" }, + "description": "You can edit the settings of your embed here", "type": "keyed", "content": { "key": "string", @@ -56,4 +29,4 @@ "disableKeyEdits": true } ] -} \ No newline at end of file +} diff --git a/modules/quiz/module.json b/modules/quiz/module.json index 1951b9c9..2bf2a817 100644 --- a/modules/quiz/module.json +++ b/modules/quiz/module.json @@ -1,18 +1,13 @@ { "name": "quiz", - "humanReadableName": { - "en": "Quiz Module", - "de": "Quiz-Modul" - }, + "humanReadableName": "Quiz Module", "author": { "scnxOrgID": "60", "name": "TomatoCake", "link": "https://github.com/DEVTomatoCake" }, - "description": { - "en": "Create quiz for your users and let them compete against each other.", - "de": "Erstelle Quiz für deine Nutzer und lasse sie gegeneinander antreten." - }, + "description": "Create quiz for your users and let them compete against each other.", + "fa-icon": "fas fa-clipboard-question", "events-dir": "/events", "commands-dir": "/commands", "models-dir": "/models", @@ -25,4 +20,4 @@ "fun" ], "openSourceURL": "https://github.com/DEVTomatoCake/ScootKit-CustomBot/tree/main/modules/quiz" -} \ No newline at end of file +} diff --git a/modules/quiz/quizUtil.js b/modules/quiz/quizUtil.js index e85e7554..01644d30 100644 --- a/modules/quiz/quizUtil.js +++ b/modules/quiz/quizUtil.js @@ -7,7 +7,8 @@ const {ChannelType, MessageEmbed} = require('discord.js'); const { renderProgressbar, formatDate, - parseEmbedColor + parseEmbedColor, + safeSetFooter } = require('../../src/functions/helpers'); const {localize} = require('../../src/functions/localize'); @@ -226,10 +227,11 @@ async function updateLeaderboard(client, force = false) { const embed = new MessageEmbed() .setTitle(moduleStrings.embed.leaderboardTitle) .setColor(parseEmbedColor(moduleStrings.embed.leaderboardColor)) - .setFooter({text: client.strings.footer, iconURL: client.strings.footerImgUrl}) .setThumbnail(channel.guild.iconURL()) .addField(moduleStrings.embed.leaderboardSubtitle, leaderboardString); + safeSetFooter(embed, client); + if (!client.strings.disableFooterTimestamp) embed.setTimestamp(); const components = [{ diff --git a/modules/reminders/config.json b/modules/reminders/config.json index 3eb33511..98aee3d8 100644 --- a/modules/reminders/config.json +++ b/modules/reminders/config.json @@ -1,69 +1,37 @@ { "filename": "config.json", - "description": { - "en": "Configure the behavior of this module here", - "de": "Passe hier die Funktionen des Modules hier an" - }, - "humanName": { - "en": "Configuration", - "de": "Konfiguration" - }, + "description": "Configure the behavior of this module here", + "humanName": "Configuration", "content": [ { "name": "notificationMessage", "type": "string", "allowEmbed": true, - "humanName": { - "de": "Erinnerung-Nachricht", - "en": "Reminder-Message" - }, - "description": { - "de": "Diese Nachricht wird gesendet, wenn jemand erinnert wird", - "en": "This message gets send when someone gets remaindered" - }, + "humanName": "Reminder-Message", + "description": "This message gets send when someone gets remaindered", "default": { - "en": { - "title": "\uD83D\uDD14 Reminder", - "color": "#F1C40F", - "description": "%message%", - "message": "%mention%" - }, - "de": { - "title": "\uD83D\uDD14 Erinnerung", - "color": "#F1C40F", - "description": "%message%", - "message": "%mention%" - } + "title": "🔔 Reminder", + "color": "#F1C40F", + "description": "%message%", + "message": "%mention%" }, "params": [ { "name": "mention", - "description": { - "en": "Mention of the user", - "de": "Erwähnung des Nutzers" - } + "description": "Mention of the user" }, { "name": "message", - "description": { - "en": "Reminder message set by the user", - "de": "Vom Nutzer gesetze Erwähnungsnachricht" - } + "description": "Reminder message set by the user" }, { "name": "userTag", - "description": { - "en": "Tag of the user", - "de": "Tag des Nutzers" - } + "description": "Tag of the user" }, { "name": "userAvatarURL", "isImage": true, - "description": { - "en": "Avatar-URL of the user", - "de": "Profilbild-URL des Nutzers" - } + "description": "Avatar-URL of the user" } ] } diff --git a/modules/reminders/events/interactionCreate.js b/modules/reminders/events/interactionCreate.js new file mode 100644 index 00000000..0ad59d94 --- /dev/null +++ b/modules/reminders/events/interactionCreate.js @@ -0,0 +1,46 @@ +const {localize} = require('../../../src/functions/localize'); +const {formatDate} = require('../../../src/functions/helpers'); +const {planReminder} = require('../reminders'); + +const snoozeDurations = { + '10m': 10 * 60 * 1000, + '30m': 30 * 60 * 1000, + '1h': 60 * 60 * 1000, + '1d': 24 * 60 * 60 * 1000 +}; + +/** + * Handle snooze button interactions for reminders + * @param {Client} client Discord client + * @param {Interaction} interaction Button interaction + */ +module.exports.run = async function (client, interaction) { + if (!interaction.isButton()) return; + if (!interaction.customId.startsWith('reminder-snooze-')) return; + + const parts = interaction.customId.split('-'); + const durationKey = parts[2]; + const reminderID = parts[3]; + const duration = snoozeDurations[durationKey]; + if (!duration) return; + + const originalReminder = await client.models['reminders']['Reminder'].findOne({where: {id: reminderID}}); + if (!originalReminder || originalReminder.userID !== interaction.user.id) { + return interaction.reply({ephemeral: true, content: '⚠️ ' + localize('reminders', 'snooze-not-allowed')}); + } + + const newDate = new Date(new Date().getTime() + duration); + const newReminder = await client.models['reminders']['Reminder'].create({ + userID: interaction.user.id, + reminderText: originalReminder.reminderText, + date: newDate, + channelID: originalReminder.channelID + }); + planReminder(client, newReminder); + + await interaction.update({components: []}); + await interaction.followUp({ + ephemeral: true, + content: '✅ ' + localize('reminders', 'snoozed', {d: formatDate(newDate)}) + }); +}; diff --git a/modules/reminders/module.json b/modules/reminders/module.json index d790c2af..38187286 100644 --- a/modules/reminders/module.json +++ b/modules/reminders/module.json @@ -1,18 +1,12 @@ { "name": "reminders", - "humanReadableName": { - "en": "Reminders", - "de": "Erinnerungen" - }, + "humanReadableName": "Reminders", "author": { "scnxOrgID": "1", "name": "SCDerox (SC Network Team)", "link": "https://github.com/SCDerox" }, - "description": { - "en": "Let users set reminders for themselves - either via DMs or Channels", - "de": "Erlaubt es deinen Nutzer Erinnerungen für sich selbst zu setzen - entweder per PNs oder direkt in den Kanal" - }, + "description": "Let users set reminders for themselves - either via DMs or Channels", "commands-dir": "/commands", "events-dir": "/events", "config-example-files": [ @@ -22,5 +16,6 @@ "community" ], "models-dir": "/models", + "fa-icon": "far fa-bell", "holidayGift": true -} \ No newline at end of file +} diff --git a/modules/reminders/reminders.js b/modules/reminders/reminders.js index 3ce62595..0ceffd7a 100644 --- a/modules/reminders/reminders.js +++ b/modules/reminders/reminders.js @@ -1,6 +1,12 @@ const {scheduleJob} = require('node-schedule'); const {embedType, formatDiscordUserName} = require('../../src/functions/helpers'); +const {localize} = require('../../src/functions/localize'); +/** + * Plan a reminder notification + * @param {Client} client Discord client + * @param {Object} notificationObject Reminder database object + */ function planReminder(client, notificationObject) { if (!notificationObject.date || isNaN(notificationObject.date) || notificationObject.date.getTime() <= new Date().getTime()) return; const bj = scheduleJob(notificationObject.date, async () => { @@ -14,6 +20,40 @@ function planReminder(client, notificationObject) { '%message%': notificationObject.reminderText, '%userTag%': formatDiscordUserName(member.user), '%userAvatarURL%': member.user.avatarURL() + }, { + components: [{ + type: 'ACTION_ROW', + components: [ + { + type: 'BUTTON', + style: 'SECONDARY', + customId: `reminder-snooze-10m-${notificationObject.id}`, + label: localize('reminders', 'snooze-10m'), + emoji: '🔔' + }, + { + type: 'BUTTON', + style: 'SECONDARY', + customId: `reminder-snooze-30m-${notificationObject.id}`, + label: localize('reminders', 'snooze-30m'), + emoji: '🔔' + }, + { + type: 'BUTTON', + style: 'SECONDARY', + customId: `reminder-snooze-1h-${notificationObject.id}`, + label: localize('reminders', 'snooze-1h'), + emoji: '🔔' + }, + { + type: 'BUTTON', + style: 'SECONDARY', + customId: `reminder-snooze-1d-${notificationObject.id}`, + label: localize('reminders', 'snooze-1d'), + emoji: '🔔' + } + ] + }] })); }); client.jobs.push(bj); diff --git a/modules/rock-paper-scissors/commands/rock-paper-scissors.js b/modules/rock-paper-scissors/commands/rock-paper-scissors.js index 38dbb36d..127738c1 100644 --- a/modules/rock-paper-scissors/commands/rock-paper-scissors.js +++ b/modules/rock-paper-scissors/commands/rock-paper-scissors.js @@ -238,7 +238,11 @@ module.exports.run = async function (interaction) { const collector = msg.createMessageComponentCollector({ componentType: ComponentType.Button, - filter: i => i.user.id === interaction.user.id || i.user.id === user2.id + filter: i => i.user.id === interaction.user.id || i.user.id === user2.id, + time: 300000 + }); + collector.on('end', () => { + delete rpsgames[msg.id]; }); collector.on('collect', i => { const game = rpsgames[i.message.id]; diff --git a/modules/rock-paper-scissors/module.json b/modules/rock-paper-scissors/module.json index 6289dabe..43be0845 100644 --- a/modules/rock-paper-scissors/module.json +++ b/modules/rock-paper-scissors/module.json @@ -1,18 +1,13 @@ { "name": "rock-paper-scissors", - "humanReadableName": { - "en": "Rock Paper Scissors", - "de": "Schere Stein Papier" - }, + "humanReadableName": "Rock Paper Scissors", + "fa-icon": "fa-solid fa-scissors", "author": { "scnxOrgID": "60", "name": "TomatoCake", "link": "https://github.com/DEVTomatoCake" }, - "description": { - "en": "Let your users play Rock Paper Scissors against the bot and each other!", - "de": "Lasse Nutzer auf deinem Server Schere Stein Papier gegen den Bot und gegeneinander spielen" - }, + "description": "Let your users play Rock Paper Scissors against the bot and each other!", "commands-dir": "/commands", "noConfig": true, "releaseDate": "0", @@ -20,4 +15,4 @@ "fun" ], "openSourceURL": "https://github.com/DEVTomatoCake/ScootKit-CustomBot/tree/main/modules/rock-paper-scissors" -} \ No newline at end of file +} diff --git a/modules/staff-management-system/commands/duty.js b/modules/staff-management-system/commands/duty.js index be878195..df2c0506 100644 --- a/modules/staff-management-system/commands/duty.js +++ b/modules/staff-management-system/commands/duty.js @@ -1,6 +1,13 @@ const { MessageFlags, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, StringSelectMenuBuilder, ModalBuilder, TextInputBuilder, TextInputStyle } = require('discord.js'); const { Op, fn, col, literal } = require('sequelize'); -const { getConfig, applyFooter, getSafeChannelId, formatDuration, buildPaginationRow, checkStaffPermissions } = require('../staff-management'); +const { + getConfig, + applyFooter, + getSafeChannelId, + formatDuration, + buildPaginationRow, + checkStaffPermissions +} = require('../staff-management'); const { localize } = require('../../../src/functions/localize'); function getLookbackDate(config) { @@ -50,18 +57,18 @@ function getQuotaForMember(member, config) { let bestQuota = null; let highestPosition = -1; - + for (const [roleId, hoursStr] of Object.entries(config.quotas)) { const hours = parseFloat(hoursStr); if (isNaN(hours)) continue; - + const role = member.guild.roles.cache.get(roleId); if (role && member.roles.cache.has(roleId) && role.position > highestPosition) { highestPosition = role.position; bestQuota = { roleId, hours }; } } - + return bestQuota; } @@ -70,15 +77,15 @@ async function sendShiftEndDm(client, member, shift) { const embed = applyFooter(client, new EmbedBuilder() .setTitle(localize('staff-management-system', 'duty-shift-report-title')) - .setThumbnail(member.user.displayAvatarURL({ dynamic: true })) + .setThumbnail(member.user.displayAvatarURL({dynamic: true})) .addFields( { name: localize('staff-management-system', 'duty-shift-information'), value: - `>>> **${localize('staff-management-system', 'label-shift-type')}:** ${shift.type || 'Staff'}\n` + - `**${localize('staff-management-system', 'general-start')}:** \n` + - `**${localize('staff-management-system', 'general-end')}:** \n` + - `**${localize('staff-management-system', 'label-breaks')}:** ${shift.breakCount || 0}` + `>>> **${localize('staff-management-system', 'label-shift-type')}:** ${shift.type || 'Staff'}\n` + + `**${localize('staff-management-system', 'general-start')}:** \n` + + `**${localize('staff-management-system', 'general-end')}:** \n` + + `**${localize('staff-management-system', 'label-breaks')}:** ${shift.breakCount || 0}` }, { name: localize('staff-management-system', 'label-elapsed-time'), @@ -88,7 +95,7 @@ async function sendShiftEndDm(client, member, shift) { ); try { - await member.user.send({ embeds: [embed.toJSON()] }); + await member.user.send({embeds: [embed.toJSON()]}); } catch (e) { client.logger.warn(localize('staff-management-system', 'log-duty-dm-fail', { user: member.user.tag, @@ -115,13 +122,13 @@ async function logShiftChange(client, action, data) { const username = targetUserObj ? targetUserObj.username : data.userId; const embed = new EmbedBuilder() - .setThumbnail(targetUserObj?.displayAvatarURL({ dynamic: true }) || null); + .setThumbnail(targetUserObj?.displayAvatarURL({dynamic: true}) || null); if (action === 'start') { embed - .setTitle(localize('staff-management-system', 'log-duty-start-title', { username })) + .setTitle(localize('staff-management-system', 'log-duty-start-title', {username})) .setColor('Green') - .setDescription(localize('staff-management-system', 'log-duty-start-desc', { mention })) + .setDescription(localize('staff-management-system', 'log-duty-start-desc', {mention})) .addFields({ name: localize('staff-management-system', 'log-duty-info-hdr'), value: @@ -130,9 +137,9 @@ async function logShiftChange(client, action, data) { }); } else if (action === 'break') { embed - .setTitle(localize('staff-management-system', 'log-duty-break-title', { username })) + .setTitle(localize('staff-management-system', 'log-duty-break-title', {username})) .setColor('Yellow') - .setDescription(localize('staff-management-system', 'log-duty-break-desc', { mention })) + .setDescription(localize('staff-management-system', 'log-duty-break-desc', {mention})) .addFields({ name: localize('staff-management-system', 'log-duty-info-hdr'), value: @@ -143,9 +150,9 @@ async function logShiftChange(client, action, data) { }); } else if (action === 'resume') { embed - .setTitle(localize('staff-management-system', 'log-duty-resume-title', { username })) + .setTitle(localize('staff-management-system', 'log-duty-resume-title', {username})) .setColor('Green') - .setDescription(localize('staff-management-system', 'log-duty-resume-desc', { mention })) + .setDescription(localize('staff-management-system', 'log-duty-resume-desc', {mention})) .addFields({ name: localize('staff-management-system', 'log-duty-info-hdr'), value: @@ -156,9 +163,9 @@ async function logShiftChange(client, action, data) { }); } else if (action === 'end') { embed - .setTitle(localize('staff-management-system', 'log-duty-end-title', { username })) + .setTitle(localize('staff-management-system', 'log-duty-end-title', {username})) .setColor('Red') - .setDescription(localize('staff-management-system', 'log-duty-end-desc', { mention })) + .setDescription(localize('staff-management-system', 'log-duty-end-desc', {mention})) .addFields({ name: localize('staff-management-system', 'log-duty-info-hdr'), value: @@ -173,7 +180,7 @@ async function logShiftChange(client, action, data) { }); } else if (action === 'void') { embed - .setTitle(localize('staff-management-system', 'log-duty-void-title', { username })) + .setTitle(localize('staff-management-system', 'log-duty-void-title', {username})) .setColor('DarkRed') .setDescription(localize('staff-management-system', 'log-duty-void-desc', { mention, @@ -193,7 +200,7 @@ async function logShiftChange(client, action, data) { applyFooter(client, embed); try { - await channel.send({ embeds: [embed.toJSON()] }); + await channel.send({embeds: [embed.toJSON()]}); } catch (e) { client.logger.error(localize('staff-management-system', 'log-duty-log-fail', { action, @@ -222,22 +229,25 @@ async function buildDutyManagePayload(client, userId, shiftType, endedShift = nu } const completedShifts = await Shift.findAll({ - where: { - userId, - type: shiftType, - endTime: { [Op.not]: null }, - duration: { [Op.not]: null } + where: { + userId, + type: shiftType, + endTime: {[Op.not]: null}, + duration: {[Op.not]: null} } }); const totalShifts = completedShifts.length; const totalSeconds = completedShifts.reduce((sum, s) => sum + (parseInt(s.duration) || 0), 0); - const avgSeconds = totalShifts > 0 - ? Math.floor(totalSeconds / totalShifts) + const avgSeconds = totalShifts > 0 + ? Math.floor(totalSeconds / totalShifts) : 0; const activeShift = onDuty ? await Shift.findOne({ - where: { userId, endTime: null }, + where: { + userId, + endTime: null + }, order: [['startTime', 'DESC']] }) : null; @@ -248,7 +258,7 @@ async function buildDutyManagePayload(client, userId, shiftType, endedShift = nu else if (endedShift) titleKey = 'duty-ended-title'; const embed = applyFooter(client, new EmbedBuilder() - .setTitle(localize('staff-management-system', titleKey, { type: shiftType })) + .setTitle(localize('staff-management-system', titleKey, {type: shiftType})) .setColor(statusColor) .setThumbnail(user?.displayAvatarURL({ dynamic: true }) || null) ); @@ -316,9 +326,9 @@ async function buildDutyManagePayload(client, userId, shiftType, endedShift = nu .setDisabled(!onDuty) ); - return { - embeds: [embed.toJSON()], - components: [row.toJSON()] + return { + embeds: [embed.toJSON()], + components: [row.toJSON()] }; } @@ -327,10 +337,10 @@ async function buildDutyTimePayload(client, interaction, shiftType) { const Shift = client.models['staff-management-system']['StaffShift']; const user = interaction.user; - const whereClause = { - userId: user.id, - endTime: { [Op.not]: null }, - duration: { [Op.not]: null } + const whereClause = { + userId: user.id, + endTime: {[Op.not]: null}, + duration: {[Op.not]: null} }; if (shiftType !== 'All') whereClause.type = shiftType; @@ -362,11 +372,11 @@ async function buildDutyTimePayload(client, interaction, shiftType) { if (timeframe === 'Weekly') cutoff.setDate(cutoff.getDate() - 7); else cutoff.setMonth(cutoff.getMonth() - 1); - const recentWhere = { - userId: user.id, - startTime: { [Op.gt]: cutoff }, - endTime: { [Op.not]: null }, - duration: { [Op.not]: null } + const recentWhere = { + userId: user.id, + startTime: {[Op.gt]: cutoff}, + endTime: {[Op.not]: null}, + duration: {[Op.not]: null} }; if (shiftType !== 'All') recentWhere.type = shiftType; @@ -374,13 +384,13 @@ async function buildDutyTimePayload(client, interaction, shiftType) { const recentSeconds = recentShifts.reduce((sum, s) => sum + (parseInt(s.duration) || 0), 0); const requiredSeconds = quota.hours * 3600; const metQuota = recentSeconds >= requiredSeconds; - quotaText = localize('staff-management-system', 'duty-quota-str', { - timeframe, + quotaText = localize('staff-management-system', 'duty-quota-str', { + timeframe, duration: formatDuration(recentSeconds), - hours: quota.hours, - result: metQuota - ? localize('staff-management-system', 'quota-met') - : localize('staff-management-system', 'quota-fail') + hours: quota.hours, + result: metQuota + ? localize('staff-management-system', 'quota-met') + : localize('staff-management-system', 'quota-fail') }); } } @@ -389,9 +399,9 @@ async function buildDutyTimePayload(client, interaction, shiftType) { .setTitle(localize('staff-management-system', 'duty-time-title', { type: shiftType })) .setColor('Blue') .setThumbnail(user.displayAvatarURL({ dynamic: true })) - .setDescription(localize('staff-management-system', 'duty-time-desc', { - count: shiftCount, - duration: formatDuration(totalSeconds) + .setDescription(localize('staff-management-system', 'duty-time-desc', { + count: shiftCount, + duration: formatDuration(totalSeconds) }) + breakdownText + quotaText) ); @@ -403,9 +413,9 @@ async function buildDutyTimePayload(client, interaction, shiftType) { .setDisabled(shiftCount === 0) ); - return { - embeds: [embed.toJSON()], - components: [row.toJSON()] + return { + embeds: [embed.toJSON()], + components: [row.toJSON()] }; } @@ -415,12 +425,12 @@ async function buildLeaderboardPayload(client, page = 1, shiftType) { const limit = 15; const offset = (page - 1) * limit; - const whereClause = { - endTime: { [Op.not]: null }, - duration: { [Op.not]: null } + const whereClause = { + endTime: {[Op.not]: null}, + duration: {[Op.not]: null} }; if (shiftType !== 'All') whereClause.type = shiftType; - + const lookbackDate = getLookbackDate(config); if (lookbackDate) whereClause.startTime = { [Op.gt]: lookbackDate }; @@ -436,10 +446,10 @@ async function buildLeaderboardPayload(client, page = 1, shiftType) { }); const total = allResults.length; - if (total === 0) return { - content: localize('staff-management-system', 'err-no-lb', { - type: shiftType - }) + if (total === 0) return { + content: localize('staff-management-system', 'err-no-lb', { + type: shiftType + }) }; const totalPages = Math.ceil(total / limit) || 1; @@ -479,21 +489,21 @@ async function buildLeaderboardPayload(client, page = 1, shiftType) { page, totalPages, 'back', 'next' ); - return { - embeds: [embed.toJSON()], - components: [row.toJSON()] + return { + embeds: [embed.toJSON()], + components: [row.toJSON()] }; } async function buildShiftHistoryPayload(client, userId, page = 1, shiftType) { const Shift = client.models['staff-management-system']['StaffShift']; - const limit = 10; + const limit = 10; const offset = (page - 1) * limit; - const whereClause = { - userId, - endTime: { [Op.not]: null }, - duration: { [Op.not]: null } + const whereClause = { + userId, + endTime: {[Op.not]: null}, + duration: {[Op.not]: null} }; if (shiftType !== 'All') whereClause.type = shiftType; @@ -512,7 +522,7 @@ async function buildShiftHistoryPayload(client, userId, page = 1, shiftType) { const startTs = Math.floor(new Date(shift.startTime).getTime() / 1000); const endTs = Math.floor(new Date(shift.endTime).getTime() / 1000); const typeBadge = shiftType === 'All' ? ` \`[${shift.type || 'Staff'}]\`` : ''; - + return `**${offset + i + 1}. ${dur}${typeBadge}:**\nStart: | End: `; }); @@ -530,7 +540,7 @@ async function buildShiftHistoryPayload(client, userId, page = 1, shiftType) { page, total: totalPages }) - }); + }); const row = buildPaginationRow( `duty-mgmt_hist_${userId}_${page - 1}_${shiftType}`, @@ -539,9 +549,9 @@ async function buildShiftHistoryPayload(client, userId, page = 1, shiftType) { page, totalPages ); - return { - embeds: [embed.toJSON()], - components: [row.toJSON()] + return { + embeds: [embed.toJSON()], + components: [row.toJSON()] }; } @@ -556,47 +566,45 @@ async function buildDutyAdminPayload(client, targetMember, requestingMember) { const onBreak = profile?.onBreak || false; let statusText, statusColor; - if (onDuty && onBreak) { - statusText = localize('staff-management-system', 'stat-brk'); - statusColor = 'Yellow'; - } - else if (onDuty) { - statusText = localize('staff-management-system', 'stat-on'); - statusColor = 'Green'; - } - else { - statusText = localize('staff-management-system', 'stat-off'); - statusColor = 'Red'; + if (onDuty && onBreak) { + statusText = localize('staff-management-system', 'stat-brk'); + statusColor = 'Yellow'; + } else if (onDuty) { + statusText = localize('staff-management-system', 'stat-on'); + statusColor = 'Green'; + } else { + statusText = localize('staff-management-system', 'stat-off'); + statusColor = 'Red'; } const completedShifts = await Shift.findAll({ - where: { - userId: targetUser.id, - endTime: { [Op.not]: null }, - duration: { [Op.not]: null } + where: { + userId: targetUser.id, + endTime: {[Op.not]: null}, + duration: {[Op.not]: null} } }); const totalShifts = completedShifts.length; const totalSeconds = completedShifts.reduce((sum, s) => sum + (parseInt(s.duration) || 0), 0); - const avgSeconds = totalShifts > 0 - ? Math.floor(totalSeconds / totalShifts) + const avgSeconds = totalShifts > 0 + ? Math.floor(totalSeconds / totalShifts) : 0; const embed = applyFooter(client, new EmbedBuilder() - .setTitle(localize('staff-management-system', 'duty-adm-title', { - user: targetUser.username + .setTitle(localize('staff-management-system', 'duty-adm-title', { + user: targetUser.username })) .setColor(statusColor) .setThumbnail(targetUser.displayAvatarURL({ dynamic: true })) .setDescription(`**${statusText}**`) .addFields( - { - name: localize('staff-management-system', 'duty-stats'), - value: localize('staff-management-system', 'duty-stat-desc', { - duration: formatDuration(totalSeconds), - count: totalShifts, - average: formatDuration(avgSeconds) - }) + { + name: localize('staff-management-system', 'duty-stats'), + value: localize('staff-management-system', 'duty-stat-desc', { + duration: formatDuration(totalSeconds), + count: totalShifts, + average: formatDuration(avgSeconds) + }) } ) ); @@ -630,9 +638,9 @@ async function buildDutyAdminPayload(client, targetMember, requestingMember) { .setDisabled(!isManagement) ); - return { - embeds: [embed.toJSON()], - components: [row.toJSON()] + return { + embeds: [embed.toJSON()], + components: [row.toJSON()] }; } @@ -640,11 +648,11 @@ async function buildDutyAdminPayload(client, targetMember, requestingMember) { async function handleDutyStartButton(client, interaction) { const parts = interaction.customId.split('_'); const userId = parts[2]; - const shiftType = parts[3] || 'Staff'; + const shiftType = parts[3] || 'Staff'; - if (interaction.user.id !== userId) return interaction.editReply({ - content: localize('staff-management-system', 'err-not-yours'), - flags: MessageFlags.Ephemeral + if (interaction.user.id !== userId) return interaction.editReply({ + content: localize('staff-management-system', 'err-not-yours'), + flags: MessageFlags.Ephemeral }); const config = getConfig(client, 'shifts'); @@ -652,9 +660,9 @@ async function handleDutyStartButton(client, interaction) { const Shift = client.models['staff-management-system']['StaffShift']; const profile = await Profile.findByPk(userId); - if (profile?.onDuty) return interaction.followUp({ - content: localize('staff-management-system', 'err-alr-on'), - flags: MessageFlags.Ephemeral + if (profile?.onDuty) return interaction.followUp({ + content: localize('staff-management-system', 'err-alr-on'), + flags: MessageFlags.Ephemeral }); const startTime = new Date(); @@ -688,22 +696,22 @@ async function handleDutyStartButton(client, interaction) { async function handleDutyBreakButton(client, interaction) { const userId = interaction.customId.split('_')[2]; - if (interaction.user.id !== userId) return interaction.editReply({ - content: localize('staff-management-system', 'err-not-yours'), - flags: MessageFlags.Ephemeral + if (interaction.user.id !== userId) return interaction.editReply({ + content: localize('staff-management-system', 'err-not-yours'), + flags: MessageFlags.Ephemeral }); const Profile = client.models['staff-management-system']['StaffProfile']; const Shift = client.models['staff-management-system']['StaffShift']; const profile = await Profile.findByPk(userId); - - if (!profile?.onDuty) return interaction.followUp({ - content: localize('staff-management-system', 'err-not-on'), - flags: MessageFlags.Ephemeral + + if (!profile?.onDuty) return interaction.followUp({ + content: localize('staff-management-system', 'err-not-on'), + flags: MessageFlags.Ephemeral }); - const activeShift = await Shift.findOne({ - where: { userId, endTime: null } + const activeShift = await Shift.findOne({ + where: {userId, endTime: null} }); const shiftType = activeShift?.type || 'Staff'; @@ -720,21 +728,21 @@ async function handleDutyBreakButton(client, interaction) { } const elapsedSeconds = activeShift - ? Math.max( - 0, - Math.floor( - ((nowOnBreak ? new Date() : new Date(profile.breakStartTime || Date.now())).getTime() - - new Date(activeShift.startTime).getTime()) / 1000 + ? Math.max( + 0, + Math.floor( + ((nowOnBreak ? new Date() : new Date(profile.breakStartTime || Date.now())).getTime() - + new Date(activeShift.startTime).getTime()) / 1000 + ) ) - ) - : 0; + : 0; const breakStartTime = nowOnBreak ? new Date() : null; await Profile.update({ onBreak: nowOnBreak, breakStartTime }, { - where: { userId } + where: {userId} }); if (activeShift) { @@ -765,9 +773,9 @@ async function handleDutyBreakButton(client, interaction) { async function handleDutyEndButton(client, interaction) { const userId = interaction.customId.split('_')[2]; - if (interaction.user.id !== userId) return interaction.editReply({ - content: localize('staff-management-system', 'err-not-yours'), - flags: MessageFlags.Ephemeral + if (interaction.user.id !== userId) return interaction.editReply({ + content: localize('staff-management-system', 'err-not-yours'), + flags: MessageFlags.Ephemeral }); const config = getConfig(client, 'shifts'); @@ -775,9 +783,9 @@ async function handleDutyEndButton(client, interaction) { const Shift = client.models['staff-management-system']['StaffShift']; const profile = await Profile.findByPk(userId); - if (!profile?.onDuty) return interaction.followUp({ - content: localize('staff-management-system', 'err-not-on'), - flags: MessageFlags.Ephemeral + if (!profile?.onDuty) return interaction.followUp({ + content: localize('staff-management-system', 'err-not-on'), + flags: MessageFlags.Ephemeral }); const activeShifts = await Shift.findAll({ where: { userId, endTime: null } }); @@ -807,33 +815,34 @@ async function handleDutyEndButton(client, interaction) { } } - await Profile.update({ - onDuty: false, - onBreak: false, + await Profile.update({ + onDuty: false, + onBreak: false, breakStartTime: null }, { - where: { userId } + where: {userId} }); const member = await interaction.guild.members.fetch(userId).catch(() => null); if (config.onDutyRole && member) { - await member.roles.remove(config.onDutyRole).catch(() => {}); + await member.roles.remove(config.onDutyRole).catch(() => { + }); } if (member && endedShiftForDisplay) { await sendShiftEndDm(client, member, endedShiftForDisplay); } if (endedShiftForDisplay) { - await logShiftChange(client, 'end', { - userId, - targetUser: interaction.user, - shiftType: endedShiftForDisplay.type || shiftType, - startTime: endedShiftForDisplay.startTime, - endTime: endedShiftForDisplay.endTime, - breakCount: endedShiftForDisplay.breakCount || 0, - durationSeconds: parseInt(endedShiftForDisplay.duration) || 0 - }); -} + await logShiftChange(client, 'end', { + userId, + targetUser: interaction.user, + shiftType: endedShiftForDisplay.type || shiftType, + startTime: endedShiftForDisplay.startTime, + endTime: endedShiftForDisplay.endTime, + breakCount: endedShiftForDisplay.breakCount || 0, + durationSeconds: parseInt(endedShiftForDisplay.duration) || 0 + }); + } const payload = await buildDutyManagePayload(client, userId, shiftType, endedShiftForDisplay); await interaction.editReply(payload); @@ -855,24 +864,24 @@ async function handleDutyHistPageButton(client, interaction) { const page = parseInt(parts[3]); const shiftType = parts[4] || 'Staff'; - if (interaction.user.id !== userId) return interaction.followUp({ - content: localize('staff-management-system', 'err-hist-oth'), - flags: MessageFlags.Ephemeral + if (interaction.user.id !== userId) return interaction.followUp({ + content: localize('staff-management-system', 'err-hist-oth'), + flags: MessageFlags.Ephemeral }); const payload = await buildShiftHistoryPayload(client, userId, page, shiftType); - if (payload.content) return interaction.followUp({ - ...payload, - flags: MessageFlags.Ephemeral + if (payload.content) return interaction.followUp({ + ...payload, + flags: MessageFlags.Ephemeral }); const isOnHistEmbed = interaction.message?.embeds?.[0]?.title?.startsWith(localize('staff-management-system', 'duty-hi-title', { type: '' }).replace(' - ', '')); if (isOnHistEmbed) { return interaction.editReply(payload); } else { - return interaction.followUp({ - ...payload, - flags: MessageFlags.Ephemeral + return interaction.followUp({ + ...payload, + flags: MessageFlags.Ephemeral }); } } @@ -899,8 +908,8 @@ async function handleDutyAdminForceEnd(client, interaction) { const profile = await Profile.findByPk(targetUserId); let endedShiftForDisplay = null; - const activeShifts = await Shift.findAll({ - where: { userId: targetUserId, endTime: null } + const activeShifts = await Shift.findAll({ + where: {userId: targetUserId, endTime: null} }); for (const activeShift of activeShifts) { if (profile?.onBreak && profile.breakStartTime) { @@ -919,11 +928,12 @@ async function handleDutyAdminForceEnd(client, interaction) { endedShiftForDisplay = activeShift; } - await Profile.update({ - onDuty: false, - onBreak: false, - breakStartTime: null }, { - where: { userId: targetUserId } + await Profile.update({ + onDuty: false, + onBreak: false, + breakStartTime: null + }, { + where: {userId: targetUserId} }); if (config.onDutyRole) { const member = await interaction.guild.members.fetch(targetUserId).catch(() => null); @@ -965,19 +975,23 @@ async function handleDutyAdminVoidActive(client, interaction) { const Shift = client.models['staff-management-system']['StaffShift']; const activeShifts = await Shift.findAll({ - where: { userId: targetUserId, endTime: null }, + where: { + userId: targetUserId, + endTime: null + }, order: [['startTime', 'DESC']] }); - const shiftForLog = activeShifts.length > 0 - ? activeShifts[0] + const shiftForLog = activeShifts.length > 0 + ? activeShifts[0] : null; for (const activeShift of activeShifts) await activeShift.destroy(); - await Profile.update({ - onDuty: false, - onBreak: false, - breakStartTime: null }, { - where: { userId: targetUserId } + await Profile.update({ + onDuty: false, + onBreak: false, + breakStartTime: null + }, { + where: {userId: targetUserId} }); if (config.onDutyRole) { const member = await interaction.guild.members.fetch(targetUserId).catch(() => null); @@ -985,14 +999,14 @@ async function handleDutyAdminVoidActive(client, interaction) { } if (shiftForLog) { - await logShiftChange(client, 'void', { - userId: targetUserId, - shiftType: shiftForLog.type || 'Staff', - startTime: shiftForLog.startTime, - breakCount: shiftForLog.breakCount || 0, - executorId: interaction.user.id - }); -} + await logShiftChange(client, 'void', { + userId: targetUserId, + shiftType: shiftForLog.type || 'Staff', + startTime: shiftForLog.startTime, + breakCount: shiftForLog.breakCount || 0, + executorId: interaction.user.id + }); + } const targetMember = await interaction.guild.members.fetch(targetUserId).catch(() => null); if (!targetMember) { @@ -1036,11 +1050,11 @@ async function handleDutyAdminVoidAllSubmit(client, interaction) { const targetUserId = interaction.customId.split('_')[2]; const confirmPhrase = localize('staff-management-system', 'del-conf-phrase'); - + if (interaction.fields.getTextInputValue('confirm').trim() !== confirmPhrase) { - return interaction.reply({ - content: localize('staff-management-system', 'err-conf-fail'), - flags: MessageFlags.Ephemeral + return interaction.reply({ + content: localize('staff-management-system', 'err-conf-fail'), + flags: MessageFlags.Ephemeral }); } @@ -1048,43 +1062,43 @@ async function handleDutyAdminVoidAllSubmit(client, interaction) { const Profile = client.models['staff-management-system']['StaffProfile']; const Shift = client.models['staff-management-system']['StaffShift']; - await Shift.destroy({ - where: { userId: targetUserId } + await Shift.destroy({ + where: {userId: targetUserId} }); - await Profile.update({ - onDuty: false, - onBreak: false, - breakStartTime: null - }, { - where: { userId: targetUserId } + await Profile.update({ + onDuty: false, + onBreak: false, + breakStartTime: null + }, { + where: {userId: targetUserId} }); - + if (config.onDutyRole) { const member = await interaction.guild.members.fetch(targetUserId).catch(() => null); if (member) await member.roles.remove(config.onDutyRole).catch(() => {}); } - client.logger.info(localize('staff-management-system', 'log-void-all', { - target: targetUserId, - admin: interaction.user.id + client.logger.info(localize('staff-management-system', 'log-void-all', { + target: targetUserId, + admin: interaction.user.id })); - - return interaction.reply({ - content: localize('staff-management-system', 'succ-v-all', { user: targetUserId }), - flags: MessageFlags.Ephemeral + + return interaction.reply({ + content: localize('staff-management-system', 'succ-v-all', {user: targetUserId}), + flags: MessageFlags.Ephemeral }); } async function handleDutyAdminAddTimeButton(client, interaction) { const permCheck = checkDutyAdminPermission(client, interaction); if (permCheck) return permCheck; - + const targetUserId = interaction.customId.split('_')[2]; const config = getConfig(client, 'shifts'); - const dutyTypes = config.dutyTypes && config.dutyTypes.length > 0 - ? config.dutyTypes + const dutyTypes = config.dutyTypes && config.dutyTypes.length > 0 + ? config.dutyTypes : ['Staff']; - + const modal = new ModalBuilder() .setCustomId(`duty-mgmt_admin-addtime-submit_${targetUserId}`) .setTitle(localize('staff-management-system', 'mod-add-t')); @@ -1119,7 +1133,7 @@ async function handleDutyAdminAddTimeSubmit(client, interaction) { const targetUserId = interaction.customId.split('_')[2]; const minutesRaw = interaction.fields.getTextInputValue('minutes'); const shiftType = interaction.fields.getTextInputValue('type'); - + const maxMinutes = 10080; const minutes = parseInt(minutesRaw, 10); @@ -1131,21 +1145,21 @@ async function handleDutyAdminAddTimeSubmit(client, interaction) { } const config = getConfig(client, 'shifts'); - const dutyTypes = config.dutyTypes && config.dutyTypes.length > 0 - ? config.dutyTypes + const dutyTypes = config.dutyTypes && config.dutyTypes.length > 0 + ? config.dutyTypes : ['Staff']; - + if (!dutyTypes.includes(shiftType)) { - return interaction.reply({ - content: localize('staff-management-system', 'err-inv-type', { - types: dutyTypes.join(', ') - }), - flags: MessageFlags.Ephemeral + return interaction.reply({ + content: localize('staff-management-system', 'err-inv-type', { + types: dutyTypes.join(', ') + }), + flags: MessageFlags.Ephemeral }); } const Shift = client.models['staff-management-system']['StaffShift']; - + const durationSeconds = minutes * 60; const endTime = new Date(); const startTime = new Date(endTime.getTime() - (durationSeconds * 1000)); @@ -1158,11 +1172,11 @@ async function handleDutyAdminAddTimeSubmit(client, interaction) { type: shiftType }); - client.logger.info(localize('staff-management-system', 'log-add-time', { - admin: interaction.user.tag, - min: minutes, - type: shiftType, - target: targetUserId + client.logger.info(localize('staff-management-system', 'log-add-time', { + admin: interaction.user.tag, + min: minutes, + type: shiftType, + target: targetUserId })); const targetMember = await interaction.guild.members.fetch(targetUserId).catch(() => null); @@ -1199,7 +1213,7 @@ async function handleDutyDropdown(client, interaction, action, selectedType) { async function handleCommonDutyCommand(i, action) { const config = getConfig(i.client, 'shifts'); if (!config || !config.enableShifts) return i.editReply({ content: localize('staff-management-system', 'err-sh-dis') }); - + const dutyTypes = config.dutyTypes && config.dutyTypes.length > 0 ? config.dutyTypes : ['Staff']; let shiftType = i.options.getString('type'); @@ -1219,12 +1233,12 @@ async function handleCommonDutyCommand(i, action) { if (dutyTypes.length === 1 && action === 'manage') { shiftType = dutyTypes[0]; } else if (dutyTypes.length === 1 && (action === 'leaderboard' || action === 'time')) { - shiftType = 'All'; + shiftType = 'All'; } else { const selectMenu = new StringSelectMenuBuilder() .setCustomId(`duty-mgmt_dropdown_${action}`) .setPlaceholder(localize('staff-management-system', 'ph-sel-type')); - + allowedTypes.forEach(t => selectMenu.addOptions({ label: t, value: t })); const row = new ActionRowBuilder().addComponents(selectMenu); return i.editReply({ content: localize('staff-management-system', 'msg-sel-type'), components: [row.toJSON()] }); @@ -1249,55 +1263,55 @@ module.exports.autoComplete = { 'manage': { 'type': async function (interaction) { const config = getConfig(interaction.client, 'shifts'); - const dutyTypes = config.dutyTypes && config.dutyTypes.length > 0 - ? config.dutyTypes + const dutyTypes = config.dutyTypes && config.dutyTypes.length > 0 + ? config.dutyTypes : ['Staff']; const focusedValue = interaction.value || ''; - + const filtered = dutyTypes.filter(choice => choice.toLowerCase().startsWith(focusedValue.toLowerCase())); - await interaction.respond(filtered.slice(0, 25).map(choice => ({ - name: choice, - value: choice + await interaction.respond(filtered.slice(0, 25).map(choice => ({ + name: choice, + value: choice }))); } }, 'leaderboard': { 'type': async function (interaction) { const config = getConfig(interaction.client, 'shifts'); - const dutyTypes = config.dutyTypes && config.dutyTypes.length > 0 - ? config.dutyTypes + const dutyTypes = config.dutyTypes && config.dutyTypes.length > 0 + ? config.dutyTypes : ['Staff']; - const options = ['All', ...dutyTypes]; + const options = ['All', ...dutyTypes]; const focusedValue = interaction.value || ''; - + const filtered = options.filter(choice => choice.toLowerCase().startsWith(focusedValue.toLowerCase())); - await interaction.respond(filtered.slice(0, 25).map(choice => ({ - name: choice, - value: choice + await interaction.respond(filtered.slice(0, 25).map(choice => ({ + name: choice, + value: choice }))); } }, 'time': { 'type': async function (interaction) { const config = getConfig(interaction.client, 'shifts'); - const dutyTypes = config.dutyTypes && config.dutyTypes.length > 0 - ? config.dutyTypes + const dutyTypes = config.dutyTypes && config.dutyTypes.length > 0 + ? config.dutyTypes : ['Staff']; - const options = ['All', ...dutyTypes]; + const options = ['All', ...dutyTypes]; const focusedValue = interaction.value || ''; - + const filtered = options.filter(choice => choice.toLowerCase().startsWith(focusedValue.toLowerCase())); - await interaction.respond(filtered.slice(0, 25).map(choice => ({ - name: choice, - value: choice + await interaction.respond(filtered.slice(0, 25).map(choice => ({ + name: choice, + value: choice }))); } } }; module.exports.beforeSubcommand = async function (interaction) { - await interaction.deferReply({ - flags: MessageFlags.Ephemeral + await interaction.deferReply({ + flags: MessageFlags.Ephemeral }); }; @@ -1307,19 +1321,19 @@ module.exports.subcommands = { }, 'active': async function (i) { const config = getConfig(i.client, 'shifts'); - if (!config || !config.enableShifts) return i.editReply({ - content: localize('staff-management-system', 'err-sh-dis') + if (!config || !config.enableShifts) return i.editReply({ + content: localize('staff-management-system', 'err-sh-dis') }); const Shift = i.client.models['staff-management-system']['StaffShift']; const Profile = i.client.models['staff-management-system']['StaffProfile']; - const activeShifts = await Shift.findAll({ - where: { endTime: null }, - order: [['startTime', 'ASC']] + const activeShifts = await Shift.findAll({ + where: {endTime: null}, + order: [['startTime', 'ASC']] }); - if (activeShifts.length === 0) return i.editReply({ - content: localize('staff-management-system', 'info-no-act-sh') + if (activeShifts.length === 0) return i.editReply({ + content: localize('staff-management-system', 'info-no-act-sh') }); const profiles = await Profile.findAll({ @@ -1329,8 +1343,8 @@ module.exports.subcommands = { }); const profileMap = new Map(profiles.map(profile => [profile.userId, profile])); - const dutyTypes = config.dutyTypes && config.dutyTypes.length > 0 - ? config.dutyTypes + const dutyTypes = config.dutyTypes && config.dutyTypes.length > 0 + ? config.dutyTypes : ['Staff']; const grouped = {}; @@ -1343,7 +1357,7 @@ module.exports.subcommands = { const embed = applyFooter(i.client, new EmbedBuilder() .setTitle(localize('staff-management-system', 'duty-act-title')) .setColor('Green') - .setDescription(localize('staff-management-system', 'duty-act-desc', { + .setDescription(localize('staff-management-system', 'duty-act-desc', { count: activeShifts.length })) ); @@ -1374,9 +1388,9 @@ module.exports.subcommands = { lines.push(`${index}. **<@${shift.userId}>** • ${formatDuration(elapsed)}${breakSuffix}`); index++; } - embed.addFields({ - name: `${type} (${grouped[type].length})`, - value: lines.join('\n') + embed.addFields({ + name: `${type} (${grouped[type].length})`, + value: lines.join('\n') }); delete grouped[type]; } @@ -1411,8 +1425,8 @@ module.exports.subcommands = { value: lines.join('\n') }); } - await i.editReply({ - embeds: [embed.toJSON()] + await i.editReply({ + embeds: [embed.toJSON()] }); }, 'leaderboard': async function (i) { @@ -1423,21 +1437,21 @@ module.exports.subcommands = { }, 'admin': async function (i) { const config = getConfig(i.client, 'shifts'); - if (!config || !config.enableShifts) return i.editReply({ - content: localize('staff-management-system', 'err-sh-dis') + if (!config || !config.enableShifts) return i.editReply({ + content: localize('staff-management-system', 'err-sh-dis') }); - + const generalConfig = getConfig(i.client, 'configuration'); const canManage = i.member.roles.cache.some(r => [...(generalConfig.supervisorRoles || []), ...(generalConfig.managementRoles || [])].includes(r.id)) || i.member.permissions.has('Administrator'); - if (!canManage) return i.editReply({ - content: localize('staff-management-system', 'err-no-perm') + if (!canManage) return i.editReply({ + content: localize('staff-management-system', 'err-no-perm') }); const target = i.options.getMember('user'); - if (!target) return i.editReply({ - content: localize('staff-management-system', 'err-no-mem') + if (!target) return i.editReply({ + content: localize('staff-management-system', 'err-no-mem') }); - + const payload = await buildDutyAdminPayload(i.client, target, i.member); await i.editReply(payload); } @@ -1453,63 +1467,63 @@ module.exports.config = { return !client.configurations['staff-management-system']['shifts']?.enableShifts; }, options: [ - { - type: 'SUB_COMMAND', - name: 'manage', + { + type: 'SUB_COMMAND', + name: 'manage', description: localize('staff-management-system', 'cmd-desc-duty-manage'), options: [ - { - type: 'STRING', - name: 'type', - description: localize('staff-management-system', 'cmd-desc-duty-manage-type'), - required: false, - autocomplete: true + { + type: 'STRING', + name: 'type', + description: localize('staff-management-system', 'cmd-desc-duty-manage-type'), + required: false, + autocomplete: true } ] }, - { - type: 'SUB_COMMAND', - name: 'active', - description: localize('staff-management-system', 'cmd-desc-duty-active') + { + type: 'SUB_COMMAND', + name: 'active', + description: localize('staff-management-system', 'cmd-desc-duty-active') }, - { - type: 'SUB_COMMAND', - name: 'leaderboard', + { + type: 'SUB_COMMAND', + name: 'leaderboard', description: localize('staff-management-system', 'cmd-desc-duty-lb'), options: [ - { - type: 'STRING', - name: 'type', - description: localize('staff-management-system', 'cmd-desc-duty-lb-type'), - required: false, - autocomplete: true + { + type: 'STRING', + name: 'type', + description: localize('staff-management-system', 'cmd-desc-duty-lb-type'), + required: false, + autocomplete: true } ] }, - { - type: 'SUB_COMMAND', - name: 'time', + { + type: 'SUB_COMMAND', + name: 'time', description: localize('staff-management-system', 'cmd-desc-duty-time'), options: [ - { - type: 'STRING', - name: 'type', - description: localize('staff-management-system', 'cmd-desc-duty-time-type'), - required: false, - autocomplete: true + { + type: 'STRING', + name: 'type', + description: localize('staff-management-system', 'cmd-desc-duty-time-type'), + required: false, + autocomplete: true } ] }, - { - type: 'SUB_COMMAND', - name: 'admin', - description: localize('staff-management-system', 'cmd-desc-duty-admin'), + { + type: 'SUB_COMMAND', + name: 'admin', + description: localize('staff-management-system', 'cmd-desc-duty-admin'), options: [ - { - type: 'USER', - name: 'user', - description: localize('staff-management-system', 'cmd-desc-duty-admin-user'), - required: true + { + type: 'USER', + name: 'user', + description: localize('staff-management-system', 'cmd-desc-duty-admin-user'), + required: true } ] } @@ -1518,16 +1532,16 @@ module.exports.config = { // Export handlers module.exports.buttonHandlers = { - handleDutyStartButton, + handleDutyStartButton, handleDutyAdminAddTimeButton, - handleDutyBreakButton, - handleDutyEndButton, - handleDutyDropdown, - handleDutyHistPageButton, + handleDutyBreakButton, + handleDutyEndButton, + handleDutyDropdown, + handleDutyHistPageButton, handleDutyLbPageButton, - handleDutyAdminForceEnd, - handleDutyAdminVoidActive, - handleDutyAdminVoidAll, - handleDutyAdminVoidAllSubmit, + handleDutyAdminForceEnd, + handleDutyAdminVoidActive, + handleDutyAdminVoidAll, + handleDutyAdminVoidAllSubmit, handleDutyAdminAddTimeSubmit }; \ No newline at end of file diff --git a/modules/staff-management-system/commands/staff-management.js b/modules/staff-management-system/commands/staff-management.js index 7b633014..e667ee19 100644 --- a/modules/staff-management-system/commands/staff-management.js +++ b/modules/staff-management-system/commands/staff-management.js @@ -1,15 +1,15 @@ const { MessageFlags, EmbedBuilder, ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder } = require('discord.js'); const { embedTypeV2 } = require('../../../src/functions/helpers'); const { localize } = require('../../../src/functions/localize'); -const { +const { applyFooter, - issueInfraction, + issueInfraction, getInfractionHistory, issueSuspension, voidInfraction, promoteUser, getPromotionHistory, - submitReview, + submitReview, getReviewHistory, startActivityCheck, endActivityCheckProcess, @@ -26,57 +26,57 @@ function canManageChecks(client, member) { async function handleProfileView(client, interaction, targetUser) { const config = client.configurations['staff-management-system']['profiles']; - if (!config || !config.enableProfiles) return interaction.editReply({ - content: localize('staff-management-system', 'err-prof-dis') + if (!config || !config.enableProfiles) return interaction.editReply({ + content: localize('staff-management-system', 'err-prof-dis') }); if (!config.profileEmbedMessage) { - return interaction.editReply({ - content: localize('staff-management-system', 'err-prof-cfg') + return interaction.editReply({ + content: localize('staff-management-system', 'err-prof-cfg') }); } const user = targetUser || interaction.user; const member = await interaction.guild.members.fetch(user.id).catch(() => null); - if (!member) return interaction.editReply({ - content: localize('staff-management-system', 'err-no-mem') + if (!member) return interaction.editReply({ + content: localize('staff-management-system', 'err-no-mem') }); - const restrictToStaff = config.onlyAllowStaffProfile !== false; + const restrictToStaff = config.onlyAllowStaffProfile !== false; if (restrictToStaff) { const generalConfig = client.configurations['staff-management-system']['configuration'] || {}; - - const staffRoles = Array.isArray(generalConfig.staffRoles) - ? generalConfig.staffRoles - : (generalConfig.staffRoles - ? [generalConfig.staffRoles] + + const staffRoles = Array.isArray(generalConfig.staffRoles) + ? generalConfig.staffRoles + : (generalConfig.staffRoles + ? [generalConfig.staffRoles] : [] ); - const supRoles = Array.isArray(generalConfig.supervisorRoles) - ? generalConfig.supervisorRoles - : (generalConfig.supervisorRoles - ? [generalConfig.supervisorRoles] + const supRoles = Array.isArray(generalConfig.supervisorRoles) + ? generalConfig.supervisorRoles + : (generalConfig.supervisorRoles + ? [generalConfig.supervisorRoles] : [] ); - const mgmtRoles = Array.isArray(generalConfig.managementRoles) - ? generalConfig.managementRoles - : (generalConfig.managementRoles - ? [generalConfig.managementRoles] + const mgmtRoles = Array.isArray(generalConfig.managementRoles) + ? generalConfig.managementRoles + : (generalConfig.managementRoles + ? [generalConfig.managementRoles] : [] ); - + const allStaffRoles = [...staffRoles, ...supRoles, ...mgmtRoles]; const isAdmin = member.permissions.has('Administrator'); const isStaff = allStaffRoles.length > 0 && member.roles.cache.some(r => allStaffRoles.includes(r.id)); if (!isAdmin && !isStaff) { if (user.id === interaction.user.id) { - return interaction.editReply({ - content: localize('staff-management-system', 'err-prof-no-own') + return interaction.editReply({ + content: localize('staff-management-system', 'err-prof-no-own') }); } else { - return interaction.editReply({ - content: localize('staff-management-system', 'err-prof-no-tgt') + return interaction.editReply({ + content: localize('staff-management-system', 'err-prof-no-tgt') }); } } @@ -84,20 +84,20 @@ async function handleProfileView(client, interaction, targetUser) { const Profile = client.models['staff-management-system']['StaffProfile']; const Review = client.models['staff-management-system']['StaffReview']; - - const [profile] = await Profile.findOrCreate({ - where: { userId: user.id } + + const [profile] = await Profile.findOrCreate({ + where: {userId: user.id} }); const reviewsConfig = client.configurations['staff-management-system']['reviews']; const reviewsEnabled = reviewsConfig && reviewsConfig.enableReviews; - + let ratingDisplay = localize('staff-management-system', 'rev-dis-text'); if (reviewsEnabled) { let avgRatingText = localize('staff-management-system', 'rev-no-rate'); - const allReviews = await Review.findAll({ - where: { targetId: user.id }, - attributes: ['stars'] + const allReviews = await Review.findAll({ + where: {targetId: user.id}, + attributes: ['stars'] }); if (allReviews.length > 0) { avgRatingText = (allReviews.reduce((a, b) => a + b.stars, 0) / allReviews.length).toFixed(1); @@ -129,11 +129,11 @@ async function handleProfileView(client, interaction, targetUser) { '%nickname%': nicknameText, '%intro%': introText, '%status%': statusLines.join('\n'), - '%rating%': ratingDisplay, - '%avatar%': user.displayAvatarURL({ - dynamic: true, - format: 'png', - size: 1024 + '%rating%': ratingDisplay, + '%avatar%': user.displayAvatarURL({ + dynamic: true, + format: 'png', + size: 1024 }) || '' }; @@ -143,10 +143,10 @@ async function handleProfileView(client, interaction, targetUser) { } let msgOpts = await embedTypeV2(embedTemplate, placeholders); - + if (!msgOpts) { - return interaction.editReply({ - content: localize('staff-management-system', 'err-prof-empty') + return interaction.editReply({ + content: localize('staff-management-system', 'err-prof-empty') }); } @@ -155,51 +155,51 @@ async function handleProfileView(client, interaction, targetUser) { async function handleProfileEdit(client, interaction) { const config = client.configurations['staff-management-system']['profiles']; - if (!config || !config.enableProfiles) return interaction.reply({ - content: localize('staff-management-system', 'err-prof-dis'), - flags: MessageFlags.Ephemeral + if (!config || !config.enableProfiles) return interaction.reply({ + content: localize('staff-management-system', 'err-prof-dis'), + flags: MessageFlags.Ephemeral }); - const restrictToStaff = config.onlyAllowStaffProfile !== false; + const restrictToStaff = config.onlyAllowStaffProfile !== false; if (restrictToStaff) { const generalConfig = client.configurations['staff-management-system']['configuration'] || {}; - - const staffRoles = Array.isArray(generalConfig.staffRoles) - ? generalConfig.staffRoles - : (generalConfig.staffRoles - ? [generalConfig.staffRoles] + + const staffRoles = Array.isArray(generalConfig.staffRoles) + ? generalConfig.staffRoles + : (generalConfig.staffRoles + ? [generalConfig.staffRoles] : [] ); - const supRoles = Array.isArray(generalConfig.supervisorRoles) - ? generalConfig.supervisorRoles - : (generalConfig.supervisorRoles - ? [generalConfig.supervisorRoles] + const supRoles = Array.isArray(generalConfig.supervisorRoles) + ? generalConfig.supervisorRoles + : (generalConfig.supervisorRoles + ? [generalConfig.supervisorRoles] : [] ); - const mgmtRoles = Array.isArray(generalConfig.managementRoles) - ? generalConfig.managementRoles - : (generalConfig.managementRoles - ? [generalConfig.managementRoles] + const mgmtRoles = Array.isArray(generalConfig.managementRoles) + ? generalConfig.managementRoles + : (generalConfig.managementRoles + ? [generalConfig.managementRoles] : [] ); - + const allStaffRoles = [ - ...staffRoles, - ...supRoles, + ...staffRoles, + ...supRoles, ...mgmtRoles ]; - + const isAdmin = interaction.member.permissions.has('Administrator'); const hasStaffRole = allStaffRoles.length > 0 && interaction.member.roles.cache.some(r => allStaffRoles.includes(r.id)); - + if (!isAdmin && !hasStaffRole) { - return interaction.reply({ - content: localize('staff-management-system', 'err-prof-perm'), - flags: MessageFlags.Ephemeral + return interaction.reply({ + content: localize('staff-management-system', 'err-prof-perm'), + flags: MessageFlags.Ephemeral }); } } - + const Profile = client.models['staff-management-system']['StaffProfile']; const profile = await Profile.findByPk(interaction.user.id); @@ -233,50 +233,50 @@ async function handleProfileEdit(client, interaction) { async function handleProfileAdminWipe(client, interaction, targetUser) { const profilesConfig = client.configurations['staff-management-system']['profiles']; const generalConfig = client.configurations['staff-management-system']['configuration'] || {}; - + if (!profilesConfig || !profilesConfig.enableProfiles) { - return interaction.editReply({ - content: localize('staff-management-system', 'err-prof-dis') + return interaction.editReply({ + content: localize('staff-management-system', 'err-prof-dis') }); } - const mRoles = Array.isArray(generalConfig.managementRoles) - ? generalConfig.managementRoles - : (generalConfig.managementRoles - ? [generalConfig.managementRoles] + const mRoles = Array.isArray(generalConfig.managementRoles) + ? generalConfig.managementRoles + : (generalConfig.managementRoles + ? [generalConfig.managementRoles] : [] ); - const sRoles = Array.isArray(generalConfig.supervisorRoles) - ? generalConfig.supervisorRoles - : (generalConfig.supervisorRoles - ? [generalConfig.supervisorRoles] + const sRoles = Array.isArray(generalConfig.supervisorRoles) + ? generalConfig.supervisorRoles + : (generalConfig.supervisorRoles + ? [generalConfig.supervisorRoles] : [] ); - const requiredRoles = profilesConfig.managePermission === 'Management' - ? mRoles + const requiredRoles = profilesConfig.managePermission === 'Management' + ? mRoles : [...sRoles, ...mRoles]; const isAdmin = interaction.member.permissions.has('Administrator'); const hasRequiredRole = requiredRoles.length > 0 && interaction.member.roles.cache.some(r => requiredRoles.includes(r.id)); if (!isAdmin && !hasRequiredRole) { - return interaction.editReply({ - content: localize('staff-management-system', 'err-no-perm') + return interaction.editReply({ + content: localize('staff-management-system', 'err-no-perm') }); } const Profile = client.models['staff-management-system']['StaffProfile']; - await Profile.update({ - customNickname: null, - customIntro: null - }, - { - where: { userId: targetUser.id } + await Profile.update({ + customNickname: null, + customIntro: null + }, + { + where: {userId: targetUser.id} }); - await interaction.editReply({ - content: localize('staff-management-system', 'succ-prof-wipe', { u: targetUser.username }) + await interaction.editReply({ + content: localize('staff-management-system', 'succ-prof-wipe', {u: targetUser.username}) }); } @@ -285,10 +285,10 @@ module.exports.autoComplete = { 'issue': { 'type': async function (interaction) { const config = interaction.client.configurations['staff-management-system']['infractions'] || {}; - const types = config.infractionTypes && config.infractionTypes.length > 0 - ? config.infractionTypes + const types = config.infractionTypes && config.infractionTypes.length > 0 + ? config.infractionTypes : ['Warning', 'Strike']; - + const focusedValue = interaction.options.getFocused() || ''; const filtered = types.filter(choice => choice.toLowerCase().startsWith(focusedValue.toLowerCase())); await interaction.respond(filtered.slice(0, 25).map(choice => ({ name: choice, value: choice }))); @@ -301,14 +301,14 @@ module.exports.subcommands = { 'panel': async (i) => { const user = i.options.getUser('user'); const payload = await generateUserPanel(i.client, user); - await i.reply({ - ...payload, - flags: MessageFlags.Ephemeral + await i.reply({ + ...payload, + flags: MessageFlags.Ephemeral }); }, 'infraction': { 'issue': async (i) => { - const user = i.options.getMember('user'); + const user = i.options.getMember('user'); const type = i.options.getString('type'); const reason = i.options.getString('reason'); const expiry = i.options.getString('expiry'); @@ -331,7 +331,7 @@ module.exports.subcommands = { }, 'promotion': { 'promote': async (i) => { - const user = i.options.getMember('user'); + const user = i.options.getMember('user'); const role = i.options.getRole('rank'); const reason = i.options.getString('reason'); await promoteUser(i.client, i, user, role, reason); @@ -344,21 +344,21 @@ module.exports.subcommands = { 'activity-check': { 'start': async (i) => { await i.deferReply({ flags: MessageFlags.Ephemeral }); - if (!canManageChecks(i.client, i.member)) return i.editReply({ - content: localize('staff-management-system', 'err-no-perm') + if (!canManageChecks(i.client, i.member)) return i.editReply({ + content: localize('staff-management-system', 'err-no-perm') }); await startActivityCheck(i.client, i, false); }, 'view': async (i) => { await i.deferReply({ flags: MessageFlags.Ephemeral }); - if (!canManageChecks(i.client, i.member)) return i.editReply({ - content: localize('staff-management-system', 'err-no-perm') + if (!canManageChecks(i.client, i.member)) return i.editReply({ + content: localize('staff-management-system', 'err-no-perm') }); - + const ActivityCheck = i.client.models['staff-management-system']['ActivityCheck']; const ActivityCheckResponse = i.client.models['staff-management-system']['ActivityCheckResponse']; - const activeCheck = await ActivityCheck.findOne({ - where: { status: 'ACTIVE' } + const activeCheck = await ActivityCheck.findOne({ + where: {status: 'ACTIVE'} }); if (!activeCheck) { @@ -368,12 +368,12 @@ module.exports.subcommands = { if (!logChannelId || (Array.isArray(logChannelId) && logChannelId.length === 0)) logChannelId = generalConfig.generalLogChannel; if (Array.isArray(logChannelId)) logChannelId = logChannelId[0]; - const channelPing = logChannelId + const channelPing = logChannelId ? `<#${logChannelId}>` : localize('staff-management-system', 'lbl-log-chan'); return i.editReply({ - content: localize('staff-management-system', 'info-ac-none', { c: channelPing }) + content: localize('staff-management-system', 'info-ac-none', {c: channelPing}) }); } @@ -390,33 +390,33 @@ module.exports.subcommands = { `**${localize('staff-management-system', 'ac-tot-res')}:** ${responseCount}` ) ); - await i.editReply({ - embeds: [embed] + await i.editReply({ + embeds: [embed] }); }, 'end': async (i) => { await i.deferReply({ flags: MessageFlags.Ephemeral }); - if (!canManageChecks(i.client, i.member)) return i.editReply({ - content: localize('staff-management-system', 'err-no-perm') + if (!canManageChecks(i.client, i.member)) return i.editReply({ + content: localize('staff-management-system', 'err-no-perm') }); - + const ActivityCheck = i.client.models['staff-management-system']['ActivityCheck']; const activeCheck = await ActivityCheck.findOne({ where: { status: 'ACTIVE' } }); - if (!activeCheck) return i.editReply({ - content: localize('staff-management-system', 'err-ac-noact') + if (!activeCheck) return i.editReply({ + content: localize('staff-management-system', 'err-ac-noact') }); await endActivityCheckProcess(i.client, activeCheck); - await i.editReply({ - content: localize('staff-management-system', 'succ-ac-end') + await i.editReply({ + content: localize('staff-management-system', 'succ-ac-end') }); } }, 'profile': { 'view': async (i) => { - await i.deferReply({ - flags: MessageFlags.Ephemeral + await i.deferReply({ + flags: MessageFlags.Ephemeral }); const user = i.options.getUser('user') || i.user; await handleProfileView(i.client, i, user); @@ -425,8 +425,8 @@ module.exports.subcommands = { await handleProfileEdit(i.client, i); }, 'wipe': async (i) => { - await i.deferReply({ - flags: MessageFlags.Ephemeral + await i.deferReply({ + flags: MessageFlags.Ephemeral }); const user = i.options.getUser('user'); await handleProfileAdminWipe(i.client, i, user); @@ -466,11 +466,11 @@ module.exports.config = { name: 'panel', description: localize('staff-management-system', 'cmd-desc-panel'), options: [ - { - type: 'USER', - name: 'user', - description: localize('staff-management-system', 'cmd-desc-panel-user'), - required: true + { + type: 'USER', + name: 'user', + description: localize('staff-management-system', 'cmd-desc-panel-user'), + required: true } ] }); @@ -597,12 +597,12 @@ module.exports.config = { description: localize('staff-management-system', 'cmd-desc-promote-reason'), required: true }, - { - type: 'CHANNEL', - name: 'channel', - description: localize('staff-management-system', 'cmd-desc-promote-channel'), - required: false, - channelTypes: [0, 5] + { + type: 'CHANNEL', + name: 'channel', + description: localize('staff-management-system', 'cmd-desc-promote-channel'), + required: false, + channelTypes: [0, 5] } ] }, @@ -721,11 +721,26 @@ module.exports.config = { description: localize('staff-management-system', 'cmd-desc-review-submit-stars'), required: true, choices: [ - { name: '1 ⭐', value: 1 }, - { name: '2 ⭐⭐', value: 2 }, - { name: '3 ⭐⭐⭐', value: 3 }, - { name: '4 ⭐⭐⭐⭐', value: 4 }, - { name: '5 ⭐⭐⭐⭐⭐', value: 5 } + { + name: '1 ⭐', + value: 1 + }, + { + name: '2 ⭐⭐', + value: 2 + }, + { + name: '3 ⭐⭐⭐', + value: 3 + }, + { + name: '4 ⭐⭐⭐⭐', + value: 4 + }, + { + name: '5 ⭐⭐⭐⭐⭐', + value: 5 + } ] }, { diff --git a/modules/staff-management-system/commands/status.js b/modules/staff-management-system/commands/staff-status.js similarity index 65% rename from modules/staff-management-system/commands/status.js rename to modules/staff-management-system/commands/staff-status.js index 6ca28798..e7e7be70 100644 --- a/modules/staff-management-system/commands/status.js +++ b/modules/staff-management-system/commands/staff-status.js @@ -23,47 +23,47 @@ const { // ---------- Status DM's and logging ---------- async function sendStatusDm(user, type, dmType, data = {}) { - const label = type === 'LOA' - ? 'LoA' + const label = type === 'LOA' + ? 'LoA' : 'RA'; - const viewCmd = type === 'LOA' - ? '`/status loa view`' - : '`/status ra view`'; - const endFmt = data.endDate - ? `` + const viewCmd = type === 'LOA' + ? '`/staff-status loa view`' + : '`/staff-status ra view`'; + const endFmt = data.endDate + ? `` : ''; - + // These messages use the locales key to be easily used later const messages = { - approved: { - title: 'dm-appr-title', - color: 'Green', - desc: 'dm-appr-desc', - params: { label, approver: data.approver, endFmt, viewCmd } + approved: { + title: 'dm-appr-title', + color: 'Green', + desc: 'dm-appr-desc', + params: {label, approver: data.approver, endFmt, viewCmd} }, - denied: { - title: 'dm-deny-title', - color: 'Red', - desc: 'dm-deny-desc', - params: { label, denier: data.denier, reason: data.reason } + denied: { + title: 'dm-deny-title', + color: 'Red', + desc: 'dm-deny-desc', + params: {label, denier: data.denier, reason: data.reason} }, - extended: { - title: 'dm-ext-title', - color: 'Yellow', - desc: 'dm-ext-desc', - params: { label, extender: data.extender, days: data.days, endFmt, reason: data.reason, viewCmd } + extended: { + title: 'dm-ext-title', + color: 'Yellow', + desc: 'dm-ext-desc', + params: {label, extender: data.extender, days: data.days, endFmt, reason: data.reason, viewCmd} }, - ended_early: { - title: 'dm-early-title', - color: 'Red', - desc: 'dm-early-desc', - params: { label, ender: data.ender, reason: data.reason } + ended_early: { + title: 'dm-early-title', + color: 'Red', + desc: 'dm-early-desc', + params: {label, ender: data.ender, reason: data.reason} }, - ended: { - title: 'dm-end-title', - color: 'Black', - desc: 'dm-end-desc', - params: { label } + ended: { + title: 'dm-end-title', + color: 'Black', + desc: 'dm-end-desc', + params: {label} } }; @@ -74,12 +74,12 @@ async function sendStatusDm(user, type, dmType, data = {}) { .setTitle(localize('staff-management-system', msg.title, msg.params)) .setDescription(localize('staff-management-system', msg.desc, msg.params)) .setColor(msg.color); - applyFooter(user.client, embed); + applyFooter(user.client, embed); - try { - await user.send({ - embeds: [embed.toJSON()] - }); + try { + await user.send({ + embeds: [embed.toJSON()] + }); } catch (e) { user.client.logger.error( localize('staff-management-system', 'log-stat-dm-error', { @@ -87,13 +87,14 @@ async function sendStatusDm(user, type, dmType, data = {}) { u: user.tag }) ); -}} + } +} function isStatusTypeEnabled(config, type) { if (!config?.enableStatusSystem) return false; - return type === 'LOA' - ? !!config.enableLoa - : !!config.enableRa; + return type === 'LOA' + ? !!config.enableLoa + : !!config.enableRa; } async function logStatusChange(client, type, action, data) { @@ -108,15 +109,15 @@ async function logStatusChange(client, type, action, data) { const channel = await guild.channels.fetch(channelId).catch(() => null); if (!channel) return; - const label = type === 'LOA' - ? 'LoA' + const label = type === 'LOA' + ? 'LoA' : 'RA'; const targetUserObj = data.targetUser || await client.users.fetch(data.userId).catch(() => null); - const mention = targetUserObj - ? targetUserObj.toString() + const mention = targetUserObj + ? targetUserObj.toString() : `<@${data.userId}>`; - const username = targetUserObj - ? targetUserObj.username + const username = targetUserObj + ? targetUserObj.username : data.userId; const embed = new EmbedBuilder() @@ -126,43 +127,44 @@ async function logStatusChange(client, type, action, data) { if (action === 'start') { embed.setTitle(localize('staff-management-system', 'log-start-title', { label, username })) .setColor('Green') - .setDescription(localize('staff-management-system', 'log-start-desc', - { label, mention, apprText: data.approverId - ? ` ${localize('staff-management-system', 'label-appr-by')}: <@${data.approverId}>.` - : '' + .setDescription(localize('staff-management-system', 'log-start-desc', + { + label, mention, apprText: data.approverId + ? ` ${localize('staff-management-system', 'label-appr-by')}: <@${data.approverId}>.` + : '' })) - .addFields({ - name: localize('staff-management-system', 'log-info-hdr', { label }), - value: `**${localize('staff-management-system', 'general-start')}:** \n**${localize('staff-management-system', 'general-end')}:** \n**${localize('staff-management-system', 'general-rsn')}:** ${data.reason || localize('staff-management-system', 'none-provided')}` + .addFields({ + name: localize('staff-management-system', 'log-info-hdr', {label}), + value: `**${localize('staff-management-system', 'general-start')}:** \n**${localize('staff-management-system', 'general-end')}:** \n**${localize('staff-management-system', 'general-rsn')}:** ${data.reason || localize('staff-management-system', 'none-provided')}` }); - + } else if (action === 'end') { embed.setTitle(localize('staff-management-system', 'log-end-title', { label, username })) .setColor('Red') .setDescription(localize('staff-management-system', 'log-end-desc', { label, mention })) - .addFields({ - name: localize('staff-management-system', 'log-info-hdr', { label }), - value: `**${localize('staff-management-system', 'general-started')}:** \n**${localize('staff-management-system', 'general-ended')}:** \n**${localize('staff-management-system','general-req-reason')}:** ${data.reqReason}\n**${localize('staff-management-system', 'general-rsn')}:** ${data.reason || localize('staff-management-system', 'none-provided')}` + .addFields({ + name: localize('staff-management-system', 'log-info-hdr', {label}), + value: `**${localize('staff-management-system', 'general-started')}:** \n**${localize('staff-management-system', 'general-ended')}:** \n**${localize('staff-management-system', 'general-req-reason')}:** ${data.reqReason}\n**${localize('staff-management-system', 'general-rsn')}:** ${data.reason || localize('staff-management-system', 'none-provided')}` }); - + } else if (action === 'adjusted') { embed.setTitle(localize('staff-management-system', 'log-adj-title', { label, username })) .setColor('Yellow') .setDescription(localize('staff-management-system', 'log-adj-desc', { label, mention, executor: data.executorId })) - .addFields({ - name: localize('staff-management-system', 'log-changes'), - value: data.changesText + .addFields({ + name: localize('staff-management-system', 'log-changes'), + value: data.changesText }); } applyFooter(client, embed); - try { - await channel.send({ - embeds: [embed.toJSON()] - }); + try { + await channel.send({ + embeds: [embed.toJSON()] + }); } catch (e) { client.logger.error( - localize('staff-management-system', 'log-status-adj-error', { + localize('staff-management-system', 'log-status-adj-error', { e: e.message }) ); @@ -171,79 +173,81 @@ async function logStatusChange(client, type, action, data) { // ----- Status ----- const getStatusMeta = (type) => ({ - isLoa: type === 'LOA', - label: type === 'LOA' - ? 'LoA' - : 'RA', - enableKey: type === 'LOA' - ? 'enableLoa' + isLoa: type === 'LOA', + label: type === 'LOA' + ? 'LoA' + : 'RA', + enableKey: type === 'LOA' + ? 'enableLoa' : 'enableRa', - roleKey: type === 'LOA' - ? 'loaRole' - : 'raRole', - maxDaysKey: type === 'LOA' - ? 'loaMaxDays' - : 'raMaxDays', - color: type === 'LOA' - ? 'Green' + roleKey: type === 'LOA' + ? 'loaRole' + : 'raRole', + maxDaysKey: type === 'LOA' + ? 'loaMaxDays' + : 'raMaxDays', + color: type === 'LOA' + ? 'Green' : 'Orange', - activeText: localize('staff-management-system', type === 'LOA' - ? 'status-active-loa' + activeText: localize('staff-management-system', type === 'LOA' + ? 'status-active-loa' : 'status-active-ra' ), - histTitle: localize('staff-management-system', type === 'LOA' - ? 'status-hist-loa' + histTitle: localize('staff-management-system', type === 'LOA' + ? 'status-hist-loa' : 'status-hist-ra' - ), - actionPrefix: type === 'LOA' - ? 'loa' + ), + actionPrefix: type === 'LOA' + ? 'loa' : 'ra' }); async function handleStatusRequest(client, interaction, type, durationInput, reason) { const config = getConfig(client, 'status'); const isLoa = type === 'LOA'; - if (!isStatusTypeEnabled(config, type)) - return interaction.editReply({ - content: localize('staff-management-system', 'err-status-disabled', { type }) + if (!isStatusTypeEnabled(config, type)) + return interaction.editReply({ + content: localize('staff-management-system', 'err-status-disabled', {type}) } ); const days = parseDurationToDays(durationInput?.trim()); - if (!days || isNaN(days) || days <= 0) return interaction.editReply({ - content: localize('staff-management-system', 'err-invalid-duration') + if (!days || isNaN(days) || days <= 0) return interaction.editReply({ + content: localize('staff-management-system', 'err-invalid-duration') }); - + const maxDays = (isLoa ? config.loaMaxDays : config.raMaxDays) || (isLoa ? 60 : 30); - if (days > maxDays) return interaction.editReply({ - content: localize('staff-management-system', 'err-duration-max', { max: maxDays }) + if (days > maxDays) return interaction.editReply({ + content: localize('staff-management-system', 'err-duration-max', {max: maxDays}) }); const LoaRequest = client.models['staff-management-system']['LoaRequest']; - if (await LoaRequest.findOne({ - where: { userId: interaction.user.id, type, status: { [Op.in]: ['PENDING', 'APPROVED'] }, - endDate: { [Op.gt]: new Date() } } + if (await LoaRequest.findOne({ + where: { + userId: interaction.user.id, type, status: {[Op.in]: ['PENDING', 'APPROVED']}, + endDate: {[Op.gt]: new Date()} + } })) { - return interaction.editReply({ - content: localize('staff-management-system', 'err-status-exists', { type }) + return interaction.editReply({ + content: localize('staff-management-system', 'err-status-exists', {type}) }); } const startDate = new Date(); const endDate = new Date(startDate.getTime() + days * 24 * 60 * 60 * 1000); - const needsApproval = isLoa - ? config.requireLoaApproval !== false + const needsApproval = isLoa + ? config.requireLoaApproval !== false : config.requireRaApproval !== false; - const req = await LoaRequest.create({ - userId: interaction.user.id, - type, - reason, - startDate, - endDate, - status: needsApproval - ? 'PENDING' - : 'APPROVED' + const req = await LoaRequest.create({ + userId: interaction.user.id, + type, + reason, + startDate, + endDate, + status: needsApproval + ? 'PENDING' + : 'APPROVED' }); const logChannelId = getSafeChannelId(config.statusLogChannel); @@ -255,25 +259,28 @@ async function handleStatusRequest(client, interaction, type, durationInput, rea .setColor('Blue') .setAuthor({ name: `Request ID: ${req.id}`}) .addFields( - { name: localize('staff-management-system', 'status-req-user'), - value: interaction.user.toString(), - inline: true - }, - { name: localize('staff-management-system', 'status-req-duration'), - value: `${days} ${localize('staff-management-system', 'label-days')}`, - inline: true - }, - { name: localize('staff-management-system', 'general-rsn'), - value: reason + { + name: localize('staff-management-system', 'status-req-user'), + value: interaction.user.toString(), + inline: true + }, + { + name: localize('staff-management-system', 'status-req-duration'), + value: `${days} ${localize('staff-management-system', 'label-days')}`, + inline: true + }, + { + name: localize('staff-management-system', 'general-rsn'), + value: reason } ); - + applyFooter(client, embed); const row = new ActionRowBuilder() .addComponents(new ButtonBuilder() .setCustomId(`staff-mgmt_approve_${req.id}`) .setLabel(localize('staff-management-system', 'btn-approve')) - .setStyle(ButtonStyle.Success), + .setStyle(ButtonStyle.Success), new ButtonBuilder() .setCustomId(`staff-mgmt_deny_${req.id}`) .setLabel(localize('staff-management-system', 'btn-deny')) @@ -285,57 +292,61 @@ async function handleStatusRequest(client, interaction, type, durationInput, rea if (!needsApproval) { const roleId = config[isLoa ? 'loaRole' : 'raRole']; if (roleId) interaction.member.roles.add(roleId).catch(()=>{}); - await logStatusChange(client, type, 'start', { - targetUser: interaction.user, - startDate, - endDate, - reason, - approverId: null + await logStatusChange(client, type, 'start', { + targetUser: interaction.user, + startDate, + endDate, + reason, + approverId: null }); } - await interaction.editReply({ - content: localize('staff-management-system', 'success-status-request', { - type, state: needsApproval - ? localize('staff-management-system', 'state-pending') - : localize('staff-management-system', 'state-auto') - }) + await interaction.editReply({ + content: localize('staff-management-system', 'success-status-request', { + type, state: needsApproval + ? localize('staff-management-system', 'state-pending') + : localize('staff-management-system', 'state-auto') + }) }); } async function handleStatusView(client, interaction, type, targetUser) { const user = targetUser || interaction.user; - const request = await client.models['staff-management-system']['LoaRequest'].findOne({ - where: { userId: user.id, type, status: { [Op.in]: ['APPROVED', 'PENDING'] }, - endDate: { [Op.gt]: new Date() } }, - order: [['createdAt', 'DESC']] + const request = await client.models['staff-management-system']['LoaRequest'].findOne({ + where: { + userId: user.id, type, status: {[Op.in]: ['APPROVED', 'PENDING']}, + endDate: {[Op.gt]: new Date()} + }, + order: [['createdAt', 'DESC']] }); - if (!request) return interaction.editReply({ - content: localize('staff-management-system', 'no-active-status', { - user: user.username, - type - }) + if (!request) return interaction.editReply({ + content: localize('staff-management-system', 'no-active-status', { + user: user.username, + type + }) }); const embed = new EmbedBuilder() .setTitle(`${type} Status: ${user.username}`) - .setColor(request.status === 'APPROVED' - ? 'Green' + .setColor(request.status === 'APPROVED' + ? 'Green' : 'Yellow' ) .addFields( - { + { name: localize('staff-management-system', 'label-stat'), - value: request.status, - inline: true }, - { - name: localize('staff-management-system', 'label-end'), - value: formatDate(request.endDate), - inline: true }, - { - name: localize('staff-management-system', 'general-rsn'), - value: request.reason || localize('staff-management-system', 'info-none') + value: request.status, + inline: true + }, + { + name: localize('staff-management-system', 'label-end'), + value: formatDate(request.endDate), + inline: true + }, + { + name: localize('staff-management-system', 'general-rsn'), + value: request.reason || localize('staff-management-system', 'info-none') }) .setThumbnail(user.displayAvatarURL({ dynamic: true })); applyFooter(client, embed); @@ -351,20 +362,18 @@ async function handleStatusList(client, interaction, type, filter) { let whereClause = { type }; let title = `${type} List`; - if (filter === 'active') { - whereClause.status = 'APPROVED'; - whereClause.endDate = { [Op.gt]: now }; - title += localize('staff-management-system', 'filter-active'); - } - else if (filter === 'expired') { - whereClause.status = { [Op.in]: ['APPROVED', 'ENDED'] }; - whereClause.endDate = { [Op.between]: [cutoff, now] }; - title += localize('staff-management-system', 'filter-expired'); - } - else { - whereClause.status = { [Op.in]: ['APPROVED', 'ENDED'] }; - whereClause.endDate = { [Op.between]: [cutoff, now] }; - title += localize('staff-management-system', 'filter-history'); + if (filter === 'active') { + whereClause.status = 'APPROVED'; + whereClause.endDate = {[Op.gt]: now}; + title += localize('staff-management-system', 'filter-active'); + } else if (filter === 'expired') { + whereClause.status = {[Op.in]: ['APPROVED', 'ENDED']}; + whereClause.endDate = {[Op.between]: [cutoff, now]}; + title += localize('staff-management-system', 'filter-expired'); + } else { + whereClause.status = {[Op.in]: ['APPROVED', 'ENDED']}; + whereClause.endDate = {[Op.between]: [cutoff, now]}; + title += localize('staff-management-system', 'filter-history'); } const rows = await LoaRequest.findAll({ @@ -396,9 +405,9 @@ async function handleStatusList(client, interaction, type, filter) { async function handleStatusManage(client, interaction, targetMember, type) { const config = getConfig(client, 'status'); const meta = getStatusMeta(type); - if (!isStatusTypeEnabled(config, type)) - return interaction.editReply({ - content: localize('staff-management-system', 'err-status-disabled', { type }) + if (!isStatusTypeEnabled(config, type)) + return interaction.editReply({ + content: localize('staff-management-system', 'err-status-disabled', {type}) }); const generalConfig = getConfig(client, 'configuration'); @@ -408,44 +417,44 @@ async function handleStatusManage(client, interaction, targetMember, type) { })}; const LoaRequest = client.models['staff-management-system']['LoaRequest']; - const activeRequest = await LoaRequest.findOne({ + const activeRequest = await LoaRequest.findOne({ where: { - userId: targetMember.user.id, - type, - status: { [Op.in]: ['APPROVED', 'PENDING'] }, - endDate: { [Op.gt]: new Date() } - }, - order: [['createdAt', 'DESC']] + userId: targetMember.user.id, + type, + status: {[Op.in]: ['APPROVED', 'PENDING']}, + endDate: {[Op.gt]: new Date()} + }, + order: [['createdAt', 'DESC']] } ); - const totalCount = await LoaRequest.count({ - where: { userId: targetMember.user.id, type } + const totalCount = await LoaRequest.count({ + where: {userId: targetMember.user.id, type} }); const embed = applyFooter(client, new EmbedBuilder() - .setTitle(localize('staff-management-system', 'manage-status-title', { - label: meta.label, - username: targetMember.user.username + .setTitle(localize('staff-management-system', 'manage-status-title', { + label: meta.label, + username: targetMember.user.username })) .setThumbnail(targetMember.user.displayAvatarURL({ dynamic: true })) - .setColor(activeRequest - ? meta.color + .setColor(activeRequest + ? meta.color : 'Grey' ) - .setDescription(localize('staff-management-system', 'manage-stat-desc', { - status: activeRequest - ? meta.activeText - : localize('staff-management-system', 'no-act-stat', { - label: meta.label - }), - label: meta.label, - count: Math.max(0, totalCount - (activeRequest ? 1 : 0)) + .setDescription(localize('staff-management-system', 'manage-stat-desc', { + status: activeRequest + ? meta.activeText + : localize('staff-management-system', 'no-act-stat', { + label: meta.label + }), + label: meta.label, + count: Math.max(0, totalCount - (activeRequest ? 1 : 0)) })) ); - embed.addFields({ - name: localize('staff-management-system', 'manage-active-details', { label: meta.label }), - value: activeRequest ? `**${localize('staff-management-system', 'general-start')}:** ${formatDate(activeRequest.startDate)}\n**${localize('staff-management-system', 'general-end')}:** ${formatDate(activeRequest.endDate)}\n**${localize('staff-management-system', 'label-stat')}:** ${activeRequest.status}\n**${localize('staff-management-system', 'label-appr-by')}:** ${activeRequest.approverId ? `<@${activeRequest.approverId}>` : localize('staff-management-system', 'label-auto')}\n**${localize('staff-management-system', 'general-rsn')}:** ${activeRequest.reason || localize('staff-management-system', 'info-none')}` : localize('staff-management-system', 'manage-no-active-user', { label: meta.label }) + embed.addFields({ + name: localize('staff-management-system', 'manage-active-details', {label: meta.label}), + value: activeRequest ? `**${localize('staff-management-system', 'general-start')}:** ${formatDate(activeRequest.startDate)}\n**${localize('staff-management-system', 'general-end')}:** ${formatDate(activeRequest.endDate)}\n**${localize('staff-management-system', 'label-stat')}:** ${activeRequest.status}\n**${localize('staff-management-system', 'label-appr-by')}:** ${activeRequest.approverId ? `<@${activeRequest.approverId}>` : localize('staff-management-system', 'label-auto')}\n**${localize('staff-management-system', 'general-rsn')}:** ${activeRequest.reason || localize('staff-management-system', 'info-none')}` : localize('staff-management-system', 'manage-no-active-user', {label: meta.label}) }); const p = meta.actionPrefix; @@ -469,18 +478,18 @@ async function handleStatusManage(client, interaction, targetMember, type) { .setStyle(ButtonStyle.Secondary) .setDisabled(totalCount === 0) ); - await interaction.editReply({ - embeds: [embed.toJSON()], - components: [row.toJSON()] + await interaction.editReply({ + embeds: [embed.toJSON()], + components: [row.toJSON()] }); } async function handleStatusEnd(interaction, type) { const meta = getStatusMeta(type); const requestId = interaction.customId.split('_')[2]; - if (requestId === 'none') return interaction.reply({ - content: localize('staff-management-system', 'err-no-active-end', { label: meta.label }), - flags: MessageFlags.Ephemeral + if (requestId === 'none') return interaction.reply({ + content: localize('staff-management-system', 'err-no-active-end', {label: meta.label}), + flags: MessageFlags.Ephemeral }); const modal = new ModalBuilder() @@ -508,40 +517,40 @@ async function handleStatusEndSubmit(client, interaction, type) { const meta = getStatusMeta(type); const request = await client.models['staff-management-system']['LoaRequest'].findByPk(interaction.customId.split('_')[2]); - if (!request || request.status === 'ENDED' || request.status === 'DENIED') return interaction.reply({ - content: localize('staff-management-system', 'err-stat-inact', { label: meta.label }), - flags: MessageFlags.Ephemeral + if (!request || request.status === 'ENDED' || request.status === 'DENIED') return interaction.reply({ + content: localize('staff-management-system', 'err-stat-inact', {label: meta.label}), + flags: MessageFlags.Ephemeral }); const reason = interaction.fields.getTextInputValue('end_reason'); const member = await interaction.guild.members.fetch(request.userId).catch(() => null); - + if (member && getConfig(client, 'status')[meta.roleKey]) await member.roles.remove(getConfig(client, 'status')[meta.roleKey]).catch(() => {}); await request.update({ status: 'ENDED', endDate: new Date() }); - await client.models['staff-management-system']['StaffProfile'].update({ activityStatus: 'ACTIVE' }, { - where: { userId: request.userId } + await client.models['staff-management-system']['StaffProfile'].update({activityStatus: 'ACTIVE'}, { + where: {userId: request.userId} }); - if (member) await sendStatusDm(member.user, type, 'ended_early', { - ender: interaction.user.tag, - reason + if (member) await sendStatusDm(member.user, type, 'ended_early', { + ender: interaction.user.tag, + reason }); - await logStatusChange(client, type, 'end', { - userId: request.userId, - startDate: request.startDate, + await logStatusChange(client, type, 'end', { + userId: request.userId, + startDate: request.startDate, reason: reason, reqReason: request.reason }); const updatedEmbed = EmbedBuilder.from(interaction.message.embeds[0]) .setColor('Grey') - .setDescription(localize('staff-management-system', 'status-ended-embed-desc', { - label: meta.label, user: interaction.user.tag, reason + .setDescription(localize('staff-management-system', 'status-ended-embed-desc', { + label: meta.label, user: interaction.user.tag, reason })) - .spliceFields(0, 1, { - name: localize('staff-management-system', 'manage-active-details', { label: meta.label }), - value: localize('staff-management-system', 'manage-no-active-user', { label: meta.label }) + .spliceFields(0, 1, { + name: localize('staff-management-system', 'manage-active-details', {label: meta.label}), + value: localize('staff-management-system', 'manage-no-active-user', {label: meta.label}) }); const p = meta.actionPrefix; @@ -575,15 +584,15 @@ async function handleStatusEndSubmit(client, interaction, type) { async function handleStatusExtend(interaction, type) { const meta = getStatusMeta(type); const requestId = interaction.customId.split('_')[2]; - if (requestId === 'none') return interaction.reply({ - content: localize('staff-management-system', 'err-no-active-extend', { label: meta.label }), - flags: MessageFlags.Ephemeral + if (requestId === 'none') return interaction.reply({ + content: localize('staff-management-system', 'err-no-active-extend', {label: meta.label}), + flags: MessageFlags.Ephemeral }); const modal = new ModalBuilder() .setCustomId(`staff-mgmt_${meta.actionPrefix}-extend-submit_${requestId}`) - .setTitle(localize('staff-management-system', 'modal-extend-title', { - label: meta.label + .setTitle(localize('staff-management-system', 'modal-extend-title', { + label: meta.label })); modal.addComponents( new ActionRowBuilder() @@ -667,9 +676,9 @@ async function handleStatusExtendSubmit(client, interaction, type) { const days = parseInt(interaction.fields.getTextInputValue('extend_days'), 10); const reason = interaction.fields.getTextInputValue('extend_reason'); - if (isNaN(days) || days <= 0 || days > 180) return interaction.reply({ - content: localize('staff-management-system', 'err-inv-dur'), - flags: MessageFlags.Ephemeral + if (isNaN(days) || days <= 0 || days > 180) return interaction.reply({ + content: localize('staff-management-system', 'err-inv-dur'), + flags: MessageFlags.Ephemeral }); const oldEndDate = new Date(request.endDate); @@ -679,36 +688,36 @@ async function handleStatusExtendSubmit(client, interaction, type) { scheduleStatusExpiry(client, request); const member = await interaction.guild.members.fetch(request.userId).catch(() => null); - if (member) await sendStatusDm(member.user, type, 'extended', { - extender: interaction.user.tag, - days, - endDate: newEndDate, - reason + if (member) await sendStatusDm(member.user, type, 'extended', { + extender: interaction.user.tag, + days, + endDate: newEndDate, + reason }); - await logStatusChange(client, type, 'adjusted', { - userId: request.userId, - executorId: interaction.user.id, - changesText: localize('staff-management-system', 'status-adjusted-log', { - label: meta.label, - newEnd: ``, + await logStatusChange(client, type, 'adjusted', { + userId: request.userId, + executorId: interaction.user.id, + changesText: localize('staff-management-system', 'status-adjusted-log', { + label: meta.label, + newEnd: ``, oldEnd: ``, - reason - }) + reason + }) }); const updatedEmbed = EmbedBuilder.from(interaction.message.embeds[0]) - .spliceFields(0, 1, { - name: localize('staff-management-system', 'manage-active-details', { label: meta.label }), - value: localize('staff-management-system', 'mod-stat-ext', { - s: formatDate(request.startDate), - e: formatDate(newEndDate), - d: days, - t: request.status, - a: request.approverId - ? `<@${request.approverId}>` - : localize('staff-management-system', 'label-auto'), - r: request.reason || localize('staff-management-system', 'info-none') - }) + .spliceFields(0, 1, { + name: localize('staff-management-system', 'manage-active-details', {label: meta.label}), + value: localize('staff-management-system', 'mod-stat-ext', { + s: formatDate(request.startDate), + e: formatDate(newEndDate), + d: days, + t: request.status, + a: request.approverId + ? `<@${request.approverId}>` + : localize('staff-management-system', 'label-auto'), + r: request.reason || localize('staff-management-system', 'info-none') + }) }); return interaction.reply({ embeds: [updatedEmbed.toJSON()], @@ -722,15 +731,15 @@ async function generateStatusHistoryResponse(client, targetUser, page = 1, type) const limit = 5; const offset = (page - 1) * limit; - const { count, rows } = await client.models['staff-management-system']['LoaRequest'].findAndCountAll({ - where: { userId: targetUser.id, type }, - order: [['createdAt', 'DESC']], - limit, - offset + const {count, rows} = await client.models['staff-management-system']['LoaRequest'].findAndCountAll({ + where: {userId: targetUser.id, type}, + order: [['createdAt', 'DESC']], + limit, + offset }); - if (count === 0) return { - content: localize('staff-management-system', 'info-no-status-history', { label: meta.label }), - flags: MessageFlags.Ephemeral + if (count === 0) return { + content: localize('staff-management-system', 'info-no-status-history', {label: meta.label}), + flags: MessageFlags.Ephemeral }; const totalPages = Math.ceil(count / limit) || 1; @@ -739,62 +748,62 @@ async function generateStatusHistoryResponse(client, targetUser, page = 1, type) .setThumbnail(targetUser.displayAvatarURL({ dynamic: true })) .setColor(meta.color) .setDescription(localize('staff-management-system', 'status-history-desc', { - count: rows.length, - total: count, - label: meta.label + count: rows.length, + total: count, + label: meta.label } )) ); - const statusIcons = { - APPROVED: '✅', - DENIED: '❌', - ENDED: '⏹️', - PENDING: '🕐' + const statusIcons = { + APPROVED: '✅', + DENIED: '❌', + ENDED: '⏹️', + PENDING: '🕐' }; - rows.forEach((req, index) => embed.addFields({ - name: `${statusIcons[req.status] ?? '❓'} ${meta.label} #${offset + index + 1} - ${req.status}`, + rows.forEach((req, index) => embed.addFields({ + name: `${statusIcons[req.status] ?? '❓'} ${meta.label} #${offset + index + 1} - ${req.status}`, value: `**${localize('staff-management-system', 'general-start')}:** ${formatDate(req.startDate)}\n**${localize('staff-management-system', 'general-end')}:** ${formatDate(req.endDate)}\n**${localize('staff-management-system', 'label-appr-by')}:** ${req.approverId ? `<@${req.approverId}>` : localize('staff-management-system', 'label-auto')}\n**${localize('staff-management-system', 'general-rsn')}:** ${req.reason || localize('staff-management-system', 'info-none')}` })); - embed.addFields({ - name: '\u200b', - value: localize('staff-management-system', 'page-count', { page, total: totalPages }) + embed.addFields({ + name: '\u200b', + value: localize('staff-management-system', 'page-count', {page, total: totalPages}) }); const row = buildPaginationRow( - `staff-mgmt_${meta.actionPrefix}-hist_${targetUser.id}_${page - 1}`, - `${meta.actionPrefix}_hist_page_count`, - `staff-mgmt_${meta.actionPrefix}-hist_${targetUser.id}_${page + 1}`, - page, + `staff-mgmt_${meta.actionPrefix}-hist_${targetUser.id}_${page - 1}`, + `${meta.actionPrefix}_hist_page_count`, + `staff-mgmt_${meta.actionPrefix}-hist_${targetUser.id}_${page + 1}`, + page, totalPages ); - return { - embeds: [embed.toJSON()], - components: [row.toJSON()] + return { + embeds: [embed.toJSON()], + components: [row.toJSON()] }; } async function handleStatusHistPage(client, interaction, type) { const parts = interaction.customId.split('_'); const targetUser = await client.users.fetch(parts[2]).catch(() => null); - if (!targetUser) return interaction.reply({ - content: localize('staff-management-system', 'err-gen-no-user'), - flags: MessageFlags.Ephemeral + if (!targetUser) return interaction.reply({ + content: localize('staff-management-system', 'err-gen-no-user'), + flags: MessageFlags.Ephemeral }); const payload = await generateStatusHistoryResponse(client, targetUser, parseInt(parts[3], 10), type); - if (payload.content) return interaction.reply({ - ...payload, - flags: MessageFlags.Ephemeral + if (payload.content) return interaction.reply({ + ...payload, + flags: MessageFlags.Ephemeral }); - return interaction.message?.embeds?.[0]?.title?.startsWith(getStatusMeta(type).histTitle) - ? interaction.update(payload) + return interaction.message?.embeds?.[0]?.title?.startsWith(getStatusMeta(type).histTitle) + ? interaction.update(payload) : interaction.reply({ ...payload, flags: MessageFlags.Ephemeral }); } module.exports.beforeSubcommand = async function (interaction) { if (!interaction.replied && !interaction.deferred) { - await interaction.deferReply({ - flags: MessageFlags.Ephemeral + await interaction.deferReply({ + flags: MessageFlags.Ephemeral }); } }; @@ -804,7 +813,7 @@ module.exports.subcommands = { 'request': async function (interaction) { const duration = interaction.options.getString('duration'); const reason = interaction.options.getString('reason'); - await handleStatusRequest(interaction.client, interaction, 'LOA', duration, reason); + await handleStatusRequest(interaction.client, interaction, 'LOA', duration, reason); }, 'view': async function (interaction) { const user = interaction.options.getUser('user') || interaction.user; @@ -814,10 +823,10 @@ module.exports.subcommands = { const filter = interaction.options.getString('filter'); await handleStatusList(interaction.client, interaction, 'LOA', filter); }, - 'admin': async function (interaction) { + 'admin': async function (interaction) { const user = interaction.options.getMember('user'); - if (!user) return interaction.editReply({ - content: localize('staff-management-system', 'err-no-mem') + if (!user) return interaction.editReply({ + content: localize('staff-management-system', 'err-no-mem') }); await handleStatusManage(interaction.client, interaction, user, 'LOA'); } @@ -826,7 +835,7 @@ module.exports.subcommands = { 'request': async function (interaction) { const duration = interaction.options.getString('duration'); const reason = interaction.options.getString('reason'); - await handleStatusRequest(interaction.client, interaction, 'RA', duration, reason); + await handleStatusRequest(interaction.client, interaction, 'RA', duration, reason); }, 'view': async function (interaction) { const user = interaction.options.getUser('user') || interaction.user; @@ -836,10 +845,10 @@ module.exports.subcommands = { const filter = interaction.options.getString('filter'); await handleStatusList(interaction.client, interaction, 'RA', filter); }, - 'admin': async function (interaction) { + 'admin': async function (interaction) { const user = interaction.options.getMember('user'); - if (!user) return interaction.editReply({ - content: localize('staff-management-system', 'err-no-mem') + if (!user) return interaction.editReply({ + content: localize('staff-management-system', 'err-no-mem') }); await handleStatusManage(interaction.client, interaction, user, 'RA'); } @@ -847,9 +856,9 @@ module.exports.subcommands = { }; module.exports.config = { - name: 'status', + name: 'staff-status', description: localize('staff-management-system', 'cmd-desc-status'), - usage: '/status', + usage: '/staff-status', type: 'slash', defaultPermission: false, disabled: function (client) { @@ -868,76 +877,77 @@ module.exports.config = { name: 'loa', description: localize('staff-management-system', 'cmd-desc-loa'), options: [ - { - type: 'SUB_COMMAND', - name: 'request', - description: localize('staff-management-system', 'cmd-desc-loa-request'), + { + type: 'SUB_COMMAND', + name: 'request', + description: localize('staff-management-system', 'cmd-desc-loa-request'), options: [ - { - type: 'STRING', - name: 'duration', - description: localize('staff-management-system', 'cmd-desc-loar-duration'), - required: true - }, - { - type: 'STRING', - name: 'reason', - description: localize('staff-management-system', 'cmd-desc-loar-reason'), - required: true + { + type: 'STRING', + name: 'duration', + description: localize('staff-management-system', 'cmd-desc-loar-duration'), + required: true + }, + { + type: 'STRING', + name: 'reason', + description: localize('staff-management-system', 'cmd-desc-loar-reason'), + required: true } - ] + ] }, - { - type: 'SUB_COMMAND', - name: 'view', - description: localize('staff-management-system', 'cmd-desc-loa-view'), + { + type: 'SUB_COMMAND', + name: 'view', + description: localize('staff-management-system', 'cmd-desc-loa-view'), options: [ - { - type: 'USER', - name: 'user', - description: localize('staff-management-system', 'cmd-desc-loav-user'), - required: false + { + type: 'USER', + name: 'user', + description: localize('staff-management-system', 'cmd-desc-loav-user'), + required: false } - ] + ] }, - { - type: 'SUB_COMMAND', - name: 'list', - description: localize('staff-management-system', 'cmd-desc-loa-list'), - options: [{ - type: 'STRING', - name: 'filter', - description: localize('staff-management-system', 'cmd-desc-loal-filter'), - required: true, + { + type: 'SUB_COMMAND', + name: 'list', + description: localize('staff-management-system', 'cmd-desc-loa-list'), + options: [{ + type: 'STRING', + name: 'filter', + description: localize('staff-management-system', 'cmd-desc-loal-filter'), + required: true, choices: [ { - name: 'Active', + name: 'Active', value: 'active' - }, + }, { - name: 'Expired', + name: 'Expired', value: 'expired' - }, + }, { - name: 'All', + name: 'All', value: 'all' - }] - }] + }] + }] }, - { - type: 'SUB_COMMAND', - name: 'admin', - description: localize('staff-management-system', 'cmd-desc-loa-admin'), + { + type: 'SUB_COMMAND', + name: 'admin', + description: localize('staff-management-system', 'cmd-desc-loa-admin'), options: [ - { - type: 'USER', - name: 'user', - description: localize('staff-management-system', 'cmd-desc-loaa-user'), - required: true + { + type: 'USER', + name: 'user', + description: localize('staff-management-system', 'cmd-desc-loaa-user'), + required: true } - ] + ] } - ]}); + ] + }); } if (config.enableRa) { @@ -946,77 +956,78 @@ module.exports.config = { name: 'ra', description: localize('staff-management-system', 'cmd-desc-ra'), options: [ - { - type: 'SUB_COMMAND', - name: 'request', - description: localize('staff-management-system', 'cmd-desc-ra-request'), + { + type: 'SUB_COMMAND', + name: 'request', + description: localize('staff-management-system', 'cmd-desc-ra-request'), options: [ - { - type: 'STRING', - name: 'duration', + { + type: 'STRING', + name: 'duration', description: localize('staff-management-system', 'cmd-desc-rar-duration'), - required: true - }, - { - type: 'STRING', - name: 'reason', + required: true + }, + { + type: 'STRING', + name: 'reason', description: localize('staff-management-system', 'cmd-desc-rar-reason'), - required: true + required: true } - ] + ] }, - { - type: 'SUB_COMMAND', - name: 'view', - description: localize('staff-management-system', 'cmd-desc-ra-view'), + { + type: 'SUB_COMMAND', + name: 'view', + description: localize('staff-management-system', 'cmd-desc-ra-view'), options: [ - { - type: 'USER', - name: 'user', + { + type: 'USER', + name: 'user', description: localize('staff-management-system', 'cmd-desc-rav-user'), - required: false - }] + required: false + }] }, - { - type: 'SUB_COMMAND', - name: 'list', - description: localize('staff-management-system', 'cmd-desc-ra-list'), + { + type: 'SUB_COMMAND', + name: 'list', + description: localize('staff-management-system', 'cmd-desc-ra-list'), options: [ - { - type: 'STRING', - name: 'filter', - description: localize('staff-management-system', 'cmd-desc-ral-filter'), - required: true, + { + type: 'STRING', + name: 'filter', + description: localize('staff-management-system', 'cmd-desc-ral-filter'), + required: true, choices: [ { - name: 'Active', + name: 'Active', value: 'active' - }, + }, { - name: 'Expired', + name: 'Expired', value: 'expired' - }, + }, { - name: 'All', + name: 'All', value: 'all' } - ] - }] + ] + }] }, - { - type: 'SUB_COMMAND', - name: 'admin', - description: localize('staff-management-system', 'cmd-desc-ra-admin'), + { + type: 'SUB_COMMAND', + name: 'admin', + description: localize('staff-management-system', 'cmd-desc-ra-admin'), options: [ - { - type: 'USER', - name: 'user', - description: localize('staff-management-system', 'cmd-desc-raa-user'), - required: true + { + type: 'USER', + name: 'user', + description: localize('staff-management-system', 'cmd-desc-raa-user'), + required: true } - ] + ] } - ]}); + ] + }); } return array; diff --git a/modules/staff-management-system/configs/activity-checks.json b/modules/staff-management-system/configs/activity-checks.json index 8fbab120..634c0162 100644 --- a/modules/staff-management-system/configs/activity-checks.json +++ b/modules/staff-management-system/configs/activity-checks.json @@ -1,150 +1,100 @@ { "filename": "activity-checks.json", - "humanName": { - "en": "Activity Checks" - }, - "description": { - "en": "Configure automated staff activity checks and response logging." - }, + "humanName": "Activity Checks", + "description": "Configure automated staff activity checks and response logging.", "categories": [ { "id": "general", "icon": "fas fa-clipboard-user", - "displayName": { - "en": "General Settings" - } + "displayName": "General Settings" }, { "id": "exceptions", "icon": "fa-solid fa-badge-check", - "displayName": { - "en": "Exceptions" - } + "displayName": "Exceptions" }, { "id": "automation", "icon": "far fa-robot", - "displayName": { - "en": "Automation" - } + "displayName": "Automation" }, { "id": "results", "icon": "fa-solid fa-check-to-slot", - "displayName": { - "en": "Results & Logging" - } + "displayName": "Results & Logging" } ], "content": [ { "name": "enableActivityChecks", "category": "general", - "humanName": { - "en": "Enable Activity Checks" - }, - "description": { - "en": "Allows admins to start an activity check to see who is active." - }, + "humanName": "Enable Activity Checks", + "description": "Allows admins to start an activity check to see who is active.", "type": "boolean", - "default": { - "en": true - }, + "default": true, "elementToggle": true }, { "name": "targetRoles", "category": "general", - "humanName": { - "en": "Roles to Check" - }, - "description": { - "en": "The roles required to respond to the activity check. Anyone with these roles will be expected to click the button. Leave empty to default to the General Staff Roles." - }, + "humanName": "Roles to Check", + "description": "The roles required to respond to the activity check. Anyone with these roles will be expected to click the button. Leave empty to default to the General Staff Roles.", "type": "array", "content": "roleID", - "default": { - "en": [] - }, + "default": [], "allowNull": true }, { "name": "timeframe", "category": "general", - "humanName": { - "en": "Check Duration (Hours)" - }, - "description": { - "en": "How long staff have to respond to the activity check (Max 168 hours / 1 week)." - }, + "humanName": "Check Duration (Hours)", + "description": "How long staff have to respond to the activity check (Max 168 hours / 1 week).", "type": "integer", "minValue": 1, "maxValue": 168, - "default": { - "en": 24 - } + "default": 24 }, { "name": "checkMessage", "category": "general", - "humanName": { - "en": "Activity Check Embed" - }, - "description": { - "en": "The message sent when an activity check starts." - }, + "humanName": "Activity Check Embed", + "description": "The message sent when an activity check starts.", "type": "string", "allowEmbed": true, "params": [ - { - "name": "end-time", - "description": { - "en": "The Discord timestamp when the check ends." - } + { + "name": "end-time", + "description": "The Discord timestamp when the check ends." }, - { - "name": "duration", - "description": { - "en": "The configured duration in hours." - } + { + "name": "duration", + "description": "The configured duration in hours." } ], "default": { - "en": { - "title": "📋 Staff Activity Check", - "description": "Please click the button below to confirm your activity before %endtime%.", - "color": "#3498db" - } + "title": "📋 Staff Activity Check", + "description": "Please click the button below to confirm your activity before %endtime%.", + "color": "#3498db" } }, { "name": "sendingChannel", "category": "general", - "humanName": { - "en": "Default Sending Channel" - }, - "description": { - "en": "The default channel where the activity check message will be posted. This can manually be overridden with the command." - }, + "humanName": "Default Sending Channel", + "description": "The default channel where the activity check message will be posted. This can manually be overridden with the command.", "type": "channelID", "channelTypes": [ "GUILD_TEXT", "GUILD_NEWS" ], - "default": { - "en": "" - }, + "default": "", "allowNull": true }, { "name": "exceptionsType", "category": "exceptions", - "humanName": { - "en": "Exceptions Rule" - }, - "description": { - "en": "Who are excused from the activity checks?" - }, + "humanName": "Exceptions Rule", + "description": "Who are excused from the activity checks?", "type": "select", "content": [ "No exceptions", @@ -153,49 +103,31 @@ "LoA and RA", "Custom role(s)" ], - "default": { - "en": "LoA and RA" - } + "default": "LoA and RA" }, { "name": "customExceptionRoles", "category": "exceptions", - "humanName": { - "en": "Custom Exception Roles" - }, - "description": { - "en": "Only applies if 'Custom role(s)' is selected above." - }, + "humanName": "Custom Exception Roles", + "description": "Only applies if 'Custom role(s)' is selected above.", "type": "array", "content": "roleID", - "default": { - "en": [] - }, + "default": [], "allowNull": true }, { "name": "automatedChecks", "category": "automation", - "humanName": { - "en": "Automated Checks" - }, - "description": { - "en": "If enabled, the bot will automatically start activity checks at configured intervals." - }, + "humanName": "Automated Checks", + "description": "If enabled, the bot will automatically start activity checks at configured intervals.", "type": "boolean", - "default": { - "en": false - } + "default": false }, { "name": "automatedCheckInterval", "category": "automation", - "humanName": { - "en": "Automated Check Interval" - }, - "description": { - "en": "On which interval to start automatic checks. Choose cronjob for full customzation." - }, + "humanName": "Automated Check Interval", + "description": "On which interval to start automatic checks. Choose cronjob for full customzation.", "type": "select", "content": [ "Weekly", @@ -203,36 +135,24 @@ "Monthly", "Cronjob" ], - "default": { - "en": "Biweekly" - }, + "default": "Biweekly", "dependsOn": "automatedChecks" }, { "name": "automatedCheckCronjob", "category": "automation", - "humanName": { - "en": "Automated Check Cronjob" - }, - "description": { - "en": "The cronjob schedule for automatic checks. Only applies if 'Cronjob' is selected above." - }, + "humanName": "Automated Check Cronjob", + "description": "The cronjob schedule for automatic checks. Only applies if 'Cronjob' is selected above.", "type": "string", - "default": { - "en": "" - }, + "default": "", "dependsOn": "automatedChecks", "allowNull": true }, { "name": "automatedCheckWeekDay", "category": "automation", - "humanName": { - "en": "Automated Check Week Day" - }, - "description": { - "en": "The week day to start automatic checks." - }, + "humanName": "Automated Check Week Day", + "description": "The week day to start automatic checks.", "type": "select", "content": [ "Monday", @@ -243,41 +163,27 @@ "Saturday", "Sunday" ], - "default":{ - "en": "Monday" - }, + "default": "Monday", "dependsOn": "automatedChecks" }, { "name": "automatedCheckMonthWeek", "category": "automation", - "humanName": { - "en": "Automated Check Month Week" - }, - "description": { - "en": "The week of the month to start automatic checks. Only applies if 'Monthly' is selected above." - }, + "humanName": "Automated Check Month Week", + "description": "The week of the month to start automatic checks. Only applies if 'Monthly' is selected above.", "type": "integer", "minValue": 1, "maxValue": 4, - "default": { - "en": 1 - }, + "default": 1, "dependsOn": "automatedChecks" }, { "name": "logChannel", "category": "results", - "humanName": { - "en": "Results Channel" - }, - "description": { - "en": "Where the final results are posted. Leave empty if you want to use the general log channel." - }, + "humanName": "Results Channel", + "description": "Where the final results are posted. Leave empty if you want to use the general log channel.", "type": "channelID", - "default": { - "en": "" - }, + "default": "", "channelTypes": [ "GUILD_TEXT", "GUILD_NEWS" @@ -287,31 +193,19 @@ { "name": "pingResults", "category": "results", - "humanName": { - "en": "Ping on Results" - }, - "description": { - "en": "Ping specific roles when the results are posted." - }, + "humanName": "Ping on Results", + "description": "Ping specific roles when the results are posted.", "type": "boolean", - "default": { - "en": false - } + "default": false }, { "name": "pingRoles", "category": "results", - "humanName": { - "en": "Roles to Ping" - }, - "description": { - "en": "The roles to ping with the results message." - }, + "humanName": "Roles to Ping", + "description": "The roles to ping with the results message.", "type": "array", "content": "roleID", - "default": { - "en": [] - }, + "default": [], "dependsOn": "pingResults" } ] diff --git a/modules/staff-management-system/configs/configuration.json b/modules/staff-management-system/configs/configuration.json index a29c0df5..9b978d2c 100644 --- a/modules/staff-management-system/configs/configuration.json +++ b/modules/staff-management-system/configs/configuration.json @@ -1,90 +1,58 @@ { "filename": "configuration.json", - "humanName": { - "en": "General Configuration" - }, - "description": { - "en": "Configure the main staff roles and the default log channel." - }, + "humanName": "General Configuration", + "description": "Configure the main staff roles and the default log channel.", "categories": [ { "id": "roles", "icon": "fas fa-clipboard-user", - "displayName": { - "en": "Staff Roles" - } + "displayName": "Staff Roles" }, { "id": "logging", "icon": "fa-solid fa-clipboard-list", - "displayName": { - "en": "Logging" - } + "displayName": "Logging" } ], "content": [ { "name": "staffRoles", "category": "roles", - "humanName": { - "en": "Staff Roles" - }, - "description": { - "en": "Roles that can use basic staff commands (Shifts, LoA Request and RA Request, reviews etc.)." - }, + "humanName": "Staff Roles", + "description": "Roles that can use basic staff commands (Shifts, LoA Request and RA Request, reviews etc.).", "type": "array", "content": "roleID", - "default": { - "en": [] - } + "default": [] }, { "name": "supervisorRoles", "category": "roles", - "humanName": { - "en": "Supervisor Roles" - }, - "description": { - "en": "Roles that can manage other staff members (Approve/Deny/Manage LoA's and RA's, Manage Shifts, promote and infract users)." - }, + "humanName": "Supervisor Roles", + "description": "Roles that can manage other staff members (Approve/Deny/Manage LoA's and RA's, Manage Shifts, promote and infract users).", "type": "array", "content": "roleID", - "default": { - "en": [] - } + "default": [] }, { "name": "managementRoles", "category": "roles", - "humanName": { - "en": "Management Roles" - }, - "description": { - "en": "Roles with full access, including data deletion abilities." - }, + "humanName": "Management Roles", + "description": "Roles with full access, including data deletion abilities.", "type": "array", "content": "roleID", - "default": { - "en": [] - } + "default": [] }, { "name": "generalLogChannel", "category": "logging", - "humanName": { - "en": "General Log Channel" - }, - "description": { - "en": "The default channel where logs happen such as status changes/request and more. This can be overridden in some features." - }, + "humanName": "General Log Channel", + "description": "The default channel where logs happen such as status changes/request and more. This can be overridden in some features.", "type": "channelID", "channelTypes": [ "GUILD_TEXT", "GUILD_NEWS" ], - "default": { - "en": "" - } + "default": "" } ] } \ No newline at end of file diff --git a/modules/staff-management-system/configs/infractions.json b/modules/staff-management-system/configs/infractions.json index 971285db..89a6bc18 100644 --- a/modules/staff-management-system/configs/infractions.json +++ b/modules/staff-management-system/configs/infractions.json @@ -1,462 +1,324 @@ { "filename": "infractions.json", - "humanName": { - "en": "Infractions & Suspensions" - }, - "description": { - "en": "Configure how staff infractions, strikes, and suspensions are handled." - }, + "humanName": "Infractions & Suspensions", + "description": "Configure how staff infractions, strikes, and suspensions are handled.", "categories": [ { "id": "logic", "icon": "fas fa-hammer", - "displayName": { - "en": "General Logic" - } + "displayName": "General Logic" }, { "id": "suspensions", "icon": "fa fa-bell-exclamation", - "displayName": { - "en": "Suspensions Logic" - } + "displayName": "Suspensions Logic" }, { "id": "messages", "icon": "fa fa-messages", - "displayName": { - "en": "Messages & Embeds" - } + "displayName": "Messages & Embeds" } ], "content": [ { "name": "enableInfractions", "category": "logic", - "humanName": { - "en": "Enable Infractions System" - }, - "description": { - "en": "Enabling this will unlock features such as issuing infractions to staff members, suspensions and more." - }, + "humanName": "Enable Infractions System", + "description": "Enabling this will unlock features such as issuing infractions to staff members, suspensions and more.", "type": "boolean", "elementToggle": true, - "default": { - "en": true - } + "default": true }, { "name": "infractionTypes", "category": "logic", - "humanName": { - "en": "Infraction Types" - }, - "description": { - "en": "These are the types of infractions that can be issued to staff members. You can customize these to fit your infractions system." - }, + "humanName": "Infraction Types", + "description": "These are the types of infractions that can be issued to staff members. You can customize these to fit your infractions system.", "type": "array", "content": "string", - "default": { - "en": [ - "Warning", - "Strike", - "Demotion", - "Termination", - "Under Investigation" - ] - } + "default": [ + "Warning", + "Strike", + "Demotion", + "Termination", + "Under Investigation" + ] }, { "name": "enableSuspensions", "category": "suspensions", - "humanName": { - "en": "Enable Suspensions System" - }, - "description": { - "en": "Suspensions temporarily strip a staff member of their roles." - }, + "humanName": "Enable Suspensions System", + "description": "Suspensions temporarily strip a staff member of their roles.", "type": "boolean", - "default": { - "en": true - } + "default": true }, { "name": "suspensionHierarchyRole", "category": "suspensions", - "humanName": { - "en": "Hierarchy Base Role" - }, - "description": { - "en": "When suspending, the bot will remove all roles above and including this one. This would usually be your lowest 'Staff' role." - }, + "humanName": "Hierarchy Base Role", + "description": "When suspending, the bot will remove all roles above and including this one. This would usually be your lowest 'Staff' role.", "type": "roleID", "allowNull": true, "dependsOn": "enableSuspensions", - "default": { - "en": "" - } + "default": "" }, { "name": "suspensionRole", "category": "suspensions", - "humanName": { - "en": "Suspended Role (Optional)" - }, - "description": { - "en": "A role to assign the user while they are suspended (e.g., 'Suspended Staff')." - }, + "humanName": "Suspended Role (Optional)", + "description": "A role to assign the user while they are suspended (e.g., 'Suspended Staff').", "type": "roleID", "allowNull": true, "dependsOn": "enableSuspensions", - "default": { - "en": "" - } + "default": "" }, { "name": "suspensionMessage", "category": "suspensions", - "humanName": { - "en": "Suspension Announcement Message" - }, - "description": { - "en": "The message sent to the log channel when a staff member is suspended." - }, + "humanName": "Suspension Announcement Message", + "description": "The message sent to the log channel when a staff member is suspended.", "type": "string", "allowEmbed": true, "dependsOn": "enableSuspensions", "params": [ - { - "name": "user", - "description": { - "en": "Mention of the staff member" - } + { + "name": "user", + "description": "Mention of the staff member" }, - { - "name": "user-avatar", - "description": { - "en": "Avatar of the staff member" - }, - "isImage": true + { + "name": "user-avatar", + "description": "Avatar of the staff member", + "isImage": true }, - { - "name": "issuer-mention", - "description": { - "en": "Mention of the manager issuing it" - } + { + "name": "issuer-mention", + "description": "Mention of the manager issuing it" }, - { - "name": "issuer-name", - "description": { - "en": "Name of the issuer" - } + { + "name": "issuer-name", + "description": "Name of the issuer" }, - { - "name": "issuer-avatar", - "description": { - "en": "Avatar of the issuer" - }, - "isImage": true + { + "name": "issuer-avatar", + "description": "Avatar of the issuer", + "isImage": true }, - { - "name": "duration", - "description": { - "en": "Duration of the suspension" - } + { + "name": "duration", + "description": "Duration of the suspension" }, - { - "name": "end-date", - "description": { - "en": "Timestamp of when the suspension ends" - } + { + "name": "end-date", + "description": "Timestamp of when the suspension ends" }, - { - "name": "reason", - "description": { - "en": "Reason provided" - } + { + "name": "reason", + "description": "Reason provided" }, - { - "name": "case-id", - "description": { - "en": "Database Case ID" - } + { + "name": "case-id", + "description": "Database Case ID" } ], "default": { - "en": { - "_schema": "v3", - "content": "%user%", - "embeds": [ - { - "author": { - "name": "Signed, %issuer-name% • Case #%case-id%", - "iconURL": "%issuer-avatar%" - }, - "title": "⛔ Staff Suspension", - "description": "**Staff Member:** %user%\n**Duration:** %duration%\n**Ends:** %end-date%\n**Reason:** %reason%", - "color": "#ed4245", - "thumbnailURL": "%user-avatar%" - } - ] - } + "_schema": "v3", + "content": "%user%", + "embeds": [ + { + "author": { + "name": "Signed, %issuer-name% • Case #%case-id%", + "iconURL": "%issuer-avatar%" + }, + "title": "⛔ Staff Suspension", + "description": "**Staff Member:** %user%\n**Duration:** %duration%\n**Ends:** %end-date%\n**Reason:** %reason%", + "color": "#ed4245", + "thumbnailURL": "%user-avatar%" + } + ] } }, { "name": "infractionLogChannel", "category": "messages", - "humanName": { - "en": "Infraction Log Channel" - }, - "description": { - "en": "Where should infractions and suspensions be announced?" - }, + "humanName": "Infraction Log Channel", + "description": "Where should infractions and suspensions be announced?", "type": "channelID", "channelTypes": [ - "GUILD_TEXT", + "GUILD_TEXT", "GUILD_NEWS" ], - "default": { - "en": "" - } + "default": "" }, { "name": "infractionMessage", "category": "messages", - "humanName": { - "en": "Infraction Announcement Message" - }, - "description": { - "en": "The message sent to the log channel for regular infractions." - }, + "humanName": "Infraction Announcement Message", + "description": "The message sent to the log channel for regular infractions.", "type": "string", "allowEmbed": true, "params": [ - { - "name": "user", - "description": { - "en": "Mention of the staff member" - } + { + "name": "user", + "description": "Mention of the staff member" }, - { - "name": "user-avatar", - "description": { - "en": "Avatar of the staff member" - }, - "isImage": true + { + "name": "user-avatar", + "description": "Avatar of the staff member", + "isImage": true }, - { - "name": "issuer-mention", - "description": { - "en": "Mention of the manager issuing it" - } + { + "name": "issuer-mention", + "description": "Mention of the manager issuing it" }, - { - "name": "issuer-name", - "description": { - "en": "Name of the issuer" - } + { + "name": "issuer-name", + "description": "Name of the issuer" }, - { - "name": "issuer-avatar", - "description": { - "en": "Avatar of the issuer" - }, - "isImage": true + { + "name": "issuer-avatar", + "description": "Avatar of the issuer", + "isImage": true }, - { - "name": "type", - "description": { - "en": "Type of infraction (e.g., Warning, Strike)" - } + { + "name": "type", + "description": "Type of infraction (e.g., Warning, Strike)" }, - { - "name": "end-date", - "description": { - "en": "Timestamp of when this infraction expires" - } + { + "name": "end-date", + "description": "Timestamp of when this infraction expires" }, - { - "name": "reason", - "description": { - "en": "Reason provided" - } + { + "name": "reason", + "description": "Reason provided" }, - { - "name": "case-id", - "description": { - "en": "Database Case ID" - } + { + "name": "case-id", + "description": "Database Case ID" } ], "default": { - "en": { - "_schema": "v3", - "content": "%user%", - "embeds": [ - { - "author": { - "name": "Signed, %issuer-name% • Case #%case-id%", - "iconURL": "%issuer-avatar%" - }, - "title": "⚠️ New infraction", - "description": "**Staff Member:** %user%\n**Action Taken:** %type%\n**Expires:** %end-date%\n**Reason:** %reason%", - "color": "#e67e22", - "thumbnailURL": "%user-avatar%" - } - ] - } + "_schema": "v3", + "content": "%user%", + "embeds": [ + { + "author": { + "name": "Signed, %issuer-name% • Case #%case-id%", + "iconURL": "%issuer-avatar%" + }, + "title": "⚠️ New infraction", + "description": "**Staff Member:** %user%\n**Action Taken:** %type%\n**Expires:** %end-date%\n**Reason:** %reason%", + "color": "#e67e22", + "thumbnailURL": "%user-avatar%" + } + ] } }, { "name": "dmInfractedUser", "category": "messages", - "humanName": { - "en": "DM User on infraction?" - }, - "description": { - "en": "If enabled, the bot will DM the staff member when they receive an infraction or suspension." - }, + "humanName": "DM User on infraction?", + "description": "If enabled, the bot will DM the staff member when they receive an infraction or suspension.", "type": "boolean", - "default": { - "en": true - } + "default": true }, { "name": "infractionDmMessage", "category": "messages", - "humanName": { - "en": "Infraction DM Message" - }, - "description": { - "en": "The message sent directly to the staff member." - }, + "humanName": "Infraction DM Message", + "description": "The message sent directly to the staff member.", "type": "string", "allowEmbed": true, "dependsOn": "dmInfractedUser", "params": [ - { - "name": "user", - "description": { - "en": "Mention of the staff member" - } + { + "name": "user", + "description": "Mention of the staff member" }, - { - "name": "issuer-name", - "description": { - "en": "Name of the issuer" - } + { + "name": "issuer-name", + "description": "Name of the issuer" }, - { - "name": "type", - "description": { - "en": "Type of infraction (e.g., Warning, Strike)" - } + { + "name": "type", + "description": "Type of infraction (e.g., Warning, Strike)" }, - { - "name": "end-date", - "description": { - "en": "Timestamp of when this infraction expires" - } + { + "name": "end-date", + "description": "Timestamp of when this infraction expires" }, - { - "name": "reason", - "description": { - "en": "Reason provided" - } + { + "name": "reason", + "description": "Reason provided" }, - { - "name": "case-id", - "description": { - "en": "Database Case ID" - } + { + "name": "case-id", + "description": "Database Case ID" } ], "default": { - "en": { - "_schema": "v3", - "embeds": [ - { - "author": { - "name": "Signed, %issuer-name% • Case #%case-id%" - }, - "title": "⚠️ You have been infracted", - "description": "**Type:** %type%\n**Reason:** %reason%\n**Expires:** %end-date%", - "color": "#e67e22" - } - ] - } + "_schema": "v3", + "embeds": [ + { + "author": { + "name": "Signed, %issuer-name% • Case #%case-id%" + }, + "title": "⚠️ You have been infracted", + "description": "**Type:** %type%\n**Reason:** %reason%\n**Expires:** %end-date%", + "color": "#e67e22" + } + ] } }, { "name": "suspensionDmMessage", "category": "messages", - "humanName": { - "en": "Suspension DM Message1" - }, - "description": { - "en": "The message sent directly to the staff member when suspended." - }, + "humanName": "Suspension DM Message1", + "description": "The message sent directly to the staff member when suspended.", "type": "string", "allowEmbed": true, "dependsOn": "dmInfractedUser", "params": [ - { - "name": "user", - "description": { - "en": "Mention of the staff member" - } + { + "name": "user", + "description": "Mention of the staff member" }, - { - "name": "issuer-name", - "description": { - "en": "Name of the issuer" - } + { + "name": "issuer-name", + "description": "Name of the issuer" }, - { - "name": "type", - "description": { - "en": "Type of infraction (e.g., Warning, Strike)" - } + { + "name": "type", + "description": "Type of infraction (e.g., Warning, Strike)" }, - { - "name": "duration", - "description": { - "en": "Duration of the suspension" - } + { + "name": "duration", + "description": "Duration of the suspension" }, - { - "name": "end-date", - "description": { - "en": "Timestamp of when this infraction expires" - } + { + "name": "end-date", + "description": "Timestamp of when this infraction expires" }, - { - "name": "reason", - "description": { - "en": "Reason provided" - } + { + "name": "reason", + "description": "Reason provided" }, - { - "name": "case-id", - "description": { - "en": "Database Case ID" - } + { + "name": "case-id", + "description": "Database Case ID" } ], "default": { - "en": { - "_schema": "v3", - "embeds": [ - { - "author": { - "name": "Signed, %issuer-name% • Case #%case-id%" - }, - "title": "⛔ Staff Suspension", - "description": "You have been temporarily suspended.\n\n**Duration:** %duration%\n**Returns:** %end-date%\n**Reason:** %reason%", - "color": "#ed4245" - } - ] - } + "_schema": "v3", + "embeds": [ + { + "author": { + "name": "Signed, %issuer-name% • Case #%case-id%" + }, + "title": "⛔ Staff Suspension", + "description": "You have been temporarily suspended.\n\n**Duration:** %duration%\n**Returns:** %end-date%\n**Reason:** %reason%", + "color": "#ed4245" + } + ] } } ] diff --git a/modules/staff-management-system/configs/profiles.json b/modules/staff-management-system/configs/profiles.json index 5865ccd6..90737ac9 100644 --- a/modules/staff-management-system/configs/profiles.json +++ b/modules/staff-management-system/configs/profiles.json @@ -1,148 +1,104 @@ { "filename": "profiles.json", - "humanName": { - "en": "Staff Profiles" - }, - "description": { - "en": "Configure the staff profile system (Intros, custom nicknames, and stats)." - }, + "humanName": "Staff Profiles", + "description": "Configure the staff profile system (Intros, custom nicknames, and stats).", "categories": [ { "id": "settings", "icon": "fa-user-tie", - "displayName": { - "en": "Profile Settings" - } + "displayName": "Profile Settings" } ], "content": [ { "name": "enableProfiles", "category": "settings", - "humanName": { - "en": "Enable Staff Profiles" - }, - "description": { - "en": "Allows staff to have a profile tracking their shifts, reviews, and a custom introduction." - }, + "humanName": "Enable Staff Profiles", + "description": "Allows staff to have a profile tracking their shifts, reviews, and a custom introduction.", "type": "boolean", - "default": { - "en": true - }, + "default": true, "elementToggle": true }, { "name": "onlyAllowStaffProfile", "category": "settings", - "humanName": { - "en": "Only allow staff and higher to have their own customizable profile" - }, - "description": { - "en": "If enabled, only staff members and higher will be able to set a custom profile nickname and introduction. If disabled, all members will be able to set a custom profile nickname and introduction." - }, + "humanName": "Only allow staff and higher to have their own customizable profile", + "description": "If enabled, only staff members and higher will be able to set a custom profile nickname and introduction. If disabled, all members will be able to set a custom profile nickname and introduction.", "type": "boolean", - "default": { - "en": true - } + "default": true }, { "name": "managePermission", "category": "settings", - "humanName": { - "en": "Profile Moderation Permission" - }, - "description": { - "en": "Which group is allowed to forcibly wipe another staff member's profile?" - }, + "humanName": "Profile Moderation Permission", + "description": "Which group is allowed to forcibly wipe another staff member's profile?", "type": "select", "content": [ "Supervisor", "Management" ], - "default": { - "en": "Supervisor" - } + "default": "Supervisor" }, { "name": "profileEmbedMessage", "category": "settings", - "humanName": { - "en": "Profile Embed" - }, - "description": { - "en": "Customize the embed shown when viewing a staff profile." - }, + "humanName": "Profile Embed", + "description": "Customize the embed shown when viewing a staff profile.", "type": "string", "allowEmbed": true, "params": [ { "name": "user-mention", - "description": { - "en": "The user's mention." - } + "description": "The user's mention." }, { "name": "username", - "description": { - "en": "The user's standard Discord username." - } + "description": "The user's standard Discord username." }, { "name": "nickname", - "description": { - "en": "The user's custom profile nickname (uses default username if not set)." - } + "description": "The user's custom profile nickname (uses default username if not set)." }, { "name": "intro", - "description": { - "en": "The user's custom introduction." - } + "description": "The user's custom introduction." }, { "name": "status", - "description": { - "en": "The user's current status (LoA, RA, etc.)." - } + "description": "The user's current status (LoA, RA, etc.)." }, { "name": "rating", - "description": { - "en": "The user's average review rating." - } + "description": "The user's average review rating." }, { "name": "avatar", - "description": { - "en": "The user's avatar URL." - }, + "description": "The user's avatar URL.", "isImage": true } ], "default": { - "en": { - "_schema": "v3", - "embeds": [ - { - "title": "Staff Profile: %nickname%", - "description": "%intro%", - "color": "#2b2d31", - "thumbnailURL": "%avatar%", - "fields": [ - { - "name": "Status", - "value": "%status%", - "inline": true - }, - { - "name": "Average Rating", - "value": "%rating%", - "inline": true - } - ] - } - ] - } + "_schema": "v3", + "embeds": [ + { + "title": "Staff Profile: %nickname%", + "description": "%intro%", + "color": "#2b2d31", + "thumbnailURL": "%avatar%", + "fields": [ + { + "name": "Status", + "value": "%status%", + "inline": true + }, + { + "name": "Average Rating", + "value": "%rating%", + "inline": true + } + ] + } + ] } } ] diff --git a/modules/staff-management-system/configs/promotions.json b/modules/staff-management-system/configs/promotions.json index 20fb6a05..9bb70557 100644 --- a/modules/staff-management-system/configs/promotions.json +++ b/modules/staff-management-system/configs/promotions.json @@ -1,252 +1,176 @@ { "filename": "promotions.json", - "humanName": { - "en": "Promotions" - }, - "description": { - "en": "Configure how staff promotions are handled and announced." - }, + "humanName": "Promotions", + "description": "Configure how staff promotions are handled and announced.", "categories": [ { "id": "logic", "icon": "fas fa-gears", - "displayName": { - "en": "General logic" - } + "displayName": "General logic" }, { "id": "messages", "icon": "fas fa-comment-dots", - "displayName": { - "en": "Announcements" - } + "displayName": "Announcements" } ], "content": [ { "name": "enablePromotions", "category": "logic", - "humanName": { - "en": "Enable Promotions System" - }, - "description": { - "en": "If disabled, the /staff-management promote command will not work." - }, + "humanName": "Enable Promotions System", + "description": "If disabled, the /staff-management promote command will not work.", "type": "boolean", - "default": { - "en": true - }, + "default": true, "elementToggle": true }, { "name": "autoAddRole", "category": "logic", - "humanName": { - "en": "Auto-Add New Role?" - }, - "description": { - "en": "If enabled, the bot will automatically give the user the new rank role. Note: This makes your server prone to raids by promoting someone to a role with more dangerous permissions which can be used to do malicious actions. It is recommended to keep this setting disabled." - }, + "humanName": "Auto-Add New Role?", + "description": "If enabled, the bot will automatically give the user the new rank role. Note: This makes your server prone to raids by promoting someone to a role with more dangerous permissions which can be used to do malicious actions. It is recommended to keep this setting disabled.", "type": "boolean", - "default": { - "en": true - } + "default": true }, { "name": "promotionsChannel", "category": "messages", - "humanName": { - "en": "Promotions Channel" - }, - "description": { - "en": "The channel where promotion announcements will be sent." - }, + "humanName": "Promotions Channel", + "description": "The channel where promotion announcements will be sent.", "type": "channelID", "channelTypes": [ "GUILD_TEXT", "GUILD_NEWS" ], - "default": { - "en": "" - } + "default": "" }, { "name": "promotionMessage", "category": "messages", - "humanName": { - "en": "Promotion Announcement Embed" - }, - "description": { - "en": "This will be the message sent when someone is promoted." - }, + "humanName": "Promotion Announcement Embed", + "description": "This will be the message sent when someone is promoted.", "type": "string", "allowEmbed": true, "params": [ { "name": "user-mention", - "description": { - "en": "Pings the promoted user." - } + "description": "Pings the promoted user." }, { "name": "new-role-name", - "description": { - "en": "The plain text name of the new role." - } + "description": "The plain text name of the new role." }, { "name": "new-role-mention", - "description": { - "en": "The pingable mention of the new role." - } + "description": "The pingable mention of the new role." }, { "name": "promoter-mention", - "description": { - "en": "Pings the staff member who issued the promotion." - } + "description": "Pings the staff member who issued the promotion." }, { "name": "promoter-name", - "description": { - "en": "The username of the staff member who issued the promotion." - } + "description": "The username of the staff member who issued the promotion." }, { "name": "reason", - "description": { - "en": "The reason for the promotion." - } + "description": "The reason for the promotion." }, { "name": "user-avatar", - "description": { - "en": "The avatar URL of the promoted user." - }, + "description": "The avatar URL of the promoted user.", "isImage": true }, { "name": "promoter-avatar", - "description": { - "en": "The avatar URL of the promoter." - }, + "description": "The avatar URL of the promoter.", "isImage": true } ], "default": { - "en": { - "_schema": "v3", - "content": "%user-mention%", - "embeds": [ - { - "author": { - "name": "Signed, %promoter-name%", - "imageURL": "%promoter-avatar%" - }, - "title": "🎉 New promotion!", - "description": "Congratulations, you have been promoted to **%new-role-name%**!\n\n**Promoted to:** %new-role-mention%\n**On behalf of:** %promoter-mention%\n**Reason:** %reason%", - "color": "#f1c40f", - "thumbnailURL": "%user-avatar%" - } - ] - } + "_schema": "v3", + "content": "%user-mention%", + "embeds": [ + { + "author": { + "name": "Signed, %promoter-name%", + "imageURL": "%promoter-avatar%" + }, + "title": "🎉 New promotion!", + "description": "Congratulations, you have been promoted to **%new-role-name%**!\n\n**Promoted to:** %new-role-mention%\n**On behalf of:** %promoter-mention%\n**Reason:** %reason%", + "color": "#f1c40f", + "thumbnailURL": "%user-avatar%" + } + ] } }, { "name": "dmPromotedUser", "category": "messages", - "humanName": { - "en": "DM Promoted User?" - }, - "description": { - "en": "If enabled, the user will receive a direct message when promoted." - }, + "humanName": "DM Promoted User?", + "description": "If enabled, the user will receive a direct message when promoted.", "type": "boolean", - "default": { - "en": false - } + "default": false }, { "name": "promotionDmMessage", "category": "messages", - "humanName": { - "en": "Promotion DM Embed" - }, - "description": { - "en": "The message sent directly to the user." - }, + "humanName": "Promotion DM Embed", + "description": "The message sent directly to the user.", "type": "string", "allowEmbed": true, "dependsOn": "dmPromotedUser", "params": [ { "name": "user-mention", - "description": { - "en": "Pings the promoted user." - } + "description": "Pings the promoted user." }, { "name": "new-role-name", - "description": { - "en": "The plain text name of the new role." - } + "description": "The plain text name of the new role." }, { "name": "new-role-mention", - "description": { - "en": "The pingable mention of the new role." - } + "description": "The pingable mention of the new role." }, { "name": "promoter-mention", - "description": { - "en": "Pings the staff member who issued the promotion." - } + "description": "Pings the staff member who issued the promotion." }, { "name": "promoter-name", - "description": { - "en": "The username of the staff member who issued the promotion." - } + "description": "The username of the staff member who issued the promotion." }, { "name": "reason", - "description": { - "en": "The reason for the promotion." - } + "description": "The reason for the promotion." }, { "name": "user-avatar", - "description": { - "en": "The avatar URL of the promoted user." - }, + "description": "The avatar URL of the promoted user.", "isImage": true }, { "name": "promoter-avatar", - "description": { - "en": "The avatar URL of the promoter." - }, + "description": "The avatar URL of the promoter.", "isImage": true } ], "default": { - "en": { - "_schema": "v3", - "content": "%user-mention%", - "embeds": [ - { - "author": { - "name": "Signed, %promoter-name%", - "imageURL": "%promoter-avatar%" - }, - "title": "🎉 New promotion!", - "description": "Congratulations, you have been promoted to **%new-role-name%**!\n\n**Promoted to:** %new-role-mention%\n**On behalf of:** %promoter-mention%\n**Reason:** %reason%", - "color": "#f1c40f", - "thumbnailURL": "%user-avatar%" - } - ] - } + "_schema": "v3", + "content": "%user-mention%", + "embeds": [ + { + "author": { + "name": "Signed, %promoter-name%", + "imageURL": "%promoter-avatar%" + }, + "title": "🎉 New promotion!", + "description": "Congratulations, you have been promoted to **%new-role-name%**!\n\n**Promoted to:** %new-role-mention%\n**On behalf of:** %promoter-mention%\n**Reason:** %reason%", + "color": "#f1c40f", + "thumbnailURL": "%user-avatar%" + } + ] } } ] diff --git a/modules/staff-management-system/configs/reviews.json b/modules/staff-management-system/configs/reviews.json index cb1d3eda..b23dd317 100644 --- a/modules/staff-management-system/configs/reviews.json +++ b/modules/staff-management-system/configs/reviews.json @@ -1,158 +1,106 @@ { "filename": "reviews.json", - "humanName": { - "en": "Staff Reviews" - }, - "description": { - "en": "Configure the staff rating system and feedback channels." - }, + "humanName": "Staff Reviews", + "description": "Configure the staff rating system and feedback channels.", "categories": [ { "id": "settings", "icon": "fas fa-gears", - "displayName": { - "en": "Settings" - } + "displayName": "Settings" }, { "id": "messages", "icon": "fa fa-messages", - "displayName": { - "en": "Notifications" - } + "displayName": "Notifications" } ], "content": [ { "name": "enableReviews", "category": "settings", - "humanName": { - "en": "Enable Reviews System" - }, - "description": { - "en": "Enabling this unlocks the staff review system, allowing users to submit ratings and feedback for staff members." - }, + "humanName": "Enable Reviews System", + "description": "Enabling this unlocks the staff review system, allowing users to submit ratings and feedback for staff members.", "type": "boolean", - "default": { - "en": true - } + "default": true }, { "name": "reviewLogChannel", "category": "settings", - "humanName": { - "en": "Reviews Log Channel" - }, - "description": { - "en": "Channel where new reviews are posted." - }, + "humanName": "Reviews Log Channel", + "description": "Channel where new reviews are posted.", "type": "channelID", "channelTypes": [ "GUILD_TEXT", "GUILD_NEWS" ], - "default": { - "en": "" - } + "default": "" }, { "name": "allowSelfRating", "category": "settings", - "humanName": { - "en": "Allow Self-Rating?" - }, - "description": { - "en": "If enabled, staff can review themselves. This is not recommended to keep a fair ratings system." - }, + "humanName": "Allow Self-Rating?", + "description": "If enabled, staff can review themselves. This is not recommended to keep a fair ratings system.", "type": "boolean", - "default": { - "en": false - } + "default": false }, { "name": "onlyAllowStaffReview", "category": "settings", - "humanName": { - "en": "Only let users review staff" - }, - "description": { - "en": "If enabled, only staff members can review other staff members." - }, + "humanName": "Only let users review staff", + "description": "If enabled, only staff members can review other staff members.", "type": "boolean", - "default": { - "en": true - } + "default": true }, { "name": "ratingMessage", "category": "messages", - "humanName": { - "en": "Review Message" - }, - "description": { - "en": "The message sent when a review is submitted." - }, + "humanName": "Review Message", + "description": "The message sent when a review is submitted.", "type": "string", "allowEmbed": true, "params": [ - { - "name": "staff-mention", - "description": { - "en": "Mention of the staff member" - } + { + "name": "staff-mention", + "description": "Mention of the staff member" }, - { - "name": "reviewer-mention", - "description": { - "en": "Mention of the reviewer" - } + { + "name": "reviewer-mention", + "description": "Mention of the reviewer" }, - { - "name": "stars", - "description": { - "en": "Amount of stars rated in emoji's (⭐⭐⭐⭐⭐)" - } + { + "name": "stars", + "description": "Amount of stars rated in emoji's (⭐⭐⭐⭐⭐)" }, - { - "name": "rating", - "description": { - "en": "Amount of stars rated in text (1-5)" - } + { + "name": "rating", + "description": "Amount of stars rated in text (1-5)" }, - { - "name": "comment", - "description": { - "en": "The review's text" - } + { + "name": "comment", + "description": "The review's text" }, - { - "name": "staff-avatar", - "description": { - "en": "The staff member's profile picture (URL)" - }, + { + "name": "staff-avatar", + "description": "The staff member's profile picture (URL)", "isImage": true }, - { - "name": "reviewer-avatar", - "description": { - "en": "The reviewer's profile picture (URL)" - }, + { + "name": "reviewer-avatar", + "description": "The reviewer's profile picture (URL)", "isImage": true } ], "default": { - "en": { - "_schema": "v3", - "content": "%staff%", - "embeds": [ - { - "title": "🌟 New Staff Rating", - "description": "**Staff:** %staff-mention%\n**Rated by:** %reviewer-mention%\n\n**Rating:** %stars% (%rating%/5)\n**Comment:**\n%comment%", - "color": "#f1c40f", - "thumbnailURL": "%staff-avatar%" - } - ] - } + "_schema": "v3", + "content": "%staff%", + "embeds": [ + { + "title": "🌟 New Staff Rating", + "description": "**Staff:** %staff-mention%\n**Rated by:** %reviewer-mention%\n\n**Rating:** %stars% (%rating%/5)\n**Comment:**\n%comment%", + "color": "#f1c40f", + "thumbnailURL": "%staff-avatar%" + } + ] } } ] diff --git a/modules/staff-management-system/configs/shifts.json b/modules/staff-management-system/configs/shifts.json index 614b5c9a..728afd4a 100644 --- a/modules/staff-management-system/configs/shifts.json +++ b/modules/staff-management-system/configs/shifts.json @@ -1,219 +1,143 @@ { "filename": "shifts.json", - "humanName": { - "en": "Shift Management" - }, - "description": { - "en": "Configure shift requirements, duty roles, leaderboards, and quotas." - }, + "humanName": "Shift Management", + "description": "Configure shift requirements, duty roles, leaderboards, and quotas.", "categories": [ { "id": "settings", "icon": "fas fa-gears", - "displayName": { - "en": "Shift Settings" - } + "displayName": "Shift Settings" }, { "id": "leaderboard", "icon": "fas fa-ranking-stars", - "displayName": { - "en": "Leaderboard" - } + "displayName": "Leaderboard" }, { "id": "quotas", "icon": "fa-solid fa-check-to-slot", - "displayName": { - "en": "Quotas" - } + "displayName": "Quotas" }, { "id": "logging", "icon": "fas fa-message-lines", - "displayName": { - "en": "Logging" - } + "displayName": "Logging" } ], "content": [ { "name": "enableShifts", "category": "settings", - "humanName": { - "en": "Enable Shifts" - }, - "description": { - "en": "This unlocks the ability for staff to use a shifts system, where they can get on-duty, off-duty, take a break and see their total duty time." - }, + "humanName": "Enable Shifts", + "description": "This unlocks the ability for staff to use a shifts system, where they can get on-duty, off-duty, take a break and see their total duty time.", "type": "boolean", - "default": { - "en": true - }, + "default": true, "elementToggle": true }, { "name": "onDutyRole", "category": "settings", - "humanName": { - "en": "On-Duty Role" - }, - "description": { - "en": "Role given to users when they are on-duty. This is optional, but recommended to easily identify who is on-duty." - }, + "humanName": "On-Duty Role", + "description": "Role given to users when they are on-duty. This is optional, but recommended to easily identify who is on-duty.", "type": "roleID", "allowNull": true, - "default": { - "en": "" - } + "default": "" }, { "name": "dutyTypes", "category": "settings", - "humanName": { - "en": "Duty Types" - }, - "description": { - "en": "The types of duty a staff member can select when going on-duty." - }, + "humanName": "Duty Types", + "description": "The types of duty a staff member can select when going on-duty.", "type": "array", "content": "string", - "default": { - "en": ["Staff"] - } + "default": [ + "Staff" + ] }, { "name": "minShiftDuration", "category": "settings", - "humanName": { - "en": "Minimum Shift Duration (minutes)" - }, - "description": { - "en": "A minimum shift duration for a shift to count towards their duty time. Default is 0, which means all shift time counts." - }, + "humanName": "Minimum Shift Duration (minutes)", + "description": "A minimum shift duration for a shift to count towards their duty time. Default is 0, which means all shift time counts.", "type": "integer", - "default": { - "en": 0 - }, + "default": 0, "minValue": 0 }, { "name": "enableLeaderboard", "category": "leaderboard", - "humanName": { - "en": "Enable duty leaderboard" - }, - "description": { - "en": "If enabled, staff can see a leaderboard of who has the most duty time in the configured timeframe." - }, + "humanName": "Enable duty leaderboard", + "description": "If enabled, staff can see a leaderboard of who has the most duty time in the configured timeframe.", "type": "boolean", - "default": { - "en": true - } + "default": true }, { "name": "leaderboardLookback", "category": "leaderboard", - "humanName": { - "en": "Leaderboard Timeframe" - }, - "description": { - "en": "The timeframe of the duty time shown on the leaderboard." - }, + "humanName": "Leaderboard Timeframe", + "description": "The timeframe of the duty time shown on the leaderboard.", "type": "select", "content": [ "Weekly", "Monthly", "All-time" ], - "default": { - "en": "Weekly" - }, + "default": "Weekly", "dependsOn": "enableLeaderboard" }, { "name": "enableQuotas", "category": "quotas", - "humanName": { - "en": "Enable Quota System" - }, - "description": { - "en": "If enabled, you can set a custom quota of hours for staff to meet in the configured timeframe." - }, + "humanName": "Enable Quota System", + "description": "If enabled, you can set a custom quota of hours for staff to meet in the configured timeframe.", "type": "boolean", - "default": { - "en": false - } + "default": false }, { "name": "quotaTimeframe", "category": "quotas", - "humanName": { - "en": "Quota Timeframe" - }, - "description": { - "en": "The timeframe in which the quota must be met." - }, + "humanName": "Quota Timeframe", + "description": "The timeframe in which the quota must be met.", "type": "select", "content": [ "Weekly", "Monthly" ], - "default": { - "en": "Weekly" - }, + "default": "Weekly", "dependsOn": "enableQuotas" }, { "name": "quotas", "category": "quotas", - "humanName": { - "en": "Role Quotas" - }, - "description": { - "en": "Set required hours per role - the left side will be the role, and the right side is a number which is the hours for the quota. The user's highest role counts as their quota." - }, + "humanName": "Role Quotas", + "description": "Set required hours per role - the left side will be the role, and the right side is a number which is the hours for the quota. The user's highest role counts as their quota.", "type": "keyed", "content": { "key": "roleID", "value": "integer" }, - "default": { - "en": {} - }, + "default": {}, "dependsOn": "enableQuotas" }, { "name": "logShiftChanges", "category": "logging", - "humanName": { - "en": "Log Shift Changes" - }, - "description":{ - "en": "When enabled, shift changes (such as going on-duty, on break, or off-duty) will be logged in a custom channel." - }, + "humanName": "Log Shift Changes", + "description": "When enabled, shift changes (such as going on-duty, on break, or off-duty) will be logged in a custom channel.", "type": "boolean", - "default": { - "en": true - } + "default": true }, { "name": "logShiftChangesChannel", "category": "logging", - "humanName": { - "en": "Channel for shift change logs" - }, - "description": { - "en": "The channel where shift changes will be logged. You can set this empty to use the general log channel." - }, + "humanName": "Channel for shift change logs", + "description": "The channel where shift changes will be logged. You can set this empty to use the general log channel.", "type": "channelID", "channelTypes": [ "GUILD_TEXT", "GUILD_NEWS" ], - "default": { - "en": "" - }, + "default": "", "allowNull": true, "dependsOn": "logShiftChanges" } diff --git a/modules/staff-management-system/configs/status.json b/modules/staff-management-system/configs/status.json index e59d216c..ae37834e 100644 --- a/modules/staff-management-system/configs/status.json +++ b/modules/staff-management-system/configs/status.json @@ -1,230 +1,146 @@ { "filename": "status.json", - "humanName": { - "en": "LoA & RA Status" - }, - "description": { - "en": "Configure Leave of Absence (LoA) and Reduced Activity (RA) settings." - }, + "humanName": "LoA & RA Status", + "description": "Configure Leave of Absence (LoA) and Reduced Activity (RA) settings.", "categories": [ { "id": "base", "icon": "fas fa-gears", - "displayName": { - "en": "Base Settings" - } + "displayName": "Base Settings" }, { "id": "loa", "icon": "fas fa-door-open", - "displayName": { - "en": "LoA Settings" - } + "displayName": "LoA Settings" }, { "id": "ra", "icon": "fa-user-tie", - "displayName": { - "en": "RA Settings" - } + "displayName": "RA Settings" }, { "id": "logging", "icon": "fa-solid fa-clipboard-list", - "displayName": { - "en": "Requests Log" - } + "displayName": "Requests Log" } ], "content": [ { "name": "enableStatusSystem", "category": "base", - "humanName": { - "en": "Enable Status System" - }, - "description": { - "en": "Enabling this unlocks the Leave of Absence (LoA) and Reduced Activity (RA) system, allowing staff to request these statuses and have them tracked." - }, + "humanName": "Enable Status System", + "description": "Enabling this unlocks the Leave of Absence (LoA) and Reduced Activity (RA) system, allowing staff to request these statuses and have them tracked.", "type": "boolean", - "default": { - "en": false - }, + "default": false, "elementToggle": true }, { "name": "enableLoa", "category": "loa", - "humanName": { - "en": "Enable LoA System" - }, - "description": { - "en": "If enabled, staff can request a Leave of Absence (LoA)." - }, + "humanName": "Enable LoA System", + "description": "If enabled, staff can request a Leave of Absence (LoA).", "type": "boolean", - "default": { - "en": true - } + "default": true }, { "name": "loaRole", "category": "loa", - "humanName": { - "en": "LoA Role" - }, - "description": { - "en": "Role given to users when they are on a Leave of Absence. This is optional, but recommended to easily identify who is on LoA." - }, + "humanName": "LoA Role", + "description": "Role given to users when they are on a Leave of Absence. This is optional, but recommended to easily identify who is on LoA.", "type": "roleID", "allowNull": true, - "default": { - "en": "" - }, + "default": "", "dependsOn": "enableLoa" }, { "name": "loaMaxDays", "category": "loa", - "humanName": { - "en": "Maximum LoA Duration (days)" - }, - "description": { - "en": "The maximum duration for a Leave of Absence in days. This limits how long staff can request to be on LoA for." - }, + "humanName": "Maximum LoA Duration (days)", + "description": "The maximum duration for a Leave of Absence in days. This limits how long staff can request to be on LoA for.", "type": "integer", - "default": { - "en": 60 - }, + "default": 60, "minValue": 1, "dependsOn": "enableLoa" }, { "name": "requireLoaApproval", "category": "loa", - "humanName": { - "en": "Require Approval for LoA?" - }, - "description": { - "en": "If enabled, LoA requests will require approval from staff who have supervisor permissions or higher." - }, + "humanName": "Require Approval for LoA?", + "description": "If enabled, LoA requests will require approval from staff who have supervisor permissions or higher.", "type": "boolean", - "default": { - "en": true - }, + "default": true, "dependsOn": "enableLoa" }, { "name": "enableRa", "category": "ra", - "humanName": { - "en": "Enable RA System" - }, - "description": { - "en": "If enabled, staff can request Reduced Activity (RA) status for when they are still working but at a reduced load." - }, + "humanName": "Enable RA System", + "description": "If enabled, staff can request Reduced Activity (RA) status for when they are still working but at a reduced load.", "type": "boolean", - "default": { - "en": true - } + "default": true }, { "name": "raRole", "category": "ra", - "humanName": { - "en": "RA Role" - }, - "description": { - "en": "Role given to users when they are on Reduced Activity. This is optional, but recommended to easily identify who is on RA." - }, + "humanName": "RA Role", + "description": "Role given to users when they are on Reduced Activity. This is optional, but recommended to easily identify who is on RA.", "type": "roleID", "allowNull": true, - "default": { - "en": "" - }, + "default": "", "dependsOn": "enableRa" }, { "name": "raMaxDays", "category": "ra", - "humanName": { - "en": "Maximum RA Duration (days)" - }, - "description": { - "en": "The maximum duration for RA in days. This limits how long staff can request to be on RA for." - }, + "humanName": "Maximum RA Duration (days)", + "description": "The maximum duration for RA in days. This limits how long staff can request to be on RA for.", "type": "integer", - "default": { - "en": 30 - }, + "default": 30, "minValue": 1, "dependsOn": "enableRa" }, { "name": "requireRaApproval", "category": "ra", - "humanName": { - "en": "Require Approval for RA?" - }, - "description": { - "en": "If enabled, RA requests will require approval from staff who have supervisor permissions or higher." - }, + "humanName": "Require Approval for RA?", + "description": "If enabled, RA requests will require approval from staff who have supervisor permissions or higher.", "type": "boolean", - "default": { - "en": true - }, + "default": true, "dependsOn": "enableRa" }, { "name": "statusLogChannel", "category": "logging", - "humanName": { - "en": "Status Request Channel" - }, - "description": { - "en": "Channel where requests are sent for approval." - }, + "humanName": "Status Request Channel", + "description": "Channel where requests are sent for approval.", "type": "channelID", "allowNull": true, "channelTypes": [ "GUILD_TEXT", "GUILD_NEWS" ], - "default": { - "en": "" - } + "default": "" }, { "name": "logStatusChanges", "category": "logging", - "humanName": { - "en": "Log status changes" - }, - "description": { - "en": "If enabled, any changes in staff status (going on/off LoA or RA) will be logged in the configured channel." - }, + "humanName": "Log status changes", + "description": "If enabled, any changes in staff status (going on/off LoA or RA) will be logged in the configured channel.", "type": "boolean", - "default": { - "en": true - } + "default": true }, { "name": "statusChangeLogChannel", "category": "logging", - "humanName": { - "en": "Status Change Log Channel" - }, - "description": { - "en": "Channel where status changes are logged. By default this uses your main log channel, but you can set a separate channel here." - }, + "humanName": "Status Change Log Channel", + "description": "Channel where status changes are logged. By default this uses your main log channel, but you can set a separate channel here.", "type": "channelID", "allowNull": true, "channelTypes": [ "GUILD_TEXT", "GUILD_NEWS" ], - "default": { - "en": "" - }, + "default": "", "dependsOn": "logStatusChanges" } ] diff --git a/modules/staff-management-system/events/botReady.js b/modules/staff-management-system/events/botReady.js index eff0a9e7..e2d42cab 100644 --- a/modules/staff-management-system/events/botReady.js +++ b/modules/staff-management-system/events/botReady.js @@ -1,7 +1,7 @@ const schedule = require('node-schedule'); const { localize } = require('../../../src/functions/localize'); const { Op } = require('sequelize'); -const { scheduleStatusExpiry } = require('../commands/status.js'); +const {scheduleStatusExpiry} = require('../commands/staff-status.js'); const { initActivityCheckAutomation } = require('../staff-management'); const suspension_check_job = 'staff-management-checks'; diff --git a/modules/staff-management-system/events/interactionCreate.js b/modules/staff-management-system/events/interactionCreate.js index 9aa2ed04..804f7400 100644 --- a/modules/staff-management-system/events/interactionCreate.js +++ b/modules/staff-management-system/events/interactionCreate.js @@ -25,7 +25,7 @@ const { handleStatusHistPage, sendStatusDm, logStatusChange -} = require('../commands/status.js'); +} = require('../commands/staff-status.js'); const { localize } = require('../../../src/functions/localize'); const dutyHandlers = require('../commands/duty.js').buttonHandlers; const { ActionRowBuilder, ButtonBuilder, ButtonStyle, ComponentType, EmbedBuilder, MessageFlags, ModalBuilder, TextInputBuilder, TextInputStyle } = require('discord.js'); @@ -65,14 +65,14 @@ module.exports.run = async (client, interaction) => { if (dutyAction === 'admin-addtime-submit') return await dutyHandlers.handleDutyAdminAddTimeSubmit(client, interaction); return; } - + // ----- Review history pagination ----- if (action === 'rev-page') { await interaction.deferUpdate(); const targetUser = await client.users.fetch(parts[2]).catch(() => null); - if (!targetUser) return interaction.followUp({ - content: localize('staff-management-system', 'err-gen-no-user'), - flags: MessageFlags.Ephemeral + if (!targetUser) return interaction.followUp({ + content: localize('staff-management-system', 'err-gen-no-user'), + flags: MessageFlags.Ephemeral }); const payload = await generateReviewHistoryResponse(client, targetUser, parseInt(parts[3], 10)); @@ -93,15 +93,15 @@ module.exports.run = async (client, interaction) => { if (base === 'extend') return handleStatusExtend(interaction, type); if (base === 'extend-submit') return handleStatusExtendSubmit(client, interaction, type); if (base === 'hist') return handleStatusHistPage(client, interaction, type); - } + } // ----- Promotion history pagination ----- if (action === 'prom-hist') { await interaction.deferUpdate(); const targetUser = await client.users.fetch(parts[2]).catch(() => null); - if (!targetUser) return interaction.followUp({ - content: localize('staff-management-system', 'err-gen-no-user'), - flags: MessageFlags.Ephemeral + if (!targetUser) return interaction.followUp({ + content: localize('staff-management-system', 'err-gen-no-user'), + flags: MessageFlags.Ephemeral }); const payload = await generatePromotionHistoryResponse(client, targetUser, parseInt(parts[3], 10)); @@ -113,9 +113,9 @@ module.exports.run = async (client, interaction) => { if (action === 'inf-hist') { await interaction.deferUpdate(); const targetUser = await client.users.fetch(parts[2]).catch(() => null); - if (!targetUser) return interaction.followUp({ - content: localize('staff-management-system', 'err-gen-no-user'), - flags: MessageFlags.Ephemeral + if (!targetUser) return interaction.followUp({ + content: localize('staff-management-system', 'err-gen-no-user'), + flags: MessageFlags.Ephemeral }); const payload = await generateInfractionHistoryResponse(client, targetUser, parseInt(parts[3], 10)); @@ -128,9 +128,9 @@ module.exports.run = async (client, interaction) => { const targetId = interaction.customId.split('_')[2]; await interaction.deferUpdate(); const targetUser = await client.users.fetch(targetId).catch(() => null); - if (!targetUser) return interaction.followUp({ - content: localize('staff-management-system', 'err-gen-no-user'), - flags: MessageFlags.Ephemeral + if (!targetUser) return interaction.followUp({ + content: localize('staff-management-system', 'err-gen-no-user'), + flags: MessageFlags.Ephemeral }); const selection = interaction.values[0]; @@ -154,9 +154,9 @@ module.exports.run = async (client, interaction) => { if (selection === 'back') { const targetUser = await client.users.fetch(targetId).catch(() => null); - if (!targetUser) return interaction.reply({ - content: localize('staff-management-system', 'err-gen-no-user'), - flags: MessageFlags.Ephemeral + if (!targetUser) return interaction.reply({ + content: localize('staff-management-system', 'err-gen-no-user'), + flags: MessageFlags.Ephemeral }); const payload = await generateUserPanel(client, targetUser); @@ -182,7 +182,7 @@ module.exports.run = async (client, interaction) => { // ----- Data deletion modal submission ----- if (interaction.isModalSubmit() && interaction.customId.startsWith('staff-mgmt_del-confirm_')) { - await interaction.deferReply({ flags: MessageFlags.Ephemeral }); + await interaction.deferReply({flags: MessageFlags.Ephemeral}); const configuration = getConfig(client, 'configuration'); if (!checkStaffPermissions(interaction.member, configuration, 'management')) { @@ -190,16 +190,16 @@ module.exports.run = async (client, interaction) => { content: localize('staff-management-system', 'del-no-perm') }); } - + const parts = interaction.customId.split('_'); const targetId = parts[2]; - const selection = parts.slice(3).join('_'); - + const selection = parts.slice(3).join('_'); + const confirmPhrase = localize('staff-management-system', 'del-conf-phrase'); - + if (interaction.fields.getTextInputValue('confirm').trim() !== confirmPhrase) { - return interaction.editReply({ - content: localize('staff-management-system', 'err-conf-fail') + return interaction.editReply({ + content: localize('staff-management-system', 'err-conf-fail') }); } @@ -222,8 +222,8 @@ module.exports.run = async (client, interaction) => { .setStyle(ButtonStyle.Secondary) ); - await interaction.editReply({ - embeds: [embed.toJSON()], + await interaction.editReply({ + embeds: [embed.toJSON()], components: [row.toJSON()] }); @@ -242,12 +242,12 @@ module.exports.run = async (client, interaction) => { flags: MessageFlags.Ephemeral }); } - + if (btnInt.customId.includes('cancel')) { - await btnInt.update({ - content: localize('staff-management-system', 'succ-del-canc'), - embeds: [], - components: [] + await btnInt.update({ + content: localize('staff-management-system', 'succ-del-canc'), + embeds: [], + components: [] }); return; } @@ -255,9 +255,9 @@ module.exports.run = async (client, interaction) => { if (btnInt.customId.includes('confirm')) { await executeDataDeletion(client, targetId, 'del_all'); - client.logger.info(localize('staff-management-system', 'log-del-all', { - target: targetId, - admin: btnInt.user.id + client.logger.info(localize('staff-management-system', 'log-del-all', { + target: targetId, + admin: btnInt.user.id })); const targetUser = await client.users.fetch(targetId).catch(() => null); @@ -277,9 +277,9 @@ module.exports.run = async (client, interaction) => { collector.on('end', async (_collected, reason) => { if (reason === 'time') { await interaction.editReply({ - content: localize('staff-management-system', 'err-del-time'), - embeds: [], - components: [] + content: localize('staff-management-system', 'err-del-time'), + embeds: [], + components: [] }).catch(()=>{}); } }); @@ -287,20 +287,19 @@ module.exports.run = async (client, interaction) => { } await executeDataDeletion(client, targetId, selection); - client.logger.info(localize('staff-management-system', 'log-del-type', { - type: selection, - target: targetId, - admin: interaction.user.id + client.logger.info(localize('staff-management-system', 'log-del-type', { + type: selection, + target: targetId, + admin: interaction.user.id })); const targetUser = await client.users.fetch(targetId).catch(() => null); if (targetUser) { const payload = await generateUserPanel(client, targetUser); await interaction.message.edit(payload).catch(()=>{}); } - - return interaction.editReply({ - content: localize('staff-management-system', 'succ-del-tgt'), - flags: MessageFlags.Ephemeral + + return interaction.editReply({ + content: localize('staff-management-system', 'succ-del-tgt') }); } @@ -309,19 +308,19 @@ module.exports.run = async (client, interaction) => { const parts = interaction.customId.split('_'); const targetId = parts[2]; const page = parseInt(parts[3], 10); - + const targetUser = await client.users.fetch(targetId).catch(() => null); - if (!targetUser) return interaction.reply({ - content: localize('staff-management-system', 'err-gen-no-user'), - flags: MessageFlags.Ephemeral + if (!targetUser) return interaction.reply({ + content: localize('staff-management-system', 'err-gen-no-user'), + flags: MessageFlags.Ephemeral }); - - const typeMap = { - 'inf': 'infractions', - 'prom': 'promotions', - 'rev': 'reviews', - 'stat': 'status', - 'act': 'activity' + + const typeMap = { + 'inf': 'infractions', + 'prom': 'promotions', + 'rev': 'reviews', + 'stat': 'status', + 'act': 'activity' }; const fullType = typeMap[parts[1].split('-')[1]]; @@ -338,23 +337,23 @@ module.exports.run = async (client, interaction) => { const statusConfig = client.configurations['staff-management-system']['status']; if (action === 'approve' || action === 'deny') { - const isSupervisor = interaction.member.roles.cache.some(r => config.supervisorRoles.includes(r.id)) || + const isSupervisor = interaction.member.roles.cache.some(r => config.supervisorRoles.includes(r.id)) || interaction.member.roles.cache.some(r => config.managementRoles.includes(r.id)) || interaction.member.permissions.has('Administrator'); - if (!isSupervisor) return interaction.reply({ - content: localize('staff-management-system', 'err-gen-no-perm'), - flags: MessageFlags.Ephemeral + if (!isSupervisor) return interaction.reply({ + content: localize('staff-management-system', 'err-gen-no-perm'), + flags: MessageFlags.Ephemeral }); const request = await LoARequest.findByPk(parts[2]); - if (!request) return interaction.reply({ - content: localize('staff-management-system', 'err-no-req'), - flags: MessageFlags.Ephemeral + if (!request) return interaction.reply({ + content: localize('staff-management-system', 'err-no-req'), + flags: MessageFlags.Ephemeral }); - if (request.status !== 'PENDING') return interaction.reply({ - content: localize('staff-management-system', 'err-req-hndl', { status: request.status }), - flags: MessageFlags.Ephemeral + if (request.status !== 'PENDING') return interaction.reply({ + content: localize('staff-management-system', 'err-req-hndl', {status: request.status}), + flags: MessageFlags.Ephemeral }); if (action === 'deny') { @@ -376,48 +375,48 @@ module.exports.run = async (client, interaction) => { if (action === 'approve') { await interaction.deferUpdate(); - await request.update({ - status: 'APPROVED', - approverId: interaction.user.id + await request.update({ + status: 'APPROVED', + approverId: interaction.user.id }); - await StaffProfile.upsert({ - userId: request.userId, - activityStatus: request.type + await StaffProfile.upsert({ + userId: request.userId, + activityStatus: request.type }); scheduleStatusExpiry(client, request); const member = await interaction.guild.members.fetch(request.userId).catch(() => null); if (member) { - const roleId = request.type === 'LOA' - ? statusConfig.loaRole + const roleId = request.type === 'LOA' + ? statusConfig.loaRole : statusConfig.raRole; if (roleId) await member.roles.add(roleId).catch(() => {}); - await sendStatusDm(member.user, request.type, 'approved', { - approver: interaction.user.tag, - endDate: request.endDate + await sendStatusDm(member.user, request.type, 'approved', { + approver: interaction.user.tag, + endDate: request.endDate }); } await logStatusChange(client, request.type, 'start', { - userId: request.userId, - startDate: request.startDate, + userId: request.userId, + startDate: request.startDate, endDate: request.endDate, - reason: request.reason, + reason: request.reason, approverId: interaction.user.id }); const embed = EmbedBuilder .from(interaction.message.embeds[0]) .setColor('Green') - .addFields({ - name: localize('staff-management-system', 'general-stat'), - value: localize('staff-management-system', 'req-appr-by', { - user: interaction.user.tag - }) + .addFields({ + name: localize('staff-management-system', 'general-stat'), + value: localize('staff-management-system', 'req-appr-by', { + user: interaction.user.tag + }) }); - return interaction.editReply({ - embeds: [embed.toJSON()], - components: [] + return interaction.editReply({ + embeds: [embed.toJSON()], + components: [] }); } } @@ -443,7 +442,7 @@ module.exports.run = async (client, interaction) => { } if (request.status !== 'PENDING') { return interaction.reply({ - content: localize('staff-management-system', 'err-req-hndl', { status: request.status }), + content: localize('staff-management-system', 'err-req-hndl', {status: request.status}), flags: MessageFlags.Ephemeral }); } @@ -497,14 +496,14 @@ module.exports.run = async (client, interaction) => { const intro = interaction.fields.getTextInputValue('intro'); const Profile = client.models['staff-management-system']['StaffProfile']; - await Profile.upsert({ - userId: interaction.user.id, - customNickname: nickname || null, - customIntro: intro || null + await Profile.upsert({ + userId: interaction.user.id, + customNickname: nickname || null, + customIntro: intro || null }); - return interaction.reply({ - content: localize('staff-management-system', 'succ-prof-upd'), - flags: MessageFlags.Ephemeral + return interaction.reply({ + content: localize('staff-management-system', 'succ-prof-upd'), + flags: MessageFlags.Ephemeral }); } @@ -513,23 +512,23 @@ module.exports.run = async (client, interaction) => { const ActivityCheck = client.models['staff-management-system']['ActivityCheck']; const ActivityCheckResponse = client.models['staff-management-system']['ActivityCheckResponse']; - const activeCheck = await ActivityCheck.findOne({ - where: { - status: 'ACTIVE', - messageId: interaction.message.id - } + const activeCheck = await ActivityCheck.findOne({ + where: { + status: 'ACTIVE', + messageId: interaction.message.id + } }); - if (!activeCheck) return interaction.reply({ - content: localize('staff-management-system', 'err-ac-alr-end'), - flags: MessageFlags.Ephemeral + if (!activeCheck) return interaction.reply({ + content: localize('staff-management-system', 'err-ac-alr-end'), + flags: MessageFlags.Ephemeral }); const targetRoles = JSON.parse(activeCheck.targetRoles || '[]'); const hasRole = targetRoles.length === 0 || interaction.member.roles.cache.some(r => targetRoles.includes(r.id)); - if (!hasRole) return interaction.reply({ - content: localize('staff-management-system', 'err-ac-not-req'), - flags: MessageFlags.Ephemeral + if (!hasRole) return interaction.reply({ + content: localize('staff-management-system', 'err-ac-not-req'), + flags: MessageFlags.Ephemeral }); const existingResponse = await ActivityCheckResponse.findOne({ @@ -568,14 +567,16 @@ module.exports.run = async (client, interaction) => { } catch (e) { client.logger.error(localize('staff-management-system', 'log-int-error', { error: e.stack })); if (!interaction.replied && !interaction.deferred) { - try { await interaction.reply({ - content: localize('staff-management-system', 'err-internal'), - flags: MessageFlags.Ephemeral + try { + await interaction.reply({ + content: localize('staff-management-system', 'err-internal'), + flags: MessageFlags.Ephemeral }); } catch (err) {} } else { - try { await interaction.followUp({ - content: localize('staff-management-system', 'err-internal'), - flags: MessageFlags.Ephemeral + try { + await interaction.followUp({ + content: localize('staff-management-system', 'err-internal'), + flags: MessageFlags.Ephemeral }); } catch (err) {} } } diff --git a/modules/staff-management-system/module.json b/modules/staff-management-system/module.json index 19ebc862..0d5c0613 100644 --- a/modules/staff-management-system/module.json +++ b/modules/staff-management-system/module.json @@ -5,6 +5,7 @@ "name": "Kevin", "link": "https://github.com/Kevinking500" }, + "fa-icon": "far fa-gear looks", "openSourceURL": "https://github.com/Kevinking500/CustomDCBot/tree/main/modules/staff-management-system", "commands-dir": "/commands", "events-dir": "/events", @@ -22,12 +23,6 @@ "tags": [ "moderation" ], - "humanReadableName": { - "en": "Staff Management System", - "de": "Mitarbeiter-Management-System" - }, - "description": { - "en": "A powerful, highly customizable staff management system designed to track activity, moderate personnel, and maintain detailed staff records seamlessly.", - "de": "Ein leistungsstarkes, hochgradig anpassbares Mitarbeiter-Management-System, das entwickelt wurde, um die Aktivität zu verfolgen, das Personal zu moderieren und detaillierte Mitarbeiterakten nahtlos zu pflegen." - } -} \ No newline at end of file + "humanReadableName": "Staff Management System", + "description": "A powerful, highly customizable staff management system designed to track activity, moderate personnel, and maintain detailed staff records seamlessly." +} diff --git a/modules/staff-management-system/staff-management.js b/modules/staff-management-system/staff-management.js index 64fa6644..9d96d2f7 100644 --- a/modules/staff-management-system/staff-management.js +++ b/modules/staff-management-system/staff-management.js @@ -12,9 +12,9 @@ const { localize } = require('../../src/functions/localize'); // --- Local helpers --- const getConfig = (client, file) => client.configurations['staff-management-system'][file]; const getSafeChannelId = (val) => Array.isArray(val) && val.length > 0 // Helper to get safe channel ID from config -? val[0] -: (typeof val === 'string' - ? val + ? val[0] + : (typeof val === 'string' + ? val : null ); const parseDurationToDays = (input) => { @@ -23,10 +23,10 @@ const parseDurationToDays = (input) => { if (!match) return null; const value = parseInt(match[1], 10); const unit = match[2]?.toLowerCase() || 'd'; - return unit === 'm' - ? value * 30 - : (unit === 'w' - ? value * 7 + return unit === 'm' + ? value * 30 + : (unit === 'w' + ? value * 7 : value ); }; @@ -105,10 +105,10 @@ function formatDuration(seconds) { // ---------- Infractions ---------- async function issueInfraction(client, interaction, targetMember, type, reason, expiryInput) { - await interaction.deferReply({ ephemeral: true }); + await interaction.deferReply({ephemeral: true}); const config = getConfig(client, 'infractions'); - if (!config?.enableInfractions) return interaction.editReply({ - content: localize('staff-management-system', 'err-feat-disabled', { feature: 'Infractions' }) + if (!config?.enableInfractions) return interaction.editReply({ + content: localize('staff-management-system', 'err-feat-disabled', {feature: 'Infractions'}) }); if (targetMember.id === interaction.user.id) { @@ -118,7 +118,7 @@ async function issueInfraction(client, interaction, targetMember, type, reason, } if (type.toLowerCase() === 'suspension') { - return interaction.editReply({ + return interaction.editReply({ content: localize('staff-management-system', 'err-use-susp') }); } @@ -126,30 +126,38 @@ async function issueInfraction(client, interaction, targetMember, type, reason, let expiresAt = null; if (expiryInput) { const days = parseDurationToDays(expiryInput); - if (!days) return interaction.editReply({ - content: localize('staff-management-system', 'err-inv-dur') + if (!days) return interaction.editReply({ + content: localize('staff-management-system', 'err-inv-dur') }); expiresAt = new Date(Date.now() + days * 24 * 60 * 60 * 1000); } const record = await client.models['staff-management-system']['Infraction'].create({ - userId: targetMember.id, - issuerId: interaction.user.id, - type, reason, expiresAt, + userId: targetMember.id, + issuerId: interaction.user.id, + type, reason, expiresAt, active: true }); const placeholders = { '%user%': targetMember.user.toString(), - '%user-avatar%': targetMember.user.displayAvatarURL({ dynamic: true, format: 'png', size: 1024 }) || '', + '%user-avatar%': targetMember.user.displayAvatarURL({ + dynamic: true, + format: 'png', + size: 1024 + }) || '', '%issuer-mention%': interaction.user.toString(), '%issuer-name%': interaction.user.username, - '%issuer-avatar%': interaction.user.displayAvatarURL({ dynamic: true, format: 'png', size: 1024 }) || '', + '%issuer-avatar%': interaction.user.displayAvatarURL({ + dynamic: true, + format: 'png', + size: 1024 + }) || '', '%type%': type, '%reason%': reason, '%case-id%': record.caseId.toString(), - '%end-date%': expiresAt - ? `` + '%end-date%': expiresAt + ? `` : localize('staff-management-system', 'label-never') }; @@ -158,14 +166,15 @@ async function issueInfraction(client, interaction, targetMember, type, reason, const channel = await interaction.guild.channels.fetch(channelId).catch(() => null); if (channel) { let template = config.infractionMessage; - if (typeof template === 'string') { - try { template = JSON.parse(template); } - catch (e) {} - } - else if (typeof template === 'object') { - template = JSON.parse(JSON.stringify(template)); + if (typeof template === 'string') { + try { + template = JSON.parse(template); + } catch (e) { + } + } else if (typeof template === 'object') { + template = JSON.parse(JSON.stringify(template)); } - + if (template && template.embeds && !template._schema) template._schema = 'v3'; let msgOpts = await embedTypeV2(template, placeholders); if (msgOpts?.content?.trim() === '') delete msgOpts.content; @@ -186,7 +195,8 @@ async function issueInfraction(client, interaction, targetMember, type, reason, if (typeof dmTemplate === 'string') { try { dmTemplate = JSON.parse(dmTemplate); - } catch (e) {} + } catch (e) { + } } else if (typeof dmTemplate === 'object') { dmTemplate = JSON.parse(JSON.stringify(dmTemplate)); } @@ -207,31 +217,31 @@ async function issueInfraction(client, interaction, targetMember, type, reason, } } - await interaction.editReply({ - content: localize('staff-management-system', 'succ-infract', { - type, - caseId: record.caseId, - user: targetMember.user.tag + await interaction.editReply({ + content: localize('staff-management-system', 'succ-infract', { + type, + caseId: record.caseId, + user: targetMember.user.tag }) }); } // ---------- Suspensions ---------- async function issueSuspension(client, interaction, targetMember, durationInput, reason) { - await interaction.deferReply({ ephemeral: true }); + await interaction.deferReply({ephemeral: true}); const config = getConfig(client, 'infractions'); - if (!config?.enableInfractions) - return interaction.editReply({ - content: localize('staff-management-system', 'err-feat-disabled', { - feature: 'Infractions' - }) + if (!config?.enableInfractions) + return interaction.editReply({ + content: localize('staff-management-system', 'err-feat-disabled', { + feature: 'Infractions' + }) }); - if (!config?.enableSuspensions) - return interaction.editReply({ - content: localize('staff-management-system', 'err-feat-disabled', { - feature: 'Suspensions' - }) + if (!config?.enableSuspensions) + return interaction.editReply({ + content: localize('staff-management-system', 'err-feat-disabled', { + feature: 'Suspensions' + }) }); if (targetMember.id === interaction.user.id) { @@ -241,11 +251,11 @@ async function issueSuspension(client, interaction, targetMember, durationInput, } const durationDays = parseDurationToDays(durationInput); - if (!durationDays) - return interaction.editReply({ - content: localize('staff-management-system', 'err-inv-dur') + if (!durationDays) + return interaction.editReply({ + content: localize('staff-management-system', 'err-inv-dur') }); - + const expiresAt = new Date(Date.now() + durationDays * 24 * 60 * 60 * 1000); const durationString = `${durationDays} ${localize('staff-management-system', 'label-days')}`; @@ -269,19 +279,27 @@ async function issueSuspension(client, interaction, targetMember, durationInput, if (config.suspensionRole) await targetMember.roles.add(config.suspensionRole).catch(() => {}); const record = await client.models['staff-management-system']['Infraction'].create({ - userId: targetMember.id, - issuerId: interaction.user.id, - type: 'Suspension', - reason, durationDays, expiresAt, + userId: targetMember.id, + issuerId: interaction.user.id, + type: 'Suspension', + reason, durationDays, expiresAt, active: true }); const placeholders = { '%user%': targetMember.user.toString(), - '%user-avatar%': targetMember.user.displayAvatarURL({ dynamic: true, format: 'png', size: 1024 }) || '', + '%user-avatar%': targetMember.user.displayAvatarURL({ + dynamic: true, + format: 'png', + size: 1024 + }) || '', '%issuer-mention%': interaction.user.toString(), '%issuer-name%': interaction.user.username, - '%issuer-avatar%': interaction.user.displayAvatarURL({ dynamic: true, format: 'png', size: 1024 }) || '', + '%issuer-avatar%': interaction.user.displayAvatarURL({ + dynamic: true, + format: 'png', + size: 1024 + }) || '', '%duration%': durationString, '%reason%': reason, '%case-id%': record.caseId.toString(), @@ -293,16 +311,15 @@ async function issueSuspension(client, interaction, targetMember, durationInput, const channel = await interaction.guild.channels.fetch(channelId).catch(() => null); if (channel) { let template = config.suspensionMessage; - if (typeof template === 'string') { - try { - template = JSON.parse(template); - } - catch (e) {} - } - else if (typeof template === 'object') { - template = JSON.parse(JSON.stringify(template)); + if (typeof template === 'string') { + try { + template = JSON.parse(template); + } catch (e) { + } + } else if (typeof template === 'object') { + template = JSON.parse(JSON.stringify(template)); } - + if (template && template.embeds && !template._schema) template._schema = 'v3'; let msgOpts = await embedTypeV2(template, placeholders); if (msgOpts?.content?.trim() === '') delete msgOpts.content; @@ -320,14 +337,13 @@ async function issueSuspension(client, interaction, targetMember, durationInput, if (config.dmInfractedUser && config.suspensionDmMessage) { let dmTemplate = config.suspensionDmMessage; - if (typeof dmTemplate === 'string') { - try { - dmTemplate = JSON.parse(dmTemplate); - } - catch (e) {} - } - else if (typeof dmTemplate === 'object') { - dmTemplate = JSON.parse(JSON.stringify(dmTemplate)); + if (typeof dmTemplate === 'string') { + try { + dmTemplate = JSON.parse(dmTemplate); + } catch (e) { + } + } else if (typeof dmTemplate === 'object') { + dmTemplate = JSON.parse(JSON.stringify(dmTemplate)); } if (dmTemplate && dmTemplate.embeds && !dmTemplate._schema) dmTemplate._schema = 'v3'; @@ -346,11 +362,11 @@ async function issueSuspension(client, interaction, targetMember, durationInput, } } - await interaction.editReply({ - content: localize('staff-management-system', 'succ-susp', { - caseId: record.caseId, - user: targetMember.user.tag, - duration: durationString + await interaction.editReply({ + content: localize('staff-management-system', 'succ-susp', { + caseId: record.caseId, + user: targetMember.user.tag, + duration: durationString }) }); } @@ -375,7 +391,7 @@ async function resolveInfractionReference(client, reference) { if (parts.length !== 4 || parts[0] !== 'channels') return null; return await Infraction.findOne({ - where: { messageUrl: value } + where: {messageUrl: value} }); } catch (e) { return null; @@ -384,38 +400,38 @@ async function resolveInfractionReference(client, reference) { // ----- Infractions voiding ----- async function voidInfraction(client, interaction, reference) { - await interaction.deferReply({ ephemeral: true }); + await interaction.deferReply({ephemeral: true}); const config = getConfig(client, 'infractions'); - if (!config?.enableInfractions) return interaction.editReply({ - content: localize('staff-management-system', 'err-feat-disabled', { - feature: 'Infractions' + if (!config?.enableInfractions) return interaction.editReply({ + content: localize('staff-management-system', 'err-feat-disabled', { + feature: 'Infractions' }) }); const canManage = checkStaffPermissions(interaction.member, getConfig(client, 'configuration'), 'supervisor'); - if (!canManage) return interaction.editReply({ - content: localize('staff-management-system', 'err-gen-no-perm') + if (!canManage) return interaction.editReply({ + content: localize('staff-management-system', 'err-gen-no-perm') }); const record = await resolveInfractionReference(client, reference); - if (!record) { - return interaction.editReply({ - content: localize('staff-management-system', 'err-no-case-ref', { reference }) - }); + if (!record) { + return interaction.editReply({ + content: localize('staff-management-system', 'err-no-case-ref', {reference}) + }); } - if (!record.active) { - return interaction.editReply({ - content: localize('staff-management-system', 'err-case-inact', { caseId: record.caseId }) - }); + if (!record.active) { + return interaction.editReply({ + content: localize('staff-management-system', 'err-case-inact', {caseId: record.caseId}) + }); } if (record.type.toLowerCase() === 'suspension') { const Profile = client.models['staff-management-system']['StaffProfile']; - const profile = await Profile.findOne({ - where: { userId: record.userId } + const profile = await Profile.findOne({ + where: {userId: record.userId} }); const member = await interaction.guild.members.fetch(record.userId).catch(() => null); - + if (member && profile && profile.isSuspended) { try { const rolesToRestore = JSON.parse(profile.suspendedRoles || '[]'); @@ -423,15 +439,15 @@ async function voidInfraction(client, interaction, reference) { if (config.suspensionRole) await member.roles.remove(config.suspensionRole); await profile.update({ isSuspended: false, suspendedRoles: '[]' }); } catch (e) { - return interaction.editReply({ - content: localize('staff-management-system', 'succ-void-fail', { caseId: record.caseId }) + return interaction.editReply({ + content: localize('staff-management-system', 'succ-void-fail', {caseId: record.caseId}) }); } } } - await record.update({ active: false }); - await interaction.editReply({ - content: localize('staff-management-system', 'succ-void', { caseId: record.caseId }) + await record.update({active: false}); + await interaction.editReply({ + content: localize('staff-management-system', 'succ-void', {caseId: record.caseId}) }); } @@ -439,18 +455,18 @@ async function voidInfraction(client, interaction, reference) { async function generateInfractionHistoryResponse(client, targetUser, page = 1) { const limit = 5; const offset = (page - 1) * limit; - const { count, rows } = await client.models['staff-management-system']['Infraction'].findAndCountAll({ - where: { userId: targetUser.id }, - order: [['createdAt', 'DESC']], - limit, offset + const {count, rows} = await client.models['staff-management-system']['Infraction'].findAndCountAll({ + where: {userId: targetUser.id}, + order: [['createdAt', 'DESC']], + limit, offset }); - if (count === 0) - return { - content: localize('staff-management-system', 'info-clean-rec', { - username: targetUser.username - }), - flags: MessageFlags.Ephemeral + if (count === 0) + return { + content: localize('staff-management-system', 'info-clean-rec', { + username: targetUser.username + }), + flags: MessageFlags.Ephemeral }; const totalPages = Math.ceil(count / limit) || 1; @@ -461,23 +477,24 @@ async function generateInfractionHistoryResponse(client, targetUser, page = 1) { ); const desc = rows.map(r => { - const link = r.messageUrl - ? ` • [Jump](${r.messageUrl})` + const link = r.messageUrl + ? ` • [Jump](${r.messageUrl})` : ''; - const statusIcon = r.active - ? '🔴' + const statusIcon = r.active + ? '🔴' : localize('staff-management-system', 'icon-voided'); - const expiry = r.expiresAt - ? `\n**${localize('staff-management-system', 'label-exp')}:** ` + const expiry = r.expiresAt + ? `\n**${localize('staff-management-system', 'label-exp')}:** ` : ''; return `**${statusIcon} ${localize('staff-management-system', 'label-case')} #${r.caseId} - ${r.type}**\n**${localize('staff-management-system', 'label-date')}:** \n**${localize('staff-management-system', 'label-iss')}:** <@${r.issuerId}>\n**${localize('staff-management-system', 'general-rsn')}:** ${r.reason}${expiry}${link}`; }).join('\n\n'); embed.setDescription(desc); - embed.addFields({ name: '\u200b', value: localize('staff-management-system', 'page-count', { - page, - total: totalPages + embed.addFields({ + name: '\u200b', value: localize('staff-management-system', 'page-count', { + page, + total: totalPages }) }); const row = buildPaginationRow( @@ -492,20 +509,20 @@ async function generateInfractionHistoryResponse(client, targetUser, page = 1) { // ----- Gets infraction history ----- async function getInfractionHistory(client, interaction, targetUser) { - await interaction.deferReply({ ephemeral: true }); + await interaction.deferReply({ephemeral: true}); const response = await generateInfractionHistoryResponse(client, targetUser, 1); if (response.content && response.content.startsWith('ℹ️')) return interaction.editReply(response); - await interaction.editReply({ + await interaction.editReply({ ...response }); } // ---------- Promotions ---------- async function promoteUser(client, interaction, targetMember, newRole, reason) { - await interaction.deferReply({ ephemeral: true }); + await interaction.deferReply({ephemeral: true}); const config = getConfig(client, 'promotions'); - if (!config?.enablePromotions) return interaction.editReply({ - content: localize('staff-management-system', 'err-feat-disabled', { feature: 'Promotions' }) + if (!config?.enablePromotions) return interaction.editReply({ + content: localize('staff-management-system', 'err-feat-disabled', {feature: 'Promotions'}) }); if (targetMember.id === interaction.user.id) { @@ -514,60 +531,65 @@ async function promoteUser(client, interaction, targetMember, newRole, reason) { }); } - const finalReason = reason && reason.trim() !== '' - ? reason + const finalReason = reason && reason.trim() !== '' + ? reason : localize('staff-management-system', 'none-provided'); const channelOverride = interaction.options.getChannel('channel'); if (config.autoAddRole) { if (interaction.guild.members.me.roles.highest.position <= newRole.position) { - return interaction.editReply({ + return interaction.editReply({ content: localize('staff-management-system', 'err-role-hier') }); } - try { - await targetMember.roles.add(newRole); - } - catch (e) { - return interaction.editReply({ - content: localize('staff-management-system', 'err-add-role', { e: e.message }) + try { + await targetMember.roles.add(newRole); + } catch (e) { + return interaction.editReply({ + content: localize('staff-management-system', 'err-add-role', {e: e.message}) }); } } - const record = await client.models['staff-management-system']['Promotion'].create({ - userId: targetMember.id, - issuerId: interaction.user.id, - newRole: newRole.id, - reason: finalReason + const record = await client.models['staff-management-system']['Promotion'].create({ + userId: targetMember.id, + issuerId: interaction.user.id, + newRole: newRole.id, + reason: finalReason }); const placeholders = { - '%user-mention%': targetMember.user.toString(), - '%new-role-name%': newRole.name, + '%user-mention%': targetMember.user.toString(), + '%new-role-name%': newRole.name, '%new-role-mention%': newRole.toString(), - '%promoter-mention%': interaction.user.toString(), - '%promoter-name%': interaction.user.username, + '%promoter-mention%': interaction.user.toString(), + '%promoter-name%': interaction.user.username, '%reason%': finalReason, - '%user-avatar%': targetMember.user.displayAvatarURL({ dynamic: true, format: 'png', size: 1024 }) || '', - '%promoter-avatar%': interaction.user.displayAvatarURL({ dynamic: true, format: 'png', size: 1024 }) || '' + '%user-avatar%': targetMember.user.displayAvatarURL({ + dynamic: true, + format: 'png', + size: 1024 + }) || '', + '%promoter-avatar%': interaction.user.displayAvatarURL({ + dynamic: true, + format: 'png', + size: 1024 + }) || '' }; - const targetChannelId = channelOverride - ? channelOverride.id + const targetChannelId = channelOverride + ? channelOverride.id : getSafeChannelId(config.promotionsChannel); if (targetChannelId) { const channel = await interaction.guild.channels.fetch(targetChannelId).catch(() => null); if (channel) { let embedTemplate = config.promotionMessage; - if (typeof embedTemplate === 'string') { - try { - embedTemplate = JSON.parse(embedTemplate); - } - catch (e) {} } - - else if (typeof embedTemplate === 'object') { - embedTemplate = JSON.parse(JSON.stringify(embedTemplate)); + if (typeof embedTemplate === 'string') { + try { + embedTemplate = JSON.parse(embedTemplate); + } + catch (e) {} } else if (typeof embedTemplate === 'object') { + embedTemplate = JSON.parse(JSON.stringify(embedTemplate)); } if (embedTemplate && embedTemplate.embeds && !embedTemplate._schema) embedTemplate._schema = 'v3'; @@ -588,8 +610,8 @@ async function promoteUser(client, interaction, targetMember, newRole, reason) { })); return null; }); - - if (sentMessage) await record.update({ messageUrl: sentMessage.url }); + + if (sentMessage) await record.update({messageUrl: sentMessage.url}); } } @@ -598,10 +620,9 @@ async function promoteUser(client, interaction, targetMember, newRole, reason) { if (typeof dmTemplate === 'string') { try { dmTemplate = JSON.parse(dmTemplate); - } - catch (e) {} - } - else if (typeof dmTemplate === 'object') { + } catch (e) { + } + } else if (typeof dmTemplate === 'object') { dmTemplate = JSON.parse(JSON.stringify(dmTemplate)); } @@ -620,11 +641,11 @@ async function promoteUser(client, interaction, targetMember, newRole, reason) { } } } - - await interaction.editReply({ - content: localize('staff-management-system', 'succ-promo', { - user: targetMember.user.tag, - role: newRole.name + + await interaction.editReply({ + content: localize('staff-management-system', 'succ-promo', { + user: targetMember.user.tag, + role: newRole.name }) }); } @@ -635,17 +656,17 @@ async function generatePromotionHistoryResponse(client, targetUser, page = 1) { const limit = 5; const offset = (page - 1) * limit; - const { count, rows } = await Promotion.findAndCountAll({ - where: { - userId: targetUser.id - }, - order: [['createdAt', 'DESC']], - limit, - offset + const {count, rows} = await Promotion.findAndCountAll({ + where: { + userId: targetUser.id + }, + order: [['createdAt', 'DESC']], + limit, + offset }); - if (count === 0) return { - content: localize('staff-management-system', 'info-no-promo', { username: targetUser.username }), - flags: MessageFlags.Ephemeral + if (count === 0) return { + content: localize('staff-management-system', 'info-no-promo', {username: targetUser.username}), + flags: MessageFlags.Ephemeral }; const totalPages = Math.ceil(count / limit) || 1; @@ -670,19 +691,19 @@ async function generatePromotionHistoryResponse(client, targetUser, page = 1) { page, totalPages ); - return { - embeds: [embed.toJSON()], - components: [row.toJSON()] + return { + embeds: [embed.toJSON()], + components: [row.toJSON()] }; } async function getPromotionHistory(client, interaction, targetUser) { - await interaction.deferReply({ ephemeral: true }); + await interaction.deferReply({ephemeral: true}); const response = await generatePromotionHistoryResponse(client, targetUser, 1); if (response.content && response.content.startsWith('ℹ️')) return interaction.editReply(response); - await interaction.editReply({ - ...response + await interaction.editReply({ + ...response }); } @@ -699,12 +720,12 @@ async function generatePanelSubpage(client, targetUser, type, page) { // Overview page async function generateUserPanel(client, targetUser) { const embed = applyFooter(client, new EmbedBuilder() - .setTitle(localize('staff-management-system', 'panel-title', { - username: targetUser.username + .setTitle(localize('staff-management-system', 'panel-title', { + username: targetUser.username })) - .setDescription(localize('staff-management-system', 'panel-desc', { - mention: targetUser.toString(), - id: targetUser.id + .setDescription(localize('staff-management-system', 'panel-desc', { + mention: targetUser.toString(), + id: targetUser.id })) .setThumbnail(targetUser.displayAvatarURL({ dynamic: true })) .setColor('Blurple') @@ -749,17 +770,17 @@ async function generateUserPanel(client, targetUser) { ); const row = new ActionRowBuilder().addComponents(menu); - return { - embeds: [embed.toJSON()], - components: [row.toJSON()] + return { + embeds: [embed.toJSON()], + components: [row.toJSON()] }; } // Infractions page async function generatePanelInfractions(client, targetUser, page = 1) { const Infraction = client.models['staff-management-system']['Infraction']; - const allInfractions = await Infraction.findAll({ - where: { userId: targetUser.id } + const allInfractions = await Infraction.findAll({ + where: {userId: targetUser.id} }); const count = allInfractions.length; @@ -767,7 +788,7 @@ async function generatePanelInfractions(client, targetUser, page = 1) { if (count > 3) totalPages = 1 + Math.ceil((count - 3) / 5); const limit = page === 1 ? 3 : 5; - const offset = page === 1 ? 0 : 3 + ((page - 2) * 5); + const offset = page === 1 ? 0 : 3 + ((page - 2) * 5); const typeCounts = {}; allInfractions.forEach(inf => { typeCounts[inf.type] = (typeCounts[inf.type] || 0) + 1; }); @@ -779,15 +800,15 @@ async function generatePanelInfractions(client, targetUser, page = 1) { .setThumbnail(targetUser.displayAvatarURL({ dynamic: true })) ); - let desc = localize('staff-management-system', 'p-inf-desc', { - count: count, types: typeStrings || localize('staff-management-system', 'info-none') + let desc = localize('staff-management-system', 'p-inf-desc', { + count: count, types: typeStrings || localize('staff-management-system', 'info-none') }); - const rows = await Infraction.findAll({ - where: { userId: targetUser.id }, - order: [['createdAt', 'DESC']], - limit, - offset + const rows = await Infraction.findAll({ + where: {userId: targetUser.id}, + order: [['createdAt', 'DESC']], + limit, + offset }); if (rows.length === 0) { @@ -813,43 +834,43 @@ async function generatePanelInfractions(client, targetUser, page = 1) { page, totalPages ); - return { - embeds: [embed.toJSON()], - components: [menu.toJSON(), paginationRow.toJSON()] + return { + embeds: [embed.toJSON()], + components: [menu.toJSON(), paginationRow.toJSON()] }; } // Promotions page async function generatePanelPromotions(client, targetUser, page = 1) { const Promotion = client.models['staff-management-system']['Promotion']; - const count = await Promotion.count({ - where: { userId: targetUser.id } + const count = await Promotion.count({ + where: {userId: targetUser.id} }); let totalPages = 1; if (count > 3) totalPages = 1 + Math.ceil((count - 3) / 5); - const limit = page === 1 - ? 3 + const limit = page === 1 + ? 3 : 5; - const offset = page === 1 - ? 0 - : 3 + ((page - 2) * 5); + const offset = page === 1 + ? 0 + : 3 + ((page - 2) * 5); const embed = applyFooter(client, new EmbedBuilder() - .setTitle(localize('staff-management-system', 'p-prom-title', { - username: targetUser.username + .setTitle(localize('staff-management-system', 'p-prom-title', { + username: targetUser.username })) .setColor('Gold') .setThumbnail(targetUser.displayAvatarURL({ dynamic: true })) ); let desc = localize('staff-management-system', 'p-prom-desc', { count: count }); - const rows = await Promotion.findAll({ - where: { userId: targetUser.id }, - order: [['createdAt', 'DESC']], - limit, - offset + const rows = await Promotion.findAll({ + where: {userId: targetUser.id}, + order: [['createdAt', 'DESC']], + limit, + offset }); if (rows.length === 0) { @@ -871,55 +892,55 @@ async function generatePanelPromotions(client, targetUser, page = 1) { page, totalPages ); - return { - embeds: [embed.toJSON()], - components: [menu.toJSON(), paginationRow.toJSON()] + return { + embeds: [embed.toJSON()], + components: [menu.toJSON(), paginationRow.toJSON()] }; } // Reviews page async function generatePanelReviews(client, targetUser, page = 1) { const Review = client.models['staff-management-system']['StaffReview']; - const allReviews = await Review.findAll({ - where: { targetId: targetUser.id } + const allReviews = await Review.findAll({ + where: {targetId: targetUser.id} }); const count = allReviews.length; - + let totalPages = 1; if (count > 3) totalPages = 1 + Math.ceil((count - 3) / 5); - + const limit = page === 1 ? 3 : 5; - const offset = page === 1 ? 0 : 3 + ((page - 2) * 5); - - const avg = count - ? (allReviews.reduce((a, b) => a + b.stars, 0) / count).toFixed(1) + const offset = page === 1 ? 0 : 3 + ((page - 2) * 5); + + const avg = count + ? (allReviews.reduce((a, b) => a + b.stars, 0) / count).toFixed(1) : 0; - + const embed = applyFooter(client, new EmbedBuilder() - .setTitle(localize('staff-management-system', 'p-rev-title', { - username: targetUser.username + .setTitle(localize('staff-management-system', 'p-rev-title', { + username: targetUser.username })) .setColor('Gold') .setThumbnail(targetUser.displayAvatarURL({ dynamic: true })) ); - + let desc = localize('staff-management-system', 'p-rev-desc', { count: count, avg: avg }); - - const rows = await Review.findAll({ - where: { targetId: targetUser.id }, - order: [['createdAt', 'DESC']], - limit, - offset + + const rows = await Review.findAll({ + where: {targetId: targetUser.id}, + order: [['createdAt', 'DESC']], + limit, + offset }); if (rows.length === 0) desc += localize('staff-management-system', 'p-no-hist'); else desc += rows.map(r => `**${"⭐".repeat(r.stars)}** ${localize('staff-management-system', 'label-by')} <@${r.authorId}>\n"${r.comment}"`).join('\n\n'); - + embed.setDescription(desc); - embed.addFields({ - name: '\u200b', - value: localize('staff-management-system', 'page-count', { - page, total: totalPages - }) + embed.addFields({ + name: '\u200b', + value: localize('staff-management-system', 'page-count', { + page, total: totalPages + }) }); const menu = ActionRowBuilder.from((await generateUserPanel(client, targetUser)).components[0]); @@ -932,71 +953,71 @@ async function generatePanelReviews(client, targetUser, page = 1) { page, totalPages ); - return { - embeds: [embed.toJSON()], - components: [menu.toJSON(), paginationRow.toJSON()] + return { + embeds: [embed.toJSON()], + components: [menu.toJSON(), paginationRow.toJSON()] }; } // Status page async function generatePanelStatus(client, targetUser, page = 1) { const LoaRequest = client.models['staff-management-system']['LoaRequest']; - const allStatuses = await LoaRequest.findAll({ - where: { userId: targetUser.id } + const allStatuses = await LoaRequest.findAll({ + where: {userId: targetUser.id} }); const count = allStatuses.length; - + let totalPages = 1; if (count > 3) totalPages = 1 + Math.ceil((count - 3) / 5); - const limit = page === 1 - ? 3 + const limit = page === 1 + ? 3 : 5; - const offset = page === 1 - ? 0 - : 3 + ((page - 2) * 5); - + const offset = page === 1 + ? 0 + : 3 + ((page - 2) * 5); + const activeStatus = allStatuses.find(s => ['APPROVED', 'PENDING'].includes(s.status) && new Date(s.endDate) > new Date()); let activeText = localize('staff-management-system', 'info-none'); if (activeStatus) { activeText = `**${activeStatus.type}** (${activeStatus.status})\n${localize('staff-management-system', 'label-end')}: `; } - + const embed = applyFooter(client, new EmbedBuilder() - .setTitle(localize('staff-management-system', 'p-sta-title', { - username: targetUser.username + .setTitle(localize('staff-management-system', 'p-sta-title', { + username: targetUser.username })) .setColor('Green') .setThumbnail(targetUser.displayAvatarURL({ dynamic: true })) ); - - let desc = localize('staff-management-system', 'p-sta-desc', { - count: count, active: activeText + + let desc = localize('staff-management-system', 'p-sta-desc', { + count: count, active: activeText }); - - const rows = await LoaRequest.findAll({ - where: { userId: targetUser.id }, - order: [['createdAt', 'DESC']], - limit, - offset + + const rows = await LoaRequest.findAll({ + where: {userId: targetUser.id}, + order: [['createdAt', 'DESC']], + limit, + offset }); if (rows.length === 0) desc += localize('staff-management-system', 'p-no-hist'); else { - const icons = { - APPROVED: '✅', - DENIED: '❌', - ENDED: '⏹️', - PENDING: '🕐' + const icons = { + APPROVED: '✅', + DENIED: '❌', + ENDED: '⏹️', + PENDING: '🕐' }; desc += rows.map(r => `**${icons[r.status] || '❓'} ${r.type} - ${r.status}**\n**${localize('staff-management-system', 'general-start')}:** \n**${localize('staff-management-system', 'general-end')}:** \n**${localize('staff-management-system', 'general-rsn')}:** ${r.reason}`).join('\n\n'); } - + embed.setDescription(desc); - embed.addFields({ - name: '\u200b', - value: localize('staff-management-system', 'page-count', { - page, - total: totalPages - }) + embed.addFields({ + name: '\u200b', + value: localize('staff-management-system', 'page-count', { + page, + total: totalPages + }) }); const menu = ActionRowBuilder.from((await generateUserPanel(client, targetUser)).components[0]); @@ -1009,9 +1030,9 @@ async function generatePanelStatus(client, targetUser, page = 1) { page, totalPages ); - return { - embeds: [embed.toJSON()], - components: [menu.toJSON(), paginationRow.toJSON()] + return { + embeds: [embed.toJSON()], + components: [menu.toJSON(), paginationRow.toJSON()] }; } @@ -1064,17 +1085,17 @@ async function generatePanelActivity(client, targetUser, page = 1) { const count = historyRows.length; let totalPages = 1; if (count > 3) totalPages = 1 + Math.ceil((count - 3) / 5); - const limit = page === 1 - ? 3 + const limit = page === 1 + ? 3 : 5; - const offset = page === 1 - ? 0 - : 3 + ((page - 2) * 5); + const offset = page === 1 + ? 0 + : 3 + ((page - 2) * 5); const paginatedRows = historyRows.slice(offset, offset + limit); const embed = applyFooter(client, new EmbedBuilder() - .setTitle(localize('staff-management-system', 'p-act-title', { - username: targetUser.username + .setTitle(localize('staff-management-system', 'p-act-title', { + username: targetUser.username })) .setColor('Blue') .setThumbnail(targetUser.displayAvatarURL({ dynamic: true })) @@ -1105,21 +1126,21 @@ async function generatePanelActivity(client, targetUser, page = 1) { `staff-mgmt_panel-act_${targetUser.id}_${page - 1}`, 'panel_act_count', `staff-mgmt_panel-act_${targetUser.id}_${page + 1}`, - page, + page, totalPages ); - return { - embeds: [embed.toJSON()], - components: [menu.toJSON(), paginationRow.toJSON()] + return { + embeds: [embed.toJSON()], + components: [menu.toJSON(), paginationRow.toJSON()] }; } // Shifts page async function generatePanelShifts(client, targetUser) { const embed = applyFooter(client, new EmbedBuilder() - .setTitle(localize('staff-management-system', 'p-shi-title', { - username: targetUser.username + .setTitle(localize('staff-management-system', 'p-shi-title', { + username: targetUser.username })) .setColor('Purple') .setThumbnail(targetUser.displayAvatarURL({ dynamic: true })) @@ -1127,15 +1148,15 @@ async function generatePanelShifts(client, targetUser) { try { const Shift = client.models['staff-management-system']['StaffShift']; - const config = getConfig(client, 'shifts') || {}; - const shifts = await Shift.findAll({ - where: { - userId: targetUser.id, - endTime: { [Op.not]: null }, - duration: { [Op.not]: null } - } + const config = getConfig(client, 'shifts') || {}; + const shifts = await Shift.findAll({ + where: { + userId: targetUser.id, + endTime: {[Op.not]: null}, + duration: {[Op.not]: null} + } }); - + const totalShifts = shifts.length; const totalSeconds = shifts.reduce((sum, s) => sum + (parseInt(s.duration) || 0), 0); @@ -1149,7 +1170,7 @@ async function generatePanelShifts(client, targetUser) { let quotaStr = localize('staff-management-system', 'no-quota-configured'); const guild = client.guilds.cache.get(client.guildID); const member = await guild?.members.fetch(targetUser.id).catch(() => null); - + if (member && config.enableQuotas && config.quotas) { let bestQuota = null; let highestPosition = -1; @@ -1168,25 +1189,25 @@ async function generatePanelShifts(client, targetUser) { if (timeframe === 'Weekly') cutoff.setDate(cutoff.getDate() - 7); else cutoff.setMonth(cutoff.getMonth() - 1); - const recentShifts = await Shift.findAll({ - where: { - userId: targetUser.id, - startTime: { [Op.gt]: cutoff }, - endTime: { [Op.not]: null }, - duration: { [Op.not]: null } - } + const recentShifts = await Shift.findAll({ + where: { + userId: targetUser.id, + startTime: {[Op.gt]: cutoff}, + endTime: {[Op.not]: null}, + duration: {[Op.not]: null} + } }); const recentSeconds = recentShifts.reduce((sum, s) => sum + (parseInt(s.duration) || 0), 0); const requiredSeconds = bestQuota.hours * 3600; const isMet = recentSeconds >= requiredSeconds; - - quotaStr = localize('staff-management-system', 'duty-quota-str', { - timeframe, - duration: formatDuration(recentSeconds), - hours: bestQuota.hours, - result: isMet - ? localize('staff-management-system', 'duty-quota-met') - : localize('staff-management-system', 'duty-quota-failed') + + quotaStr = localize('staff-management-system', 'duty-quota-str', { + timeframe, + duration: formatDuration(recentSeconds), + hours: bestQuota.hours, + result: isMet + ? localize('staff-management-system', 'duty-quota-met') + : localize('staff-management-system', 'duty-quota-failed') }); } } @@ -1197,18 +1218,18 @@ async function generatePanelShifts(client, targetUser) { group: ['userId'], order: [[Shift.sequelize.literal('totalDuration'), 'DESC']] }); - + const lbIndex = allResults.findIndex(p => p.userId === targetUser.id); - const lbRank = lbIndex !== -1 - ? `${lbIndex + 1} / ${allResults.length}` + const lbRank = lbIndex !== -1 + ? `${lbIndex + 1} / ${allResults.length}` : localize('staff-management-system', 'label-unranked'); - embed.setDescription(localize('staff-management-system', 'panel-shifts-desc', { - totalShifts, - totalSeconds: formatDuration(totalSeconds), - lbRank, - breakdownStr, - quotaStr + embed.setDescription(localize('staff-management-system', 'panel-shifts-desc', { + totalShifts, + totalSeconds: formatDuration(totalSeconds), + lbRank, + breakdownStr, + quotaStr })); } catch (e) { @@ -1226,9 +1247,9 @@ async function generatePanelShifts(client, targetUser) { .setStyle(ButtonStyle.Secondary) ); - return { - embeds: [embed.toJSON()], - components: [menu.toJSON(), historyBtnRow.toJSON()] + return { + embeds: [embed.toJSON()], + components: [menu.toJSON(), historyBtnRow.toJSON()] }; } @@ -1279,9 +1300,9 @@ async function generatePanelDeletion(client, targetUser) { .setEmoji('💥') ); - return { - embeds: [embed.toJSON()], - components: [new ActionRowBuilder().addComponents(menu).toJSON()] + return { + embeds: [embed.toJSON()], + components: [new ActionRowBuilder().addComponents(menu).toJSON()] }; } @@ -1341,44 +1362,44 @@ async function executeDataDeletion(client, targetId, dataType) { async function startActivityCheck(client, interactionOrChannel, isAutomated = false) { const config = getConfig(client, 'activity-checks'); const ActivityCheck = client.models['staff-management-system']['ActivityCheck']; - - if (await ActivityCheck.findOne({ - where: { status: 'ACTIVE' } + + if (await ActivityCheck.findOne({ + where: {status: 'ACTIVE'} })) { - return !isAutomated && interactionOrChannel.editReply - ? interactionOrChannel.editReply({ content: localize('staff-management-system', 'err-ac-act') }) + return !isAutomated && interactionOrChannel.editReply + ? interactionOrChannel.editReply({content: localize('staff-management-system', 'err-ac-act')}) : null; } - let rolesToCheck = config.targetRoles?.length - ? config.targetRoles + let rolesToCheck = config.targetRoles?.length + ? config.targetRoles : (getConfig(client, 'configuration')?.staffRoles || []); - if (!rolesToCheck.length) return !isAutomated && interactionOrChannel.editReply - ? interactionOrChannel.editReply({ - content: localize('staff-management-system', 'err-ac-norole') - }) + if (!rolesToCheck.length) return !isAutomated && interactionOrChannel.editReply + ? interactionOrChannel.editReply({ + content: localize('staff-management-system', 'err-ac-norole') + }) : null; - const targetChannel = isAutomated - ? interactionOrChannel + const targetChannel = isAutomated + ? interactionOrChannel : (interactionOrChannel.options.getChannel('channel') || interactionOrChannel.guild.channels.cache.get(getSafeChannelId(config.sendingChannel)) || interactionOrChannel.channel); - if (!targetChannel) return !isAutomated && interactionOrChannel.editReply - ? interactionOrChannel.editReply({ - content: localize('staff-management-system', 'err-ac-invchan') - }) + if (!targetChannel) return !isAutomated && interactionOrChannel.editReply + ? interactionOrChannel.editReply({ + content: localize('staff-management-system', 'err-ac-invchan') + }) : null; const durationHours = config.timeframe || 24; const endTime = new Date(Date.now() + durationHours * 60 * 60 * 1000); - let embedTemplate = typeof config.checkMessage === 'string' - ? JSON.parse(config.checkMessage) + let embedTemplate = typeof config.checkMessage === 'string' + ? JSON.parse(config.checkMessage) : config.checkMessage; - let msgOpts = await embedTypeV2(embedTemplate, { - '%end-time%': ``, - '%duration%': durationHours.toString() + let msgOpts = await embedTypeV2(embedTemplate, { + '%end-time%': ``, + '%duration%': durationHours.toString() }); - + if (msgOpts?.content?.trim() === '') delete msgOpts.content; msgOpts.components = [ new ActionRowBuilder() @@ -1394,27 +1415,27 @@ async function startActivityCheck(client, interactionOrChannel, isAutomated = fa try { const checkMessage = await targetChannel.send(msgOpts); - if (!isAutomated && interactionOrChannel.editReply) await interactionOrChannel.editReply({ - content: localize('staff-management-system', 'succ-ac-start', { - channel: targetChannel.id, - hours: durationHours - }) + if (!isAutomated && interactionOrChannel.editReply) await interactionOrChannel.editReply({ + content: localize('staff-management-system', 'succ-ac-start', { + channel: targetChannel.id, + hours: durationHours + }) }); - - const record = await ActivityCheck.create({ - messageId: checkMessage.id, - channelId: targetChannel.id, - endTime, - targetRoles: JSON.stringify(rolesToCheck), - status: 'ACTIVE' + + const record = await ActivityCheck.create({ + messageId: checkMessage.id, + channelId: targetChannel.id, + endTime, + targetRoles: JSON.stringify(rolesToCheck), + status: 'ACTIVE' }); schedule.scheduleJob(endTime, async () => { const currentCheck = await ActivityCheck.findByPk(record.id); if (currentCheck && currentCheck.status === 'ACTIVE') await endActivityCheckProcess(client, currentCheck); }); } catch (e) { - if (!isAutomated && interactionOrChannel.editReply) interactionOrChannel.editReply({ - content: localize('staff-management-system', 'err-ac-perms', { channel: targetChannel.id }) + if (!isAutomated && interactionOrChannel.editReply) interactionOrChannel.editReply({ + content: localize('staff-management-system', 'err-ac-perms', {channel: targetChannel.id}) }); } } @@ -1432,9 +1453,9 @@ async function endActivityCheckProcess(client, activeCheck) { .setColor('#ed4245'); originalEmbed .setTitle(localize('staff-management-system', 'ac-title-end')); - await msg.edit({ - embeds: [originalEmbed.toJSON()], - components: [] + await msg.edit({ + embeds: [originalEmbed.toJSON()], + components: [] }); } } catch (e) {} @@ -1457,13 +1478,13 @@ async function endActivityCheckProcess(client, activeCheck) { const expectedIds = [...expectedMembers.keys()]; const profiles = await StaffProfile.findAll({ where: { - userId: { [Op.in]: expectedIds } + userId: {[Op.in]: expectedIds} } }); expectedMembers.forEach(member => { if (respondedUserIds.has(member.id)) return responded.push(member); - + let isException = false; const prof = profiles.find(p => p.userId === member.id); const isLoa = prof?.activityStatus === 'LOA'; @@ -1474,8 +1495,8 @@ async function endActivityCheckProcess(client, activeCheck) { else if (config.exceptionsType === 'LoA and RA' && (isLoa || isRa)) isException = true; else if (config.exceptionsType === 'Custom role(s)' && member.roles.cache.some(r => config.customExceptionRoles?.includes(r.id))) isException = true; - isException - ? exceptions.push(member) + isException + ? exceptions.push(member) : failed.push(member); }); @@ -1483,35 +1504,35 @@ async function endActivityCheckProcess(client, activeCheck) { .setTitle(localize('staff-management-system', 'ac-res-title')) .setColor('Blurple') .addFields( - { - name: localize('staff-management-system', 'ac-f-res', { + { + name: localize('staff-management-system', 'ac-f-res', { count: responded.length } - ), - value: responded.length - ? responded.map(m => `<@${m.id}>`).join(', ').substring(0, 1024) - : localize('staff-management-system', 'info-none') + ), + value: responded.length + ? responded.map(m => `<@${m.id}>`).join(', ').substring(0, 1024) + : localize('staff-management-system', 'info-none') }, - { - name: localize('staff-management-system', 'ac-f-fail', { - count: failed.length - }), - value: failed.length - ? failed.map(m => `<@${m.id}>`).join(', ').substring(0, 1024) - : localize('staff-management-system', 'info-none') + { + name: localize('staff-management-system', 'ac-f-fail', { + count: failed.length + }), + value: failed.length + ? failed.map(m => `<@${m.id}>`).join(', ').substring(0, 1024) + : localize('staff-management-system', 'info-none') }, - { - name: localize('staff-management-system', 'ac-f-exc', { - count: exceptions.length - }), - value: exceptions.length - ? exceptions.map(m => `<@${m.id}>`).join(', ').substring(0, 1024) - : localize('staff-management-system', 'info-none') + { + name: localize('staff-management-system', 'ac-f-exc', { + count: exceptions.length + }), + value: exceptions.length + ? exceptions.map(m => `<@${m.id}>`).join(', ').substring(0, 1024) + : localize('staff-management-system', 'info-none') } ) ); - const pingText = (config.pingResults && config.pingRoles?.length) - ? config.pingRoles.map(rId => `<@&${rId}>`).join(' ') + const pingText = (config.pingResults && config.pingRoles?.length) + ? config.pingRoles.map(rId => `<@&${rId}>`).join(' ') : null; const finalMessage = { embeds: [embed.toJSON()] }; if (pingText) finalMessage.content = pingText; @@ -1537,16 +1558,16 @@ function initActivityCheckAutomation(client) { const config = getConfig(client, 'activity-checks'); if (!config?.enableActivityChecks || !config?.automatedChecks) return; - let cronString = config.automatedCheckInterval === 'Cronjob' - ? config.automatedCheckCronjob + let cronString = config.automatedCheckInterval === 'Cronjob' + ? config.automatedCheckCronjob : null; if (!cronString) { - const dayMap = { - 'Monday': 1, - 'Tuesday': 2, - 'Wednesday': 3, - 'Thursday': 4, - 'Friday': 5, + const dayMap = { + 'Monday': 1, + 'Tuesday': 2, + 'Wednesday': 3, + 'Thursday': 4, + 'Friday': 5, 'Saturday': 6, 'Sunday': 7 }[config.automatedCheckWeekDay] || 1; @@ -1576,19 +1597,19 @@ function initActivityCheckAutomation(client) { // ---------- Reviews ---------- async function submitReview(client, interaction, targetUser, stars, comment) { - await interaction.deferReply({ ephemeral: true }); + await interaction.deferReply({ephemeral: true}); const config = getConfig(client, 'reviews'); - if (!config?.enableReviews) return interaction.editReply({ - content: localize('staff-management-system', 'err-feat-disabled', { - feature: 'Reviews' + if (!config?.enableReviews) return interaction.editReply({ + content: localize('staff-management-system', 'err-feat-disabled', { + feature: 'Reviews' }) }); const targetMember = await interaction.guild.members.fetch(targetUser.id).catch(() => null); - if (!targetMember) return interaction.editReply({ - content: localize('staff-management-system', 'err-not-mem') + if (!targetMember) return interaction.editReply({ + content: localize('staff-management-system', 'err-not-mem') }); - if (!config.allowSelfRating && targetUser.id === interaction.user.id) return interaction.editReply({ + if (!config.allowSelfRating && targetUser.id === interaction.user.id) return interaction.editReply({ content: localize('staff-management-system', 'err-self-rate') }); @@ -1609,63 +1630,63 @@ async function submitReview(client, interaction, targetUser, stars, comment) { } } - const review = await client.models['staff-management-system']['StaffReview'].create({ - targetId: targetUser.id, - authorId: interaction.user.id, - stars, - comment + const review = await client.models['staff-management-system']['StaffReview'].create({ + targetId: targetUser.id, + authorId: interaction.user.id, + stars, + comment }); const channelId = getSafeChannelId(config.reviewLogChannel); if (channelId) { const channel = interaction.guild.channels.cache.get(channelId); if (channel) { - let msgOpts = await embedTypeV2(config.ratingMessage, { - '%staff-mention%': targetUser.toString(), - '%reviewer-mention%': interaction.user.toString(), - '%stars%': "⭐".repeat(stars), - '%rating%': stars.toString(), - '%comment%': comment, - '%staff-avatar%': targetUser.displayAvatarURL({ dynamic: true }), - '%reviewer-avatar%': interaction.user.displayAvatarURL({ dynamic: true }) + let msgOpts = await embedTypeV2(config.ratingMessage, { + '%staff-mention%': targetUser.toString(), + '%reviewer-mention%': interaction.user.toString(), + '%stars%': '⭐'.repeat(stars), + '%rating%': stars.toString(), + '%comment%': comment, + '%staff-avatar%': targetUser.displayAvatarURL({dynamic: true}), + '%reviewer-avatar%': interaction.user.displayAvatarURL({dynamic: true}) }); if (msgOpts?.content?.trim() === '') delete msgOpts.content; const sentMessage = await channel.send(msgOpts).catch(()=>{}); if (sentMessage) await review.update({ messageUrl: sentMessage.url }); } } - await interaction.editReply({ - content: localize('staff-management-system', 'succ-review', { - tag: targetUser.tag, - stars + await interaction.editReply({ + content: localize('staff-management-system', 'succ-review', { + tag: targetUser.tag, + stars }) }); } async function generateReviewHistoryResponse(client, targetUser, page = 1) { - if (!getConfig(client, 'reviews')?.enableReviews) return { - content: localize('staff-management-system', 'err-feat-disabled', { - feature: 'Reviews' - }), - flags: MessageFlags.Ephemeral + if (!getConfig(client, 'reviews')?.enableReviews) return { + content: localize('staff-management-system', 'err-feat-disabled', { + feature: 'Reviews' + }), + flags: MessageFlags.Ephemeral }; const limit = 8; const offset = (page - 1) * limit; const Review = client.models['staff-management-system']['StaffReview']; - const { count, rows } = await Review.findAndCountAll({ - where: { targetId: targetUser.id }, - order: [['createdAt', 'DESC']], - limit, - offset + const {count, rows} = await Review.findAndCountAll({ + where: {targetId: targetUser.id}, + order: [['createdAt', 'DESC']], + limit, + offset }); - const allReviews = await Review.findAll({ - where: { targetId: targetUser.id }, - attributes: ['stars'] + const allReviews = await Review.findAll({ + where: {targetId: targetUser.id}, + attributes: ['stars'] }); - const avg = allReviews.length - ? (allReviews.reduce((a, b) => a + b.stars, 0) / allReviews.length).toFixed(1) + const avg = allReviews.length + ? (allReviews.reduce((a, b) => a + b.stars, 0) / allReviews.length).toFixed(1) : 0; const embed = applyFooter(client, new EmbedBuilder() @@ -1675,67 +1696,67 @@ async function generateReviewHistoryResponse(client, targetUser, page = 1) { .setThumbnail(targetUser.displayAvatarURL({ dynamic: true })) ); - embed.addFields({ - name: localize('staff-management-system', 'label-hist'), - value: rows.length > 0 + embed.addFields({ + name: localize('staff-management-system', 'label-hist'), + value: rows.length > 0 ? rows.map(r => `**${"⭐".repeat(r.stars)}** ${localize('staff-management-system', 'label-by')} <@${r.authorId}>${r.messageUrl - ? ` • [Jump](${r.messageUrl})` - : ''}\n"${r.comment}"`).join('\n\n') + ? ` • [Jump](${r.messageUrl})` + : ''}\n"${r.comment}"`).join('\n\n') : localize('staff-management-system', 'p-no-hist') }); const row = buildPaginationRow( - `staff-mgmt_rev-page_${targetUser.id}_${page - 1}`, - 'page_count_disabled', - `staff-mgmt_rev-page_${targetUser.id}_${page + 1}`, - page, + `staff-mgmt_rev-page_${targetUser.id}_${page - 1}`, + 'page_count_disabled', + `staff-mgmt_rev-page_${targetUser.id}_${page + 1}`, + page, Math.ceil(count / limit) || 1 ); - return { - embeds: [embed.toJSON()], - components: [row.toJSON()] + return { + embeds: [embed.toJSON()], + components: [row.toJSON()] }; } async function getReviewHistory(client, interaction, targetUser) { - await interaction.deferReply({ ephemeral: true }); + await interaction.deferReply({ephemeral: true}); const response = await generateReviewHistoryResponse(client, targetUser, 1); if (response.content && response.content.startsWith('❌')) return interaction.editReply(response); - - await interaction.editReply({ + + await interaction.editReply({ ...response }); } module.exports = { - getConfig, + getConfig, getSafeChannelId, parseDurationToDays, - applyFooter, + applyFooter, checkStaffPermissions, - buildPaginationRow, - formatDuration, - issueInfraction, - issueSuspension, - getInfractionHistory, - voidInfraction, + buildPaginationRow, + formatDuration, + issueInfraction, + issueSuspension, + getInfractionHistory, + voidInfraction, generateInfractionHistoryResponse, - promoteUser, - generatePromotionHistoryResponse, - getPromotionHistory, - generateUserPanel, - generatePanelInfractions, - generatePanelPromotions, - generatePanelActivity, - generatePanelReviews, - generatePanelStatus, - generatePanelShifts, - generatePanelDeletion, - executeDataDeletion, + promoteUser, + generatePromotionHistoryResponse, + getPromotionHistory, + generateUserPanel, + generatePanelInfractions, + generatePanelPromotions, + generatePanelActivity, + generatePanelReviews, + generatePanelStatus, + generatePanelShifts, + generatePanelDeletion, + executeDataDeletion, generatePanelSubpage, - startActivityCheck, - initActivityCheckAutomation, + startActivityCheck, + initActivityCheckAutomation, endActivityCheckProcess, - submitReview, - getReviewHistory, - generateReviewHistoryResponse, + submitReview, + getReviewHistory, + generateReviewHistoryResponse }; \ No newline at end of file diff --git a/modules/starboard/configs/config.json b/modules/starboard/configs/config.json index 7a6ea9ae..c7c9de16 100644 --- a/modules/starboard/configs/config.json +++ b/modules/starboard/configs/config.json @@ -1,227 +1,126 @@ { - "description": {}, - "humanName": { - "en": "Configuration", - "de": "Konfiguration" - }, - "filename": "config.json", - "content": [ - { - "name": "channelId", - "humanName": { - "en": "Starboard channel", - "de": "Starboard-Kanal" - }, - "default": { - "en": "" - }, - "description": { - "en": "In which channel starred messages are sent", - "de": "In welchen Kanal gestarrte Nachrichten gesendet werden" - }, - "type": "channelID" - }, - { - "name": "emoji", - "humanName": { - "en": "Emoji" - }, - "default": { - "en": "⭐" - }, - "description": { - "en": "Which emoji should be used to star messages", - "de": "Mit welchem Emoji Nachrichten gestarrt werden sollen" - }, - "type": "emoji" - }, - { - "name": "message", - "humanName": { - "en": "Message", - "de": "Nachricht" - }, - "default": { - "en": { - "message": "**%stars%** %emoji% in %channelMention%", - "color": "#f5c91b", - "description": "%content%", - "image": "%image%", - "author": { - "name": "%displayName%", - "img": "%userAvatar%", - "url": "%link%" - } - } - }, - "description": { - "en": "This message gets send into the selected channel", - "de": "Diese Nachricht wird in den ausgewählten Kanal gesendet" - }, - "allowEmbed": true, - "type": "string", - "params": [ - { - "name": "stars", - "description": { - "en": "Amount of reactions on the message", - "de": "Anzahl der Reaktionen auf die Nachricht" - } - }, - { - "name": "content", - "description": { - "en": "The content of the starred message", - "de": "Der Inhalt der gestarrten Nachricht" - } - }, - { - "name": "link", - "description": { - "en": "A link to the starred message", - "de": "Ein Link zur gestarrten Nachricht" - } - }, - { - "name": "userID", - "description": { - "en": "The user ID of the author of the starred message", - "de": "Die Nutzer-ID des Autors der gestarrten Nachricht" - } - }, - { - "name": "userName", - "description": { - "en": "The username of the author of the starred message", - "de": "Der Benutzername des Autors der gestarrten Nachricht" - } - }, - { - "name": "displayName", - "description": { - "en": "The nickname of the author", - "de": "Der Nickname des Autors" - } - }, - { - "name": "userTag", - "description": { - "en": "The tag of the author of the starred message", - "de": "Der Tag des Autors der gestarrten Nachricht" - } - }, - { - "name": "userAvatar", - "description": { - "en": "The avatar URL of the message author", - "de": "Die Avatar-URL des Nachrichtenautors" - } - }, - { - "name": "channelName", - "description": { - "en": "The name of the channel the starred message was sent in", - "de": "Der Name des Kanals, in dem die gestarrte Nachricht gesendet wurde" - } - }, - { - "name": "channelMention", - "description": { - "en": "The channel mention of the channel the starred message was sent in", - "de": "Die Kanalerwähnung des Kanals, in dem die gestarrte Nachricht gesendet wurde" - } - }, - { - "name": "emoji", - "description": { - "en": "The set starboard emoji for lazy users", - "de": "Das festgelegte Starboard-Emoji für faule Nutzer" - } - }, - { - "name": "image", - "description": { - "en": "The first attachment or the first image url in the message", - "de": "Der erste Anhang oder die erste Bild-URL in der Nachricht" - } - } - ] - }, - { - "name": "excludedChannels", - "humanName": { - "en": "Excluded channels", - "de": "Ausgenommene Kanäle" - }, - "default": { - "en": [] - }, - "description": { - "en": "In which channels messages cannot be starred", - "de": "In welchen Kanälen Nachrichten nicht gestarrt werden können" - }, - "type": "array", - "content": "channelID" - }, - { - "name": "excludedRoles", - "humanName": { - "en": "Excluded roles", - "de": "Ausgenommene Rollen" - }, - "default": { - "en": [] - }, - "description": { - "en": "Users with these roles cannot star messages", - "de": "Nutzer mit diesen Rollen können keine Nachrichten starren" - }, - "type": "array", - "content": "roleID" - }, - { - "name": "minStars", - "humanName": { - "en": "Minimum stars", - "de": "Mindestanzahl Sterne" - }, - "default": { - "en": 3 - }, - "description": { - "en": "How many star reactions are needed for a message to land on the starboard", - "de": "Wie viele Star-Reaktionen benötigt werden, damit eine Nachricht auf dem Starboard landet" - }, - "type": "integer" - }, - { - "name": "starsPerHour", - "humanName": { - "en": "Stars per user per hour", - "de": "Sterne pro Nutzer pro Stunde" - }, - "default": { - "en": 5 - }, - "description": { - "en": "How many messages a user can star per hour", - "de": "Wie viele Nachrichten ein Nutzer pro Stunde starren kann" - }, - "type": "integer" - }, - { - "name": "selfStar", - "humanName": { - "en": "Self-Star" - }, - "default": { - "en": true - }, - "description": { - "en": "Whether users can star their own messages", - "de": "Ob Nutzer ihre eigenen Nachrichten starren können" - }, - "type": "boolean" - } - ] + "description": "Configure the starboard channel and reaction settings here", + "humanName": "Configuration", + "filename": "config.json", + "content": [ + { + "name": "channelId", + "humanName": "Starboard channel", + "default": "", + "description": "In which channel starred messages are sent", + "type": "channelID" + }, + { + "name": "emoji", + "humanName": "Emoji", + "default": "⭐", + "description": "Which emoji should be used to star messages", + "type": "emoji" + }, + { + "name": "message", + "humanName": "Message", + "default": { + "message": "**%stars%** %emoji% in %channelMention%", + "color": "#f5c91b", + "description": "%content%", + "image": "%image%", + "author": { + "name": "%displayName%", + "img": "%userAvatar%", + "url": "%link%" + } + }, + "description": "This message gets send into the selected channel", + "allowEmbed": true, + "type": "string", + "params": [ + { + "name": "stars", + "description": "Amount of reactions on the message" + }, + { + "name": "content", + "description": "The content of the starred message" + }, + { + "name": "link", + "description": "A link to the starred message" + }, + { + "name": "userID", + "description": "The user ID of the author of the starred message" + }, + { + "name": "userName", + "description": "The username of the author of the starred message" + }, + { + "name": "displayName", + "description": "The nickname of the author" + }, + { + "name": "userTag", + "description": "The tag of the author of the starred message" + }, + { + "name": "userAvatar", + "description": "The avatar URL of the message author" + }, + { + "name": "channelName", + "description": "The name of the channel the starred message was sent in" + }, + { + "name": "channelMention", + "description": "The channel mention of the channel the starred message was sent in" + }, + { + "name": "emoji", + "description": "The set starboard emoji for lazy users" + }, + { + "name": "image", + "description": "The first attachment or the first image url in the message" + } + ] + }, + { + "name": "excludedChannels", + "humanName": "Excluded channels", + "default": [], + "description": "In which channels messages cannot be starred", + "type": "array", + "content": "channelID" + }, + { + "name": "excludedRoles", + "humanName": "Excluded roles", + "default": [], + "description": "Users with these roles cannot star messages", + "type": "array", + "content": "roleID" + }, + { + "name": "minStars", + "humanName": "Minimum stars", + "default": 3, + "description": "How many star reactions are needed for a message to land on the starboard", + "type": "integer" + }, + { + "name": "starsPerHour", + "humanName": "Stars per user per hour", + "default": 5, + "description": "How many messages a user can star per hour", + "type": "integer" + }, + { + "name": "selfStar", + "humanName": "Self-Star", + "default": true, + "description": "Whether users can star their own messages", + "type": "boolean" + } + ] } \ No newline at end of file diff --git a/modules/starboard/handleStarboard.js b/modules/starboard/handleStarboard.js index 572d61b2..ded74bf6 100644 --- a/modules/starboard/handleStarboard.js +++ b/modules/starboard/handleStarboard.js @@ -1,4 +1,9 @@ -const {embedTypeV2, disableModule, formatDiscordUserName} = require('../../src/functions/helpers'); +const { + embedTypeV2, + disableModule, + formatDiscordUserName, + archiveDiscordAttachment +} = require('../../src/functions/helpers'); const {localize} = require('../../src/functions/localize'); const {Op} = require('sequelize'); @@ -70,7 +75,15 @@ module.exports = async (client, msgReaction, user, isReactionRemove = false) => return; } - let image = msg.attachments.size > 0 ? msg.attachments.first().url : null; + let image = null; + if (msg.attachments.size > 0) { + const firstAttachment = msg.attachments.first(); + image = await archiveDiscordAttachment(client, firstAttachment.url, { + displayName: `Starboard post by ${formatDiscordUserName(msg.author)} in #${msg.channel.name}`.slice(0, 100), + tags: ['starboard'], + uploaderDiscordID: msg.author.id + }); + } if (!image) { const matches = msg.content.match(/https?:\/\/.*\.(?:png|jpg|gif|jpeg|webp)/i); if (matches) image = matches[0]; diff --git a/modules/starboard/module.json b/modules/starboard/module.json index a4cbd144..038f14af 100644 --- a/modules/starboard/module.json +++ b/modules/starboard/module.json @@ -1,24 +1,20 @@ { "name": "starboard", - "humanReadableName": { - "en": "Starboard" - }, + "humanReadableName": "Starboard", "author": { "scnxOrgID": "60", "name": "TomatoCake", "link": "https://github.com/DEVTomatoCake" }, - "description": { - "en": "Let users highlight messages into a starboard channel by reacting.", - "de": "Lass Nutzer Nachrichten durch eine Reaktion in einem Starboard-Kanal hervorheben." - }, + "description": "Let users highlight messages into a starboard channel by reacting.", "events-dir": "/events", "models-dir": "/models", "config-example-files": [ "configs/config.json" ], + "fa-icon": "fas fa-star", "tags": [ "community" ], "openSourceURL": "https://github.com/DEVTomatoCake/ScootKit-CustomBot/tree/main/modules/starboard" -} \ No newline at end of file +} diff --git a/modules/status-roles/configs/config.json b/modules/status-roles/configs/config.json index 92c85945..d8f919ec 100644 --- a/modules/status-roles/configs/config.json +++ b/modules/status-roles/configs/config.json @@ -1,77 +1,37 @@ { - "description": { - "en": "Configure the function of the module here", - "de": "Stelle hier die Funktionen des Modules ein" - }, - "humanName": { - "en": "Configuration", - "de": "Konfiguration" - }, + "description": "Configure the function of the module here", + "humanName": "Configuration", "filename": "config.json", "content": [ { "name": "words", - "humanName": { - "en": "Words", - "de": "Statusinhalt" - }, - "default": { - "en": [], - "de": [] - }, - "description": { - "en": "Words users should have in their status.", - "de": "Wörter, die Nutzer in ihrem Status haben sollen." - }, + "humanName": "Words", + "default": [], + "description": "Words users should have in their status.", "type": "array", "content": "string" }, { "name": "roles", - "humanName": { - "en": "Roles", - "de": "Rollen" - }, - "default": { - "en": [], - "de": [] - }, - "description": { - "en": "Roles to give to users with one of the words in their status", - "de": "Rollen, die an Nutzer mit einem der Wörter im Status vergeben werden sollen" - }, + "humanName": "Roles", + "default": [], + "description": "Roles to give to users with one of the words in their status", "type": "array", "content": "roleID" }, { "name": "remove", - "humanName": { - "en": "Remove all other roles", - "de": "Entferne alle anderen Rollen" - }, - "default": { - "en": false - }, - "description": { - "en": "Remove all other roles from users with one of the words in their status", - "de": "Entferne alle anderen Rollen von Nutzern mit einem der Wörter im Status" - }, + "humanName": "Remove all other roles", + "default": false, + "description": "Remove all other roles from users with one of the words in their status", "type": "boolean" }, { "name": "ignoreOfflineUsers", - "humanName": { - "en": "Do not remove roles from offline users", - "de": "Rollen von offline Nutzern nicht entfernen" - }, + "humanName": "Do not remove roles from offline users", "type": "boolean", - "default": { - "en": true - }, - "description": { - "en": "When users are offline, they don't have a status, leading to the role being removed. If enabled, the status role won't be removed from offline users, only users that have a different status. Recommended on servers with more than 500 members.", - "de": "Wenn Nutzer offline sind, haben sie keinen Status, was dazu führt, dass die Rolle entfernt wird. Wenn aktiviert, wird die Status-Rolle nicht von offline Nutzern entfernt, nur von Nutzern mit anderem Status. Empfohlen auf Servern mit 500+ Nutzern." - } + "default": true, + "description": "When users are offline, they don't have a status, leading to the role being removed. If enabled, the status role won't be removed from offline users, only users that have a different status. Recommended on servers with more than 500 members." } ] } \ No newline at end of file diff --git a/modules/status-roles/events/presenceUpdate.js b/modules/status-roles/events/presenceUpdate.js index 2d618452..6242144f 100644 --- a/modules/status-roles/events/presenceUpdate.js +++ b/modules/status-roles/events/presenceUpdate.js @@ -3,6 +3,7 @@ const {ActivityType} = require('discord.js'); module.exports.run = async function (client, oldPresence, newPresence) { if (!client.botReadyAt) return; + if (!newPresence.member) return; if (newPresence.member.guild.id !== client.guildID) return; const moduleConfig = client.configurations['status-roles']['config']; const roles = moduleConfig.roles; diff --git a/modules/status-roles/module.json b/modules/status-roles/module.json index f83b1f43..a7185f10 100644 --- a/modules/status-roles/module.json +++ b/modules/status-roles/module.json @@ -10,15 +10,10 @@ "config-example-files": [ "configs/config.json" ], + "fa-icon": "fa-solid fa-user-tag", "tags": [ "administration" ], - "humanReadableName": { - "en": "Status-roles", - "de": "Status-Rollen" - }, - "description": { - "en": "Simple module to reward users who have an invite to your server in their status!", - "de": "Einfaches Modul, um Nutzer zu belohnen, die einen Link zu deinem Server in ihrem Status haben!" - } -} \ No newline at end of file + "humanReadableName": "Status-roles", + "description": "Simple module to reward users who have an invite to your server in their status!" +} diff --git a/modules/sticky-messages/configs/sticky-messages.json b/modules/sticky-messages/configs/sticky-messages.json index 9722a565..4fffc79e 100644 --- a/modules/sticky-messages/configs/sticky-messages.json +++ b/modules/sticky-messages/configs/sticky-messages.json @@ -1,60 +1,30 @@ { - "description": { - "en": "Manage the sticky messages here", - "de": "Passe hier die Sticky-Nachrichten an" - }, - "humanName": { - "en": "Sticky messages", - "de": "Sticky-Nachrichten" - }, - "filename": "sticky-messages.json", - "configElements": true, - "content": [ - { - "name": "channelId", - "humanName": { - "en": "Channel", - "de": "Kanal" - }, - "default": { - "en": "" - }, - "description": { - "en": "Channel-ID in which the message should get send", - "de": "Kanal-ID, in welchem die Nachricht gesendet werden soll" - }, - "type": "channelID" - }, - { - "name": "message", - "humanName": { - "en": "Message", - "de": "Nachricht" - }, - "default": { - "en": "" - }, - "description": { - "en": "Message that should get send", - "de": "Nachricht, die gesendet werden soll" - }, - "type": "string", - "allowEmbed": true - }, - { - "name": "respondBots", - "humanName": { - "en": "Respond to bots", - "de": "Antworten auf Bots" - }, - "default": { - "en": false - }, - "description": { - "en": "Whether your bot reacts to messages from other bots in the channel", - "de": "Ob dein Bot auf Nachrichten von anderen Bots in dem Kanal reagiert" - }, - "type": "boolean" - } - ] + "description": "Manage the sticky messages here", + "humanName": "Sticky messages", + "filename": "sticky-messages.json", + "configElements": true, + "content": [ + { + "name": "channelId", + "humanName": "Channel", + "default": "", + "description": "Channel-ID in which the message should get send", + "type": "channelID" + }, + { + "name": "message", + "humanName": "Message", + "default": "", + "description": "Message that should get send", + "type": "string", + "allowEmbed": true + }, + { + "name": "respondBots", + "humanName": "Respond to bots", + "default": false, + "description": "Whether your bot reacts to messages from other bots in the channel", + "type": "boolean" + } + ] } \ No newline at end of file diff --git a/modules/sticky-messages/events/messageCreate.js b/modules/sticky-messages/events/messageCreate.js index 540ed764..993164ed 100644 --- a/modules/sticky-messages/events/messageCreate.js +++ b/modules/sticky-messages/events/messageCreate.js @@ -14,8 +14,10 @@ async function deleteMessage(clientId, channel) { message = await channel.messages.fetch(channelData[channel.id].msg).catch(async () => { const msgs = await channel.messages.fetch({limit: 20}); message = msgs.find(m => m.author.id === clientId); + if (message) message.delete().catch(() => { + }); }); - if (message) message.delete().catch(() => { + if (message && message.deletable) message.delete().catch(() => { }); } diff --git a/modules/sticky-messages/module.json b/modules/sticky-messages/module.json index efe6db2f..2e4a9637 100644 --- a/modules/sticky-messages/module.json +++ b/modules/sticky-messages/module.json @@ -1,24 +1,19 @@ { "name": "sticky-messages", - "humanReadableName": { - "en": "Sticky messages", - "de": "Sticky-Nachrichten" - }, + "humanReadableName": "Sticky messages", "author": { "scnxOrgID": "60", "name": "TomatoCake", "link": "https://github.com/DEVTomatoCake" }, - "description": { - "en": "Let a set message always appear at the end of a channel.", - "de": "Lasse eine festgelegte Nachricht immer am Ende eines Kanals erscheinen." - }, + "description": "Let a set message always appear at the end of a channel.", "events-dir": "/events", "config-example-files": [ "configs/sticky-messages.json" ], + "fa-icon": "fas fa-thumbtack", "tags": [ "community" ], "openSourceURL": "https://github.com/DEVTomatoCake/ScootKit-CustomBot/tree/main/modules/sticky-messages" -} \ No newline at end of file +} diff --git a/modules/suggestions/commands/manage-suggestion.js b/modules/suggestions/commands/manage-suggestion.js index dc750532..fcf632da 100644 --- a/modules/suggestions/commands/manage-suggestion.js +++ b/modules/suggestions/commands/manage-suggestion.js @@ -53,6 +53,7 @@ module.exports.autoComplete = { */ async function autoCompleteSuggestionID(interaction) { const suggestions = await interaction.client.models['suggestions']['Suggestion'].findAll({ + where: {adminAnswer: null}, order: [['createdAt', 'DESC']] }); const returnValue = []; diff --git a/modules/suggestions/config.json b/modules/suggestions/config.json index f9eacca2..59c8eeaf 100644 --- a/modules/suggestions/config.json +++ b/modules/suggestions/config.json @@ -1,12 +1,6 @@ { - "description": { - "en": "Configure the function of the module here", - "de": "Stelle hier die Funktionen des Modules ein" - }, - "humanName": { - "en": "Configuration", - "de": "Konfiguration" - }, + "description": "Configure the function of the module here", + "humanName": "Configuration", "filename": "config.json", "commandsWarnings": { "normal": [ @@ -16,435 +10,228 @@ "content": [ { "name": "suggestionChannel", - "humanName": { - "en": "Suggestion-Channel", - "de": "Vorschlagskanal" - }, - "default": { - "en": "" - }, - "description": { - "en": "Channel in which this module should operate", - "de": "Kanal in dem dieses Modul arbeiten soll" - }, + "humanName": "Suggestion-Channel", + "default": "", + "description": "Channel in which this module should operate", "type": "channelID" }, { "name": "createSuggestionFromMessagesInChannel", - "humanName": { - "en": "Create suggestions from messages in channel", - "de": "Vorschläge von Nachrichten im Kanal erstellen" - }, - "default": { - "en": false, - "de": false - }, - "description": { - "en": "If enabled, the bot will create thread under each suggestion", - "de": "Wenn aktiviert, wird für jede Nachricht im Vorschlag-Kanal ein Vorschlag erstellt" - }, + "humanName": "Create suggestions from messages in channel", + "default": false, + "description": "If enabled, the bot will create thread under each suggestion", "type": "boolean" }, { "name": "reactions", - "humanName": { - "en": "Reactions", - "de": "Reaktionen" - }, - "default": { - "en": [], - "de": [] - }, - "description": { - "en": "Emojis with which the bot should react to a new suggestion", - "de": "Emojis mit denen der Bot auf neue Vorschläge reagieren soll" - }, + "humanName": "Reactions", + "default": [], + "description": "Emojis with which the bot should react to a new suggestion", "type": "array", "content": "emoji" }, { "name": "allowUserComment", - "humanName": { - "en": "User-Comments in Threads", - "de": "Nutzerkommentare in Threads" - }, - "default": { - "en": true, - "de": true - }, - "description": { - "en": "If enabled, the bot will create thread under each suggestion", - "de": "Wenn aktiviert erstellt der Bot immer einen neuen Thread unter Vorschlägen" - }, + "humanName": "User-Comments in Threads", + "default": true, + "description": "If enabled, the bot will create thread under each suggestion", "type": "boolean" }, { "name": "threadName", "dependsOn": "allowUserComment", - "humanName": { - "en": "Thread-Name" - }, - "default": { - "en": "Comments", - "de": "Kommentare" - }, - "description": { - "en": "Name of the thread", - "de": "Name des Threads" - }, + "humanName": "Thread-Name", + "default": "Comments", + "description": "Name of the thread", "type": "string" }, { "name": "successfullySubmitted", - "humanName": { - "en": "\"Successfully submitted\"-Message", - "de": "\"Erfolgreich eingereicht\"-Nachricht" - }, - "default": { - "en": "Suggestion %id% submitted successfully.", - "de": "Vorschlag %id% erfolgreich eingereicht." - }, - "description": { - "en": "This message gets send if a suggestion is submitted successfully.", - "de": "Diese Nachricht wird gesendet, wenn ein Vorschlag erfolgreich eingereicht wurde" - }, + "humanName": "\"Successfully submitted\"-Message", + "default": "Suggestion %id% submitted successfully.", + "description": "This message gets send if a suggestion is submitted successfully.", "type": "string", "allowEmbed": true, "params": [ { "name": "id", - "description": { - "en": "ID of the suggestion", - "de": "ID des Vorschlags" - } + "description": "ID of the suggestion" } ] }, { "name": "notifyRole", - "humanName": { - "en": "Notification-Role", - "de": "Benachrichtigungsrolle" - }, - "default": { - "en": "" - }, - "description": { - "en": "If set, this role gets pinged when a new suggestion gets created", - "de": "Wenn eine Rolle gesetzt ist, wird diese gepingt wenn ein neuer Vorschlag erstellt wird" - }, + "humanName": "Notification-Role", + "default": "", + "description": "If set, this role gets pinged when a new suggestion gets created", "type": "roleID", "allowNull": true }, { "name": "sendPNNotifications", - "humanName": { - "en": "Send DM-Notifications", - "de": "PN-Benachrichtigungen senden" - }, - "default": { - "en": true, - "de": true - }, - "description": { - "en": "If enabled the creator and all commentators get a notification when something changes on a suggestion", - "de": "Wenn diese Option aktiviert ist, wird der Ersteller benachrichtigt, wenn sich etwas an einem Vorschlag ändert" - }, + "humanName": "Send DM-Notifications", + "default": true, + "description": "If enabled the creator and all commentators get a notification when something changes on a suggestion", "type": "boolean" }, { "name": "teamChange", - "humanName": { - "en": "DM-Status-Notification", - "de": "PN-Statusbenachrichtigung" - }, - "default": { - "en": "Hi, a suggestion you are subscribed to got updated by a team member - read it here %url%", - "de": "Hi, ein von dir abonnierter Vorschlag wurde von einem Teammitglied beantwortet - lese ihn hier %url%" - }, - "description": { - "en": "This message gets send to the creator and all commentators when a suggestion gets updated and sendPNNotifications is enabled", - "de": "Diese Nachricht wird an den Ersteller und alle Nutzer, die einen Kommentar geschrieben haben, gesendet, wenn ein Vorschlag aktualisiert wird und \"PN-Benachrichtigungen senden\" aktiviert ist" - }, + "humanName": "DM-Status-Notification", + "default": "Hi, a suggestion you are subscribed to got updated by a team member - read it here %url%", + "description": "This message gets send to the creator and all commentators when a suggestion gets updated and sendPNNotifications is enabled", "type": "string", "dependsOn": "sendPNNotifications", "allowEmbed": true, "params": [ { "name": "url", - "description": { - "en": "URL to the suggestion", - "de": "URL zum Vorschlag" - } + "description": "URL to the suggestion" }, { "name": "title", - "description": { - "en": "Title of the suggestion", - "de": "Titel des Vorschlags" - } + "description": "Title of the suggestion" } ] }, { "name": "unansweredSuggestion", - "humanName": { - "en": "Unanswered Suggestion-Message", - "de": "Unbeantwortete Vorschlags-Nachricht" - }, + "humanName": "Unanswered Suggestion-Message", "default": { - "en": { - "title": "Suggestion #%id%", - "description": "%suggestion%", - "color": "#F1C40F", - "thumbnail": "%avatarURL%", - "author": { - "name": "%tag%", - "img": "%avatarURL%" - }, - "fields": [ - { - "name": "Suggestion-Status", - "value": "No admin answered to this suggestion yet" - } - ] - }, - "de": { - "title": "Vorschlag #%id%", - "description": "%suggestion%", - "color": "#F1C40F", - "thumbnail": "%avatarURL%", - "author": { - "name": "%tag%", - "img": "%avatarURL%" - }, - "fields": [ - { - "name": "Vorschlagsstatus", - "value": "Es hat noch kein Admin auf diesen Vorschlag geantwortet" - } - ] - } - }, - "description": { - "en": "This will be the messages that will get send when the user creates their suggestion and no admin has responded yet", - "de": "Das wird die Nachricht sein, die gesendet wird, wenn ein Nutzer seinen Vorschlag erstellt hat und noch kein Admin darauf geantwortet hat" - }, + "title": "Suggestion #%id%", + "description": "%suggestion%", + "color": "#F1C40F", + "thumbnail": "%avatarURL%", + "author": { + "name": "%tag%", + "img": "%avatarURL%" + }, + "fields": [ + { + "name": "Suggestion-Status", + "value": "No admin answered to this suggestion yet" + } + ] + }, + "description": "This will be the messages that will get send when the user creates their suggestion and no admin has responded yet", "type": "string", "allowEmbed": true, "params": [ { "name": "id", - "description": { - "en": "ID of the suggestion", - "de": "ID des Vorschlags" - } + "description": "ID of the suggestion" }, { "name": "suggestion", - "description": { - "en": "Content of the suggestion", - "de": "Inhalt des Vorschlags" - } + "description": "Content of the suggestion" }, { "name": "tag", - "description": { - "en": "Tag of the user who created this suggestion", - "de": "Tag des Users, der den Vorschlag erstellt hat" - } + "description": "Tag of the user who created this suggestion" }, { "name": "avatarURL", - "description": { - "en": "Avatar-URL of the user who created this suggestion", - "de": "Avatar-URL des Users, der den Vorschlag erstellt hat" - }, + "description": "Avatar-URL of the user who created this suggestion", "isImage": true } ] }, { "name": "deniedSuggestion", - "humanName": { - "en": "Denied Suggestion-Message", - "de": "Abgelehnte Vorschlags-Nachricht" - }, + "humanName": "Denied Suggestion-Message", "default": { - "en": { - "title": "Suggestion #%id%", - "description": "%suggestion%", - "color": "#E74C3C", - "thumbnail": "%avatarURL%", - "author": { - "name": "%tag%", - "img": "%avatarURL%" - }, - "fields": [ - { - "name": "Suggestion-Status: DENIED", - "value": "Denied by %adminUser% with the following reason: \"%adminMessage%\"" - } - ] - }, - "de": { - "title": "Vorschlag #%id%", - "description": "%suggestion%", - "color": "#E74C3C", - "thumbnail": "%avatarURL%", - "author": { - "name": "%tag%", - "img": "%avatarURL%" - }, - "fields": [ - { - "name": "Vorschlags-Status: ABGELEHNT", - "value": "Abgelehnt von %adminUser% mit folgendem Grund: \"%adminMessage%\"" - } - ] - } - }, - "description": { - "en": "The suggestion will be edited to this message, when an admin denies a suggestion", - "de": "Zu dieser Nachricht wird der Vorschlag editiert, wenn ein Admin den Vorschlag ablehnt" - }, + "title": "Suggestion #%id%", + "description": "%suggestion%", + "color": "#E74C3C", + "thumbnail": "%avatarURL%", + "author": { + "name": "%tag%", + "img": "%avatarURL%" + }, + "fields": [ + { + "name": "Suggestion-Status: DENIED", + "value": "Denied by %adminUser% with the following reason: \"%adminMessage%\"" + } + ] + }, + "description": "The suggestion will be edited to this message, when an admin denies a suggestion", "type": "string", "allowEmbed": true, "params": [ { "name": "id", - "description": { - "en": "ID of the suggestion", - "de": "ID des Vorschlags" - } + "description": "ID of the suggestion" }, { "name": "suggestion", - "description": { - "en": "Content of the suggestion", - "de": "Inhalt des Vorschlags" - } + "description": "Content of the suggestion" }, { "name": "tag", - "description": { - "en": "Tag of the user who created this suggestion", - "de": "Tag des Users, der den Vorschlag erstellt hat" - } + "description": "Tag of the user who created this suggestion" }, { "name": "avatarURL", - "description": { - "en": "Avatar-URL of the user who created this suggestion", - "de": "Avatar-URL des Users, der den Vorschlag erstellt hat" - }, + "description": "Avatar-URL of the user who created this suggestion", "isImage": true }, { "name": "adminUser", - "description": { - "en": "Mention of the administrator who denied this suggestion", - "de": "Erwähnung des Administrators, der den Vorschlag abgelehnt hat" - } + "description": "Mention of the administrator who denied this suggestion" }, { "name": "adminMessage", - "description": { - "en": "Message by administrator who denied this suggestion", - "de": "Nachricht des Administrators, der den Vorschlag abgelehnt hat" - } + "description": "Message by administrator who denied this suggestion" } ] }, { "name": "approvedSuggestion", - "humanName": { - "en": "Approved Suggestion-Message", - "de": "Angenommene Vorschlags-Nachricht" - }, + "humanName": "Approved Suggestion-Message", "default": { - "en": { - "title": "Suggestion #%id%", - "description": "%suggestion%", - "color": "#2ECC71", - "thumbnail": "%avatarURL%", - "author": { - "name": "%tag%", - "img": "%avatarURL%" - }, - "fields": [ - { - "name": "Suggestion-Status: APPROVED", - "value": "Approved by %adminUser% with the following reason: \"%adminMessage%\"" - } - ] - }, - "de": { - "title": "Vorschlag #%id%", - "description": "%suggestion%", - "color": "#2ECC71", - "thumbnail": "%avatarURL%", - "author": { - "name": "%tag%", - "img": "%avatarURL%" - }, - "fields": [ - { - "name": "Vorschlagsstatus: ANGENOMMEN", - "value": "Wurde von %adminUser% mit folgendem Grund angenommen: \"%adminMessage%\"" - } - ] - } - }, - "description": { - "en": "The suggestion will be edited to this message, when an admin approves a suggestion", - "de": "Zu dieser Nachricht wird der Vorschlag editiert, wenn ein Admin den Vorschlag annimt" - }, + "title": "Suggestion #%id%", + "description": "%suggestion%", + "color": "#2ECC71", + "thumbnail": "%avatarURL%", + "author": { + "name": "%tag%", + "img": "%avatarURL%" + }, + "fields": [ + { + "name": "Suggestion-Status: APPROVED", + "value": "Approved by %adminUser% with the following reason: \"%adminMessage%\"" + } + ] + }, + "description": "The suggestion will be edited to this message, when an admin approves a suggestion", "type": "string", "allowEmbed": true, "params": [ { "name": "id", - "description": { - "en": "ID of the suggestion", - "de": "ID des Vorschlags" - } + "description": "ID of the suggestion" }, { "name": "suggestion", - "description": { - "en": "Content of the suggestion", - "de": "Inhalt des Vorschlags" - } + "description": "Content of the suggestion" }, { "name": "tag", - "description": { - "en": "Tag of the user who created this suggestion", - "de": "Tag des Users, der den Vorschlag erstellt hat" - } + "description": "Tag of the user who created this suggestion" }, { "name": "avatarURL", - "description": { - "en": "Avatar-URL of the user who created this suggestion", - "de": "Avatar-URL des Users, der den Vorschlag erstellt hat" - }, + "description": "Avatar-URL of the user who created this suggestion", "isImage": true }, { "name": "adminUser", - "description": { - "en": "Mention of the administrator who approved this suggestion", - "de": "Erwähnung des Administrators, der den Vorschlag angenommen hat" - } + "description": "Mention of the administrator who approved this suggestion" }, { "name": "adminMessage", - "description": { - "en": "Message by administrator who approved this suggestion", - "de": "Nachricht des Administrators, der den Vorschlag angenommen hat" - } + "description": "Message by administrator who approved this suggestion" } ] } diff --git a/modules/suggestions/module.json b/modules/suggestions/module.json index 395821b4..202e130d 100644 --- a/modules/suggestions/module.json +++ b/modules/suggestions/module.json @@ -8,19 +8,14 @@ "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/suggestions", "commands-dir": "/commands", "models-dir": "/models", + "fa-icon": "far fa-lightbulb", "config-example-files": [ "config.json" ], "tags": [ "administration" ], - "humanReadableName": { - "en": "Suggestions", - "de": "Vorschläge" - }, + "humanReadableName": "Suggestions", "events-dir": "/events", - "description": { - "en": "Advanced module to manage suggestions on your guild", - "de": "Modul mit vielen Funktionen, um Vorschläge auf deinem Discord zu managen" - } -} \ No newline at end of file + "description": "Advanced module to manage suggestions on your guild" +} diff --git a/modules/team-list/config.json b/modules/team-list/config.json index 02ad5a74..4c1e39c7 100644 --- a/modules/team-list/config.json +++ b/modules/team-list/config.json @@ -1,59 +1,30 @@ { - "description": {}, - "humanName": { - "en": "Configuration", - "de": "Konfiguration" - }, + "description": "Configure your team list embeds and displayed roles here", + "humanName": "Configuration", "filename": "config.json", "configElements": true, "content": [ { "name": "channelID", - "humanName": { - "en": "Channel", - "de": "Kanal" - }, - "default": { - "en": "" - }, - "description": { - "en": "Channel-ID to run all operations in it", - "de": "Kanal-ID, in welchem alle Aktionen ausgeführt werden" - }, + "humanName": "Channel", + "default": "", + "description": "Channel-ID to run all operations in it", "type": "channelID" }, { "name": "roles", - "humanName": { - "en": "Listed Roles", - "de": "Gelistete Rollen" - }, - "default": { - "en": [], - "de": [] - }, - "description": { - "en": "Roles that should be listed in the embed", - "de": "Jede Rolle, die im Embed gelistet werden soll" - }, + "humanName": "Listed Roles", + "default": [], + "description": "Roles that should be listed in the embed", "type": "array", "maxLength": 25, "content": "roleID" }, { "name": "descriptions", - "humanName": { - "en": "Descriptions of roles", - "de": "Beschreibung von Rollen" - }, - "default": { - "en": [], - "de": {} - }, - "description": { - "en": "Optional description of a listed role (Field 1: Role-ID, Field 2: Description)", - "de": "Optionale Beschreibung einer gelisteten Rolle (Feld 1: Rollen-ID, Feld 2: Beschreibung)" - }, + "humanName": "Descriptions of roles", + "default": {}, + "description": "Optional description of a listed role (Field 1: Role-ID, Field 2: Description)", "type": "keyed", "content": { "key": "roleID", @@ -62,29 +33,15 @@ }, { "name": "embed", - "humanName": { - "en": "Embed" - }, + "humanName": "Embed", "default": { - "en": { - "title": "Our staff", - "description": "Meet our staff here", - "color": "GREEN", - "thumbnail-url": "", - "img-url": "" - }, - "de": { - "title": "Unser Team", - "description": "Hier findest du alle unsere Teammitglieder", - "color": "GREEN", - "thumbnail-url": "", - "img-url": "" - } - }, - "description": { - "en": "Configuration of the member-embed", - "de": "Konfiguration des Partner-Embeds" + "title": "Our staff", + "description": "Meet our staff here", + "color": "GREEN", + "thumbnail-url": "", + "img-url": "" }, + "description": "Configuration of the member-embed", "type": "keyed", "content": { "key": "string", @@ -94,18 +51,9 @@ }, { "name": "nameOverwrites", - "humanName": { - "en": "Name-Overwrites", - "de": "Name-Overwrites" - }, - "default": { - "en": [], - "de": {} - }, - "description": { - "en": "optional; Allows to overwrite the displayed name of roles (Field 1: Role-ID, Field 2: Displayed Name)", - "de": "optional; Allows to overwrite the displayed name of a role (Feld 1: Rollen-ID, Feld 2: Angezeigter Name)" - }, + "humanName": "Name-Overwrites", + "default": {}, + "description": "optional; Allows to overwrite the displayed name of roles (Field 1: Role-ID, Field 2: Displayed Name)", "type": "keyed", "content": { "key": "roleID", @@ -114,33 +62,17 @@ }, { "name": "includeStatus", - "humanName": { - "en": "Include Online-Status of Staff-Members", - "de": "Online-Status von Teammitgliedern anzeigen" - }, - "description": { - "en": "If enabled, the current online status will be displayed in the staffmember-list", - "de": "Wenn aktiviert, wird der aktuelle Status in der Teammitglieder-Liste angezeigt" - }, + "humanName": "Include Online-Status of Staff-Members", + "description": "If enabled, the current online status will be displayed in the staffmember-list", "type": "boolean", - "default": { - "en": false - } + "default": false }, { "name": "onlineShowHighestRole", - "humanName": { - "en": "Only list the highest role of a user?", - "de": "Nur die höchste Rolle eines Nutzers anzeigen?" - }, - "description": { - "en": "If enabled, a staff member will only be listed under their highest role in the list.", - "de": "Wenn aktiviert, wird ein Teammitglied nur unter seiner höchsten Rolle in der Liste angezeigt." - }, + "humanName": "Only list the highest role of a user?", + "description": "If enabled, a staff member will only be listed under their highest role in the list.", "type": "boolean", - "default": { - "en": false - } + "default": false } ] -} \ No newline at end of file +} diff --git a/modules/team-list/events/botReady.js b/modules/team-list/events/botReady.js index 9cdcfef3..433a7edf 100644 --- a/modules/team-list/events/botReady.js +++ b/modules/team-list/events/botReady.js @@ -1,8 +1,8 @@ const isEqual = require('is-equal'); const { - disableModule, truncate, - parseEmbedColor + parseEmbedColor, + safeSetFooter } = require('../../../src/functions/helpers'); const {localize} = require('../../../src/functions/localize'); const {MessageEmbed} = require('discord.js'); @@ -32,26 +32,31 @@ let lastSavedEmbed = {}; */ async function updateEmbedsIfNeeded(client) { const channels = client.configurations['team-list']['config']; - for (const channelConfig of channels) { + for (let configIndex = 0; configIndex < channels.length; configIndex++) { + const channelConfig = channels[configIndex]; const embed = new MessageEmbed() - .setColor(parseEmbedColor(channelConfig.embed.color)) - .setTitle(channelConfig.embed.title) - .setDescription(channelConfig.embed.description) - .setTimestamp() - .setFooter({text: client.strings.footer, iconURL: client.strings.footerImgUrl}); + .setColor(parseEmbedColor(channelConfig.embed.color)); + safeSetFooter(embed, client); + + if (!client.strings.disableFooterTimestamp) embed.setTimestamp(); + if (channelConfig.embed.description) embed.setDescription(channelConfig.embed.description); + if (channelConfig.embed.title) embed.setTitle(channelConfig.embed.title); if (channelConfig.embed['thumbnail-url']) embed.setThumbnail(channelConfig.embed['thumbnail-url']); if (channelConfig.embed['img-url']) embed.setImage(channelConfig.embed['img-url']); const channel = await client.channels.fetch(channelConfig['channelID']).catch(() => { }); - if (!channel) return disableModule('team-list', localize('team-list', 'channel-not-found', {c: channelConfig['channelID']})); - const messages = (await channel.messages.fetch()).filter(msg => msg.author.id === client.user.id); + if (!channel) { + client.logger.error(`[team-list] Could not find channel with id ${channelConfig['channelID']}`); + continue; + } + const guildMembers = client.guild.members.cache; const roles = (await channel.guild.roles.fetch()).filter(f => channelConfig.roles.includes(f.id)).sort((a, b) => a.position < b.position ? 1 : -1); const listedUserIDs = []; - let i = 0; + let fieldCount = 0; for (const role of roles.values()) { let userString = ''; for (const member of guildMembers.filter(m => m.roles.cache.has(role.id)).values()) { @@ -61,16 +66,40 @@ async function updateEmbedsIfNeeded(client) { } if (userString === '') userString = localize('team-list', 'no-users-with-role', {r: role.toString()}); else if (!channelConfig.includeStatus) userString = userString.substring(0, userString.length - 2); - i++; + fieldCount++; embed.addField(channelConfig['nameOverwrites'][role.id] || role.name, truncate((channelConfig['descriptions'][role.id] ? `${channelConfig['descriptions'][role.id]}\n` : '') + userString, 1024)); } - if (i === 0) embed.addField('⚠️', localize('team-list', 'no-roles-selected')); + if (fieldCount === 0) embed.addField('⚠️', localize('team-list', 'no-roles-selected')); - if (isEqual(lastSavedEmbed[channelConfig['channelID']], embed.toJSON())) continue; - lastSavedEmbed[channelConfig['channelID']] = embed.toJSON(); + const cacheKey = `${channelConfig['channelID']}-${configIndex}`; + if (isEqual(lastSavedEmbed[cacheKey], embed.toJSON())) continue; + lastSavedEmbed[cacheKey] = embed.toJSON(); - if (messages.last()) await messages.last().edit({embeds: [embed]}); - else channel.send({embeds: [embed]}); + const [messageData] = await client.models['team-list']['TeamListMessage'].findOrCreate({ + where: { + channelID: channel.id, + configIndex + }, + defaults: { + channelID: channel.id, + configIndex + } + }); + + let message = messageData.messageID ? await channel.messages.fetch(messageData.messageID).catch(() => { + }) : null; + + try { + if (message) { + await message.edit({embeds: [embed]}); + } else { + message = await channel.send({embeds: [embed]}); + messageData.messageID = message.id; + await messageData.save(); + } + } catch (e) { + client.logger.error(`[team-list] Failed to send/edit message in channel ${channelConfig['channelID']}: ${e.message}`); + } } } \ No newline at end of file diff --git a/modules/team-list/models/TeamListMessage.js b/modules/team-list/models/TeamListMessage.js new file mode 100644 index 00000000..bfc5f506 --- /dev/null +++ b/modules/team-list/models/TeamListMessage.js @@ -0,0 +1,28 @@ +const { + DataTypes, + Model +} = require('sequelize'); + +module.exports = class TeamListMessage extends Model { + static init(sequelize) { + return super.init({ + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true + }, + channelID: DataTypes.STRING, + messageID: DataTypes.STRING, + configIndex: DataTypes.INTEGER + }, { + tableName: 'team-list_message', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'TeamListMessage', + 'module': 'team-list' +}; diff --git a/modules/team-list/module.json b/modules/team-list/module.json index 11a6e39f..72aa9446 100644 --- a/modules/team-list/module.json +++ b/modules/team-list/module.json @@ -7,6 +7,7 @@ "link": "https://github.com/SCDerox" }, "events-dir": "/events", + "models-dir": "/models", "config-example-files": [ "config.json" ], @@ -14,12 +15,6 @@ "administration" ], "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/team-list", - "humanReadableName": { - "en": "Staff-List", - "de": "Teammitglieder-Liste" - }, - "description": { - "en": "List all your staff members and explain team roles in always up-to-date embed", - "de": "Liste alle deine Teammitglieder und erkläre sie in einem immer aktuellem Embed" - } -} \ No newline at end of file + "humanReadableName": "Staff-List", + "description": "List all your staff members and explain team roles in always up-to-date embed" +} diff --git a/modules/temp-channels/channel-settings.js b/modules/temp-channels/channel-settings.js index 75d8c86b..63981d8a 100644 --- a/modules/temp-channels/channel-settings.js +++ b/modules/temp-channels/channel-settings.js @@ -30,30 +30,47 @@ module.exports.channelMode = async function (interaction, callerInfo) { publicTemp = false; } if (publicTemp) { - await vchann.lockPermissions(); - await vchann.permissionOverwrites.delete(vchann.guild.roles.everyone); + await vchann.permissionOverwrites.create(interaction.guild.members.me, { + 'CONNECT': true, + 'VIEW_CHANNEL': true, + 'MANAGE_CHANNELS': true + }); await interaction.editReply(embedType(moduleConfig['modeSwitched'], {'%mode%': 'public'}, {ephemeral: true})); - - } else if (!publicTemp) { - - await vchann.lockPermissions(); - const guildRoles = await interaction.guild.roles.fetch(); - for (const [, role] of guildRoles) { - await vchann.permissionOverwrites.create(role, {'CONNECT': false}); - } - await vchann.permissionOverwrites.create(interaction.guild.members.me, {'CONNECT': true}); - await vchann.permissionOverwrites.create(interaction.member, {'CONNECT': true}); + } else { + await vchann.permissionOverwrites.create(vchann.guild.roles.everyone, { + 'CONNECT': false, + 'VIEW_CHANNEL': false + }); + await vchann.permissionOverwrites.create(interaction.guild.members.me, { + 'CONNECT': true, + 'VIEW_CHANNEL': true + }); + await vchann.permissionOverwrites.create(interaction.member, { + 'CONNECT': true, + 'VIEW_CHANNEL': true + }); if (allowedUsers.at(0) !== '') { for (const user of allowedUsers) { - await vchann.permissionOverwrites.create(interaction.guild.members.cache.get(user), {'CONNECT': true}); + const member = interaction.guild.members.cache.get(user); + if (member) await vchann.permissionOverwrites.create(member, { + 'CONNECT': true, + 'VIEW_CHANNEL': true + }); } } - interaction.editReply(embedType(moduleConfig['modeSwitched'], {'%mode%': 'private'}, {ephemeral: true})); + for (const roleId of (moduleConfig['privateBypassRoles'] || [])) { + await vchann.permissionOverwrites.create(roleId, { + 'CONNECT': true, + 'VIEW_CHANNEL': true + }).catch(() => { + }); + } + await interaction.editReply(embedType(moduleConfig['modeSwitched'], {'%mode%': 'private'}, {ephemeral: true})); } vc.isPublic = publicTemp; - await vc.save; + await vc.save(); }; /** @@ -75,8 +92,10 @@ module.exports.userAdd = async function (interaction, callerInfo) { let addedUser = null; if (callerInfo === 'command') { addedUser = interaction.options.getUser('user'); - } - if (callerInfo === 'modal') { + } else if (callerInfo === 'select') { + addedUser = await client.users.fetch(interaction.values[0]).catch(() => null); + if (!addedUser) return interaction.editReply(localize('temp-channels', 'user-not-found')); + } else if (callerInfo === 'modal') { const addedUserString = interaction.fields.getTextInputValue('add-modal-input'); try { addedUser = interaction.guild.members.cache.find(member => formatDiscordUserName(member.user).replaceAll('@', '') === addedUserString).user; @@ -90,11 +109,13 @@ module.exports.userAdd = async function (interaction, callerInfo) { } } - if (allowedUsers === '') { - allowedUsers = addedUser.id; - } else { - allowedUsers = allowedUsers + ',' + addedUser.id; + const existingUsers = (allowedUsers || '').split(',').filter(u => u.trim() !== ''); + if (existingUsers.includes(addedUser.id)) { + await interaction.editReply(embedType(moduleConfig['userAdded'], {'%user%': formatDiscordUserName(addedUser)}, {ephemeral: true})); + return; } + existingUsers.push(addedUser.id); + allowedUsers = existingUsers.join(','); vc.allowedUsers = allowedUsers; await vc.save(); const vchann = interaction.guild.channels.cache.get(vc.id); @@ -120,12 +141,14 @@ module.exports.userRemove = async function (interaction, callerInfo) { ] } }); - let allowedUsers = vc.allowedUsers.split(','); + let allowedUsers = (vc.allowedUsers || '').split(',').filter(u => u.trim() !== ''); let removedUser = null; if (callerInfo === 'command') { removedUser = interaction.options.getUser('user'); - } - if (callerInfo === 'modal') { + } else if (callerInfo === 'select') { + removedUser = await client.users.fetch(interaction.values[0]).catch(() => null); + if (!removedUser) return interaction.editReply(localize('temp-channels', 'user-not-found')); + } else if (callerInfo === 'modal') { const removedUserString = interaction.fields.getTextInputValue('remove-modal-input'); try { removedUser = interaction.guild.members.cache.find(member => formatDiscordUserName(member.user).replaceAll('@', '') === removedUserString).user; @@ -145,7 +168,14 @@ module.exports.userRemove = async function (interaction, callerInfo) { await vc.save(); const vchann = interaction.guild.channels.cache.get(vc.id); try { - await vchann.permissionOverwrites.delete(removedUser); + if (vc.isPublic) { + await vchann.permissionOverwrites.delete(removedUser); + } else { + await vchann.permissionOverwrites.create(removedUser, { + 'CONNECT': false, + 'VIEW_CHANNEL': false + }); + } } catch (e) { console.log(e); } @@ -171,16 +201,33 @@ module.exports.usersList = async function (interaction) { ] } }); - const allowedUsersArray = vc.allowedUsers.split(','); + if (!vc) { + interaction.editReply(embedType(moduleConfig['notInChannel'], {}, {ephemeral: true})); + return; + } + if (!vc.allowedUsers || vc.allowedUsers.trim() === '') { + interaction.editReply(embedType(localize('temp-channels', 'no-added-user'), {}, {ephemeral: true})); + return; + } + const allowedUsersArray = vc.allowedUsers.split(',').filter(u => u.trim() !== ''); let allowedUsers = ''; for (const user of allowedUsersArray) { allowedUsers = allowedUsers + '\n • <@' + user + '>'; } - if (allowedUsersArray.at(0) === '') { - interaction.editReply(localize('temp-channels', 'no-added-user')); + if (allowedUsersArray.length === 0) { + interaction.editReply(embedType(localize('temp-channels', 'no-added-user'), {}, {ephemeral: true})); return; } - interaction.editReply(moduleConfig['listUsers'] + ' ' + allowedUsers); + const listMsg = moduleConfig['listUsers']; + const hasParam = typeof listMsg === 'string' ? listMsg.includes('%users%') : JSON.stringify(listMsg).includes('%users%'); + if (hasParam) { + interaction.editReply(embedType(listMsg, {'%users%': allowedUsers}, {ephemeral: true})); + } else { + const result = embedType(listMsg, {}, {ephemeral: true}); + if (result.content) result.content += ' ' + allowedUsers; + else if (result.embeds && result.embeds[0]) result.embeds[0].description = (result.embeds[0].description || '') + '\n' + allowedUsers; + interaction.editReply(result); + } }; module.exports.channelEdit = async function (interaction, callerInfo) { @@ -202,7 +249,7 @@ module.exports.channelEdit = async function (interaction, callerInfo) { if (callerInfo === 'command') { if (interaction.options.getInteger('user-limit') >= 0) { if (interaction.options.getInteger('user-limit') < 0 || interaction.options.getInteger('user-limit') > 99) { - interaction.editReply(localize('temp-channels', 'edit-error')); + interaction.editReply(embedType(moduleConfig['edit-error'], {}, {ephemeral: true})); return; } vcLimit = interaction.options.getInteger('user-limit'); @@ -210,7 +257,7 @@ module.exports.channelEdit = async function (interaction, callerInfo) { } else vcLimit = vchann.userLimit; if (interaction.options.getInteger('bitrate')) { if (interaction.options.getInteger('bitrate') <= 8000 || interaction.options.getInteger('bitrate') >= interaction.guild.maximumBitrate) { - interaction.editReply(localize('temp-channels', 'edit-error')); + interaction.editReply(embedType(moduleConfig['edit-error'], {}, {ephemeral: true})); return; } vcBitrate = interaction.options.getInteger('bitrate'); @@ -227,30 +274,23 @@ module.exports.channelEdit = async function (interaction, callerInfo) { } if (callerInfo === 'modal') { if (isNaN(interaction.fields.getTextInputValue('edit-modal-limit-input'))) { - interaction.editReply(localize('temp-channels', 'edit-error')); + interaction.editReply(embedType(moduleConfig['edit-error'], {}, {ephemeral: true})); return; } if (interaction.fields.getTextInputValue('edit-modal-limit-input') < 0 || interaction.fields.getTextInputValue('edit-modal-limit-input') > 99) { - interaction.editReply(localize('temp-channels', 'edit-error')); - return; - } - if (isNaN(interaction.fields.getTextInputValue('edit-modal-bitrate-input'))) { - interaction.editReply(localize('temp-channels', 'edit-error')); - return; - } - if (interaction.fields.getTextInputValue('edit-modal-bitrate-input') <= 8000 || interaction.fields.getTextInputValue('edit-modal-bitrate-input') >= interaction.guild.maximumBitrate) { - interaction.editReply(localize('temp-channels', 'edit-error')); + interaction.editReply(embedType(moduleConfig['edit-error'], {}, {ephemeral: true})); return; } vcLimit = interaction.fields.getTextInputValue('edit-modal-limit-input'); - vcBitrate = interaction.fields.getTextInputValue('edit-modal-bitrate-input'); + const bitrateValues = interaction.fields.getStringSelectValues('edit-modal-bitrate-input'); + vcBitrate = parseInt(bitrateValues[0]); vcName = interaction.fields.getTextInputValue('edit-modal-name-input'); - const nsfwInput = interaction.fields.getTextInputValue('edit-modal-nsfw-input'); - vcNsfw = (nsfwInput === 'true'); + const nsfwValues = interaction.fields.getStringSelectValues('edit-modal-nsfw-input'); + vcNsfw = (nsfwValues[0] === 'true'); edited++; } @@ -259,7 +299,7 @@ module.exports.channelEdit = async function (interaction, callerInfo) { try { vchann.edit({userLimit: vcLimit, nsfw: vcNsfw, name: vcName, bitrate: vcBitrate}); } catch (e) { - interaction.editReply(localize('temp-channels', 'edit-error')); + interaction.editReply(embedType(moduleConfig['edit-error'], {}, {ephemeral: true})); } } else { interaction.editReply(localize('temp-channels', 'nothing-changed')); diff --git a/modules/temp-channels/commands/temp-channel.js b/modules/temp-channels/commands/temp-channel.js index 44ed867a..59b4bf8a 100644 --- a/modules/temp-channels/commands/temp-channel.js +++ b/modules/temp-channels/commands/temp-channel.js @@ -1,4 +1,5 @@ const {localize} = require('../../../src/functions/localize'); +const {embedType} = require('../../../src/functions/helpers'); const {client} = require('../../../main'); const {Op} = require('sequelize'); const {channelMode, userAdd, userRemove, usersList, channelEdit} = require('../channel-settings'); @@ -15,7 +16,7 @@ module.exports.beforeSubcommand = async function (interaction) { }); if (!vc) { - interaction.editReply(interaction.client.configurations['temp-channels']['config']['notInChannel']); + interaction.editReply(embedType(interaction.client.configurations['temp-channels']['config']['notInChannel'], {}, {ephemeral: true})); interaction.cancel = true; } else interaction.cancel = false; }; diff --git a/modules/temp-channels/config.json b/modules/temp-channels/config.json index 844a52f6..cef7546f 100644 --- a/modules/temp-channels/config.json +++ b/modules/temp-channels/config.json @@ -1,400 +1,344 @@ { - "description": {}, - "humanName": { - "en": "Configuration", - "de": "Konfiguration" - }, + "description": "Configure temporary voice channel creation settings here", + "humanName": "Configuration", "filename": "config.json", "content": [ { "name": "channelID", - "humanName": { - "en": "Channel", - "de": "Kanal" - }, - "default": { - "en": "" - }, - "description": { - "en": "Set the channel here where users have to join to create their temp-channel", - "de": "Gebe hier die ID des Channels ein, in welchem Nutzer joinen müssen, um einen neuen Channel zu erstellen" - }, + "humanName": "Channel", + "default": "", + "description": "Set the channel here where users have to join to create their temp-channel", "type": "channelID", "content": [ "GUILD_VOICE" - ] - }, - { - "name": "allowUserToChangeName", - "humanName": { - "en": "Allow editing the channel", - "de": "Kanaländerungen erlauben" - }, - "default": { - "en": true, - "de": true - }, - "description": { - "en": "If enabled the user has the permission to change the name and settings of the voice channel via both, the Discord-integrated menus and the corresponding /-commands", - "de": "Wenn aktiviert erhält der Ersteller des Channel die Permission \"MANAGE_CHANNEL\" auf diesem Channel, sowie Zugriff auf die entsprechenden Befehle" - }, - "type": "boolean" - }, - { - "name": "timeout", - "humanName": { - "en": "Deletion timeout", - "de": "Löschverzögerung" - }, - "default": { - "en": 3, - "de": 3 - }, - "description": { - "en": "Set a timeout here in which the bot should wait before deleting the voice channel (in seconds)", - "de": "Die Anzahl von Sekunden nach einem Channel-Leave, die der Bot warten soll, bevor er einen Channel löscht" - }, - "type": "integer", - "allowNull": true + ], + "category": "general" }, { "name": "category", - "humanName": { - "en": "Category", - "de": "Kategorie" - }, - "default": { - "en": "" - }, - "description": { - "en": "You can set a category here in which the new channel should be created", - "de": "Gebe hier die ID der Kategorie an, in welcher neue Temp-Channel erstellt werden sollen" - }, + "humanName": "Category", + "default": "", + "description": "You can set a category here in which the new channel should be created", "type": "channelID", "content": [ "GUILD_CATEGORY" - ] + ], + "category": "general" }, { "name": "channelname_format", - "humanName": { - "en": "Channel name", - "de": "Kanalname" - }, - "default": { - "en": "⏳ %username%", - "de": "⏳ %username%" - }, - "description": { - "en": "Change the format of the channel name here", - "de": "Du kannst das Format des Kanalnamens hier bearbeiten" - }, + "humanName": "Channel name", + "default": "⏳ %username%", + "description": "Change the format of the channel name here", "type": "string", "params": [ { "name": "username", - "description": { - "en": "Username of the user", - "de": "Nutzername des Nutzers" - } + "description": "Username of the user" }, { "name": "nickname", - "description": { - "en": "Nickname of the member", - "de": "Nickname des Mitglieds" - } + "description": "Nickname of the member" }, { "name": "number", - "description": { - "en": "The current number of the channel", - "de": "Aktuelle Nummer des Kanals" - } + "description": "The current number of the channel" }, { "name": "tag", - "description": { - "en": "Tag of the user", - "de": "Tag des Nutzers" - } + "description": "Tag of the user" } - ] + ], + "category": "general" + }, + { + "name": "timeout", + "humanName": "Deletion timeout", + "default": 3, + "description": "Set a timeout here in which the bot should wait before deleting the voice channel (in seconds)", + "type": "integer", + "allowNull": true, + "category": "general" + }, + { + "name": "publicChannels", + "humanName": "Default to public channels", + "default": true, + "description": "If enabled, new temp channels start public (synced with category). If disabled, channels start private (only the creator can join).", + "type": "boolean", + "category": "permissions" + }, + { + "name": "allowUserToChangeMode", + "humanName": "Allow change of channel mode", + "default": true, + "description": "If enabled the user has the permission to change the access-mode of the voice channel", + "type": "boolean", + "category": "permissions" + }, + { + "name": "privateBypassRoles", + "humanName": "Private Mode Bypass Roles", + "default": [], + "description": "Roles that can always join and see private temporary channels, regardless of who created them.", + "type": "array", + "content": "roleID", + "category": "permissions" + }, + { + "name": "allowUserToChangeName", + "humanName": "Allow editing the channel", + "default": true, + "description": "If enabled the user has the permission to change the name and settings of the voice channel via both, the Discord-integrated menus and the corresponding /-commands", + "type": "boolean", + "category": "permissions" }, { "name": "create_no_mic_channel", - "humanName": { - "en": "Create no-mic-channel", - "de": "No-Mic-Kanal erstellen" - }, - "default": { - "en": true, - "de": true - }, - "description": { - "en": "If enabled the bot will create a new channel for each voice channel which can be only seen by users in the voice channel", - "de": "Wenn aktiviert wird ein No-Mic-Textchannel für jeden Temp-Channel erstellt, auf welchen nur Nutzer Zugriff haben, die im VC sind" - }, - "type": "boolean" + "humanName": "Create no-mic-channel", + "default": false, + "description": "If enabled the bot will create a separate text channel for each voice channel, visible only to users in the voice channel. Note: Discord now has built-in text-in-voice channels, so this is usually not needed.", + "type": "boolean", + "category": "features" }, { "name": "noMicChannelMessage", - "humanName": { - "en": "no-mic-channel-message", - "de": "No-Mic-Kanal-Nachricht" - }, - "default": { - "en": "Welcome to your no-mic-channel - you can only see this channel if you are in the connected voicechat", - "de": "Willkommen im deinem No-Mic-Kanal! Dieser wurde zu deinem Temp-Kanal erstellt, damit du mit Leuten chatten kannst, die kein Mikrofon haben. Beachte, dass dieser Channel nur von Nutzern gesehen werden kann, die im Sprachkanal mit dir sind. Beachte außerdem, dass dieser Channel gelöscht wird, wenn dein VC nicht mehr in Benutzung ist." - }, - "description": { - "en": "You can set a message here that should be send in the no-mic-channel when created", - "de": "Hier kannst du eine Nachricht festlegen, welche in einem No-Mic-Channel gesendet werden soll." - }, + "humanName": "No-Mic Channel Message", + "default": "Welcome to your no-mic-channel - you can only see this channel if you are in the connected voicechat", + "description": "You can set a message here that should be send in the no-mic-channel when created", "type": "string", - "allowEmbed": true + "allowEmbed": true, + "dependsOn": "create_no_mic_channel", + "category": "features" + }, + { + "name": "useNoMic", + "humanName": "No-Mic Channel for Settings", + "default": true, + "description": "If enabled the settings menu will be sent into the no-mic-channel. If no-mic-channels aren't enabled, the menu will instead be sent to Discord's integrated text-in-voice channels", + "type": "boolean", + "category": "features" + }, + { + "name": "settingsChannel", + "humanName": "Settings channel", + "default": "", + "description": "You can set a channel here in which the settings menu should be created. Leave this field empty, if you don't want to use this feature.", + "type": "channelID", + "content": [ + "GUILD_TEXT" + ], + "allowNull": true, + "category": "features" }, { "name": "send_dm", - "humanName": { - "en": "Send DM", - "de": "PN senden" - }, - "default": { - "en": true, - "de": true - }, - "description": { - "en": "Should the bot send a direct message to a user when a new channel is created for them?", - "de": "Sollte beim Erstellen eines Temp-Channels eine PN an den Nutzer geschrieben werden?" - }, - "type": "boolean" + "humanName": "Send DM", + "default": true, + "description": "Should the bot send a direct message to a user when a new channel is created for them?", + "type": "boolean", + "category": "messages" }, { "name": "dm", - "humanName": { - "en": "DM", - "de": "Privatnachricht" - }, - "default": { - "en": "I have created and moved you to your new voice-channel - have fun ^^", - "de": "Tach - ich habe dir nen eigenen Channel erstellt und dich verschoben - Dieser wird nach Inaktivität gelöscht - Have fun^^" - }, - "description": { - "en": "Set the message that should get send to the user if they join the voice channel", - "de": "Hier kannst du die Nachricht festlegen, die an den Nutzer geschrieben soll (wenn aktiviert)" - }, + "humanName": "DM Message Content", + "default": "I have created and moved you to your new voice-channel - have fun ^^", + "description": "The direct message content sent to the user when their temporary channel is created.", "type": "string", "allowEmbed": true, + "dependsOn": "send_dm", "params": [ { "name": "channelname", - "description": { - "en": "Name of the channel", - "de": "Name des Kanals" - } + "description": "Name of the channel" } - ] - }, - { - "name": "publicChannels", - "humanName": { - "en": "Public channels", - "de": "Öffentliche Channel" - }, - "default": { - "en": true - }, - "description": { - "en": "Should the permissions for channels created by the bot be synced with their category?", - "de": "Sollen die Berechtigungen für vom Bot erstellte Kanäle mit deren Kategorie synchronisiert werden?" - }, - "type": "boolean" - }, - { - "name": "allowUserToChangeMode", - "humanName": { - "en": "Allow change of channel mode", - "de": "Kanaländerungen erlauben" - }, - "default": { - "en": true, - "de": true - }, - "description": { - "en": "If enabled the user has the permission to change the access-mode of the voice chanel", - "de": "Wenn aktiviert erhält der Ersteller des Channel die Möglichkeit die Zugriffsberechtigungen für den Kanal festzulegen" - }, - "type": "boolean" + ], + "category": "messages" }, { "name": "notInChannel", - "humanName": {}, - "default": { - "de": "Du musst in deinem Temp-Channel sein um das zu tun", - "en": "You have to be in your temp-channel to do this" - }, - "description": { - "en": "This message gets sent to a user, who tries to edit their channel, while not being in it", - "de": "Diese Nachricht wird an Nutzer gesendet, die versuchen ihren Kanal zu bearbeiten, während sie sich nicht darin befinden" - }, - "type": "string" + "humanName": "Not in Channel Message", + "default": "You have to be in your temp-channel to do this", + "description": "This message gets sent to a user who tries to edit their channel while not being in it.", + "type": "string", + "allowEmbed": true, + "category": "messages" }, { "name": "modeSwitched", - "humanName": {}, - "default": { - "en": "The access-mode of your channel has been switched to %mode%", - "de": "Der Zugriffsmodus deines Kanals wurde auf %mode% geändert" - }, - "description": { - "en": "This message gets sent to a user, after they changed the mode of their channel", - "de": "Diese Nachricht wird an Nutzer gesendet, nachdem sie ihren Kanal bearbeitet haben" - }, + "humanName": "Mode Switched Message", + "default": "The access-mode of your channel has been switched to %mode%", + "description": "This message gets sent to a user, after they changed the mode of their channel", "type": "string", + "allowEmbed": true, "params": [ { "name": "mode", - "description": { - "en": "Mode of the channel", - "de": "Modus des Kanals" - } + "description": "Mode of the channel" } - ] + ], + "category": "messages" }, { "name": "userAdded", - "humanName": {}, - "default": { - "en": "The user %user% has beed added to your channel. They can now access it whenever they like to", - "de": "Der Nutzer %user% wurde zu deinem Kanal hinzugefügt. Er/Sie hat nun uneingeschränkten Zugang dazu" - }, - "description": { - "en": "This message gets sent to a user, after they added an user to their channel", - "de": "Diese Nachricht wird an Nutzer gesendet, nachdem sie einen Nutzer zu ihrem Kanal hinzugefügt haben" - }, + "humanName": "User Added Message", + "default": "the user %user% has been added to your channel. They can now access it whenever they like to", + "description": "This message gets sent to a user, after they added an user to their channel", "type": "string", + "allowEmbed": true, "params": [ { "name": "user", - "description": { - "en": "The user, that was added", - "de": "Der hinzugefügte Nutzer" - } + "description": "The user, that was added" } - ] + ], + "category": "messages" }, { "name": "userRemoved", - "humanName": {}, - "default": { - "en": "The user %user% has beed removed from your channel. They can no longer access it, while your channel is private", - "de": "Der Nutzer %user% wurde von deinem Kanal entfernt. Er/Sie hat nun keinen Zugriff mehr, während dein Kanal privat ist" - }, - "description": { - "en": "This message gets sent to a user, after they removed an user from their channel", - "de": "Diese Nachricht wird an Nutzer gesendet, nachdem sie einen Nutzer von ihrem Kanal entfernt haben" - }, + "humanName": "User Removed Message", + "default": "the user %user% has been removed from your channel. They can no longer access it, while your channel is private", + "description": "This message gets sent to a user, after they removed an user from their channel", "type": "string", + "allowEmbed": true, "params": [ { "name": "user", - "description": { - "en": "The user, that was removed", - "de": "Der Nutzer, der entfernt wurde" - } + "description": "The user, that was removed" } - ] + ], + "category": "messages" }, { "name": "listUsers", - "humanName": {}, - "default": { - "en": "Here is a list of all the users that have access to your channel:", - "de": "Hier ist eine Liste aller Nutzer mit Zugang zu deinem Kanal:" - }, - "description": { - "de": "Die Nachricht die gesendet wird, wenn ein Nutzer eine Liste der Nutzer mit Zugang zu seinem Temp-Channel anfragt. Dieser Nachricht folgt automatisch eine Liste der Nutzer.", - "en": "The message to be sent, if a user requests a list of the users with access to their channel. This is automatically followed by a list of the users' tags." - }, - "type": "string" + "humanName": "List Users Message", + "default": "Here is a list of all the users that have access to your channel: %users%", + "description": "The message to be sent when a user requests a list of users with access to their channel.", + "type": "string", + "allowEmbed": true, + "params": [ + { + "name": "users", + "description": "List of users with access" + } + ], + "category": "messages" }, { "name": "channelEdited", - "humanName": {}, - "default": { - "en": "Your channel was edited", - "de": "Dein Kanal wurde bearbeitet" - }, - "description": { - "en": "The message to be sent, if a user edited their channel", - "de": "Die Nachricht, die gesendet wird, wenn ein Nutzer seinen Kanal bearbeitet" - }, - "type": "string" + "humanName": "Channel Edited Message", + "default": "Your channel was edited", + "description": "The message to be sent when a user edits their channel.", + "type": "string", + "allowEmbed": true, + "category": "messages" }, { "name": "edit-error", - "humanName": {}, - "default": { - "en": "An error occurred while editing your channel. One or more of your settings could not be applied. This could be due to missing permissions or an invalid value", - "de": "Beim Bearbeiten des Kanals ist ein Fehler aufgetreten. Eine oder mehr deiner Einstellungen konnten nicht angewendet werden. Dies kann an fehlenden Rechten oder einem ungültigen Eingabewert liegen" - }, - "description": { - "en": "The message to be sent, if a user edited their channel, but it failed", - "de": "Die Nachricht, die gesendet wird, wenn das Bearbeiten eines Kanals fehlschlägt" - }, - "type": "string" + "humanName": "Edit Error Message", + "default": "An error occurred while editing your channel. One or more of your settings could not be applied. This could be due to missing permissions or an invalid value", + "description": "The message sent when a channel edit fails.", + "type": "string", + "allowEmbed": true, + "category": "messages" }, { - "name": "settingsChannel", - "humanName": { - "de": "Einstellungskanal", - "en": "Settings channel" - }, - "default": { - "en": "" - }, - "description": { - "en": "You can set a channel here in which the settings menu should be created. Leave this field empty, if you don't want to use this feature.", - "de": "Gebe hier die ID des Kanals an, in welcher das Einstellungsmenü erstellt werden soll. Lass dieses Feld leer, wenn du diese Funktion nicht verwenden willst." - }, + "name": "settingsMessage", + "humanName": "Settings Panel Message", + "default": "Change the Settings of your temporary channel here", + "description": "Set the message that should get send in the channel specified above to let the users change the settings of their temp-channels", + "type": "string", + "allowEmbed": true, + "params": [], + "category": "messages" + }, + { + "name": "enableMaxActiveChannels", + "humanName": "Enable channel limit", + "default": false, + "description": "If enabled, the bot will limit the number of temporary channels that can exist at the same time.", + "type": "boolean", + "category": "limits" + }, + { + "name": "maxActiveChannels", + "humanName": "Maximum active channels", + "default": 10, + "description": "Maximum number of temp channels that can exist at the same time.", + "type": "integer", + "dependsOn": "enableMaxActiveChannels", + "category": "limits" + }, + { + "name": "maxActiveChannelsMessage", + "humanName": "Channel Limit Reached Message", + "default": "⚠️ The maximum number of temporary channels has been reached. Please try again later.", + "description": "This message is sent via DM when a user tries to create a temp channel but the limit has been reached.", + "type": "string", + "allowEmbed": true, + "dependsOn": "enableMaxActiveChannels", + "category": "limits" + }, + { + "name": "enableArchiving", + "humanName": "Enable channel archiving", + "default": false, + "description": "If enabled, empty temp channels will be moved to an archive category instead of being deleted. Channels are restored when the creator rejoins the trigger channel.", + "type": "boolean", + "category": "archiving" + }, + { + "name": "archiveCategory", + "humanName": "Archive category", + "dependsOn": "enableArchiving", + "default": "", + "description": "Category where archived temp channels are moved to. Make this category hidden from regular users.", "type": "channelID", "content": [ - "GUILD_TEXT" + "GUILD_CATEGORY" ], - "allowNull": true + "category": "archiving" }, { - "name": "useNoMic", - "humanName": { - "de": "No-Mic-Channel für Einstellungen verwenden" - }, - "default": { - "en": true, - "de": true - }, - "description": { - "en": "If enabled the settings menu will be sent into the no-mic-channel. If no-mic-channels aren't enabled, the menu will instead be sent to Discord's integrated text-in-voice channels", - "de": "Wenn aktiviert wird das Einstellungsmenü in den No-Mic-Channel gesendet. Wenn No-Mic-Channels nicht aktiviert sind, wird es stattdessen in die in Sprachkanälen integrierten Textkanäle gesendet." - }, - "type": "boolean" + "name": "archiveDeleteAfterHours", + "humanName": "Delete archived channels after (hours)", + "dependsOn": "enableArchiving", + "default": 168, + "description": "Hours after which archived channels are permanently deleted. Set to 0 to never auto-delete. Default: 168 (7 days).", + "type": "integer", + "category": "archiving" + } + ], + "categories": [ + { + "id": "general", + "icon": "fas fa-gears", + "displayName": "General" }, { - "name": "settingsMessage", - "humanName": { - "de": "Einstellungsnachricht" - }, - "default": { - "en": "Change the Settings of your temporary channel here", - "de": "Ändere die Einstellungen deines Temp-Channels hier" - }, - "description": { - "en": "Set the message that should get send in the channel specified above to let the users change the settings of their temp-channels", - "de": "Hier kannst du die Nachricht festlegen, die in den weiter oben festgelegten Kanal gesendet werden soll, damit Nutzer ihre Temp-Channels bearbeiten können" - }, - "type": "string", - "allowEmbed": true, - "params": [] + "id": "permissions", + "icon": "fas fa-lock", + "displayName": "Permissions & Mode" + }, + { + "id": "features", + "icon": "fas fa-star", + "displayName": "Features" + }, + { + "id": "messages", + "icon": "fas fa-comment-dots", + "displayName": "Messages" + }, + { + "id": "limits", + "icon": "fa-solid fa-shield", + "displayName": "Limits" + }, + { + "id": "archiving", + "icon": "fa-regular fa-clock-rotate-left", + "displayName": "Archiving" } ] } \ No newline at end of file diff --git a/modules/temp-channels/events/botReady.js b/modules/temp-channels/events/botReady.js index 50c303b5..0bb8e50f 100644 --- a/modules/temp-channels/events/botReady.js +++ b/modules/temp-channels/events/botReady.js @@ -1,11 +1,54 @@ const {migrate} = require('../../../src/functions/helpers'); const {client} = require('../../../main'); +const { + migrationStart, + migrationEnd +} = require('../../../main'); const {sendMessage} = require('../channel-settings'); const {localize} = require('../../../src/functions/localize'); +const {scheduleJob} = require('node-schedule'); +const {Op} = require('sequelize'); + module.exports.run = async function () { - const settingsChannel = client.channels.cache.get(client.configurations['temp-channels']['config']['settingsChannel']); + const moduleConfig = client.configurations['temp-channels']['config']; + const settingsChannel = client.channels.cache.get(moduleConfig['settingsChannel']); await migrate('temp-channels', 'TempChannelV1', 'TempChannel'); + // Migration V2: add archivedAt column + const dbVersionV2 = await client.models['DatabaseSchemeVersion'].findOne({ + where: { + model: 'temp-channels_TempChannel', + version: 'V2' + } + }); + if (!dbVersionV2) { + migrationStart(); + try { + client.logger.info('[temp-channels] Running V2 migration (adding archivedAt field)...'); + const data = await client.models['temp-channels']['TempChannel'].findAll({ + attributes: ['id', 'creatorID', 'noMicChannel', 'allowedUsers', 'isPublic'] + }).catch(() => []); + await client.models['temp-channels']['TempChannel'].sync({force: true}); + for (const tc of data) { + await client.models['temp-channels']['TempChannel'].create({ + id: tc.id, + creatorID: tc.creatorID, + noMicChannel: tc.noMicChannel, + allowedUsers: tc.allowedUsers, + isPublic: tc.isPublic, + archivedAt: null + }); + } + client.logger.info('[temp-channels] V2 migration complete.'); + await client.models['DatabaseSchemeVersion'].upsert({ + model: 'temp-channels_TempChannel', + version: 'V2' + }); + } finally { + migrationEnd(); + } + } + // Cleanup orphaned temp channels on startup const tempChannels = await client.models['temp-channels']['TempChannel'].findAll(); let cleanedCount = 0; @@ -19,6 +62,9 @@ module.exports.run = async function () { continue; } + // Skip archived channels — they're supposed to be empty + if (tempChannel.archivedAt) continue; + if (dcChannel.members.size === 0) { await dcChannel.delete(`[temp-channels] ${localize('temp-channels', 'removed-audit-log-reason')}`).catch(() => {}); await tempChannel.destroy(); @@ -33,6 +79,38 @@ module.exports.run = async function () { client.logger.info(`[temp-channels] Cleaned up ${cleanedCount} empty or orphaned temp channel(s) on startup`); } + // Schedule archive cleanup job (every hour) + if (moduleConfig.enableArchiving && moduleConfig.archiveDeleteAfterHours > 0) { + const archiveCleanupJob = scheduleJob('0 * * * *', async () => { + const cutoff = new Date(Date.now() - moduleConfig.archiveDeleteAfterHours * 3600000); + const expiredChannels = await client.models['temp-channels']['TempChannel'].findAll({ + where: { + archivedAt: { + [Op.ne]: null, + [Op.lt]: cutoff + } + } + }); + for (const tc of expiredChannels) { + try { + const dcChannel = await client.channels.fetch(tc.id).catch(() => null); + if (dcChannel) await dcChannel.delete('[temp-channels] Archived channel expired').catch(() => { + }); + if (tc.noMicChannel) { + const noMic = await client.channels.fetch(tc.noMicChannel).catch(() => null); + if (noMic) await noMic.delete('[temp-channels] Archived no-mic channel expired').catch(() => { + }); + } + await tc.destroy(); + } catch (e) { + client.logger.warn(`[temp-channels] Failed to delete expired archive ${tc.id}: ${e.message}`); + } + } + if (expiredChannels.length > 0) client.logger.info(`[temp-channels] Deleted ${expiredChannels.length} expired archived channel(s)`); + }); + client.jobs.push(archiveCleanupJob); + } + if (settingsChannel) { await sendMessage(settingsChannel); } diff --git a/modules/temp-channels/events/interactionCreate.js b/modules/temp-channels/events/interactionCreate.js index c4944c56..f1bd7450 100644 --- a/modules/temp-channels/events/interactionCreate.js +++ b/modules/temp-channels/events/interactionCreate.js @@ -1,6 +1,14 @@ -const {ActionRowBuilder, ModalBuilder, TextInputBuilder, TextInputStyle} = require('discord.js'); +const { + ActionRowBuilder, + ModalBuilder, + TextInputBuilder, + TextInputStyle, + LabelBuilder, + UserSelectMenuBuilder +} = require('discord.js'); const {usersList, channelMode, userAdd, userRemove, channelEdit} = require('../channel-settings'); const {localize} = require('../../../src/functions/localize'); +const {embedType} = require('../../../src/functions/helpers'); const {Op} = require('sequelize'); module.exports.run = async function (client, interaction) { @@ -19,50 +27,41 @@ module.exports.run = async function (client, interaction) { if (interaction.customId === 'tempc-add') { if (!vc) { - interaction.reply({ - ephemeral: true, - content: interaction.client.configurations['temp-channels']['config']['notInChannel'] - }); + interaction.reply(embedType(interaction.client.configurations['temp-channels']['config']['notInChannel'], {}, {ephemeral: true})); return; } - const modal = new ModalBuilder() - .setCustomId('tempc-add-modal') - .setTitle(localize('temp-channels', 'add-modal-title')); - const userInput = new TextInputBuilder() - .setCustomId('add-modal-input') - .setLabel(localize('temp-channels', 'add-modal-prompt')) - .setStyle(TextInputStyle.Short) - .setPlaceholder(localize('temp-channels', 'edit-modal-username-placeholder')); - const actionRow = new ActionRowBuilder().addComponents(userInput); - modal.addComponents(actionRow); - await interaction.showModal(modal); + const selectMenu = new UserSelectMenuBuilder() + .setCustomId('tempc-add-select') + .setPlaceholder(localize('temp-channels', 'add-modal-prompt')) + .setMinValues(1) + .setMaxValues(1); + await interaction.reply({ + ephemeral: true, + content: localize('temp-channels', 'add-modal-prompt'), + components: [new ActionRowBuilder().addComponents(selectMenu)] + }); + return; } if (interaction.customId === 'tempc-remove') { if (!vc) { - interaction.reply({ - ephemeral: true, - content: interaction.client.configurations['temp-channels']['config']['notInChannel'] - }); + interaction.reply(embedType(interaction.client.configurations['temp-channels']['config']['notInChannel'], {}, {ephemeral: true})); return; } - const modal = new ModalBuilder() - .setCustomId('tempc-remove-modal') - .setTitle(localize('temp-channels', 'remove-modal-title')); - const userInput = new TextInputBuilder() - .setCustomId('remove-modal-input') - .setLabel(localize('temp-channels', 'remove-modal-prompt')) - .setStyle(TextInputStyle.Short) - .setPlaceholder(localize('temp-channels', 'edit-modal-username-placeholder')); - const actionRow = new ActionRowBuilder().addComponents(userInput); - modal.addComponents(actionRow); - await interaction.showModal(modal); + const selectMenu = new UserSelectMenuBuilder() + .setCustomId('tempc-remove-select') + .setPlaceholder(localize('temp-channels', 'remove-modal-prompt')) + .setMinValues(1) + .setMaxValues(1); + await interaction.reply({ + ephemeral: true, + content: localize('temp-channels', 'remove-modal-prompt'), + components: [new ActionRowBuilder().addComponents(selectMenu)] + }); + return; } if (interaction.customId === 'tempc-list') { if (!vc) { - interaction.reply({ - ephemeral: true, - content: interaction.client.configurations['temp-channels']['config']['notInChannel'] - }); + interaction.reply(embedType(interaction.client.configurations['temp-channels']['config']['notInChannel'], {}, {ephemeral: true})); return; } await interaction.deferReply({ephemeral: true}); @@ -70,10 +69,7 @@ module.exports.run = async function (client, interaction) { } if (interaction.customId === 'tempc-private') { if (!vc) { - interaction.reply({ - ephemeral: true, - content: interaction.client.configurations['temp-channels']['config']['notInChannel'] - }); + interaction.reply(embedType(interaction.client.configurations['temp-channels']['config']['notInChannel'], {}, {ephemeral: true})); return; } await interaction.deferReply({ephemeral: true}); @@ -81,10 +77,7 @@ module.exports.run = async function (client, interaction) { } if (interaction.customId === 'tempc-public') { if (!vc) { - interaction.reply({ - ephemeral: true, - content: interaction.client.configurations['temp-channels']['config']['notInChannel'] - }); + interaction.reply(embedType(interaction.client.configurations['temp-channels']['config']['notInChannel'], {}, {ephemeral: true})); return; } await interaction.deferReply({ephemeral: true}); @@ -92,32 +85,44 @@ module.exports.run = async function (client, interaction) { } if (interaction.customId === 'tempc-edit') { if (!vc) { - interaction.reply({ - ephemeral: true, - content: interaction.client.configurations['temp-channels']['config']['notInChannel'] - }); + interaction.reply(embedType(interaction.client.configurations['temp-channels']['config']['notInChannel'], {}, {ephemeral: true})); return; } const vchann = interaction.guild.channels.cache.get(vc.id); const modal = new ModalBuilder() .setCustomId('tempc-edit-modal') .setTitle(localize('temp-channels', 'edit-modal-title')); - const nsfwInput = new TextInputBuilder() - .setCustomId('edit-modal-nsfw-input') + const nsfwLabel = new LabelBuilder() .setLabel(localize('temp-channels', 'edit-modal-nsfw-prompt')) - .setRequired(true) - .setStyle(TextInputStyle.Short) - .setPlaceholder(localize('temp-channels', 'edit-modal-nsfw-placeholder')) - .setValue(vchann.nsfw.toString()); + .setStringSelectMenuComponent(c => c + .setCustomId('edit-modal-nsfw-input') + .addOptions( + { + label: localize('temp-channels', 'edit-modal-nsfw-off'), + value: 'false', + default: vchann.nsfw === false + }, + { + label: localize('temp-channels', 'edit-modal-nsfw-on'), + value: 'true', + default: vchann.nsfw === true + } + )); - const bitrateInput = new TextInputBuilder() - .setCustomId('edit-modal-bitrate-input') + const bitrateLabel = new LabelBuilder() .setLabel(localize('temp-channels', 'edit-modal-bitrate-prompt')) - .setRequired(true) - .setStyle(TextInputStyle.Short) - .setPlaceholder(localize('temp-channels', 'edit-modal-bitrate-placeholder')) - .setValue(vchann.bitrate.toString()); + .setStringSelectMenuComponent(c => { + c.setCustomId('edit-modal-bitrate-input'); + for (const b of [8000, 16000, 32000, 64000, 96000, 128000, 256000, 384000].filter(b => b <= interaction.guild.maximumBitrate)) { + c.addOptions({ + label: `${b / 1000} kbps`, + value: b.toString(), + default: vchann.bitrate === b + }); + } + return c; + }); const limitInput = new TextInputBuilder() .setCustomId('edit-modal-limit-input') @@ -135,8 +140,8 @@ module.exports.run = async function (client, interaction) { .setPlaceholder(localize('temp-channels', 'edit-modal-name-placeholder')) .setValue(vchann.name); - const nsfwRow = new ActionRowBuilder().addComponents(nsfwInput); - const bitrateRow = new ActionRowBuilder().addComponents(bitrateInput); + const nsfwRow = nsfwLabel; + const bitrateRow = bitrateLabel; const limitRow = new ActionRowBuilder().addComponents(limitInput); const nameRow = new ActionRowBuilder().addComponents(nameInput); modal.addComponents(bitrateRow); @@ -156,10 +161,7 @@ module.exports.run = async function (client, interaction) { }); if (interaction.customId === 'tempc-add-modal') { if (!vc) { - interaction.reply({ - ephemeral: true, - content: interaction.client.configurations['temp-channels']['config']['notInChannel'] - }); + interaction.reply(embedType(interaction.client.configurations['temp-channels']['config']['notInChannel'], {}, {ephemeral: true})); return; } await interaction.deferReply({ephemeral: true}); @@ -167,10 +169,7 @@ module.exports.run = async function (client, interaction) { } if (interaction.customId === 'tempc-remove-modal') { if (!vc) { - interaction.reply({ - ephemeral: true, - content: interaction.client.configurations['temp-channels']['config']['notInChannel'] - }); + interaction.reply(embedType(interaction.client.configurations['temp-channels']['config']['notInChannel'], {}, {ephemeral: true})); return; } await interaction.deferReply({ephemeral: true}); @@ -178,14 +177,34 @@ module.exports.run = async function (client, interaction) { } if (interaction.customId === 'tempc-edit-modal') { if (!vc) { - interaction.reply({ - ephemeral: true, - content: interaction.client.configurations['temp-channels']['config']['notInChannel'] - }); + interaction.reply(embedType(interaction.client.configurations['temp-channels']['config']['notInChannel'], {}, {ephemeral: true})); return; } await interaction.deferReply({ephemeral: true}); await channelEdit(interaction, 'modal'); } + } else if (interaction.isUserSelectMenu()) { + const vc = await client.models['temp-channels']['TempChannel'].findOne({ + where: { + [Op.and]: [ + {id: interaction.member.voice ? interaction.member.voice.channelId : null}, + {creatorID: interaction.member.id} + ] + } + }); + if (!vc) { + return interaction.reply({ + ephemeral: true, + ...embedType(interaction.client.configurations['temp-channels']['config']['notInChannel'], {}, {ephemeral: true}) + }); + } + if (interaction.customId === 'tempc-add-select') { + await interaction.deferReply({ephemeral: true}); + await userAdd(interaction, 'select'); + } + if (interaction.customId === 'tempc-remove-select') { + await interaction.deferReply({ephemeral: true}); + await userRemove(interaction, 'select'); + } } -}; +}; \ No newline at end of file diff --git a/modules/temp-channels/events/voiceStateUpdate.js b/modules/temp-channels/events/voiceStateUpdate.js index ea4925b5..98ce0574 100644 --- a/modules/temp-channels/events/voiceStateUpdate.js +++ b/modules/temp-channels/events/voiceStateUpdate.js @@ -9,29 +9,67 @@ module.exports.run = async function (client, oldState, newState) { if (!client.botReadyAt) return; const moduleConfig = client.configurations['temp-channels']['config']; + // Handle channel leave — delete or archive if (oldState.channel) { const oldChannel = await client.models['temp-channels']['TempChannel'].findOne({ - where: { - id: oldState.channel.id - } + where: {id: oldState.channel.id} }); - if (oldChannel) { + if (oldChannel && !oldChannel.archivedAt) { setTimeout(async () => { try { const dcOldChannel = await client.channels.fetch(oldChannel.id).catch(() => null); if (dcOldChannel && dcOldChannel.members.size === 0) { - if (oldChannel.noMicChannel) { - const noMicChannel = await client.channels.fetch(oldChannel.noMicChannel).catch(() => null); - if (noMicChannel) { - await noMicChannel.delete(`[temp-channels] ${localize('temp-channels', 'removed-audit-log-reason')}`).catch((e) => { - client.logger.warn(`[temp-channels] Failed to delete no-mic channel ${oldChannel.noMicChannel}: ${e.message}`); + if (moduleConfig.enableArchiving && moduleConfig.archiveCategory) { + // Archive: move to archive category, strip permissions + await dcOldChannel.setParent(moduleConfig.archiveCategory, { + lockPermissions: false, + reason: '[temp-channels] Archiving empty temp channel' + }).catch(() => { + }); + await dcOldChannel.permissionOverwrites.set([ + { + id: dcOldChannel.guild.roles.everyone, + deny: ['CONNECT', 'VIEW_CHANNEL'] + }, + { + id: dcOldChannel.guild.members.me, + allow: ['CONNECT', 'VIEW_CHANNEL', 'MANAGE_CHANNELS'] + } + ], '[temp-channels] Archiving channel'); + if (oldChannel.noMicChannel) { + const noMicChannel = await client.channels.fetch(oldChannel.noMicChannel).catch(() => null); + if (noMicChannel) { + await noMicChannel.setParent(moduleConfig.archiveCategory, { + lockPermissions: false, + reason: '[temp-channels] Archiving no-mic channel' + }).catch(() => { + }); + await noMicChannel.permissionOverwrites.set([ + { + id: noMicChannel.guild.roles.everyone, + deny: ['VIEW_CHANNEL'] + }, + { + id: noMicChannel.guild.members.me, + allow: ['VIEW_CHANNEL'] + } + ], '[temp-channels] Archiving no-mic channel').catch(() => { + }); + } + } + oldChannel.archivedAt = new Date(); + await oldChannel.save(); + } else { + // Delete channel + if (oldChannel.noMicChannel) { + const noMicChannel = await client.channels.fetch(oldChannel.noMicChannel).catch(() => null); + if (noMicChannel) await noMicChannel.delete(`[temp-channels] ${localize('temp-channels', 'removed-audit-log-reason')}`).catch(() => { }); } + await dcOldChannel.delete(`[temp-channels] ${localize('temp-channels', 'removed-audit-log-reason')}`).catch(() => { + }); + await oldChannel.destroy(); } - await dcOldChannel.delete(`[temp-channels] ${localize('temp-channels', 'removed-audit-log-reason')}`).catch((e) => { - client.logger.warn(`[temp-channels] Failed to delete temp channel ${oldChannel.id}: ${e.message}`); - }); - await oldChannel.destroy(); } else if (!dcOldChannel) { await oldChannel.destroy(); } @@ -42,6 +80,7 @@ module.exports.run = async function (client, oldState, newState) { } } + // No-mic channel visibility sync if (moduleConfig['create_no_mic_channel']) { const possibleExistingChannel = await client.models['temp-channels']['TempChannel'].findOne({ where: { @@ -51,7 +90,7 @@ module.exports.run = async function (client, oldState, newState) { ] } }); - if (possibleExistingChannel) { + if (possibleExistingChannel && !possibleExistingChannel.archivedAt) { const existingNoMicChannel = await newState.guild.channels.cache.get(possibleExistingChannel.noMicChannel); if (existingNoMicChannel) await existingNoMicChannel.permissionOverwrites.create(newState.member, { 'VIEW_CHANNEL': newState.channel && newState.channel.id === possibleExistingChannel.id @@ -62,15 +101,103 @@ module.exports.run = async function (client, oldState, newState) { if (!newState.channel) return; if (newState.channel.id === moduleConfig['channelID']) { - const alreadyExistingChannel = await client.models['temp-channels']['TempChannel'].findOne({ - where: { - creatorID: newState.member.user.id - } - }); - if (alreadyExistingChannel) return newState.setChannel(alreadyExistingChannel.id, `[temp-channels] ` + localize('temp-channels', 'move-audit-log-reason')).catch(() => { - newState.setChannel(null, '[temp-channels] ' + localize('temp-channels', 'disconnect-audit-log-reason')); - alreadyExistingChannel.destroy(); + // Check for existing channel (active or archived) + const existingChannel = await client.models['temp-channels']['TempChannel'].findOne({ + where: {creatorID: newState.member.user.id} }); + + if (existingChannel) { + // Restore from archive if needed + if (existingChannel.archivedAt) { + const dcChannel = await client.channels.fetch(existingChannel.id).catch(() => null); + if (dcChannel) { + await dcChannel.setParent(moduleConfig['category'] || null, { + lockPermissions: false, + reason: '[temp-channels] Restoring archived channel' + }).catch(() => { + }); + // Re-apply permissions based on saved mode + if (!existingChannel.isPublic) { + await dcChannel.permissionOverwrites.create(dcChannel.guild.roles.everyone, { + 'CONNECT': false, + 'VIEW_CHANNEL': false + }); + await dcChannel.permissionOverwrites.create(dcChannel.guild.members.me, { + 'CONNECT': true, + 'VIEW_CHANNEL': true, + 'MANAGE_CHANNELS': true + }); + await dcChannel.permissionOverwrites.create(newState.member, { + 'CONNECT': true, + 'VIEW_CHANNEL': true, + 'MANAGE_CHANNELS': moduleConfig['allowUserToChangeName'] + }); + const allowedUsers = (existingChannel.allowedUsers || '').split(',').filter(u => u && u !== newState.member.user.id); + for (const userId of allowedUsers) { + const member = newState.guild.members.cache.get(userId); + if (member) await dcChannel.permissionOverwrites.create(member, { + 'CONNECT': true, + 'VIEW_CHANNEL': true + }).catch(() => { + }); + } + for (const roleId of (moduleConfig['privateBypassRoles'] || [])) { + await dcChannel.permissionOverwrites.create(roleId, { + 'CONNECT': true, + 'VIEW_CHANNEL': true + }).catch(() => { + }); + } + } else { + await dcChannel.lockPermissions().catch(() => { + }); + await dcChannel.permissionOverwrites.create(dcChannel.guild.members.me, { + 'CONNECT': true, + 'VIEW_CHANNEL': true, + 'MANAGE_CHANNELS': true + }); + if (moduleConfig['allowUserToChangeName']) await dcChannel.permissionOverwrites.create(newState.member, {'MANAGE_CHANNELS': true}); + } + if (existingChannel.noMicChannel) { + const noMicChannel = await client.channels.fetch(existingChannel.noMicChannel).catch(() => null); + if (noMicChannel) { + await noMicChannel.setParent(moduleConfig['category'] || null, { + lockPermissions: false, + reason: '[temp-channels] Restoring archived no-mic channel' + }).catch(() => { + }); + } + } + existingChannel.archivedAt = null; + await existingChannel.save(); + return newState.setChannel(dcChannel.id, '[temp-channels] ' + localize('temp-channels', 'move-audit-log-reason')); + } else { + await existingChannel.destroy(); + } + } else { + // Active channel exists, move user there + return newState.setChannel(existingChannel.id, '[temp-channels] ' + localize('temp-channels', 'move-audit-log-reason')).catch(() => { + newState.setChannel(null, '[temp-channels] ' + localize('temp-channels', 'disconnect-audit-log-reason')); + existingChannel.destroy(); + }); + } + } + + // Channel limit check + if (moduleConfig.enableMaxActiveChannels && moduleConfig.maxActiveChannels > 0) { + const activeCount = await client.models['temp-channels']['TempChannel'].count({where: {archivedAt: null}}); + if (activeCount >= moduleConfig.maxActiveChannels) { + await newState.setChannel(null, '[temp-channels] Channel limit reached').catch(() => { + }); + if (moduleConfig.maxActiveChannelsMessage) { + await newState.member.user.send(embedType(moduleConfig.maxActiveChannelsMessage, {})).catch(() => { + }); + } + return; + } + } + + // Create new channel const n = await client.models['temp-channels']['TempChannel'].count({}) + 1; const newChannel = await newState.guild.channels.create({ name: moduleConfig['channelname_format'] @@ -98,23 +225,49 @@ module.exports.run = async function (client, oldState, newState) { parent: moduleConfig['category'], topic: localize('temp-channels', 'no-mic-channel-topic', {u: formatDiscordUserName(newState.member.user)}), reason: '[temp-channels] ' + localize('temp-channels', 'created-audit-log-reason', {u: formatDiscordUserName(newState.member.user)}), - permissionOverwrites: [ - { - id: everyoneRole, - deny: ['VIEW_CHANNEL'] - } - ] + permissionOverwrites: [{ + id: everyoneRole, + deny: ['VIEW_CHANNEL'] + }] }); - await noMicChannel.permissionOverwrites.create(newState.member, { - 'VIEW_CHANNEL': true - }, { + await noMicChannel.permissionOverwrites.create(newState.member, {'VIEW_CHANNEL': true}, { reason: '[temp-channels] ' + localize('temp-channels', 'created-audit-log-reason', {u: formatDiscordUserName(newState.member.user)}) }); await noMicChannel.send(embedType(moduleConfig['noMicChannelMessage'])).then(m => m.pin()); - if (moduleConfig['useNoMic']) { - await sendMessage(noMicChannel); + if (moduleConfig['useNoMic']) await sendMessage(noMicChannel); + } + + // Apply private permissions if default is private + if (!moduleConfig['publicChannels']) { + await newChannel.permissionOverwrites.create(newState.guild.roles.everyone, { + 'CONNECT': false, + 'VIEW_CHANNEL': false + }, { + reason: '[temp-channels] ' + localize('temp-channels', 'permission-update-audit-log-reason') + }); + await newChannel.permissionOverwrites.create(newState.guild.members.me, { + 'CONNECT': true, + 'VIEW_CHANNEL': true, + 'MANAGE_CHANNELS': true + }, { + reason: '[temp-channels] ' + localize('temp-channels', 'permission-update-audit-log-reason') + }); + await newChannel.permissionOverwrites.create(newState.member, { + 'CONNECT': true, + 'VIEW_CHANNEL': true, + 'MANAGE_CHANNELS': moduleConfig['allowUserToChangeName'] + }, { + reason: '[temp-channels] ' + localize('temp-channels', 'permission-update-audit-log-reason') + }); + for (const roleId of (moduleConfig['privateBypassRoles'] || [])) { + await newChannel.permissionOverwrites.create(roleId, { + 'CONNECT': true, + 'VIEW_CHANNEL': true + }, {reason: '[temp-channels] Private bypass role'}).catch(() => { + }); } } + await client.models['temp-channels']['TempChannel'].create({ creatorID: newState.member.user.id, id: newChannel.id, @@ -122,10 +275,6 @@ module.exports.run = async function (client, oldState, newState) { allowedUsers: newState.member.user.id, isPublic: moduleConfig['publicChannels'] }); - if (moduleConfig['useNoMic']) { - if (!moduleConfig['create_no_mic_channel']) { - await sendMessage(newChannel); - } - } + if (moduleConfig['useNoMic'] && !moduleConfig['create_no_mic_channel']) await sendMessage(newChannel); } }; \ No newline at end of file diff --git a/modules/temp-channels/locales.json b/modules/temp-channels/locales.json new file mode 100644 index 00000000..3b105afc --- /dev/null +++ b/modules/temp-channels/locales.json @@ -0,0 +1,29 @@ +{ + "en": { + "temp-channels": { + "removed-audit-log-reason": "Removed temp channel, because no one was in it", + "permission-update-audit-log-reason": "Updated permissions, to make sure only people in the VC can see the no-mic-channel", + "created-audit-log-reason": "Created Temp-Channel for %u", + "move-audit-log-reason": "Moved user to their voice channel", + "no-mic-channel-topic": "Welcome to %u's no-mic-channel. You will see this channel as long as you are connected to this temp-channel.", + "disconnect-audit-log-reason": "The old channel of the user could not be found - disconnecting them - hopefully they join again", + "command-description": "Manage your temp-channel", + "mode-subcommand-description": "Change the mode of your channel", + "public-option-description": "local public-option-description", + "add-subcommand-description": "Add users, that will be able to join your channel, while it is private", + "remove-subcommand-description": "Remove users from you channel", + "add-user-option-description": "The user to be added", + "remove-user-option-description": "The user to be removed", + "list-subcommand-description": "List the users with access to your channel", + "edit-subcommand-description": "Edit various settings of yout channel", + "user-limit-option-description": "Change the user-limit of your channel", + "bitrate-option-description": "Change the bitrate of your channel (min. 8000)", + "name-option-description": "Change the name of your channel", + "nsfw-option-description": "Change, whether your channel is age-restricted or not", + "no-added-user": "There are no users to be displayed here", + "nothing-changed": "Your channel already had these settings.", + "no-disconnect": "Couldn't disconnect the user from your channel. This could be due to missing permissions, or the user not being in your voice-channel", + "edit-error": "An error occurred while editing your channel. one or more of your settings couldn't be applied. This could be due to missing permissions or an invalid value." + } + } +} \ No newline at end of file diff --git a/modules/temp-channels/models/SettingsMessage.js b/modules/temp-channels/models/SettingsMessage.js new file mode 100644 index 00000000..4c3a3540 --- /dev/null +++ b/modules/temp-channels/models/SettingsMessage.js @@ -0,0 +1,25 @@ +const { + DataTypes, + Model +} = require('sequelize'); + +module.exports = class TempChannelSettingsMessage extends Model { + static init(sequelize) { + return super.init({ + channelID: { + type: DataTypes.STRING, + primaryKey: true + }, + messageID: DataTypes.STRING + }, { + tableName: 'temp-channel_settings_message', + timestamps: true, + sequelize + }); + } +}; + +module.exports.config = { + 'name': 'SettingsMessage', + 'module': 'temp-channels' +}; \ No newline at end of file diff --git a/modules/temp-channels/models/TempChannel.js b/modules/temp-channels/models/TempChannel.js index 4858794b..f757e7a8 100644 --- a/modules/temp-channels/models/TempChannel.js +++ b/modules/temp-channels/models/TempChannel.js @@ -10,7 +10,12 @@ module.exports = class TempChannel extends Model { creatorID: DataTypes.STRING, noMicChannel: DataTypes.STRING, allowedUsers: DataTypes.STRING, - isPublic: DataTypes.BOOLEAN + isPublic: DataTypes.BOOLEAN, + archivedAt: { + type: DataTypes.DATE, + allowNull: true, + defaultValue: null + } }, { tableName: 'temp-channel_TempChannelsv2', timestamps: true, diff --git a/modules/temp-channels/module.json b/modules/temp-channels/module.json index 05f7e1a3..e5b77333 100644 --- a/modules/temp-channels/module.json +++ b/modules/temp-channels/module.json @@ -8,6 +8,7 @@ "models-dir": "/models", "events-dir": "/events", "commands-dir": "/commands", + "fa-icon": "fas fa-hourglass-half", "config-example-files": [ "config.json" ], @@ -15,12 +16,6 @@ "community" ], "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/temp-channels", - "humanReadableName": { - "en": "Temporary channels", - "de": "Temporäre Channel" - }, - "description": { - "en": "Allow users to quickly create voice channels by joining a voice channel", - "de": "Erlaube es Nutzern, ihren eigenen Voice-Channel zu erstellen, indem sie einem VC joinen" - } -} \ No newline at end of file + "humanReadableName": "Temporary channels", + "description": "Allow users to quickly create voice channels by joining a voice channel" +} diff --git a/modules/tic-tak-toe/commands/tic-tac-toe.js b/modules/tic-tak-toe/commands/tic-tac-toe.js index 2523de5e..234ad037 100644 --- a/modules/tic-tak-toe/commands/tic-tac-toe.js +++ b/modules/tic-tak-toe/commands/tic-tac-toe.js @@ -39,7 +39,7 @@ module.exports.run = async function (interaction) { let endReason = null; let gameEndReasonType = null; let currentUser = randomElementFromArray([interaction.member, member]); - const a = rep.createMessageComponentCollector({componentType: ComponentType.Button}); + const a = rep.createMessageComponentCollector({componentType: ComponentType.Button, time: 300000}); setTimeout(() => { if (started || a.ended) return; endReason = localize('tic-tac-toe', 'invite-expired', {u: interaction.user.toString(), i: member.toString()}); @@ -224,10 +224,11 @@ module.exports.run = async function (interaction) { }); }); a.on('end', () => { - rep.edit({ + if (!ended) rep.edit({ content: endReason, components: [] - }); + }).catch(() => { + }); } ); }; diff --git a/modules/tic-tak-toe/module.json b/modules/tic-tak-toe/module.json index e23b26cc..e5f682ed 100644 --- a/modules/tic-tak-toe/module.json +++ b/modules/tic-tak-toe/module.json @@ -1,18 +1,13 @@ { "name": "tic-tak-toe", - "humanReadableName": { - "en": "Tic Tac Toe", - "de": "Tic-Tac-Toe" - }, + "humanReadableName": "Tic Tac Toe", "author": { "scnxOrgID": "1", "name": "SCDerox (SC Network Team)", "link": "https://github.com/SCDerox" }, - "description": { - "en": "Let your users play Tick-Tac-Toe against each other!", - "de": "Lasse Nutzer auf deinem Server Tick-Tac-Toe gegeneinander spielen" - }, + "fa-icon": "fa-solid fa-border-all", + "description": "Let your users play Tick-Tac-Toe against each other!", "commands-dir": "/commands", "noConfig": true, "releaseDate": "1641230658000", @@ -26,4 +21,4 @@ "fun" ], "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/tic-tak-toe" -} \ No newline at end of file +} diff --git a/modules/tickets/config.json b/modules/tickets/config.json index dd71b3ab..3c995c4f 100644 --- a/modules/tickets/config.json +++ b/modules/tickets/config.json @@ -1,53 +1,25 @@ { - "description": { - "en": "Manage the basic settings of this module here", - "de": "Passe die grundlegenden Optionen des Modules hier an" - }, - "humanName": { - "en": "Configuration", - "de": "Konfiguration" - }, + "description": "Manage the basic settings of this module here", + "humanName": "Configuration", "configElementName": { - "de": { - "one": "Ticket-Kategorie", - "more": "Ticket-Kategorien" - }, - "en": { - "one": "Ticket-Category", - "more": "Ticket-Categories" - } + "one": "Ticket-Category", + "more": "Ticket-Categories" }, "configElements": true, "filename": "config.json", "content": [ { "name": "name", - "humanName": { - "en": "Name", - "de": "Name" - }, - "default": { - "en": "Support" - }, - "description": { - "en": "Name of the Ticket type. This will be shown to users", - "de": "Name des Tickettypen. Dieser wird Nutzern angezeigt" - }, + "humanName": "Name", + "default": "Support", + "description": "Name of the Ticket type. This will be shown to users", "type": "string" }, { "name": "ticket-create-category", - "humanName": { - "en": "Ticket create category", - "de": "Ticketerstellungs-Kategorie" - }, - "default": { - "en": "" - }, - "description": { - "en": "Category in which tickets should get created.", - "de": "Kategorie, in der Tickets erstellt werden sollen." - }, + "humanName": "Ticket create category", + "default": "", + "description": "Category in which tickets should get created.", "type": "channelID", "content": [ "GUILD_CATEGORY" @@ -55,17 +27,9 @@ }, { "name": "ticket-create-channel", - "humanName": { - "en": "Ticket create category", - "de": "Ticketerstellungs-Kanal" - }, - "default": { - "en": "" - }, - "description": { - "en": "Channel in which a message with a \"Create Ticket\" button should get send", - "de": "Kanal in den eine Nachticht mit \"Ticket erstellen\" button gesendet werden soll" - }, + "humanName": "Ticket creation channel", + "default": "", + "description": "Channel in which a message with a \"Create Ticket\" button should get send", "type": "channelID", "content": [ "GUILD_TEXT" @@ -73,227 +37,117 @@ }, { "name": "ticketRoles", - "humanName": { - "en": "Ticket Roles", - "de": "Ticketrollen" - }, - "default": { - "en": [] - }, - "description": { - "de": "Nutzer, die in Tickets gepingt werden und diese sehen können", - "en": "Users who get pinged in the tickets and who can see tickets" - }, + "humanName": "Ticket Roles", + "default": [], + "description": "Users who get pinged in the tickets and who can see tickets", "type": "array", "content": "roleID" }, { "name": "logChannel", - "humanName": { - "en": "Log channel", - "de": "Log-Kanal" - }, - "default": { - "en": "" - }, - "description": { - "en": "Channel in which ticket logs should get send", - "de": "Kanal in den Ticket-Logs gesendet werden sollen" - }, + "humanName": "Log channel", + "default": "", + "description": "Channel in which ticket logs should get send", "type": "channelID" }, { "name": "ticket-create-message", - "humanName": { - "en": "Ticket created message", - "de": "Ticketerstellungs-Nachricht" - }, - "default": { - "en": "Click the big button below to contact our staff and create a ticket", - "de": "Klick auf den großen Button unter dieser Nachricht um unser Team zu kontaktieren und ein Ticket zu erstellen" - }, - "description": { - "en": "Message that gets send/edited in the ticket-create-channel", - "de": "Nachricht, die im Ticketerstellungs-Kanal gesendet/bearbeitet wird" - }, + "humanName": "Ticket created message", + "default": "Click the big button below to contact our staff and create a ticket", + "description": "Message that gets send/edited in the ticket-create-channel", "type": "string", "allowEmbed": true }, { "name": "sendUserDMAfterTicketClose", - "humanName": { - "en": "Send user DM after ticket is closed", - "de": "Nach schließen PN an Nutzer senden" - }, - "default": { - "en": false - }, - "description": { - "en": "If enabled users get a DM from the bot after someone closes the ticket", - "de": "Wenn diese Option aktiviert ist, bekommen Nutzer eine PN, wenn ihr Ticket geschlossen wird" - }, + "humanName": "Send user DM after ticket is closed", + "default": false, + "description": "If enabled users get a DM from the bot after someone closes the ticket", "type": "boolean" }, { "name": "userDM", - "humanName": { - "en": "User DM", - "de": "Nutzer PN" - }, - "default": { - "en": "Thanks for contacting our support for the ticket-category \"%type%\", here is your transcript: %transcriptURL%", - "de": "Danke, dass du unseren Support für die Kategorie \"%type%\" kontaktiert hast. Hier ist dein Transcript: %transcriptURL%" - }, - "description": { - "en": "This message gets send to the user if sendUserDMAfterTicketClose is enabled", - "de": "Diese Nachricht wird an den Nutzer gesendet, wenn die entsprechende Option aktiviert ist" - }, + "humanName": "User DM", + "default": "Thanks for contacting our support for the ticket-category \"%type%\", here is your transcript: %transcriptURL%", + "description": "This message gets send to the user if sendUserDMAfterTicketClose is enabled", "type": "string", "dependsOn": "sendUserDMAfterTicketClose", "allowEmbed": true, "params": [ { "name": "transcriptURL", - "description": { - "de": "URL zum Transcript", - "en": "URL to transcript" - } + "description": "URL to transcript" }, { "name": "type", - "description": { - "de": "Name des dieses Ticket Typen", - "en": "Name of this ticket type" - } + "description": "Name of this ticket type" } ] }, { "name": "creation-message", - "humanName": { - "en": "Ticket-Created Message", - "de": "Ticket-Erstellt Nachricht" - }, + "humanName": "Ticket-Created Message", "pro": true, "type": "string", "allowEmbed": true, - "description": { - "en": "This message will get sent in new tickets. The close buttons will be added.", - "de": "Diese Nachricht wird in neue Tickets gesendet. Der Schließ-Knopf wird hinzugefügt." - }, + "description": "This message will get sent in new tickets. The close buttons will be added.", "default": { - "en": { - "title": "\uD83D\uDCE5 New ticket #%id%", - "color": "#2ECC71", - "message": "%rolePings%", - "fields": [ - { - "name": "\uD83D\uDC64 User", - "value": "%userMention%", - "inline": true - }, - { - "name": "☕ Ticket-Topic", - "value": "%ticketTopic%", - "inline": true - }, - { - "name": "ℹ\uFE0F Information", - "value": "Your issue got solved? Click the button below. You can always find this message pinned." - } - ] - }, - "de": { - "title": "\uD83D\uDCE5 Neues Ticket #%id%", - "color": "#2ECC71", - "message": "%rolePings%", - "fields": [ - { - "name": "\uD83D\uDC64 Nutzer", - "value": "%userMention%", - "inline": true - }, - { - "name": "☕ Ticket-Thema", - "value": "%ticketTopic%", - "inline": true - }, - { - "name": "ℹ\uFE0F Information", - "value": "Dein Problem wurde behoben? Klicke den Knopf unten. Du kannst diese Nachricht immer in den angepinnten Nachrichten finden." - } - ] - } + "title": "📥 New ticket #%id%", + "color": "#2ECC71", + "message": "%rolePings%", + "fields": [ + { + "name": "👤 User", + "value": "%userMention%", + "inline": true + }, + { + "name": "☕ Ticket-Topic", + "value": "%ticketTopic%", + "inline": true + }, + { + "name": "ℹ️ Information", + "value": "Your issue got solved? Click the button below. You can always find this message pinned." + } + ] }, "params": [ { "name": "id", - "description": { - "de": "Eindeutige Identifikationsnummer des Tickets", - "en": "Unique identification number of the ticket" - } + "description": "Unique identification number of the ticket" }, { "name": "userMention", - "description": { - "de": "Erwähnung des Nutzers, der das Ticket erstellt hat", - "en": "Mention of the user who created this ticket" - } + "description": "Mention of the user who created this ticket" }, { "name": "rolePings", - "description": { - "de": "Erwähnung der Rollen, die du im \"Ticket-Rollen\"-Feld eingestellt hast", - "en": "Mention of the roles you have selected in the \"Ticket roles\" field" - } + "description": "Mention of the roles you have selected in the \"Ticket roles\" field" }, { "name": "ticketTopic", - "description": { - "de": "Name des Ticket-Themas", - "en": "Name of the Ticket-Topic" - } + "description": "Name of the Ticket-Topic" }, { "name": "userTag", - "description": { - "de": "Tag des Nutzers, der das Ticket erstellt hat", - "en": "Tag of the user who created this ticket" - } + "description": "Tag of the user who created this ticket" } ] }, { "name": "ticket-create-button", - "humanName": { - "en": "Ticket create button", - "de": "Ticketerstellungs-Button" - }, - "default": { - "en": "Create ticket 🎫", - "de": "Ticket erstellen 🎫" - }, - "description": { - "en": "Button for creating a ticket", - "de": "Button zum Erstellen eines Tickets" - }, + "humanName": "Ticket create button", + "default": "Create ticket 🎫", + "description": "Button for creating a ticket", "type": "string", "pro": true }, { "name": "ticket-close-button", - "humanName": { - "en": "Ticket close button", - "de": "Ticketschließungs-Button" - }, - "default": { - "en": "❎ Close ticket", - "de": "❎ Ticket schließen" - }, - "description": { - "en": "Button for closing a ticket", - "de": "Button um ein Ticket zu schließen" - }, + "humanName": "Ticket close button", + "default": "❎ Close ticket", + "description": "Button for closing a ticket", "type": "string", "pro": true } diff --git a/modules/tickets/events/interactionCreate.js b/modules/tickets/events/interactionCreate.js index 037cf2b0..2d2ee1eb 100644 --- a/modules/tickets/events/interactionCreate.js +++ b/modules/tickets/events/interactionCreate.js @@ -5,7 +5,8 @@ const { messageLogToStringToPaste, embedType, formatDiscordUserName, - parseEmbedColor + parseEmbedColor, + safeSetFooter } = require('../../../src/functions/helpers'); module.exports.run = async function (client, interaction) { @@ -50,27 +51,23 @@ module.exports.run = async function (client, interaction) { const logChannel = element.logChannel ? interaction.guild.channels.cache.get(element.logChannel) : client.logChannel; if (!logChannel) client.logger.error('[tickets] ' + localize('tickets', 'no-log-channel')); else { + const ticketEmbed = new MessageEmbed() + .setColor(parseEmbedColor('DARK_GREEN')) + .setTitle(localize('tickets', 'ticket-log-embed-title', {i: ticket.id})) + .setAuthor({ + name: client.user.username, + iconURL: client.user.avatarURL() + }) + .addField(localize('tickets', 'ticket-with-user'), `<@${ticket.userID}>`, true) + .addField(localize('tickets', 'ticket-type'), element.name, true) + .addField(localize('tickets', 'ticket-log'), localize('tickets', 'ticket-log-value', { + u: msgLog, + n: ticket.msgCount + }), true) + .addField(localize('tickets', 'closed-by'), interaction.user.toString(), true); + safeSetFooter(ticketEmbed, client); await logChannel.send({ - embeds: [ - new MessageEmbed() - .setColor(parseEmbedColor('DARK_GREEN')) - .setTitle(localize('tickets', 'ticket-log-embed-title', {i: ticket.id})) - .setFooter({ - text: client.strings.footer, - iconURL: client.strings.footerImgUrl - }) - .setAuthor({ - name: client.user.username, - iconURL: client.user.avatarURL() - }) - .addField(localize('tickets', 'ticket-with-user'), `<@${ticket.userID}>`, true) - .addField(localize('tickets', 'ticket-type'), element.name, true) - .addField(localize('tickets', 'ticket-log'), localize('tickets', 'ticket-log-value', { - u: msgLog, - n: ticket.msgCount - }), true) - .addField(localize('tickets', 'closed-by'), interaction.user.toString(), true) - ] + embeds: [ticketEmbed] }); } setTimeout(() => { diff --git a/modules/tickets/module.json b/modules/tickets/module.json index 772f3f6b..300a6de5 100644 --- a/modules/tickets/module.json +++ b/modules/tickets/module.json @@ -5,6 +5,7 @@ "name": "SCDerox (SC Network Team)", "link": "https://github.com/SCDerox" }, + "fa-icon": "fas fa-ticket-simple", "events-dir": "/events", "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/tickets", "models-dir": "/models", @@ -14,11 +15,6 @@ "tags": [ "support" ], - "humanReadableName": { - "en": "Ticket-System" - }, - "description": { - "en": "Let users create tickets to message your staff", - "de": "Lasse deine Nutzer durch Tickets mit deinem Team kommunizieren" - } -} \ No newline at end of file + "humanReadableName": "Ticket-System", + "description": "Let users create tickets to message your staff" +} diff --git a/modules/twitch-notifications/configs/config.json b/modules/twitch-notifications/configs/config.json index f5801ea4..35a31618 100644 --- a/modules/twitch-notifications/configs/config.json +++ b/modules/twitch-notifications/configs/config.json @@ -1,47 +1,30 @@ { - "description": {}, - "humanName": { - "en": "Configuration", - "de": "Konfiguration" - }, + "description": "Twitch API credentials and polling interval. Create an app at https://dev.twitch.tv/console/apps to get your Client ID and Secret.", + "humanName": "Configuration", "filename": "config.json", "hidden": true, "content": [ { "name": "twitchClientID", - "humanName": {}, - "default": { - "en": "me3ub5wbx2jxlhkvxrc6fbgp8wgixq" - }, - "description": { - "en": "ID of the Client, which is used to check if the Streamer is live" - }, - "hidden": true, + "humanName": "Twitch Client ID", + "default": "", + "description": "Client ID of your Twitch application (https://dev.twitch.tv/console/apps).", "type": "string" }, { "name": "clientSecret", - "humanName": {}, - "default": { - "en": "58v6r0v2oips1tldmfunrxp5m8xv6r" - }, - "description": { - "en": "Secret of the Twitch-Client, which is used to check if the Streamer is live" - }, - "hidden": true, + "humanName": "Twitch Client Secret", + "default": "", + "description": "Client Secret of your Twitch application.", "type": "string" }, { "name": "interval", - "humanName": {}, - "default": { - "en": 180 - }, - "description": { - "en": "Interval (in seconds) in which it is tested whether the streamer is live. This value must be higher than 60" - }, - "hidden": true, - "type": "integer" + "humanName": "Check interval (seconds)", + "default": 180, + "description": "How often (in seconds) the bot polls Twitch for stream updates. Must be at least 60 to stay within Twitch rate limits.", + "type": "integer", + "minValue": 60 } ] -} \ No newline at end of file +} diff --git a/modules/twitch-notifications/configs/streamers.json b/modules/twitch-notifications/configs/streamers.json index 26c2fe04..83cadb8e 100644 --- a/modules/twitch-notifications/configs/streamers.json +++ b/modules/twitch-notifications/configs/streamers.json @@ -1,153 +1,77 @@ { - "description": { - "en": "Configure here, where for what streamer which message should get send", - "de": "Stelle hier ein, bei welchem Streamer in welchen Channel eine Nachricht gesendet werden soll" - }, - "humanName": { - "en": "Streamers", - "de": "Streamers" - }, - "elementLimits": { - "STARTER": 2, - "ACTIVE_GUILD": 5, - "PRO": 15, - "UNLIMITED": 5, - "PROFESSIONAL": 15 - }, + "description": "Configure here, where for what streamer which message should get send", + "humanName": "Streamers", "filename": "streamers.json", "configElements": true, "content": [ { "name": "liveMessage", - "humanName": { - "en": "Live-Messages", - "de": "Live-Nachricht" - }, - "default": { - "en": "Hey, %streamer% is live on Twitch streaming %game%! Check it out: %url%", - "de": "Hi, %streamer% ist Live auf Twitch und streamt %game%! Jetzt anschauen: %url%" - }, - "description": { - "en": "Message that gets send if the streamer goes live", - "de": "Nachricht, die gesendet wird, wenn ein Streamer anfängt zu streamen" - }, + "humanName": "Live-Messages", + "default": "Hey, %streamer% is live on Twitch streaming %game%! Check it out: %url%", + "description": "Message that gets send if the streamer goes live", "type": "string", "allowEmbed": true, "params": [ { "name": "streamer", - "description": { - "en": "Name of the Streamer", - "de": "Name des Streamers" - } + "description": "Name of the Streamer" }, { "name": "game", - "description": { - "en": "Game which is streamed", - "de": "Spiel, welches gestreamt wird" - } + "description": "Game which is streamed" }, { "name": "url", - "description": { - "en": "Link to the stream", - "de": "Link zum Twitch-Stream" - } + "description": "Link to the stream" }, { "name": "title", - "description": { - "en": "Title of the Stream", - "de": "Titel des Streams" - } + "description": "Title of the Stream" }, { "name": "thumbnailUrl", - "description": { - "en": "The Link to the thumbnail of the Stream", - "de": "Link zum Thumbnail des Streams" - }, + "description": "The Link to the thumbnail of the Stream", "isImage": true } ] }, { "name": "liveMessageChannel", - "humanName": { - "en": "Channel" - }, - "default": { - "en": "" - }, - "description": { - "en": "Channel in which live-message should get sent", - "de": "Kanal, in welchen Benachrichtigung gesendet werden soll" - }, + "humanName": "Channel", + "default": "", + "description": "Channel in which live-message should get sent", "type": "channelID" }, { "name": "streamer", - "humanName": { - "en": "Streamer", - "de": "Streamer" - }, - "default": { - "en": "" - }, - "description": { - "en": "Streamer where a notification should send when they start streaming", - "de": "Steamer, bei denen eine Benachrichtigung gesendet werden soll, wenn sie anfangen, zu streamen" - }, + "humanName": "Streamer", + "default": "", + "description": "Streamer where a notification should send when they start streaming", "type": "string" }, { "name": "liveRole", - "humanName": { - "en": "Use Live-Role", - "de": "Live-Rolle Aktivieren" - }, - "default": { - "en": false - }, - "description": { - "en": "Should the Live-Role be activated?", - "de": "Soll die Live-Rolle aktiviert sein?" - }, + "humanName": "Use Live-Role", + "default": false, + "description": "Should the Live-Role be activated?", "type": "boolean" }, { "name": "id", - "humanName": { - "en": "Discord-User ID", - "de": "Discord-Benutzer ID" - }, - "default": { - "en": "" - }, - "description": { - "en": "ID of the Discord-Account of the Streamer", - "de": "ID des Discord-Accounts des Streamers" - }, + "humanName": "Discord-User ID", + "default": "", + "description": "ID of the Discord-Account of the Streamer", "type": "userID", "dependsOn": "liveRole" }, { "name": "role", - "humanName": { - "en": "Live Role", - "de": "Live Rolle" - }, - "default": { - "en": "" - }, - "description": { - "en": "ID of the Role that the Streamer should get, when live", - "de": "ID der Rolle, die der streamer bekommen soll, wenn er live ist" - }, + "humanName": "Live Role", + "default": "", + "description": "ID of the Role that the Streamer should get, when live", "type": "roleID", "allowNull": true, "dependsOn": "liveRole" } ] -} \ No newline at end of file +} diff --git a/modules/twitch-notifications/events/botReady.js b/modules/twitch-notifications/events/botReady.js index 11bbf958..633019e1 100644 --- a/modules/twitch-notifications/events/botReady.js +++ b/modules/twitch-notifications/events/botReady.js @@ -116,13 +116,16 @@ function twitchNotifications(client, apiClient) { module.exports.run = async (client) => { const config = client.configurations['twitch-notifications']['config']; - const ClientID = config['twitchClientID']; - const ClientSecret = config['clientSecret']; - const authProvider = new ClientCredentialsAuthProvider(ClientID, ClientSecret); + if (!config['twitchClientID'] || !config['clientSecret']) { + client.logger.error('[twitch-notifications] Missing twitchClientID or clientSecret in configs/config.json — module disabled. Create a Twitch app at https://dev.twitch.tv/console/apps to obtain credentials.'); + return; + } + + const authProvider = new ClientCredentialsAuthProvider(config['twitchClientID'], config['clientSecret']); const apiClient = new ApiClient({authProvider}); await twitchNotifications(client, apiClient); - const interval = config['interval'] * 1000; + const interval = (config['interval'] || 180) * 1000; const twitchCheckInterval = setInterval(() => { twitchNotifications(client, apiClient); }, interval); diff --git a/modules/twitch-notifications/module.json b/modules/twitch-notifications/module.json index f2603317..ee7d9e0c 100644 --- a/modules/twitch-notifications/module.json +++ b/modules/twitch-notifications/module.json @@ -1,5 +1,6 @@ { "name": "twitch-notifications", + "fa-icon": "fa-brands fa-twitch", "author": { "name": "jateute", "link": "https://github.com/jateute", @@ -15,12 +16,6 @@ "tags": [ "integrations" ], - "humanReadableName": { - "en": "Twitch-Notifications", - "de": "Twitch-Benachrichtigungen" - }, - "description": { - "en": "Module that sends a message to a channel, when a streamer goes live on Twitch", - "de": "Sendet eine Nachricht in einen ausgewählten Channel, wenn ein Streamer Live auf Twitch streamt" - } -} \ No newline at end of file + "humanReadableName": "Twitch-Notifications", + "description": "Module that sends a message to a channel, when a streamer goes live on Twitch" +} diff --git a/modules/uno/commands/uno.js b/modules/uno/commands/uno.js index 64d7c882..c9974d5c 100644 --- a/modules/uno/commands/uno.js +++ b/modules/uno/commands/uno.js @@ -401,13 +401,16 @@ module.exports.run = async function (interaction) { fetchReply: true, ephemeral: true }); - m.createMessageComponentCollector({componentType: ComponentType.Button}).on('collect', i => perPlayerHandler(i, p, game)); + m.createMessageComponentCollector({ + componentType: ComponentType.Button, + time: 1800000 + }).on('collect', i => perPlayerHandler(i, p, game)); }); } const timeout = setTimeout(startGame, 179000); - const collector = msg.createMessageComponentCollector({componentType: ComponentType.Button}); + const collector = msg.createMessageComponentCollector({componentType: ComponentType.Button, time: 1800000}); collector.on('collect', async i => { if (i.customId === 'uno-join') { if (game.players.some(p => p.id === i.user.id)) return i.reply({ @@ -451,7 +454,10 @@ module.exports.run = async function (interaction) { fetchReply: true, ephemeral: true }); - m.createMessageComponentCollector({componentType: ComponentType.Button}).on('collect', int => perPlayerHandler(int, player, game)); + m.createMessageComponentCollector({ + componentType: ComponentType.Button, + time: 1800000 + }).on('collect', int => perPlayerHandler(int, player, game)); } else if (i.customId === 'uno-uno') { const player = game.players.find(p => p.id === i.user.id); if (!player) return i.reply({content: localize('uno', 'not-in-game'), ephemeral: true}); diff --git a/modules/uno/module.json b/modules/uno/module.json index d65f1e4d..0872f815 100644 --- a/modules/uno/module.json +++ b/modules/uno/module.json @@ -1,17 +1,13 @@ { "name": "uno", - "humanReadableName": { - "en": "Uno" - }, + "humanReadableName": "Uno", + "fa-icon": "fa-solid fa-cards-blank", "author": { "scnxOrgID": "60", "name": "TomatoCake", "link": "https://github.com/DEVTomatoCake" }, - "description": { - "en": "Let your users play Uno against each other!", - "de": "Lasse Nutzer auf deinem Server Uno gegeneinander spielen" - }, + "description": "Let your users play Uno against each other!", "commands-dir": "/commands", "noConfig": true, "releaseDate": "0", @@ -19,4 +15,4 @@ "fun" ], "openSourceURL": "https://github.com/DEVTomatoCake/ScootKit-CustomBot/tree/main/modules/uno" -} \ No newline at end of file +} diff --git a/modules/welcomer/configs/channels.json b/modules/welcomer/configs/channels.json index 8a90cf41..fcd15431 100644 --- a/modules/welcomer/configs/channels.json +++ b/modules/welcomer/configs/channels.json @@ -1,43 +1,21 @@ { - "description": { - "en": "Configure here in which channel which message should get send", - "de": "Passe hier an, in welchen Kanälen welche Nachricht gesendet werden soll" - }, - "humanName": { - "en": "Channel", - "de": "Kanäle" - }, + "description": "Configure here in which channel which message should get send", + "humanName": "Channel", "filename": "channels.json", "configElements": true, "content": [ { "name": "channelID", - "humanName": { - "en": "Channel", - "de": "Channel" - }, - "default": { - "en": "" - }, - "description": { - "en": "Channel in which the message should get send", - "de": "Kanal in welchen die Nachricht gesendet werden soll" - }, + "humanName": "Channel", + "default": "", + "description": "Channel in which the message should get send", "type": "channelID" }, { "name": "type", - "humanName": { - "en": "Channel-Type", - "de": "Kanal-Typ" - }, - "default": { - "en": "" - }, - "description": { - "en": "This sets in which content the channel should get used", - "de": "Dies gibt an, in welchem Kontext dieser Kanal verwendet werden soll" - }, + "humanName": "Channel-Type", + "default": "", + "description": "This sets in which content the channel should get used", "type": "select", "content": [ "join", @@ -48,234 +26,129 @@ }, { "name": "randomMessages", - "humanName": { - "en": "Random messages?", - "de": "Zufällige Nachrichten?" - }, - "default": { - "en": false - }, - "description": { - "en": "If enabled the bot will randomly pick a messages instead of using the message option below", - "de": "Wenn aktiviert wird der Bot eine zufällige Nachricht aus deiner Konfiguration wählen, anstatt die unten" - }, + "humanName": "Random messages?", + "default": false, + "description": "If enabled the bot will randomly pick a messages instead of using the message option below", "type": "boolean" }, { "name": "message", - "humanName": { - "de": "Nachricht", - "en": "Message" - }, - "default": { - "en": "" - }, - "description": { - "en": "Message that should get send", - "de": "Nachricht, die gesendet wird" - }, + "humanName": "Message", + "default": "", + "description": "Message that should get send", "type": "string", "allowEmbed": true, "allowGeneratedImage": true, "params": [ { "name": "mention", - "description": { - "en": "Mentions the user", - "de": "Erwähnung des Nutzers" - } + "description": "Mentions the user" }, { "name": "memberProfilePictureUrl", - "description": { - "en": "URL of the user's avatar", - "de": "URL zum Avatar des Nutzers" - }, + "description": "URL of the user's avatar", "isImage": true }, { "name": "servername", - "description": { - "en": "Name of the guild", - "de": "Servername" - } + "description": "Name of the guild" }, { "name": "tag", - "description": { - "en": "Tag of the user", - "de": "Tag des Nutzers" - } + "description": "Tag of the user" }, { "name": "createdAt", - "description": { - "en": "Date when account was created", - "de": "Datum an dem der Account erstellt wurde" - } + "description": "Date when account was created" }, { "name": "memberProfileBannerUrl", - "description": { - "en": "URL of the banner's avatar", - "de": "URL zum Banner des Nutzers" - }, + "description": "URL of the banner's avatar", "isImage": true }, { "name": "joinedAt", - "description": { - "en": "Date when user joined guild", - "de": "Datum, an dem der Nutzer den Server betreten hat" - } + "description": "Date when user joined guild" }, { "name": "guildUserCount", - "description": { - "en": "Count of users on the guild", - "de": "Anzahl von Nutzern auf dem Server" - } + "description": "Count of users on the guild" }, { "name": "guildMemberCount", - "description": { - "en": "Count of members (without bots) on the guild", - "de": "Anzahl von Nutzern auf dem Server" - } + "description": "Count of members (without bots) on the guild" }, { "name": "boostCount", - "description": { - "en": "Total count of boosts", - "de": "Gesamte Anzahl an Boosts" - } + "description": "Total count of boosts" }, { "name": "guildLevel", - "description": { - "en": "Boost-Level of the guild after the boost", - "de": "Boost-Level nach dem Boost" - } + "description": "Boost-Level of the guild after the boost" }, { "name": "mention", - "description": { - "en": "Mention of the user who unboosted", - "de": "Erwähnung des Nutzers" - } + "description": "Mention of the user who unboosted" } ] }, { "name": "welcome-button", - "humanName": { - "en": "Welcome-Button (only if \"Channel-Type\" = \"join\")", - "de": "Willkommens-Knopf (nur wenn \"Channel-Type\" = \"join\")" - }, - "default": { - "en": false - }, - "description": { - "en": "If enabled, a welcome-button will be attached to the welcome message. When a user clicks on it, the bot will send a welcome-ping in a configured channel. The button can be pressed once.", - "de": "If enabled, a welcome-button will be attached to the welcome message. When a user clicks on it, the bot will send a welcome-ping in a configured channel. The button can be pressed once." - }, + "humanName": "Welcome-Button (only if \"Channel-Type\" = \"join\")", + "default": false, + "description": "If enabled, a welcome-button will be attached to the welcome message. When a user clicks on it, the bot will send a welcome-ping in a configured channel. The button can be pressed once.", "type": "boolean" }, { "name": "welcome-button-content", "dependsOn": "welcome-button", - "humanName": { - "en": "Welcome-Button-Content", - "de": "Willkommens-Knopf-Inhalt" - }, - "default": { - "en": "Say hi \uD83D\uDC4B", - "de": "Hallo sagen \uD83D\uDC4B" - }, - "description": { - "en": "Content of the welcome button", - "de": "Inhalt des Willkommens-Knopfes" - }, + "humanName": "Welcome-Button-Content", + "default": "Say hi 👋", + "description": "Content of the welcome button", "type": "string" }, { "name": "welcome-button-channel", "dependsOn": "welcome-button", - "humanName": { - "en": "Channel in which the welcome-button should send a message", - "de": "Kanal, in welchen der Willkommens-Knopf die Nachricht senden soll" - }, - "default": { - "en": "", - "de": "" - }, - "description": { - "en": "The bot will send the configured message in this channel when a user presses the button", - "de": "Der Bot wird die konfigurierte Nachricht in diesen Kanal senden, wenn jemand den Knopf drückt" - }, + "humanName": "Channel in which the welcome-button should send a message", + "default": "", + "description": "The bot will send the configured message in this channel when a user presses the button", "type": "channelID" }, { "name": "welcome-button-message", "dependsOn": "welcome-button", - "humanName": { - "en": "Welcome-Button-Message", - "de": "Willkommens-Knopf-Nachricht" - }, - "default": { - "en": "%clickUserMention% welcomes %userMention% :wave:", - "de": "%clickUserMention% begrüßt %userMention% :wave:" - }, + "humanName": "Welcome-Button-Message", + "default": "%clickUserMention% welcomes %userMention% :wave:", "allowEmbed": true, - "description": { - "en": "This is the message the bot will send in the configured channel when a user presses the button", - "de": "Der Bot wird in diesen Kanal die Nachricht senden, wenn ein Nutzer den Knopf drückt" - }, + "description": "This is the message the bot will send in the configured channel when a user presses the button", "type": "string", "params": [ { "name": "userMention", - "description": { - "en": "Mention of the user who joined the server", - "de": "Erwähnung des Nutzer, der den Server beigetreten hat" - } + "description": "Mention of the user who joined the server" }, { "name": "userTag", - "description": { - "en": "Tag of the user who joined the server", - "de": "Tag des Nutzer, der den Server beigetreten hat" - } + "description": "Tag of the user who joined the server" }, { "name": "userAvatarURL", "isImage": true, - "description": { - "en": "Avatar of the user who joined the server", - "de": "Avatar des Nutzer, der den Server beigetreten hat" - } + "description": "Avatar of the user who joined the server" }, { "name": "clickUserMention", - "description": { - "en": "Mention of the user who clicked the button", - "de": "Erwähnung des Nutzer, der den Knopf gedrückt hat" - } + "description": "Mention of the user who clicked the button" }, { "name": "clickUserTag", - "description": { - "en": "Tag of the user who clicked the button", - "de": "Tag des Nutzer, der den Knopf gedrückt hat" - } + "description": "Tag of the user who clicked the button" }, { "name": "clickUserAvatarURL", "isImage": true, - "description": { - "en": "Avatar of the user who clicked the button", - "de": "Avatar des Nutzer, der den Knopf gedrückt hat" - } + "description": "Avatar of the user who clicked the button" } ] } diff --git a/modules/welcomer/configs/config.json b/modules/welcomer/configs/config.json index 69e17a70..e53a6071 100644 --- a/modules/welcomer/configs/config.json +++ b/modules/welcomer/configs/config.json @@ -1,242 +1,153 @@ { - "description": { - "en": "Manage the basic settings of this module here", - "de": "Passe die grundlegenden Optionen des Modules hier an" - }, - "humanName": { - "en": "Configuration", - "de": "Konfiguration" - }, + "description": "Manage the basic settings of this module here", + "humanName": "Configuration", "filename": "config.json", "content": [ { "name": "give-roles-on-join", - "humanName": { - "en": "Give roles on join", - "de": "Nutzer Rollen beim Beitreten geben" - }, - "default": { - "en": [], - "de": [] - }, - "description": { - "en": "Roles to give to a new member", - "de": "Rollen, die neuen Mitgliedern gegeben werden sollen" - }, + "humanName": "Give roles on join", + "default": [], + "description": "Roles to give to a new member", "type": "array", - "content": "roleID" + "content": "roleID", + "category": "roles" }, { "name": "assign-roles-immediately", - "humanName": { - "en": "Immediately give roles, instead of waiting for rules acceptance?", - "de": "Rollen sofort geben statt Regelbestätigung abzuwarten?" - }, - "default": { - "en": true - }, - "description": { - "en": "If enabled, roles will be granted immediately when a user joins your server. Otherwise, no roles will be assigned to users before they complete the Discord onboarding.", - "de": "Wenn aktiviert, werden die Rollen sofort vergeben, wenn ein Nutzer deinem Server beitritt. Ansonsten werden Rollen erst zugewiesen, wenn das Discord onboarding abgeschlossen wurde." - }, - "type": "boolean" + "humanName": "Immediately give roles, instead of waiting for rules acceptance?", + "default": true, + "description": "If enabled, roles will be granted immediately when a user joins your server. Otherwise, no roles will be assigned to users before they complete the Discord onboarding.", + "type": "boolean", + "category": "roles" }, { "name": "not-send-messages-if-member-is-bot", - "humanName": { - "en": "Ignore bots?", - "de": "Bots ignorieren?" - }, - "default": { - "en": true, - "de": true - }, - "description": { - "en": "Should bots get ignored when they join (or leave) the server", - "de": "Sollen Bots ignoriert werden, wenn sie den Server beitreten (oder diesen verlassen)" - }, - "type": "boolean" + "humanName": "Ignore bots?", + "default": true, + "description": "Should bots get ignored when they join (or leave) the server", + "type": "boolean", + "category": "welcome" }, { "name": "give-roles-on-boost", - "humanName": { - "de": "Zusätzliche Rollen beim Boost geben", - "en": "Give additional roles to boosters" - }, - "default": { - "en": [], - "de": [] - }, - "description": { - "en": "Roles to give to members who boosts the server", - "de": "Rollen, die Booster haben sollen" - }, + "humanName": "Give additional roles to boosters", + "default": [], + "description": "Roles to give to members who boosts the server", "type": "array", - "content": "roleID" + "content": "roleID", + "category": "boost" }, { "name": "delete-welcome-message", - "humanName": { - "en": "Delete welcome message", - "de": "Willkommensnachricht löschen" - }, - "default": { - "en": true - }, - "description": { - "en": "Should their welcome message be deleted, if a user leaves the server within 7 days", - "de": "Soll die Willkommensnachricht eines Nutzers, der den Server innerhalb von 7 Tagen wieder verlässt gelöscht werden" - }, - "type": "boolean" + "humanName": "Delete welcome message", + "default": true, + "description": "Should their welcome message be deleted, if a user leaves the server within 7 days", + "type": "boolean", + "category": "welcome" }, { "name": "sendDirectMessageOnJoin", - "humanName": { - "en": "Send DM on join? (often experienced by users as spam)", - "de": "PN beim Beitreten schicken? (von Nutzern oft als Spam empfunden)" - }, + "humanName": "Send DM on join? (often experienced by users as spam)", "type": "boolean", - "default": { - "en": false - }, - "description": { - "en": "If enabled, a DM will be sent to new users. This is often experienced by them as spam and can decrease your new user retention metrics. Please note that not all users will receive this DM, as a huge chunk has DMs disabled.", - "de": "Wenn aktiviert, wird eine PN an neue Nutzer gesendet. Das wird often als Spam empfunden und kann die Anzahl an Nutzern erhöhen, die direkt nach dem Beitritt deinen Server verlassen. Bitte beachte, dass nicht alle Nutzer diese PN erhalten werden, da eine großer Anzahl diese deaktiviert hat." - } + "default": false, + "description": "If enabled, a DM will be sent to new users. This is often experienced by them as spam and can decrease your new user retention metrics. Please note that not all users will receive this DM, as a huge chunk has DMs disabled.", + "category": "welcome" }, { "name": "joinDM", "dependsOn": "sendDirectMessageOnJoin", - "humanName": { - "en": "Join DM Message", - "de": "Beitritt PN Nachricht" - }, + "humanName": "Join DM Message", "allowGeneratedImage": true, - "default": { - "en": "" - }, - "description": { - "en": "Message that should get send to new users via DMs", - "de": "Nachricht, die an neue Nutzer per PN geschickt werden soll" - }, + "default": "", + "description": "Message that should get send to new users via DMs", "type": "string", "allowEmbed": true, "params": [ { "name": "mention", - "description": { - "en": "Mentions the user", - "de": "Erwähnung des Nutzers" - } + "description": "Mentions the user" }, { "name": "memberProfilePictureUrl", - "description": { - "en": "URL of the user's avatar", - "de": "URL zum Avatar des Nutzers" - }, + "description": "URL of the user's avatar", "isImage": true }, { "name": "servername", - "description": { - "en": "Name of the guild", - "de": "Servername" - } + "description": "Name of the guild" }, { "name": "tag", - "description": { - "en": "Tag of the user", - "de": "Tag des Nutzers" - } + "description": "Tag of the user" }, { "name": "createdAt", - "description": { - "en": "Date when account was created", - "de": "Datum an dem der Account erstellt wurde" - } + "description": "Date when account was created" }, { "name": "tag", - "description": { - "en": "Tag of the user", - "de": "Tag des Nutzers" - } + "description": "Tag of the user" }, { "name": "memberProfilePictureUrl", - "description": { - "en": "URL of the user's avatar", - "de": "URL zum Avatar des Nutzers" - }, + "description": "URL of the user's avatar", "isImage": true }, { "name": "joinedAt", - "description": { - "en": "Date when user joined guild", - "de": "Datum, an dem der Nutzer den Server betreten hat" - } + "description": "Date when user joined guild" }, { "name": "guildUserCount", - "description": { - "en": "Count of users on the guild", - "de": "Anzahl von Nutzern auf dem Server" - } + "description": "Count of users on the guild" }, { "name": "guildMemberCount", - "description": { - "en": "Count of members (without bots) on the guild", - "de": "Anzahl von Nutzern auf dem Server" - } + "description": "Count of members (without bots) on the guild" }, { "name": "mention", - "description": { - "en": "Mention of the user who boosted", - "de": "Erwähnung des Nutzers" - } + "description": "Mention of the user who boosted" }, { "name": "boostCount", - "description": { - "en": "Total count of boosts", - "de": "Gesamte Anzahl an Boosts" - } + "description": "Total count of boosts" }, { "name": "guildLevel", - "description": { - "en": "Boost-Level of the guild after the boost", - "de": "Boost-Level nach dem Boost" - } + "description": "Boost-Level of the guild after the boost" }, { "name": "mention", - "description": { - "en": "Mention of the user who unboosted", - "de": "Erwähnung des Nutzers" - } + "description": "Mention of the user who unboosted" }, { "name": "boostCount", - "description": { - "en": "Total count of boosts", - "de": "Gesamte Anzahl an Boosts" - } + "description": "Total count of boosts" }, { "name": "guildLevel", - "description": { - "en": "Boost-Level of the guild after the unboost", - "de": "Boost-Level nach dem Boost" - } + "description": "Boost-Level of the guild after the unboost" } - ] + ], + "category": "welcome" + } + ], + "categories": [ + { + "id": "welcome", + "icon": "fas fa-door-open", + "displayName": "Welcome" + }, + { + "id": "roles", + "icon": "fa-solid fa-users", + "displayName": "Auto-Roles" + }, + { + "id": "boost", + "icon": "fas fa-star", + "displayName": "Boosts" } ] } \ No newline at end of file diff --git a/modules/welcomer/configs/random-messages.json b/modules/welcomer/configs/random-messages.json index 1227ecbf..adff7096 100644 --- a/modules/welcomer/configs/random-messages.json +++ b/modules/welcomer/configs/random-messages.json @@ -1,28 +1,14 @@ { - "description": { - "en": "Manage the randomly send messages here", - "de": "Passe hier die Nachrichten an, die zufällig gesendet werden sollen" - }, - "humanName": { - "en": "Random messages", - "de": "Zufällige Nachrichten" - }, + "description": "Manage the randomly send messages here", + "humanName": "Random messages", "filename": "random-messages.json", "configElements": true, "content": [ { "name": "type", - "humanName": { - "en": "Message-Type", - "de": "Nachricht-Type" - }, - "default": { - "en": "" - }, - "description": { - "en": "This sets in which content the message should get send", - "de": "Dies gibt an, in welchem Kontext diese Nachricht versendet werden soll" - }, + "humanName": "Message-Type", + "default": "", + "description": "This sets in which content the message should get send", "type": "select", "content": [ "join", @@ -33,134 +19,78 @@ }, { "name": "message", - "humanName": { - "en": "Message", - "de": "Nachricht" - }, + "humanName": "Message", "allowGeneratedImage": true, - "default": { - "en": "" - }, - "description": { - "en": "Message that should get send", - "de": "Nachricht, die gesendet werden soll" - }, + "default": "", + "description": "Message that should get send", "type": "string", "allowEmbed": true, "params": [ { "name": "mention", - "description": { - "en": "Mentions the user", - "de": "Erwähnung des Nutzers" - } + "description": "Mentions the user" }, { "name": "memberProfilePictureUrl", - "description": { - "en": "URL of the user's avatar", - "de": "URL zum Avatar des Nutzers" - }, + "description": "URL of the user's avatar", "isImage": true }, { "name": "servername", - "description": { - "en": "Name of the guild", - "de": "Servername" - } + "description": "Name of the guild" }, { "name": "tag", - "description": { - "en": "Tag of the user", - "de": "Tag des Nutzers" - } + "description": "Tag of the user" }, { "name": "createdAt", - "description": { - "en": "Date when account was created", - "de": "Datum an dem der Account erstellt wurde" - } + "description": "Date when account was created" }, { "name": "tag", - "description": { - "en": "Tag of the user", - "de": "Tag des Nutzers" - } + "description": "Tag of the user" }, { "name": "memberProfilePictureUrl", - "description": { - "en": "URL of the user's avatar", - "de": "URL zum Avatar des Nutzers" - }, + "description": "URL of the user's avatar", "isImage": true }, { "name": "joinedAt", - "description": { - "en": "Date when user joined guild", - "de": "Datum, an dem der Nutzer den Server betreten hat" - } + "description": "Date when user joined guild" }, { "name": "guildUserCount", - "description": { - "en": "Count of users on the guild", - "de": "Anzahl von Nutzern auf dem Server" - } + "description": "Count of users on the guild" }, { "name": "guildMemberCount", - "description": { - "en": "Count of members (without bots) on the guild", - "de": "Anzahl von Nutzern auf dem Server" - } + "description": "Count of members (without bots) on the guild" }, { "name": "mention", - "description": { - "en": "Mention of the user who boosted", - "de": "Erwähnung des Nutzers" - } + "description": "Mention of the user who boosted" }, { "name": "boostCount", - "description": { - "en": "Total count of boosts", - "de": "Gesamte Anzahl an Boosts" - } + "description": "Total count of boosts" }, { "name": "guildLevel", - "description": { - "en": "Boost-Level of the guild after the boost", - "de": "Boost-Level nach dem Boost" - } + "description": "Boost-Level of the guild after the boost" }, { "name": "mention", - "description": { - "en": "Mention of the user who unboosted", - "de": "Erwähnung des Nutzers" - } + "description": "Mention of the user who unboosted" }, { "name": "boostCount", - "description": { - "en": "Total count of boosts", - "de": "Gesamte Anzahl an Boosts" - } + "description": "Total count of boosts" }, { "name": "guildLevel", - "description": { - "en": "Boost-Level of the guild after the unboost", - "de": "Boost-Level nach dem Boost" - } + "description": "Boost-Level of the guild after the unboost" } ] } diff --git a/modules/welcomer/events/guildMemberAdd.js b/modules/welcomer/events/guildMemberAdd.js index e19641e6..0371ed9c 100644 --- a/modules/welcomer/events/guildMemberAdd.js +++ b/modules/welcomer/events/guildMemberAdd.js @@ -70,7 +70,7 @@ module.exports.run = async function (client, guildMember) { )); const memberModel = await moduleModel.findOne({ where: { - userId: guildMember.id, + userID: guildMember.id, channelID: sentMessage.channelId } }); diff --git a/modules/welcomer/events/guildMemberRemove.js b/modules/welcomer/events/guildMemberRemove.js index 6eba7e50..0b386494 100644 --- a/modules/welcomer/events/guildMemberRemove.js +++ b/modules/welcomer/events/guildMemberRemove.js @@ -50,7 +50,7 @@ module.exports.run = async function (client, guildMember) { if (!moduleConfig['delete-welcome-message']) return; const memberModels = await moduleModel.findAll({ where: { - userId: guildMember.id + userID: guildMember.id } }); for (const memberModel of memberModels) { @@ -77,7 +77,7 @@ async function timer(client, userId) { const model = client.models['welcomer']['User']; const timeModel = await model.findOne({ where: { - userId: userId + userID: userId } }); if (timeModel) { diff --git a/modules/welcomer/module.json b/modules/welcomer/module.json index 7abfcc93..c0e4b355 100644 --- a/modules/welcomer/module.json +++ b/modules/welcomer/module.json @@ -5,6 +5,7 @@ "name": "SCDerox (SC Network Team)", "link": "https://github.com/SCDerox" }, + "fa-icon": "fas fa-door-open", "openSourceURL": "https://github.com/SCNetwork/CustomDCBot/tree/main/modules/welcomer", "events-dir": "/events", "models-dir": "/models", @@ -16,12 +17,6 @@ "tags": [ "administration" ], - "humanReadableName": { - "en": "Welcome and Boosts", - "de": "Willkommen und Boosts" - }, - "description": { - "en": "Simple module to say \"Hi\" to new members, give them roles automatically and say \"thanks\" to users who boosted", - "de": "Einfaches Modul zum Begrüßen von neuen Usern, zum automatischen Vergeben von Rollen beim Joinen und zum Bedanken bei Boosts." - } -} \ No newline at end of file + "humanReadableName": "Welcome and Boosts", + "description": "Simple module to say \"Hi\" to new members, give them roles automatically and say \"thanks\" to users who boosted" +} diff --git a/package.json b/package.json index ed3d2874..331ee76f 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "scripts": { "start": "node main.js", "test": "npx eslint ./", + "verify-configs": "node scripts/verify-config-defaults.js", "generate-config": "node generate-config.js", "generate-template": "node generate-template.js" }, @@ -30,7 +31,7 @@ "centra": "2.6.0", "discord-api-types": "0.38.37", "discord-logs": "2.2.1", - "discord.js": "14.25.1", + "discord.js": "14.26.2", "dotenv": "16.3.1", "erlpack": "github:discord/erlpack", "fparser": "3.1.0", diff --git a/scripts/verify-config-defaults.js b/scripts/verify-config-defaults.js new file mode 100644 index 00000000..5602291b --- /dev/null +++ b/scripts/verify-config-defaults.js @@ -0,0 +1,340 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); + +const VALID_TYPES = new Set([ + 'string', 'emoji', 'imgURL', 'timezone', + 'boolean', 'integer', 'float', + 'channelID', 'roleID', 'userID', 'guildID', + 'array', 'keyed', 'select' +]); + +let errors = 0; +let warnings = 0; +let filesChecked = 0; +let fieldsChecked = 0; + +function report(level, filePath, fieldName, message) { + const prefix = level === 'error' ? '\x1b[31mERROR\x1b[0m' : '\x1b[33mWARN\x1b[0m'; + const loc = fieldName ? `${filePath} -> ${fieldName}` : filePath; + console.log(` ${prefix}: ${loc}: ${message}`); + if (level === 'error') errors++; + else warnings++; +} + +function isLocalizedObject(value) { + if (value === null || value === undefined) return false; + if (typeof value !== 'object' || Array.isArray(value)) return false; + if (!('en' in value)) return false; + return Object.keys(value).every(k => /^[a-z]{2,3}$/.test(k)); +} + +function resolveDefault(field) { + if (isLocalizedObject(field.default)) return field.default['en']; + return field.default; +} + +function isValidV2Embed(obj) { + if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) return false; + const validKeys = new Set([ + 'message', 'title', 'description', 'color', 'url', + 'image', 'thumbnail', 'author', 'fields', 'footer', + 'footerImgUrl', 'embedTimestamp', '_schema' + ]); + const hasEmbedKey = obj.title || obj.description || (obj.author && obj.author.name) || obj.image || obj.message; + if (!hasEmbedKey) return false; + + for (const key of Object.keys(obj)) { + if (!validKeys.has(key)) return false; + } + + if (obj.author) { + if (typeof obj.author !== 'object' || Array.isArray(obj.author)) return false; + const authorKeys = new Set(['name', 'img', 'url']); + for (const key of Object.keys(obj.author)) { + if (!authorKeys.has(key)) return false; + } + } + if (obj.fields) { + if (!Array.isArray(obj.fields)) return false; + for (const f of obj.fields) { + if (typeof f.name !== 'string' || typeof f.value !== 'string') return false; + } + } + return true; +} + +function isValidV3Message(obj) { + if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) return false; + return obj._schema === 'v3'; +} + +function isValidV4Message(obj) { + if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) return false; + return obj._schema === 'v4'; +} + +function looksLikeV3ButMissingSchema(obj) { + if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) return false; + if (obj._schema) return false; + // Has v3-specific keys like embeds, content (as top-level message content), buttons, linkButtons, attachmentURLs + return !!(obj.embeds || obj.buttons || obj.linkButtons || obj.attachmentURLs || + (obj.content && !obj.title && !obj.description)); +} + +function verifyField(filePath, field) { + fieldsChecked++; + const name = field.name; + + if (!name) { + report('error', filePath, '(unnamed)', 'Field is missing "name" property'); + return; + } + + if (typeof field.default === 'undefined') { + report('error', filePath, name, 'Missing "default" value'); + return; + } + + if (!field.type) { + report('error', filePath, name, 'Missing "type" property'); + return; + } + + if (!VALID_TYPES.has(field.type)) { + report('error', filePath, name, `Unknown type "${field.type}"`); + return; + } + + const def = resolveDefault(field); + + // allowNull fields with null default are valid + if (field.allowNull && (def === null || def === '')) return; + + switch (field.type) { + case 'boolean': + if (typeof def !== 'boolean') { + report('error', filePath, name, `Type is "boolean" but default is ${JSON.stringify(def)} (${typeof def})`); + } + break; + + case 'integer': + if (def !== '' && def !== null && def !== 0) { + if (typeof def !== 'number' || !Number.isInteger(def)) { + report('error', filePath, name, `Type is "integer" but default is ${JSON.stringify(def)} (${typeof def})`); + } + } + if (typeof def === 'number') { + if (field.maxValue !== undefined && def > field.maxValue) { + report('error', filePath, name, `Default ${def} exceeds maxValue ${field.maxValue}`); + } + if (field.minValue !== undefined && def < field.minValue) { + report('error', filePath, name, `Default ${def} is below minValue ${field.minValue}`); + } + } + break; + + case 'float': + if (def !== '' && def !== null && def !== 0) { + if (typeof def !== 'number') { + report('error', filePath, name, `Type is "float" but default is ${JSON.stringify(def)} (${typeof def})`); + } + } + if (typeof def === 'number') { + if (field.maxValue !== undefined && def > field.maxValue) { + report('error', filePath, name, `Default ${def} exceeds maxValue ${field.maxValue}`); + } + if (field.minValue !== undefined && def < field.minValue) { + report('error', filePath, name, `Default ${def} is below minValue ${field.minValue}`); + } + } + break; + + case 'string': + case 'emoji': + case 'imgURL': + case 'timezone': + if (field.allowEmbed && typeof def === 'object' && def !== null) { + // Embed message — validate schema + if (isValidV3Message(def) || isValidV4Message(def)) { + // v3/v4 with explicit _schema are fine + } else if (looksLikeV3ButMissingSchema(def)) { + report('error', filePath, name, `Default looks like a v3 message (has ${Object.keys(def).filter(k => ['embeds', 'content', 'buttons', 'linkButtons'].includes(k)).join(', ')}) but is missing "_schema": "v3" — will be parsed as v2`); + } else if (!isValidV2Embed(def)) { + report('error', filePath, name, `Default is an object (embed) but has invalid v2 message schema. Keys: ${JSON.stringify(Object.keys(def))}`); + } + } else if (typeof def !== 'string') { + if (field.allowEmbed) { + report('error', filePath, name, `Type is "${field.type}" (allowEmbed) but default is ${typeof def}, not a string or valid embed object`); + } else if (typeof def === 'object' && def !== null && !Array.isArray(def)) { + report('error', filePath, name, `Type is "${field.type}" but default is an object — missing "allowEmbed: true"?`); + } else { + report('error', filePath, name, `Type is "${field.type}" but default is ${JSON.stringify(def)} (${typeof def})`); + } + } + break; + + case 'array': + if (!Array.isArray(def)) { + report('error', filePath, name, `Type is "array" but default is ${JSON.stringify(def)} (${typeof def})`); + } + if (!field.content) { + report('warn', filePath, name, 'Array field is missing "content" (element type)'); + } + break; + + case 'keyed': + if (typeof def !== 'object' || def === null || Array.isArray(def)) { + report('error', filePath, name, `Type is "keyed" but default is ${JSON.stringify(def)} (${typeof def})`); + } + if (!field.content) { + report('warn', filePath, name, 'Keyed field is missing "content" (key/value types)'); + } + break; + + case 'select': + if (!field.content || !Array.isArray(field.content)) { + report('error', filePath, name, 'Select field is missing "content" options array'); + } else { + const options = typeof field.content[0] !== 'string' + ? field.content.map(f => f.value) + : field.content; + if (def !== '' && def !== null && !options.includes(def)) { + report('error', filePath, name, `Default "${def}" is not in select options: [${options.join(', ')}]`); + } + } + break; + + case 'channelID': + case 'roleID': + case 'userID': + case 'guildID': + // These are typically empty strings as defaults (filled at runtime) + if (def !== '' && def !== null && typeof def !== 'string') { + report('error', filePath, name, `Type is "${field.type}" but default is ${JSON.stringify(def)} (${typeof def})`); + } + break; + } + +} + +function verifyConfigFile(filePath) { + filesChecked++; + let data; + try { + data = JSON.parse(fs.readFileSync(filePath, 'utf-8')); + } catch (e) { + report('error', filePath, null, `Failed to parse JSON: ${e.message}`); + return; + } + + const relPath = path.relative(process.cwd(), filePath); + + if (!data.content || !Array.isArray(data.content)) { + report('warn', relPath, null, 'No "content" array found — skipping field checks'); + return; + } + + if (!data.filename) { + report('warn', relPath, null, 'Missing "filename" property'); + } + + const fieldNames = new Set(data.content.map(f => f.name)); + + for (const field of data.content) { + verifyField(relPath, field); + + // Verify dependsOn references + if (field.dependsOn && !fieldNames.has(field.dependsOn)) { + report('error', relPath, field.name, `dependsOn references non-existent field "${field.dependsOn}"`); + } + if (field.dependsOnNot && !fieldNames.has(field.dependsOnNot)) { + report('error', relPath, field.name, `dependsOnNot references non-existent field "${field.dependsOnNot}"`); + } + + // Localized defaults are no longer supported + if (isLocalizedObject(field.default)) { + report('error', relPath, field.name, `Default uses deprecated localized format (keys: ${Object.keys(field.default).join(', ')}). Run the conversion script to migrate to external config-localizations`); + } + } + + // Check for multiple elementToggle fields + const toggleFields = data.content.filter(f => f.elementToggle); + if (toggleFields.length > 1) { + report('error', relPath, toggleFields.map(f => f.name).join(', '), `File has ${toggleFields.length} elementToggle fields — only one is supported. Use dependsOn for additional toggles`); + } + + // Check for duplicate field names + const seen = new Set(); + for (const field of data.content) { + if (field.name && seen.has(field.name)) { + report('error', relPath, field.name, 'Duplicate field name'); + } + seen.add(field.name); + } +} + +function discoverConfigFiles() { + const configFiles = []; + + // Core config-generator files + const generatorDir = path.join(__dirname, '..', 'config-generator'); + if (fs.existsSync(generatorDir)) { + for (const f of fs.readdirSync(generatorDir)) { + if (f.endsWith('.json')) { + configFiles.push(path.join(generatorDir, f)); + } + } + } + + // Module config files (discovered via module.json) + const modulesDir = path.join(__dirname, '..', 'modules'); + for (const moduleName of fs.readdirSync(modulesDir)) { + const moduleJsonPath = path.join(modulesDir, moduleName, 'module.json'); + if (!fs.existsSync(moduleJsonPath)) continue; + + let moduleJson; + try { + moduleJson = JSON.parse(fs.readFileSync(moduleJsonPath, 'utf-8')); + } catch { + report('error', `modules/${moduleName}/module.json`, null, 'Failed to parse module.json'); + continue; + } + + const exampleFiles = moduleJson['config-example-files'] || []; + for (const f of exampleFiles) { + const cfgPath = path.join(modulesDir, moduleName, f); + if (fs.existsSync(cfgPath)) { + configFiles.push(cfgPath); + } else { + report('error', `modules/${moduleName}/${f}`, null, 'Config example file listed in module.json but does not exist'); + } + } + } + + return configFiles; +} + +// Main +console.log('\n\x1b[1mVerifying config file default values...\x1b[0m\n'); + +const configFiles = discoverConfigFiles(); + +for (const filePath of configFiles) { + verifyConfigFile(filePath); +} + +console.log(`\n\x1b[1mResults:\x1b[0m ${filesChecked} files, ${fieldsChecked} fields checked`); +if (errors > 0) { + console.log(` \x1b[31m${errors} error(s)\x1b[0m`); +} +if (warnings > 0) { + console.log(` \x1b[33m${warnings} warning(s)\x1b[0m`); +} +if (errors === 0 && warnings === 0) { + console.log(' \x1b[32mAll checks passed!\x1b[0m'); +} + +console.log(''); +process.exit(errors > 0 ? 1 : 0); diff --git a/src/cli.js b/src/cli.js index 73c82b51..11e8bb23 100644 --- a/src/cli.js +++ b/src/cli.js @@ -37,6 +37,7 @@ module.exports.commands = [ if (inputElement.client.logChannel) await inputElement.client.logChannel.send('⚠️️ Configuration reloaded failed. Bot shutting down'); console.log('Reload failed. Exiting'); process.exit(0); + ; }); } }, diff --git a/src/commands/help.js b/src/commands/help.js index 36864556..e16f817b 100644 --- a/src/commands/help.js +++ b/src/commands/help.js @@ -17,9 +17,34 @@ const { MessageFlags } = require('discord.js'); const {localize} = require('../functions/localize'); +const { + loadConfigLocalization, + isLocalizedObject +} = require('../functions/configuration'); const SELECT_MENU_MAX = 25; +/** + * Resolve a module.json string (humanReadableName or description) for the current locale. + * Supports both old {en: ..., de: ...} format and new plain English string format. + */ +function resolveModuleString(client, moduleName, key, fallback) { + const value = client.modules[moduleName]['config'][key]; + if (typeof value === 'object' && value !== null && 'en' in value) { + return value[client.locale] || value['en'] || fallback; + } + if (typeof value === 'string') { + if (client.locale && client.locale !== 'en') { + const locData = loadConfigLocalization(client.locale); + if (locData[moduleName] && locData[moduleName]['_module'] && locData[moduleName]['_module'][key]) { + return locData[moduleName]['_module'][key]; + } + } + return value || fallback; + } + return fallback; +} + module.exports.run = async function (interaction) { const modules = {}; for (const command of interaction.client.commands) { @@ -29,21 +54,44 @@ module.exports.run = async function (interaction) { modules[command.module || 'none'].push(command); } + // Add custom slash commands as their own module group + const customCommands = (interaction.client.config || {}).customCommands || []; + const enabledCustomCommands = customCommands.filter(c => c.type === 'COMMAND' && c.enabled && c.slashCommandName && c.slashCommandDescription); + if (enabledCustomCommands.length > 0) { + modules['custom-commands'] = enabledCustomCommands.map(c => ({ + name: c.slashCommandName, + description: c.slashCommandDescription, + options: (c.slashCommandsOptions || []).map(o => ({ + type: o.type, + name: o.name, + description: o.description, + required: o.required + })) + })); + } + const moduleKeys = Object.keys(modules); const allSelectOptions = []; for (const mod of moduleKeys) { - const label = mod === 'none' - ? interaction.client.strings.helpembed.build_in - : (interaction.client.modules[mod]['config']['humanReadableName'][interaction.client.locale] || - interaction.client.modules[mod]['config']['humanReadableName']['en'] || mod); + let label, desc, emoji; + if (mod === 'none') { + label = interaction.client.strings.helpembed.build_in; + desc = localize('help', 'built-in-description'); + emoji = '⚙️'; + } else if (mod === 'custom-commands') { + label = localize('help', 'custom-commands-label'); + desc = localize('help', 'custom-commands-description'); + emoji = '🔧'; + } else { + label = resolveModuleString(interaction.client, mod, 'humanReadableName', mod); + desc = resolveModuleString(interaction.client, mod, 'description', ''); + emoji = '📦'; + } allSelectOptions.push({ label: truncate(label, 100), value: mod, - description: mod !== 'none' - ? truncate(interaction.client.modules[mod]['config']['description'][interaction.client.locale] || - interaction.client.modules[mod]['config']['description']['en'] || '', 100) - : localize('help', 'built-in-description'), - emoji: mod === 'none' ? '⚙️' : '📦' + description: truncate(desc, 100), + emoji }); } @@ -76,12 +124,19 @@ module.exports.run = async function (interaction) { let moduleList = ''; for (const mod of moduleKeys) { - const label = mod === 'none' - ? interaction.client.strings.helpembed.build_in - : (interaction.client.modules[mod]['config']['humanReadableName'][interaction.client.locale] || - interaction.client.modules[mod]['config']['humanReadableName']['en'] || mod); + let label, emoji; + if (mod === 'none') { + label = interaction.client.strings.helpembed.build_in; + emoji = '⚙️'; + } else if (mod === 'custom-commands') { + label = localize('help', 'custom-commands-label'); + emoji = '🔧'; + } else { + label = resolveModuleString(interaction.client, mod, 'humanReadableName', mod); + emoji = '📦'; + } const cmdNames = modules[mod].map(c => `\`/${c.name}\``).join(', '); - moduleList = moduleList + `${mod === 'none' ? '⚙️' : '📦'} **${label}**: ${truncate(cmdNames, 200)}\n`; + moduleList = moduleList + `${emoji} **${label}**: ${truncate(cmdNames, 200)}\n`; } headerContainer.addTextDisplayComponents(new TextDisplayBuilder().setContent(truncate(moduleList, 4000))); headerContainer.addSeparatorComponents(new SeparatorBuilder().setDivider(true).setSpacing(SeparatorSpacingSize.Small)); @@ -155,21 +210,26 @@ module.exports.run = async function (interaction) { * @returns {Promise} Array of V2 component objects */ async function buildModuleComponents(mod) { - const label = mod === 'none' - ? interaction.client.strings.helpembed.build_in - : (interaction.client.modules[mod]['config']['humanReadableName'][interaction.client.locale] || - interaction.client.modules[mod]['config']['humanReadableName']['en'] || mod); - const description = mod !== 'none' - ? (interaction.client.modules[mod]['config']['description'][interaction.client.locale] || - interaction.client.modules[mod]['config']['description']['en'] || '') - : ''; + let label, description; + if (mod === 'none') { + label = interaction.client.strings.helpembed.build_in; + description = ''; + } else if (mod === 'custom-commands') { + label = localize('help', 'custom-commands-label'); + description = localize('help', 'custom-commands-description'); + } else { + label = resolveModuleString(interaction.client, mod, 'humanReadableName', mod); + description = resolveModuleString(interaction.client, mod, 'description', ''); + } + + const emoji = mod === 'none' ? '⚙️' : mod === 'custom-commands' ? '🔧' : '📦'; const container = new ContainerBuilder() .setAccentColor(parseEmbedColor('GREEN')); const headerSection = new SectionBuilder() .addTextDisplayComponents( - new TextDisplayBuilder().setContent(`# ${mod === 'none' ? '⚙️' : '📦'} ${label}${description ? '\n*' + description + '*' : ''}`) + new TextDisplayBuilder().setContent(`# ${emoji} ${label}${description ? '\n*' + description + '*' : ''}`) ) .setThumbnailAccessory( new ThumbnailBuilder().setURL(interaction.client.user.displayAvatarURL()) diff --git a/src/commands/reload.js b/src/commands/reload.js index a7a26efb..410330ec 100644 --- a/src/commands/reload.js +++ b/src/commands/reload.js @@ -8,15 +8,16 @@ module.exports.run = async function (interaction) { ephemeral: true, content: localize('reload', 'reloading-config') }); - if (interaction.client.logChannel) interaction.client.logChannel.send('🔄 ' + localize('reload', 'reloading-config-with-name', {tag: formatDiscordUserName(interaction.user)})).then(() => { + if (interaction.client.logChannel) interaction.client.logChannel.send('🔄 ' + localize('reload', 'reloading-config-with-name', {tag: formatDiscordUserName(interaction.user)})).catch(() => { }); await reloadConfig(interaction.client).catch((async reason => { - if (interaction.client.logChannel) interaction.client.logChannel.send('⚠️️ ' + localize('reload', 'reload-failed')).then(() => { + if (interaction.client.logChannel) interaction.client.logChannel.send('⚠️️ ' + localize('reload', 'reload-failed')).catch(() => { }); await interaction.editReply({content: localize('reload', 'reload-failed-message', {reason})}); process.exit(0); + ; })).then(async (res) => { - if (interaction.client.logChannel) interaction.client.logChannel.send('✅ ' + localize('reload', 'reloaded-config', res)).then(() => { + if (interaction.client.logChannel) interaction.client.logChannel.send('✅ ' + localize('reload', 'reloaded-config', res)).catch(() => { }); await interaction.editReply(localize('reload', 'reload-successful-syncing-commands')); await syncCommandsIfNeeded(); diff --git a/src/discordjs-fix.js b/src/discordjs-fix.js index b70ef01d..e85a2a2c 100644 --- a/src/discordjs-fix.js +++ b/src/discordjs-fix.js @@ -35,10 +35,75 @@ Discord.Partials = Partials; if (EmbedBuilder && !EmbedBuilder.prototype.addField) { EmbedBuilder.prototype.addField = function (name, value, inline = false) { - return this.addFields({name, value, inline}); + return this.addFields({ + name: name || '\u200b', + value: value || '\u200b', + inline + }); }; } +const originalAddFields = EmbedBuilder.prototype.addFields; +EmbedBuilder.prototype.addFields = function (...fields) { + const normalized = fields.flat().map(f => ({ + ...f, + name: f.name || '\u200b', + value: f.value || '\u200b' + })); + return originalAddFields.call(this, normalized); +}; + +const originalSetDescription = EmbedBuilder.prototype.setDescription; +EmbedBuilder.prototype.setDescription = function (description) { + if (description === '') description = null; + return originalSetDescription.call(this, description); +}; + +const colorNames = { + 'YELLOW': 0xF1C40F, + 'GREEN': 0x2ECC71, + 'GOLD': 0xF1C40F, + 'PURPLE': 0x9B59B6, + 'LUMINOUS_VIVID_PINK': 0xE91E63, + 'FUCHSIA': 0xEB459E, + 'ORANGE': 0xE67E22, + 'DARK_AQUA': 0x11806A, + 'DARK_GREEN': 0x1F8B4C, + 'DARK_BLUE': 0x206694, + 'DARK_VIVID_PINK': 0xAD1457, + 'LIGHT_GREY': 0xBCC0C0, + 'GREYPLE': 0x99AAB5, + 'DARK_BUT_NOT_BLACK': 0x2C2F33, + 'NOT_QUITE_BLACK': 0x23272A, + 'DARK_NAVY': 0x2C3E50, + 'DARK_GOLD': 0xC27C0E, + 'DARK_RED': 0x992D22, + 'DARKER_GREY': 0x7F8C8D, + 'DARK_GREY': 0x979C9F, + 'DARK_ORANGE': 0xA84300, + 'DARK_PURPLE': 0x71368A, + 'GREY': 0x95A5A6, + 'NAVY': 0x34495E, + 'BLURPLE': 0x5865F2, + 'BLUE': 0x3498DB, + 'AQUA': 0x1ABC9C, + 'WHITE': 0xFFFFFF, + 'RED': 0xE74C3C +}; + +function resolveColor(color) { + if (typeof color !== 'string') return color; + const upper = color.toUpperCase(); + if (colorNames[upper]) return colorNames[upper]; + if (color.startsWith('#')) return parseInt(color.replace('#', ''), 16); + return color; +} + +const originalSetColor = EmbedBuilder.prototype.setColor; +EmbedBuilder.prototype.setColor = function (color) { + return originalSetColor.call(this, resolveColor(color)); +}; + const originalButtonSetStyle = ButtonBuilder.prototype.setStyle; ButtonBuilder.prototype.setStyle = function (style) { if (typeof style === 'string') { @@ -102,7 +167,14 @@ function normalizeMessageOptions(options) { const cloned = {...options}; if (cloned.components) cloned.components = normalizeComponents(cloned.components); if (cloned.embeds && Array.isArray(cloned.embeds)) { - cloned.embeds = cloned.embeds.map(e => e?.data ? e : (e instanceof EmbedBuilder ? e : new EmbedBuilder(e))); + cloned.embeds = cloned.embeds.map(e => { + if (e?.data || e instanceof EmbedBuilder) return e; + if (e && typeof e.color === 'string') e = { + ...e, + color: resolveColor(e.color) + }; + return new EmbedBuilder(e); + }); } return cloned; } diff --git a/src/events/botReady.js b/src/events/botReady.js index 32be5b4e..987d3ca8 100644 --- a/src/events/botReady.js +++ b/src/events/botReady.js @@ -1,6 +1,4 @@ module.exports.run = async (client) => { - await client.guild.members.fetch({withPresences: true}).catch(() => { - }); if (client.config.disableStatus) client.user.setActivity(null); else await client.user.setActivity(client.config.user_presence); }; \ No newline at end of file diff --git a/src/events/guildDelete.js b/src/events/guildDelete.js new file mode 100644 index 00000000..f7788d31 --- /dev/null +++ b/src/events/guildDelete.js @@ -0,0 +1,14 @@ +module.exports.run = async (client, guild) => { + if (!client.scnxSetup) return; + if (guild.id !== client.config.guildID) return; + client.logger.error(`Bot was removed from the configured guild (${guild.id}).`); + await require('../functions/scnx-integration').reportIssue(client, { + type: 'CORE_FAILURE', + errorDescription: 'bot_not_on_guild', + errorData: { + inviteURL: `https://discord.com/oauth2/authorize?client_id=${client.user.id}&guild_id=${client.config.guildID}&disable_guild_select=true&permissions=8&scope=bot%20applications.commands` + } + }); +}; + +module.exports.ignoreBotReadyCheck = true; diff --git a/src/events/interactionCreate.js b/src/events/interactionCreate.js index b2c07ad3..e527820f 100644 --- a/src/events/interactionCreate.js +++ b/src/events/interactionCreate.js @@ -18,7 +18,7 @@ module.exports.run = async (client, interaction) => { }); } if ((interaction.customId || '').startsWith('cc-') && client.scnxSetup) return require('../functions/scnx-integration').customCommandInteractionClick(interaction); - if (interaction.isSelectMenu() && interaction.customId === 'select-roles' && client.scnxSetup) return require('../functions/scnx-integration').handleSelectRoles(client, interaction); + if (interaction.isSelectMenu() && interaction.customId.startsWith('select-roles') && client.scnxSetup) return require('../functions/scnx-integration').handleSelectRoles(client, interaction); if (interaction.isButton() && interaction.customId.startsWith('srb-') && client.scnxSetup) return require('../functions/scnx-integration').handleRoleButton(client, interaction); if (!interaction.commandName) return; const command = client.commands.find(c => c.name.toLowerCase() === interaction.commandName.toLowerCase()); @@ -54,26 +54,26 @@ module.exports.run = async (client, interaction) => { if (group) return await command.autoComplete[group][subCommand][focusedOption](interaction); else return await command.autoComplete[subCommand][focusedOption](interaction); } catch (e) { - if (client.captureException) client.captureException(e, { + const sentryId = client.captureException ? client.captureException(e, { command: command.name, module: command.module, group, subCommand, focusedOption, userID: interaction.user.id - }); + }) : null; interaction.client.logger.error(localize('command', 'autcomplete-execution-failed', { e, f: focusedOption, c: command.name, g: group || '', s: subCommand || '' - })); + }) + (sentryId ? ` [Sentry: ${sentryId}]` : '')); interaction.respond([]); } } if (!interaction.isCommand()) return; - if (command.restricted === true && !client.config.botOperators.includes(interaction.user.id)) return interaction.reply(embedType(client.strings.not_enough_permissions)); + if (command.restricted === true && !client.config.botOperators.includes(interaction.user.id)) return interaction.reply(embedType(client.strings.not_enough_permissions || '⚠️ Not enough permissions', {}, {ephemeral: true})); client.logger.debug(localize('command', 'used', { tag: command.forceAnonymous ? '????????????' : formatDiscordUserName(interaction.user), @@ -103,6 +103,7 @@ module.exports.run = async (client, interaction) => { subCommand, userID: interaction.user.id }); + console.error(e, traceID); interaction.client.logger.error(localize('command', 'execution-failed', { e, c: command.name, diff --git a/src/functions/configuration.js b/src/functions/configuration.js index 2e500f74..eb47a0e4 100644 --- a/src/functions/configuration.js +++ b/src/functions/configuration.js @@ -13,6 +13,26 @@ const { const {localize} = require('./localize'); const isEqual = require('is-equal'); +// Config localization: load external translation files (cached) +const configLocalizationCache = {}; + +function loadConfigLocalization(locale) { + if (configLocalizationCache[locale]) return configLocalizationCache[locale]; + try { + configLocalizationCache[locale] = JSON.parse(fs.readFileSync(`${__dirname}/../../config-localizations/${locale}.json`, 'utf-8')); + } catch (e) { + configLocalizationCache[locale] = {}; + } + return configLocalizationCache[locale]; +} + +function isLocalizedObject(value) { + if (value === null || value === undefined) return false; + if (typeof value !== 'object' || Array.isArray(value)) return false; + if (!('en' in value)) return false; + return Object.keys(value).every(k => /^[a-z]{2,3}$/.test(k)); +} + const channelTypeMap = { GUILD_TEXT: ChannelType.GuildText, GUILD_CATEGORY: ChannelType.GuildCategory, @@ -81,6 +101,24 @@ async function checkConfigFile(file, moduleName) { return reject(`Not found config example file: ${file}`); } if (!exampleFile) return; + const locScope = builtIn ? '_core' : moduleName; + const locFileName = exampleFile.filename.replace('.json', ''); + + function resolveDefault(field) { + if (isLocalizedObject(field.default)) { + return field.default[client.locale] || field.default['en']; + } + if (['string', 'emoji', 'imgURL'].includes(field.type) && client.locale && client.locale !== 'en') { + const locData = loadConfigLocalization(client.locale); + const fileLocData = locData[locScope] && locData[locScope][locFileName]; + if (fileLocData && fileLocData.content && fileLocData.content[field.name] && + fileLocData.content[field.name].default !== undefined) { + return fileLocData.content[field.name].default; + } + } + return field.default; + } + let forceOverwrite = false; let configData = exampleFile.configElements ? [] : {}; try { @@ -98,7 +136,6 @@ async function checkConfigFile(file, moduleName) { if (typeof configData === 'object') configData = [configData]; else configData = []; } - if (exampleFile.elementLimits) configData = require('./scnx-integration').verifyLimitedConfigElementFile(client, exampleFile, configData); let skipOverwrite = false; if (exampleFile.skipContentCheck) newConfig = configData; @@ -110,12 +147,12 @@ async function checkConfigFile(file, moduleName) { const dependsOnNotField = field.dependsOnNot ? exampleFile.content.find(f => f.name === field.dependsOnNot) : null; if (field.dependsOn && !dependsOnField) return reject(`Depends-On-Field ${field.dependsOn} does not exist.`); if (field.dependsOnNot && !dependsOnNotField) return reject(`Depends-On-Field ${field.dependsOnNotField} does not exist.`); - if (dependsOnField && !(typeof object[dependsOnField.name] === 'undefined' ? (dependsOnField.default[client.locale] || dependsOnField.default['en']) : object[dependsOnField.name])) { - objectData[field.name] = configData[field.name] || (field.default[client.locale] || field.default['en']); // Otherwise disabled fields may be overwritten + if (dependsOnField && !(typeof object[dependsOnField.name] === 'undefined' ? resolveDefault(dependsOnField) : object[dependsOnField.name])) { + objectData[field.name] = configData[field.name] || resolveDefault(field); // Otherwise disabled fields may be overwritten continue; } - if (dependsOnNotField && (typeof object[dependsOnNotField.name] === 'undefined' ? (dependsOnNotField.default[client.locale] || dependsOnNotField.default['en']) : object[dependsOnNotField.name])) { - objectData[field.name] = configData[field.name] || (field.default[client.locale] || field.default['en']); // Otherwise disabled fields may be overwritten + if (dependsOnNotField && (typeof object[dependsOnNotField.name] === 'undefined' ? resolveDefault(dependsOnNotField) : object[dependsOnNotField.name])) { + objectData[field.name] = configData[field.name] || resolveDefault(field); // Otherwise disabled fields may be overwritten continue; } try { @@ -127,15 +164,18 @@ async function checkConfigFile(file, moduleName) { newConfig.push(objectData); } } else { + const elementToggleField = exampleFile.content.find(f => f.elementToggle); + const elementToggleValue = elementToggleField ? !!(typeof configData[elementToggleField.name] === 'undefined' ? resolveDefault(elementToggleField) : configData[elementToggleField.name]) : true; + if (!elementToggleValue) skipOverwrite = true; for (const field of exampleFile.content) { - if (exampleFile.content.find(f => f.elementToggle) && !configData[exampleFile.content.find(f => f.elementToggle).name]) { - skipOverwrite = true; + if (!elementToggleValue) { + newConfig[field.name] = configData[field.name] !== undefined ? configData[field.name] : resolveDefault(field); continue; } const dependsOnField = field.dependsOn ? exampleFile.content.find(f => f.name === field.dependsOn) : null; if (field.dependsOn && !dependsOnField) return reject(`Depends-On-Field ${field.dependsOn} does not exist.`); - if (dependsOnField && !(typeof configData[dependsOnField.name] === 'undefined' ? (dependsOnField.default[client.locale] || dependsOnField.default['en']) : configData[dependsOnField.name])) { - newConfig[field.name] = configData[field.name] || (field.default[client.locale] || field.default['en']); // Otherwise disabled fields may be overwritten + if (dependsOnField && !(typeof configData[dependsOnField.name] === 'undefined' ? resolveDefault(dependsOnField) : configData[dependsOnField.name])) { + newConfig[field.name] = configData[field.name] || resolveDefault(field); // Otherwise disabled fields may be overwritten continue; } try { @@ -157,16 +197,20 @@ async function checkConfigFile(file, moduleName) { const field = {...fieldData}; return new Promise(async (res, rej) => { if (!field.name) return rej('missing fieldname.'); - if (typeof field.default === 'undefined' || typeof field.default.en === 'undefined') { - console.log(field.default); + if (typeof field.default === 'undefined') { return rej('Missing default value on ' + field.name); } - if (typeof field.default !== 'object') return rej(`${field.name} has an invalid default value. The default value needs to be localized. A possible fix could be: default = "${JSON.stringify({en: field.default})}". If you want a default value for all languages, only set the "en" key.`); - field.default = field.default[client.locale] || field.default['en']; + if (isLocalizedObject(field.default)) { + // Old format: {en: ..., de: ...} — backwards compatible + field.default = field.default[client.locale] || field.default['en']; + } else { + // New format: plain value — resolve locale from external file + field.default = resolveDefault(field); + } if (typeof fieldValue === 'undefined') { fieldValue = field.default; return res(fieldValue); - } else if (field.type === 'keyed' && field.disableKeyEdits) for (const key in field.default) if (typeof fieldValue[key] === 'undefined') fieldValue[key] = field.default[key]; + } else if (field.type === 'keyed' && field.disableKeyEdits) for (const key in field.default) if (fieldValue[key] == null) fieldValue[key] = field.default[key]; if (field.allowNull && field.type !== 'boolean' && !fieldValue) return res(fieldValue); if (!await checkType(field, fieldValue)) { if (client.scnxSetup) await require('./scnx-integration').reportIssue(client, { @@ -192,7 +236,7 @@ async function checkConfigFile(file, moduleName) { if (typeof field.default[key] === 'undefined') delete fieldValue[key]; } for (const key in field.default) { - if (typeof fieldValue[key] === 'undefined') fieldValue[key] = field.default[key]; + if (fieldValue[key] == null) fieldValue[key] = field.default[key]; } } if (client.scnxSetup) fieldValue = require('./scnx-integration').setFieldValue(client, field, fieldValue); @@ -236,6 +280,8 @@ async function checkModuleConfig(moduleName, afterCheckEventFile = null) { } module.exports.loadAllConfigs = loadAllConfigs; +module.exports.loadConfigLocalization = loadConfigLocalization; +module.exports.isLocalizedObject = isLocalizedObject; /** * Check type of one field diff --git a/src/functions/helpers.js b/src/functions/helpers.js index 9848e0aa..47174a43 100644 --- a/src/functions/helpers.js +++ b/src/functions/helpers.js @@ -117,6 +117,15 @@ function getGlobalArgs() { globalArgs['%guildID%'] = guild.id; globalArgs['%guildIcon%'] = guild.iconURL() || ''; } + const now = new Date(); + globalArgs['%timestamp%'] = dateToDiscordTimestamp(now); + globalArgs['%shortTime%'] = dateToDiscordTimestamp(now, 't'); + globalArgs['%longTime%'] = dateToDiscordTimestamp(now, 'T'); + globalArgs['%shortDate%'] = dateToDiscordTimestamp(now, 'd'); + globalArgs['%longDate%'] = dateToDiscordTimestamp(now, 'D'); + globalArgs['%shortDateTime%'] = dateToDiscordTimestamp(now, 'f'); + globalArgs['%longDateTime%'] = dateToDiscordTimestamp(now, 'F'); + globalArgs['%relativeTime%'] = dateToDiscordTimestamp(now, 'R'); return globalArgs; } @@ -195,7 +204,7 @@ function embedType(input, args = {}, optionsToKeep = {}, mergeComponentsRows = [ let footer = null; if (!embedData.footer?.disabled) { const footerText = inputReplacer(args, embedData.footer?.text, true) || (client.strings && client.strings.footer); - const footerIconURL = embedData.footer?.iconURL || (client.strings && client.strings.footerImgUrl); + const footerIconURL = (embedData.footer?.iconURL || (client.strings && client.strings.footerImgUrl) || '').trim() || undefined; // Only create footer object if we have valid text if (footerText && footerText.trim().length > 0) { footer = { @@ -207,22 +216,22 @@ function embedType(input, args = {}, optionsToKeep = {}, mergeComponentsRows = [ const fields = []; for (const fieldData of embedData.fields || []) fields.push({ - name: inputReplacer(args, fieldData.name, true) || '\u200B', - value: inputReplacer(args, fieldData.value, true) || '\u200B', + name: truncate(inputReplacer(args, fieldData.name, true) || '\u200B', 256), + value: truncate(inputReplacer(args, fieldData.value, true) || '\u200B', 1024), inline: fieldData.inline }); const embed = new MessageEmbed({ - title: inputReplacer(args, embedData.title, true), - description: inputReplacer(args, embedData.description, true), + title: truncate(inputReplacer(args, embedData.title, true) || '', 256) || undefined, + description: truncate(inputReplacer(args, embedData.description, true) || '', 4096) || undefined, color: parseColor(embedData.color), - thumbnail: embedData.thumbnailURL ? {url: inputReplacer(args, embedData.thumbnailURL)} : null, - image: embedData.imageURL ? {url: inputReplacer(args, embedData.imageURL)} : null, + thumbnail: inputReplacer(args, embedData.thumbnailURL)?.trim() ? {url: inputReplacer(args, embedData.thumbnailURL).trim()} : null, + image: inputReplacer(args, embedData.imageURL)?.trim() ? {url: inputReplacer(args, embedData.imageURL).trim()} : null, timestamp: (embedData.footer?.hideTime || embedData.footer?.disabled || client.strings.disableFooterTimestamp) ? null : new Date(), author: embedData.author?.name ? { - name: inputReplacer(args, embedData.author.name), - iconURL: inputReplacer(args, embedData.author.imageURL, null), - url: inputReplacer(args, embedData.author.url, null) + name: truncate(inputReplacer(args, embedData.author.name), 256), + iconURL: inputReplacer(args, embedData.author.imageURL, null)?.trim() || null, + url: inputReplacer(args, embedData.author.url, null)?.trim() || null } : null, footer, fields @@ -232,7 +241,7 @@ function embedType(input, args = {}, optionsToKeep = {}, mergeComponentsRows = [ optionsToKeep.files = [...(optionsToKeep.files || [])]; for (const url of input.attachmentURLs || []) { - optionsToKeep.files.push({attachment: url}); + if (url && url.trim()) optionsToKeep.files.push({attachment: url.trim()}); } if (optionsToKeep.components) optionsToKeep.components = optionsToKeep.components.map(c => (typeof c.toJSON === 'function' ? c.toJSON() : c)); // polyfill for djs migration @@ -250,19 +259,22 @@ function embedTypeSchemaV2(input, args = {}, optionsToKeep = {}, mergeComponents if (client.scnxSetup) input = require('./scnx-integration').verifyEmbedType(client, input); if (input.title || input.description || (input.author || {}).name || input.image) { const emb = new MessageEmbed(); - if (input['title']) emb.setTitle(inputReplacer(args, input['title'])); - if (input['description']) emb.setDescription(inputReplacer(args, input['description'])); + if (input['title']) emb.setTitle(truncate(inputReplacer(args, input['title']), 256)); + if (input['description']) emb.setDescription(truncate(inputReplacer(args, input['description']), 4096)); if (input['color']) emb.setColor(parseColor(input['color'])); - if (input['url']) emb.setURL(input['url']); - if ((input['image'] || '').replaceAll(' ', '')) emb.setImage(inputReplacer(args, input['image'])); - if ((input['thumbnail'] || '').replaceAll(' ', '')) emb.setThumbnail(inputReplacer(args, input['thumbnail'])); + const resolvedURL = inputReplacer(args, input['url'])?.trim(); + if (resolvedURL) emb.setURL(resolvedURL); + const resolvedImage = inputReplacer(args, input['image'])?.trim(); + if (resolvedImage) emb.setImage(resolvedImage); + const resolvedThumbnail = inputReplacer(args, input['thumbnail'])?.trim(); + if (resolvedThumbnail) emb.setThumbnail(resolvedThumbnail); if (input['author'] && typeof input['author'] === 'object' && (input['author'] || {}).name) emb.setAuthor({ - name: inputReplacer(args, input['author']['name']), - iconURL: (input['author']['img'] || '').replaceAll(' ', '') ? inputReplacer(args, input['author']['img']) : null + name: truncate(inputReplacer(args, input['author']['name']), 256), + iconURL: (input['author']['img'] || '').trim() ? inputReplacer(args, input['author']['img']).trim() : null }); if (typeof input['fields'] === 'object') { input.fields.forEach(f => { - emb.addField(inputReplacer(args, f['name']), inputReplacer(args, f['value']), f['inline']); + emb.addField(truncate(inputReplacer(args, f['name']), 256), truncate(inputReplacer(args, f['value']), 1024), f['inline']); }); } if (!client.strings.disableFooterTimestamp && !input.embedTimestamp) emb.setTimestamp(); @@ -270,7 +282,7 @@ function embedTypeSchemaV2(input, args = {}, optionsToKeep = {}, mergeComponents // Safely set footer with null checks const footerText = input.footer ? inputReplacer(args, input.footer) : (client.strings && client.strings.footer); - const footerIconURL = input.footerImgUrl || (client.strings && client.strings.footerImgUrl); + const footerIconURL = (input.footerImgUrl || (client.strings && client.strings.footerImgUrl) || '').trim() || undefined; if (footerText && footerText.trim().length > 0) { emb.setFooter({ text: footerText, @@ -360,10 +372,10 @@ function buildV4Button(comp, args) { btn.setCustomId(`disabled-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`); } else if (action.type === 'linkButton') { btn.setStyle(ButtonStyle.Link); - if (comp.url) btn.setURL(inputReplacer(args, comp.url)); + if (comp.url) btn.setURL(inputReplacer(args, comp.url).trim()); } } else if (style === 5 && comp.url) { - btn.setURL(inputReplacer(args, comp.url)); + btn.setURL(inputReplacer(args, comp.url).trim()); } else if (comp.custom_id) { btn.setCustomId(comp.custom_id); } @@ -379,16 +391,16 @@ function buildV4Button(comp, args) { * @returns {StringSelectMenuBuilder|null} Built select menu or null if invalid * @private */ -function buildV4StringSelect(comp, args) { +function buildV4StringSelect(comp, args, counters) { if (!Array.isArray(comp.options) || comp.options.length === 0) return null; const select = new StringSelectMenuBuilder(); if (comp.scnx_action) { if (comp.scnx_action.type === 'roleElement') { - select.setCustomId('select-roles'); + select.setCustomId(`select-roles-${counters ? counters.roleSelect++ : 0}`); } else if (comp.scnx_action.type === 'customCommandElement') { - select.setCustomId('cc-select'); + select.setCustomId(`cc-select-${counters ? counters.ccSelect++ : 0}`); } } else if (comp.custom_id) { select.setCustomId(comp.custom_id); @@ -425,7 +437,7 @@ function buildV4StringSelect(comp, args) { * @returns {Object|null} A discord.js builder instance or null if invalid/skipped * @private */ -function buildV4Component(comp, args) { +function buildV4Component(comp, args, counters) { if (!comp || typeof comp !== 'object' || !comp.type) return null; try { @@ -450,7 +462,7 @@ function buildV4Component(comp, args) { if (!item.media || !item.media.url) continue; try { const galleryItem = new MediaGalleryItemBuilder() - .setURL(inputReplacer(args, item.media.url)); + .setURL(inputReplacer(args, item.media.url).trim()); if (item.description) galleryItem.setDescription(truncate(inputReplacer(args, item.description), 1024)); if (item.spoiler) galleryItem.setSpoiler(true); gallery.addItems(galleryItem); @@ -464,7 +476,7 @@ function buildV4Component(comp, args) { } case 13: { // File if (!comp.file || !comp.file.url) return null; - const file = new FileBuilder().setURL(inputReplacer(args, comp.file.url)); + const file = new FileBuilder().setURL(inputReplacer(args, comp.file.url).trim()); if (comp.spoiler) file.setSpoiler(true); return file; } @@ -474,7 +486,7 @@ function buildV4Component(comp, args) { const firstChild = comp.components[0]; if (firstChild && firstChild.type === 3) { // String select menu (max 1 per row) - const select = buildV4StringSelect(firstChild, args); + const select = buildV4StringSelect(firstChild, args, counters); if (!select) return null; row.addComponents(select); } else { @@ -509,7 +521,7 @@ function buildV4Component(comp, args) { if (comp.accessory.type === 11) { // Thumbnail if (comp.accessory.media && comp.accessory.media.url) { - const thumb = new ThumbnailBuilder().setURL(inputReplacer(args, comp.accessory.media.url)); + const thumb = new ThumbnailBuilder().setURL(inputReplacer(args, comp.accessory.media.url).trim()); if (comp.accessory.description) thumb.setDescription(truncate(inputReplacer(args, comp.accessory.description), 1024)); if (comp.accessory.spoiler) thumb.setSpoiler(true); section.setThumbnailAccessory(thumb); @@ -541,7 +553,7 @@ function buildV4Component(comp, args) { let addedChildren = 0; for (const child of comp.components) { try { - const built = buildV4Component(child, args); + const built = buildV4Component(child, args, counters); if (!built) continue; switch (child.type) { case 10: @@ -568,6 +580,10 @@ function buildV4Component(comp, args) { container.addSectionComponents(built); addedChildren++; break; + case 'dynamicImage': + container.addMediaGalleryComponents(built); + addedChildren++; + break; } } catch (e) { client.logger.error(`[embedType/v4] Failed to build container child (type ${child.type}): ${formatV4BuilderError(e)}`); @@ -576,6 +592,11 @@ function buildV4Component(comp, args) { if (addedChildren === 0) return null; return container; } + case 'dynamicImage': { // Placeholder for dynamic image - emits a MediaGallery component at this position + return new MediaGalleryBuilder().addItems( + new MediaGalleryItemBuilder().setURL('attachment://image.png') + ); + } default: return null; } @@ -600,19 +621,34 @@ function embedTypeSchemaV4(input, args = {}, optionsToKeep = {}, mergeComponents optionsToKeep.flags = existingFlags | MessageFlags.IsComponentsV2; const components = []; + + // Save any pre-existing components passed via optionsToKeep (e.g. giveaway buttons) to append last + const keepComponents = (optionsToKeep.components || []).map(c => typeof c.toJSON === 'function' ? c.toJSON() : c); + + const counters = {roleSelect: 0, ccSelect: 0}; for (const comp of input.components || []) { try { - const built = buildV4Component(comp, args); + const built = buildV4Component(comp, args, counters); if (built) components.push(built); } catch (e) { client.logger.error(`[embedType/v4] Failed to build top-level component (type ${(comp || {}).type}): ${formatV4BuilderError(e)}`); } } + // Check if a dynamicImage sentinel exists anywhere (including inside containers) + if ((input.components || []).some(function findSentinel(c) { + return c.type === 'dynamicImage' || (Array.isArray(c.components) && c.components.some(findSentinel)); + })) optionsToKeep._hasDynamicImagePlaceholder = true; + for (const row of mergeComponentsRows) { components.push(row); } + // Append pre-existing components from optionsToKeep at the bottom (e.g. giveaway buttons) + for (const kept of keepComponents) { + components.push(kept); + } + // Add SCNX branding for non-paid plans if (client.scnxSetup && !['PROFESSIONAL', 'PRO', 'ENTERPRISE'].includes(client.scnxData.plan)) { components.push(new TextDisplayBuilder().setContent('-# Powered by scnx.xyz \u26A1')); @@ -630,10 +666,16 @@ module.exports.embedTypeV2 = async function (input, args, otP, mergeComponentsRo let optionsToKeep = embedType(input, args, otP, mergeComponentsRows); if (!optionsToKeep.attachments && client.scnxSetup && (input.dynamicImage || {}).enabled) { optionsToKeep = await require('./scnx-integration').returnDynamicImages(input, optionsToKeep, args); - // For v4, dynamic image was added to files but embeds don't exist; add a File component to display it + // For v4, dynamic image was added to files but embeds don't exist; add a MediaGallery component to display it if ((input._schema || 'v2') === 'v4' && optionsToKeep.files && optionsToKeep.files.length > 0) { - if (!optionsToKeep.components) optionsToKeep.components = []; - optionsToKeep.components.push(new FileBuilder().setURL('attachment://image.png')); + // If a dynamicImage placeholder was placed in the components, the MediaGallery is already in position + if (!optionsToKeep._hasDynamicImagePlaceholder) { + if (!optionsToKeep.components) optionsToKeep.components = []; + optionsToKeep.components.push(new MediaGalleryBuilder().addItems( + new MediaGalleryItemBuilder().setURL('attachment://image.png') + )); + } + delete optionsToKeep._hasDynamicImagePlaceholder; } } return optionsToKeep; @@ -661,6 +703,33 @@ function formatDate(date, skipDiscordFormat = false) { module.exports.formatDate = formatDate; +/** + * Formats a duration (in milliseconds) as a short human-readable string, + * picking the largest meaningful unit. Localized via the `helpers` namespace. + * @param {number} ms Duration in milliseconds + * @return {string} e.g. "2 months", "5 days", "3 hours", "just now" + * @author Simon Csaba + */ +function formatDurationShort(ms) { + if (!Number.isFinite(ms) || ms < 60_000) return localize('helpers', 'duration-just-now'); + const units = [ + ['year', 365 * 24 * 60 * 60 * 1000], + ['month', 30 * 24 * 60 * 60 * 1000], + ['day', 24 * 60 * 60 * 1000], + ['hour', 60 * 60 * 1000], + ['minute', 60 * 1000] + ]; + for (const [unit, size] of units) { + const value = Math.floor(ms / size); + if (value >= 1) { + return localize('helpers', `duration-${unit}${value === 1 ? '' : 's'}`, {i: value}); + } + } + return localize('helpers', 'duration-just-now'); +} + +module.exports.formatDurationShort = formatDurationShort; + /** * Posts (encrypted) content to SC Network Paste * @param {String} content Content to post @@ -731,6 +800,7 @@ module.exports.messageLogToStringToPaste = messageLogToStringToPaste; * @return {string} Truncated string */ function truncate(string, length) { + if (!string) return string; return (string.length > length) ? string.substr(0, length - 3).trim() + '...' : string; } @@ -837,9 +907,10 @@ function compareArrays(array1, array2) { if (array1.length !== array2.length) return false; for (let i = 0, l = array1.length; i < l; i++) { - if (array1[i] instanceof Object) { - for (const key in array1[i]) { - if (array2[key] !== array1[key]) return false; + if (array1[i] instanceof Object || array2[i] instanceof Object) { + const keys = new Set([...Object.keys(array1[i] || {}), ...Object.keys(array2[i] || {})]); + for (const key of keys) { + if ((array1[i][key] ?? null) !== (array2[i][key] ?? null)) return false; } continue; } @@ -934,17 +1005,37 @@ async function lockChannel(channel, allowedRoles = [], reason = localize('main', permissions: Array.from(channel.permissionOverwrites.cache.values()) }); - for (const overwrite of channel.permissionOverwrites.cache.filter(e => e.allow.has(PermissionFlagsBits.SendMessages)).values()) { - if (overwrite.type === 'role' && channel.client.guild.members.me.roles.botRole?.id === overwrite.id) continue; + const allowedRoleSet = new Set(allowedRoles.map(r => typeof r === 'string' ? r : r.id || r)); + const botRoleId = channel.client.guild.members.me.roles.botRole?.id; + + for (const overwrite of channel.permissionOverwrites.cache.values()) { + if (overwrite.id === botRoleId) continue; if (overwrite.type === 'member' && channel.client.user.id === overwrite.id) continue; + if (allowedRoleSet.has(overwrite.id)) continue; + if (overwrite.deny.has(PermissionFlagsBits.SendMessages)) continue; await overwrite.edit({ SendMessages: false, SendMessagesInThreads: false }, reason); } - const everyoneRole = await channel.guild.roles.cache.find(r => r.name === '@everyone'); - if (channel.permissionsFor(everyoneRole).has(PermissionFlagsBits.ViewChannel)) await channel.permissionOverwrites.create(everyoneRole, { + // Also deny roles inheriting SendMessages from the parent category + if (channel.parent) { + for (const [id, catOverwrite] of channel.parent.permissionOverwrites.cache) { + if (catOverwrite.type !== 0) continue; // Only roles + if (id === botRoleId) continue; + if (allowedRoleSet.has(id)) continue; + if (channel.permissionOverwrites.cache.has(id)) continue; // Already handled above + if (!catOverwrite.allow.has(PermissionFlagsBits.SendMessages)) continue; + await channel.permissionOverwrites.create(id, { + SendMessages: false, + SendMessagesInThreads: false + }, {reason}); + } + } + + const everyoneRole = channel.guild.roles.everyone; + await channel.permissionOverwrites.create(everyoneRole, { SendMessages: false, SendMessagesInThreads: false }, {reason}); @@ -1024,14 +1115,30 @@ function disableModule(module, reason = null) { module.exports.disableModule = disableModule; +/** + * Checks whether a module is currently enabled. Prefer this over `client.models[X]` or + * `client.configurations[X]` as enabled-checks — models load for every module directory + * on disk regardless of enabled state, and configurations are only populated when the + * module is enabled. + * @param {Client} client + * @param {String} moduleName + * @returns {Boolean} + */ +function moduleEnabled(client, moduleName) { + return !!(client.modules[moduleName] && client.modules[moduleName].enabled); +} + +module.exports.moduleEnabled = moduleEnabled; + /** * Formates a number to make it human-readable * @param {Number|string} number + * @param {Intl.NumberFormatOptions} [options] * @returns {string} */ -module.exports.formatNumber = function (number) { - if (typeof number === 'string') number = parseInt(number); - return new Intl.NumberFormat(client.locale.split('_')[0], {}).format(number); +module.exports.formatNumber = function (number, options = {}) { + if (typeof number === 'string') number = parseFloat(number); + return new Intl.NumberFormat(client.locale.split('_')[0], options).format(number); }; /** @@ -1050,4 +1157,37 @@ module.exports.shuffleArray = function (input) { [array[i], array[j]] = [array[j], array[i]]; } return array; -} \ No newline at end of file +} + +/** + * Tries to archive a Discord CDN attachment into the guild's scnx file + * library and returns the full archival result. Returns null when the bot + * is running outside an scnx setup (OSS build — scnx-integration is not + * shipped), when archival is disabled, or on any failure. Use this when you + * need to know whether the returned URL will outlive Discord's signed TTL + * — e.g. persisting an attachment URL for later restoration. + * @param {Client} client + * @param {string} url Discord CDN URL + * @param {{displayName?: string, tags?: string[], uploaderDiscordID?: string}} meta + * @returns {Promise<{id: string, url: string, mediaCategory: string, duplicate?: boolean} | null>} + */ +module.exports.tryArchiveDiscordAttachment = async function (client, url, meta = {}) { + if (!client.scnxSetup) return null; + return require('./scnx-integration').archiveDiscordAttachment(client, url, meta); +}; + +/** + * Convenience wrapper around tryArchiveDiscordAttachment — always returns a + * URL. On success, the permanent scnx CDN URL; on any failure (disabled, + * OSS build, rate-limited, quota-exhausted, upstream error), the original + * Discord URL. Use this at display sites where the URL is only needed + * within Discord's signed-TTL window. + * @param {Client} client + * @param {string} url Discord CDN URL + * @param {{displayName?: string, tags?: string[], uploaderDiscordID?: string}} meta + * @returns {Promise} + */ +module.exports.archiveDiscordAttachment = async function (client, url, meta = {}) { + const result = await module.exports.tryArchiveDiscordAttachment(client, url, meta); + return result ? result.url : url; +}; \ No newline at end of file diff --git a/src/functions/localize.js b/src/functions/localize.js index 5b335eda..5b5aacad 100644 --- a/src/functions/localize.js +++ b/src/functions/localize.js @@ -34,7 +34,8 @@ function localize(file, string, replace = {}) { let rs = locals[client.locale][file][string]; if (!rs) rs = locals['en'][file][string]; if (!rs) throw new Error(`String ${file}/${string} not found`); - for (const key in replace) { + // Replace longest keys first to avoid e.g. %user replacing part of %username + for (const key of Object.keys(replace).sort((a, b) => b.length - a.length)) { rs = rs.replaceAll(`%${key}`, replace[key]); } return rs; diff --git a/src/global-params.json b/src/global-params.json new file mode 100644 index 00000000..d0396031 --- /dev/null +++ b/src/global-params.json @@ -0,0 +1,58 @@ +[ + { + "name": "botName", + "description": { + "en": "Display name of the bot", + "de": "Anzeigename des Bots" + } + }, + { + "name": "botID", + "description": { + "en": "User ID of the bot", + "de": "Nutzer-ID des Bots" + } + }, + { + "name": "botAvatar", + "description": { + "en": "URL of the bot's avatar", + "de": "URL des Bot-Avatars" + } + }, + { + "name": "botTag", + "description": { + "en": "Username and tag of the bot (e.g. Bot#1234)", + "de": "Nutzername und Tag des Bots (z.B. Bot#1234)" + } + }, + { + "name": "botMention", + "description": { + "en": "Mention of the bot (renders as a clickable @mention)", + "de": "Erwähnung des Bots (wird als klickbare @Erwähnung angezeigt)" + } + }, + { + "name": "guildName", + "description": { + "en": "Name of the server", + "de": "Name des Servers" + } + }, + { + "name": "guildID", + "description": { + "en": "ID of the server", + "de": "ID des Servers" + } + }, + { + "name": "guildIcon", + "description": { + "en": "URL of the server icon", + "de": "URL des Server-Icons" + } + } +] From e3c2af02aa5b789331ebbeebd006230034ced350 Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 24 Apr 2026 12:36:00 +0200 Subject: [PATCH 26/27] synced to closed source version --- README.md | 126 ++++++++++++++++++++++++++------ config-generator/config.json | 36 +-------- developer-docs/configuration.md | 1 - modules/tickets/config.json | 9 +-- 4 files changed, 108 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index a83de3d8..f321114d 100644 --- a/README.md +++ b/README.md @@ -3,38 +3,116 @@ Create your own Discord bot - fully customizable and modular. This bot is for advanced JS users with experience in JavaScript, discord.js, and JSON configuration. -## Get your own Custom-Bot for free +## Don't want to self-host? Use SCNX (free) -Go check it out on our [website](https://scnx.xyz) (the [dashboard](https://scnx.app) and bot are fully translated). In -addition to the features available here, we offer: +This repository is the **DIY path**: clone, configure JSON files by hand, run a Node process, manage uptime yourself. +If that doesn't sound fun, the same bot is available as a fully managed service at **[scnx.xyz](https://scnx.xyz)**. -* Free hosting -* Custom commands -* Easy-to-use embed editor and configuration editor -* Send and edit messages in specific channels -* Human-readable issue reporting -* Modern dashboard -* and a lot more +* **Free plan** - no credit card required. Hosting is ad-supported: watch one short ad in the dashboard every 7 days + to keep the bot running. Skip a week and the bot pauses until you log in. +* **Paid plans** start at **€4.99 / month** - no ads, higher limits, and access to premium modules and tiers. -[Get started now](https://scnx.xyz) - it's free, forever! +What you get with the hosted version that you do **not** get here: -## License +* **Zero setup.** Invite the bot, log in to the [dashboard](https://scnx.app), pick your modules, done. No Node, no + JSON, no server. +* **Hosted for you.** We run the bot so you don't have to keep a Node process alive. Restarts and updates are handled + on our side; Discord-side outages are still Discord's problem, but you're not on the hook for the rest. The free + plan keeps running as long as you watch the weekly ad; paid plans drop the ad requirement. +* **Visual editors.** Drag-and-drop embed editor, point-and-click configuration, live previews. No more hand-writing + JSON for every welcome message. +* **Custom slash commands.** Build commands in the dashboard with no code. +* **Send and edit messages anywhere.** Rich message editor for any channel, any time. +* **AI features.** AI chat channels, AI-generated images, and other LLM-backed modules - configured from the dashboard, + no API keys to manage. +* **Human-readable error reports.** When something breaks, you see what and why - in your language - in your dashboard. +* **More modules.** Several modules (anti-nuke, applications, giveaways, advanced logging, RSS / Twitter / YouTube / + TikTok integrations, and more) are exclusive to the hosted version. +* **Translated UI.** Dashboard and bot fully translated into 20+ languages. -Please read the [license](LICENSE) before using this bot. +**[Get started at scnx.xyz](https://scnx.xyz)** - the free plan covers most communities; upgrade only if you outgrow +it. -In short: +### Running professional customer support on Discord? -* **Disclose source** - your source code must be made available when using this bot -* **State changes** - every change to the source code must be documented and published +The OSS `tickets` module here is fine for small communities, but if you're running **paid customer support** through +Discord - ticket assignments, estimated wait times, voice support, escalation, analytics - SCNX ships a **separate, +dedicated support bot** with a feature set built for that use case. It's a different (paid) product, not a module of +this +bot. See [scnx.xyz](https://scnx.xyz/support-bot) for details. -Please read the full [license](LICENSE). This is not legal advice. For information on how this aligns with the -closed-source SCNX version, see [this issue](https://github.com/SCNetwork/CustomDCBot/issues/13). +> **Heads up on support.** Our customer support team only handles the SCNX hosted version. Tickets, live help, and +> account assistance all go through scnx.app. The OSS version in this repo is community-supported - GitHub issues +> only, best-effort, no SLA. + +## Why two versions? + +You might wonder why the same bot exists in two flavors. Short version: + +* **OSS (this repo).** The base bot, modular core, and a curated set of modules that work standalone. Good for + self-hosters, learners, and people who want to fork. Released under [BUSL-1.1](#license---read-before-using). +* **SCNX (hosted).** Everything in OSS, plus the dashboard, several integration modules (AI features, anti-nuke, + giveaways, the social-platform notifiers, etc.), the visual editors, the customer support, and the managed + infrastructure. + +We split it this way because the dashboard, infra, and several modules cost money to build and run. Giving them away +for free would mean we couldn't pay the people who build them. Keeping a strong OSS core (with a non-compete clause via +BUSL) lets us be transparent about how the bot works, accept community contributions, and let advanced users self-host + +- without subsidizing competitors who would just rebrand and resell our work. + +If you want the rebrand-and-resell freedom that an MIT/Apache license would give you, the [LICENSE](LICENSE) explains +how to negotiate commercial terms. For everyone else, the [Additional Use Grant](LICENSE) is broad enough to cover +the use cases people actually have: run the bot in your own community, modify it, contribute back, learn from it. + +## License - read before using + +> **This is NOT MIT, Apache, or any other permissive license.** It is the +> [Business Source License 1.1](LICENSE) (BUSL-1.1, the same license used by MariaDB, CockroachDB, and Sentry). Read +> the full [LICENSE](LICENSE) before deploying anything based on this code. + +### What you can do + +* **Self-host for your own community.** Run this bot on your own server, in your own Discord guild, for your own + members. No fees, no permission needed. +* **Modify and contribute.** Fork the repo, change the code, build your own modules. Pull requests welcome. +* **Learn from it.** Read the source, copy patterns into unrelated projects, write tutorials. + +### What you CANNOT do + +* **You may NOT offer this bot - or any modified version, fork, or derivative - as a hosted, managed, or embedded + service to third parties in a way that competes with [scnx.app](https://scnx.app).** That includes selling, + reselling, "free with ads," white-labeling, or running it for paying customers. This is the explicit "Additional Use + Grant" carve-out in the license, and we enforce it. +* **You may NOT relicense or sublicense this code** under MIT, Apache, GPL, or anything else. The license travels with + the code. + +### What you MUST do if you publish modifications + +* **Publish your source.** Any modified or derivative work distributed to third parties must be made available under + this same license. +* **Document your changes.** State what you changed and when. +* **Carry the license.** Display BUSL-1.1 prominently on every copy or fork. + +### When does it become MIT? + +The license auto-converts to **MIT License** eight years after a given version is published (or four years after +its first public distribution, whichever comes first). After that date, that specific version is fully permissive. +Newer versions stay BUSL until their own clock runs out. + +### Need a commercial license? + +If your intended use isn't covered by the Additional Use Grant - for example, you want to host this bot for paying +customers - email **oss@scootkit.net** to negotiate commercial terms. Operating outside the license without an +agreement is a violation and we will pursue it. + +This summary is not legal advice. The [LICENSE](LICENSE) file is the authoritative document. ## Support development -Our business model is hosting these bots for servers. Feel free -to [contribute](.github/CONTRIBUTING.md), [donate on Patreon](https://patreon.com/scnetwork), or -on [any other platform](https://github.com/SCNetwork/CustomDCBot?sponsor=1). +Development of this bot is funded by [SCNX](https://scnx.xyz) - our hosted version. Every paid SCNX customer keeps the +OSS core moving forward. If you want to help in other ways, [contribute code](.github/CONTRIBUTING.md): bug fixes, new +modules, and documentation improvements are all welcome via pull request. ## Installation @@ -205,8 +283,10 @@ adding fields to existing models. Use `localize(module, key, replacements)` from `src/functions/localize.js` for non-user-editable strings. Translations happen on [Weblate](https://localize.sc-network.net/projects/custombot/locales/). -For user-editable strings (config fields), provide values in multiple languages using the `{ "en": "...", "de": "..." }` -pattern - the bot and dashboard select the correct one automatically. +For user-editable strings in config files (`humanName`, `description`, defaults), use **plain English strings**. +Translations live separately in `config-localizations/.json` and are extracted by a script - see +[developer-docs/config-localization.md](developer-docs/config-localization.md). The deprecated `{ "en": "...", "de": +"..." }` inline format is rejected by `npm run verify-configs`. ### Helper functions diff --git a/config-generator/config.json b/config-generator/config.json index bfc6f280..9b46816d 100644 --- a/config-generator/config.json +++ b/config-generator/config.json @@ -11,31 +11,6 @@ "hidden": true, "type": "string" }, - { - "name": "dmAbuseButton", - "humanName": {}, - "default": false, - "description": "Used to allow mass dm reporting", - "hidden": true, - "type": "boolean" - }, - { - "name": "scnxToken", - "humanName": {}, - "default": "yourtokengoeshere", - "description": "Replace this with your token", - "hidden": true, - "type": "string" - }, - { - "name": "scnxHostOverwirde", - "humanName": {}, - "default": null, - "description": "Replace this with your token", - "hidden": true, - "type": "string", - "allowNull": true - }, { "name": "prefix", "humanName": "Prefix of your bot", @@ -71,7 +46,7 @@ { "name": "user_presence", "humanName": "Bot-Status", - "default": "Change this in your Bot-Configuration on scnx.app: https://scootk.it/change-status", + "default": "your bot status", "description": "This will show up in Discord as \"Playing \"", "type": "string" }, @@ -113,13 +88,6 @@ "description": "Allows @everyone and @here pings for messages configurable in the dashboard", "type": "boolean" }, - { - "name": "disableFileArchival", - "humanName": "Disable attachment archival", - "default": false, - "description": "When archival is enabled (recommended), the bot uploads Discord attachments (images, videos, files) to your scnx CDN so the starboard, moderation evidence, deleted-message logs and anti-nuke sticker restores keep working after Discord's signed URLs expire (usually within hours). Disabling this stops all uploads to scnx — no data leaves the bot for archival — but those features will silently break once Discord's short-lived URLs expire. Archived files count against your guild's file-storage quota (view current usage: https://scnx.app/glink?page=images). Free-plan guilds may run out of space and need to upgrade or delete files to keep archival working.", - "type": "boolean" - }, { "name": "syncCommandGlobally", "humanName": "Sync module commands as global commands", @@ -128,4 +96,4 @@ "type": "boolean" } ] -} \ No newline at end of file +} diff --git a/developer-docs/configuration.md b/developer-docs/configuration.md index a0f93487..bf00cbb3 100644 --- a/developer-docs/configuration.md +++ b/developer-docs/configuration.md @@ -75,7 +75,6 @@ Each entry in the `content` array defines one configuration field: | `minValue` | `integer`, `float` | Minimum allowed numeric value. | | `maxLength` | `array`, `string` | Maximum number of items (array) or characters (string). | | `disableKeyEdits` | `keyed` | Prevent users from adding/removing keys; only existing values are editable. | -| `pro` | All types | Mark the field as paid-tier only (SCNX dashboard hides/disables for free plans). | | `optional` | `string` | Field can be skipped without being explicitly null. | | `links` | All types | Help links shown next to the field. Format: `[{"label": "...", "url": "..."}]`. | | `hidden` | All types | Hide the field from the dashboard UI. The value is still loaded - useful for migration shims. | diff --git a/modules/tickets/config.json b/modules/tickets/config.json index 3c995c4f..d10e46a1 100644 --- a/modules/tickets/config.json +++ b/modules/tickets/config.json @@ -87,7 +87,6 @@ { "name": "creation-message", "humanName": "Ticket-Created Message", - "pro": true, "type": "string", "allowEmbed": true, "description": "This message will get sent in new tickets. The close buttons will be added.", @@ -140,16 +139,14 @@ "humanName": "Ticket create button", "default": "Create ticket 🎫", "description": "Button for creating a ticket", - "type": "string", - "pro": true + "type": "string" }, { "name": "ticket-close-button", "humanName": "Ticket close button", "default": "❎ Close ticket", "description": "Button for closing a ticket", - "type": "string", - "pro": true + "type": "string" } ] -} \ No newline at end of file +} From b6020d77b4ba9638cc57e0ce192ff17faa8e1ec8 Mon Sep 17 00:00:00 2001 From: Simon Date: Fri, 24 Apr 2026 12:40:46 +0200 Subject: [PATCH 27/27] synced to closed source version --- README.md | 66 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index f321114d..8aad868e 100644 --- a/README.md +++ b/README.md @@ -135,29 +135,6 @@ instance). The open-source version does not contact SCNX or share any data. * **Custom modules** - add your own modules with commands, events, and database models * **Auto-generated configs** - every config field has a description and default value -### Modules - -The bot ships with 30+ modules including: - -| Module | Description | -|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------| -| **Moderation** | Auto-mod (bad words, invite blocking with smart resolution, scam links), lockdown with configurable notification channels, warnings, quarantine | -| **Levels** | XP system with role rewards, leaderboard, multipliers per role/channel, custom formulas | -| **Birthdays** | Birthday tracking with admin management (`/manage-birthday`), lock/unlock, auto-announcements | -| **Tickets** | Multi-category ticket system with transcripts | -| **Giveaways** | Giveaway creation and management | -| **Activity Streaks** | Daily/weekly/monthly streak tracking with nickname display, milestone roles, leaderboard, hide option, staff-managed or automatic mode | -| **Guess the Number** | Number guessing game with leaderboard and player statistics | -| **Welcome/Leave** | Customizable welcome and leave messages | -| **Logging** | Audit log forwarding to Discord channels | -| **Auto-React** | Automatic reactions per channel, role, or user | -| **Temp Channels** | Temporary voice channels | -| **RSS Notifications** | RSS feed monitoring with notifications | -| **Status Roles** | Roles based on user presence/status | -| **Applications** | Application/form system with approval workflow | -| **Economy** | Virtual currency with shop system | -| **And more** | Team list, team goals, polls, partner list, invite tracking, starboard, live messages, etc. | - ## Configuration All configuration files live in your `config` folder. Each enabled module gets its own subfolder with config files. @@ -171,13 +148,38 @@ For full details on writing config files, see [developer-docs/configuration.md]( ## Developer Documentation -Detailed guides for module developers: +Full guides live in [developer-docs/](developer-docs/) (start with the [index](developer-docs/README.md)). The +short version: + +**Module authors - start here:** + +| Document | Covers | +|--------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------| +| [Writing a module](developer-docs/writing-a-module.md) | File layout, `module.json`, lifecycle, end-to-end example | +| [Events](developer-docs/events.md) | Handler shape, `botReadyAt` / `allowPartial` / `ignoreBotReadyCheck` gates, custom `botReady` / `configReload` events | +| [Slash commands](developer-docs/commands.md) | `config` / `run` / `subcommands` / `autocomplete`, options, permissions, deferring | +| [Database models](developer-docs/database-models.md) | Sequelize `Model.init` pattern, conventions, `sequelize.sync()` behavior, associations | +| [Localization](developer-docs/localization.md) | Adding strings to `locales/en.json`, using `localize()`, runtime fallback | + +**Configuration schema:** + +| Document | Covers | +|---------------------------------------------------------------|-----------------------------------------------------------------------------------| +| [Configuration files](developer-docs/configuration.md) | Schema reference: field types, defaults, `dependsOn`, `elementToggle`, validation | +| [Country localization](developer-docs/config-localization.md) | How user-facing config strings are extracted and translated | -| Document | Description | -|--------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------| -| [Configuration](developer-docs/configuration.md) | How to write `config.json` files - field types, categories, conditional fields, parameters, config elements | -| [Migrations](developer-docs/migration.md) | How to write safe database migrations - the `DatabaseSchemeVersion` pattern, shutdown protection, multi-version migrations | -| [Config Localization](developer-docs/config-localization.md) | How config translations work - external localization files, what gets localized, extraction script | +**Operations:** + +| Document | Covers | +|------------------------------------------|--------------------------------------| +| [Migration](developer-docs/migration.md) | Upgrading between major bot versions | + +**Message schemas** (canonical reference at docs.scnx.xyz): + +* [V2 schema](https://docs.scnx.xyz/docs/scnx-api/reference/message-schema-v2/) - legacy, parsed when `_schema` is + absent +* [V3 schema](https://docs.scnx.xyz/docs/scnx-api/reference/message-schema-v3/) - tag with `"_schema": "v3"` +* [V4 schema](https://docs.scnx.xyz/docs/scnx-api/reference/message-schema-v4/) - tag with `"_schema": "v4"` ## Creating modules @@ -291,4 +293,8 @@ Translations live separately in `config-localizations/.json` and are extra ### Helper functions Check `src/functions/helpers.js` for utilities: `embedType()`, `formatDiscordUserName()`, `parseEmbedColor()`, -`formatDate()`, `truncate()`, and more. \ No newline at end of file +`formatDate()`, `truncate()`, and more. + +--- + +Copyright © 2026 ScootKit UG (haftungsbeschränkt). [BUSL-1.1](LICENSE) applies. \ No newline at end of file