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 index 320d9b3e..79aa22d8 100644 --- a/locales/de.json +++ b/locales/de.json @@ -256,7 +256,28 @@ "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" + "granted-rewards-audit-log": "Rollen aktualisiert um sicherzustellen, dass der/die Nutzer:in die benötigten Rollen hat", + "rewards-command-description": "Level-Belohnungen verwalten", + "rewards-add-description": "Rollen zu einer Level-Belohnung hinzufügen", + "rewards-set-description": "Rollen für eine Level-Belohnung setzen", + "rewards-remove-description": "Eine Rolle aus einer Level-Belohnung entfernen", + "rewards-clear-description": "Alle Belohnungen für ein Level entfernen", + "rewards-list-description": "Konfigurierte Level-Belohnungen anzeigen", + "rewards-level-description": "Level zum Konfigurieren", + "rewards-role-description": "Rolle, die vergeben wird", + "rewards-replace-description": "Ersetzt vorherige ersetzbare Belohnungen", + "rewards-replace-on": "ersetzbar", + "rewards-replace-off": "behalten", + "rewards-none": "keine", + "rewards-added": "Level %l Belohnungen: %roles (%replace)", + "rewards-set": "Level %l Belohnungen gesetzt: %roles (%replace)", + "rewards-removed": "%role aus Level %l Belohnungen entfernt", + "rewards-cleared": "Belohnungen für Level %l entfernt", + "rewards-level-not-found": "Keine Belohnungen für Level %l konfiguriert", + "rewards-list-empty": "Noch keine Level-Belohnungen konfiguriert", + "rewards-list-one": "Level %l: %roles (%replace)", + "rewards-list-line": "Level %l: %roles (%replace)", + "rewards-commands-disabled": "Belohnungs-Befehle sind in der Konfiguration deaktiviert." }, "partner-list": { "could-not-give-role": "%u konnte keine Rolle gegeben werden", @@ -505,6 +526,7 @@ "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)", + "moderate-only-target-description": "Nur auf den ausgewaehlten Account anwenden (nicht auf verknuepfte Accounts spiegeln)", "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", @@ -526,6 +548,8 @@ "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", + "moderate-clear-punishments-command-description": "Alle Moderationsaktionen eines Nutzers loeschen", + "moderate-clear-punishments-confirm-description": "Gib CONFIRM ein, um fortzufahren", "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", @@ -578,10 +602,57 @@ "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", + "action-reason-line": "> Grund: %r", + "action-by-line": "> Von: %u", + "action-at-line": "> Am: %t", + "action-expires-line": "> Laeuft ab am: %d", + "action-automod-line": "> AutoMod: %a", "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.", + "clear-punishments-disabled": "Strafen loeschen ist in der Konfiguration deaktiviert.", + "clear-punishments-done": "%n Aktionen fuer %u geloescht.", + "clear-punishments-confirm-required": "Bitte CONFIRM eingeben, um den Befehl auszufuehren.", + "clear-punishments-reason": "Alle Strafen geloescht", + "automod-log-line": "%d %a %r", + "moderate-actions-show-notes": "Notizen in der Akte anzeigen", + "actions-channel-not-allowed": "Dieser Befehl ist auf bestimmte Kanaele beschraenkt.", + "dossier-subtitle": "**Dies ist die Akte von %m**", + "dossier-joined": "**Gejoint:** %d", + "dossier-created": "**Account alter:** %d", + "dossier-counts": "%b **ban** %q **quarantaene** %m **mute** %w **warn**", + "dossier-separator": "----------------", + "dossier-notes-title": "**Notizen**", + "dossier-notes-empty": "Keine Notizen vorhanden.", + "dossier-linked-title": "**Verlinkte Zweitaccounts**", + "dossier-actions-title": "**Sanktionen:**", + "dossier-note-alt-inline": "**Alt-Acc %u**", + "dossier-action-alt-prefix": "-# Alt-Acc %u:", + "action-alt-line": "> -# Alt-Acc %u", + "dossier-note-line": "**#%i: %t von %author:**\n> %c%altInfo", + "action-header": "**#%i: %t**", + "action-block": "%a", + "linked-accounts-command-description": "Verknuepfte Accounts verwalten", + "linked-accounts-link-description": "Einen oder mehrere Accounts mit einem Hauptaccount verknuepfen (bis zu 5 pro Befehl)", + "linked-accounts-unlink-description": "Einen Account entkoppeln", + "linked-accounts-clear-description": "Alle Verknuepfungen fuer einen Hauptaccount entfernen", + "linked-accounts-list-description": "Verknuepfte Accounts fuer einen Nutzer anzeigen", + "linked-accounts-main-description": "Hauptaccount", + "linked-accounts-account-description": "Verknuepfter Account", + "linked-accounts-user-description": "Nutzer zum Anzeigen/Entkoppeln", + "linked-accounts-disabled": "Verknuepfte Accounts sind in der Konfiguration deaktiviert.", + "linked-accounts-no-accounts": "Bitte mindestens einen Account zum Verknuepfen angeben.", + "linked-accounts-linked": "Hauptaccount %m verknuepft mit: %a", + "linked-accounts-unlinked": "%u wurde entkoppelt", + "linked-accounts-cleared": "Verknuepfung fuer %m entfernt", + "linked-accounts-none-for-user": "Keine verknuepften Accounts fuer %u gefunden", + "linked-accounts-list": "Hauptaccount: %m | Verknuepft: %a", + "linked-accounts-log-field": "Verknuepfte Accounts", + "automod-log-field": "AutoMod Aktionen", + "linked-accounts-single-reason": "Verknuepft mit Hauptaccount %m", + "linked-accounts-none": "keine", + "unknown": "Unbekannt", "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", diff --git a/locales/en.json b/locales/en.json index 4cf4437e..0d9cb0ab 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,13 +74,15 @@ "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 ):", - "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." + "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", @@ -87,11 +90,16 @@ "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", - "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 +120,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 +186,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 +206,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,45 +240,44 @@ "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" + "granted-rewards-audit-log": "Updated roles to make sure, they have the level role they need", + "rewards-command-description": "Manage level reward roles", + "rewards-add-description": "Add roles to a level reward", + "rewards-set-description": "Set roles for a level reward", + "rewards-remove-description": "Remove a role from a level reward", + "rewards-clear-description": "Remove all rewards for a level", + "rewards-list-description": "List configured level rewards", + "rewards-level-description": "Level to configure", + "rewards-role-description": "Role to grant", + "rewards-replace-description": "Replace previous replaceable rewards", + "rewards-replace-on": "replaceable", + "rewards-replace-off": "kept", + "rewards-none": "none", + "rewards-added": "Level %l rewards: %roles (%replace)", + "rewards-set": "Level %l rewards set to: %roles (%replace)", + "rewards-removed": "Removed %role from level %l rewards", + "rewards-cleared": "Cleared rewards for level %l", + "rewards-level-not-found": "No rewards configured for level %l", + "rewards-list-empty": "No level rewards configured yet", + "rewards-list-one": "Level %l: %roles (%replace)", + "rewards-list-line": "Level %l: %roles (%replace)", + "rewards-commands-disabled": "Reward commands are disabled in the configuration." }, "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 +309,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 +338,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 +383,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,37 +441,9 @@ "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.", - "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", @@ -491,7 +455,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" }, @@ -560,6 +524,7 @@ "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)", + "moderate-only-target-description": "Apply only to the selected account (do not mirror to linked accounts)", "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", @@ -575,6 +540,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", @@ -583,6 +570,8 @@ "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", + "moderate-clear-punishments-command-description": "Clear all moderation actions for a user", + "moderate-clear-punishments-confirm-description": "Type CONFIRM to proceed", "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", @@ -594,8 +583,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 +602,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)", @@ -638,10 +627,57 @@ "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", + "action-reason-line": "> Reason: %r", + "action-by-line": "> By: %u", + "action-at-line": "> At: %t", + "action-expires-line": "> Expires: %d", + "action-automod-line": "> AutoMod: %a", "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.", + "clear-punishments-disabled": "Clear punishments is disabled in the configuration.", + "clear-punishments-done": "Cleared %n actions for %u.", + "clear-punishments-confirm-required": "Please type CONFIRM to run this command.", + "clear-punishments-reason": "Cleared all punishments", + "automod-log-line": "%d %a %r", + "moderate-actions-show-notes": "Show notes in the dossier", + "actions-channel-not-allowed": "This command is restricted to specific channels.", + "dossier-subtitle": "**This is the dossier of %m**", + "dossier-joined": "**Joined:** %d", + "dossier-created": "**Account age:** %d", + "dossier-counts": "%b **ban** %q **quarantine** %m **mute** %w **warn**", + "dossier-separator": "----------------", + "dossier-notes-title": "**Notes**", + "dossier-notes-empty": "No notes available.", + "dossier-linked-title": "**Linked accounts**", + "dossier-actions-title": "**Sanctions:**", + "dossier-note-alt-inline": "**alt account %u**", + "dossier-action-alt-prefix": "-# Alt acc %u:", + "action-alt-line": "> -# Alt acc %u", + "dossier-note-line": "**#%i: %t from %author:**\n> %c%altInfo", + "action-header": "**#%i: %t**", + "action-block": "%a", + "linked-accounts-command-description": "Manage linked accounts", + "linked-accounts-link-description": "Link one or more accounts to a main account (up to 5 per command)", + "linked-accounts-unlink-description": "Unlink a single account", + "linked-accounts-clear-description": "Clear all links for a main account", + "linked-accounts-list-description": "Show linked accounts for a user", + "linked-accounts-main-description": "Main account", + "linked-accounts-account-description": "Linked account", + "linked-accounts-user-description": "User to check/unlink", + "linked-accounts-disabled": "Linked accounts are disabled in the configuration.", + "linked-accounts-no-accounts": "Please provide at least one account to link.", + "linked-accounts-linked": "Linked main %m with: %a", + "linked-accounts-unlinked": "Unlinked %u", + "linked-accounts-cleared": "Cleared linked accounts for %m", + "linked-accounts-none-for-user": "No linked accounts found for %u", + "linked-accounts-list": "Main: %m | Linked: %a", + "linked-accounts-log-field": "Linked accounts", + "automod-log-field": "AutoMod actions", + "linked-accounts-single-reason": "Linked to main account %m", + "linked-accounts-none": "none", + "unknown": "Unknown", "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", @@ -678,7 +714,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 +744,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 +752,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 +763,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 +781,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 +816,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", @@ -865,16 +845,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", @@ -891,7 +874,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", @@ -934,7 +918,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 +938,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 +964,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.", @@ -1017,5 +1001,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-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/main.js b/main.js index 9ca9b0e8..6191a4a7 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, GatewayIntentBits.AutoModerationExecution] }); client.intervals = []; client.jobs = []; @@ -10,6 +20,7 @@ const fs = require('fs'); const {Sequelize} = require('sequelize'); const log4js = require('log4js'); const jsonfile = require('jsonfile'); +const centra = require('centra'); const readline = require('readline'); // Parsing parameters @@ -27,6 +38,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 +51,8 @@ log4js.configure({ level: 'debug' }, output: { - type: 'stdout', layout: { + type: 'stdout', + layout: { type: 'pattern', pattern: '[%p] %m' } @@ -50,14 +63,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 +85,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 +100,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,40 +120,48 @@ 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) => { - 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 ((await client.application.fetch()).botRequireCodeGrant) { + 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, { type: 'CORE_ISSUE', errorDescription: 'require_code_grant_active', @@ -139,6 +169,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 +191,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 +199,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 +219,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) { @@ -231,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; }); /** @@ -247,14 +293,81 @@ 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); + 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') { + 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 +377,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 +428,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 +468,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 +493,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 +502,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 +544,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 +556,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 +590,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,10 +613,12 @@ 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, options: props.config.options || [], + disabled: props.config.disabled || null, subcommands: props.subcommands, beforeSubcommand: props.beforeSubcommand, run: props.run, 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 deleted file mode 100644 index 01d454c6..00000000 --- a/modules/birthday/birthday.js +++ /dev/null @@ -1,197 +0,0 @@ -/** - * Manages the birthday-embed - * @module Birthdays - * @author Simon Csaba - */ -const {embedType, disableModule, truncate, embedTypeV2, formatDiscordUserName} = 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); - await channel.guild.members.fetch({force: true}); - - if (notifyUsers && !moduleConf.notificationChannelOverwriteID) { - for (const m of messages.filter(msg => msg.id !== messages.last().id)) { - if (m.deletable) await m.delete(); // Removing old messages - } - } - - const embeds = [ - new MessageEmbed() - .setTitle(moduleConf['birthdayEmbed']['title']) - .setDescription(moduleConf['birthdayEmbed']['description']) - .setColor(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 = await channel.guild.members.fetch(); - 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://sc-network.net/age?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 bea02177..00000000 --- a/modules/birthday/commands/birthday.js +++ /dev/null @@ -1,127 +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') - }); - 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})) : '' - }) - }); - - }, - '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 b2a4bec2..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": "Giveaway-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": "Giveaway-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 [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/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..7dd5e64e 100644 --- a/modules/economy-system/commands/shop.js +++ b/modules/economy-system/commands/shop.js @@ -1,24 +1,22 @@ -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 {boolean} Result + * @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({ + if (!result) await interaction.reply({ content: interaction.client.strings['not_enough_permissions'], ephemeral: !interaction.client.configurations['economy-system']['config']['publicCommandReplies'] }); - } return result; } 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); @@ -34,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); } }; @@ -119,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/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..f4ff4528 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", @@ -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": { @@ -506,7 +534,7 @@ "content": "string", "params": [ { - "name": "erned", + "name": "earned", "description": {} } ] @@ -683,4 +711,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..0167b685 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 @@ -179,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]: [ @@ -240,16 +253,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 +270,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 +336,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 +346,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 @@ -380,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(); @@ -393,7 +494,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 +523,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 +581,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}); - - if (model.length !== 0) embed.addFields({name: 'Leaderboard:', value: await topTen(model, client)}); + .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 ((moduleStr['leaderboardEmbed']['thumbnail'] || '').replaceAll(' ', '')) embed.setThumbnail(moduleStr['leaderboardEmbed']['thumbnail']); if ((moduleStr['leaderboardEmbed']['image'] || '').replaceAll(' ', '')) embed.setImage(moduleStr['leaderboardEmbed']['image']); @@ -495,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 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..a7b7f4f7 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('❌'); @@ -30,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, @@ -38,4 +43,5 @@ module.exports.run = async (client, msg) => { '%guessCount%': game.guessCount, '%number%': game.number })); + 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 new file mode 100644 index 00000000..748a2a53 --- /dev/null +++ b/modules/guess-the-number/guessTheNumber.js @@ -0,0 +1,39 @@ +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(); + + 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/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..1c063dd7 100644 --- a/modules/info-commands/commands/info.js +++ b/modules/info-commands/commands/info.js @@ -3,20 +3,42 @@ 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 +58,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 +68,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 +104,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 +116,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 +144,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 +169,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,31 +188,15 @@ 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']) { - 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}, `; @@ -199,9 +205,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..ca127ffd 100644 --- a/modules/levels/commands/manage-levels.js +++ b/modules/levels/commands/manage-levels.js @@ -1,8 +1,78 @@ +const fs = require('fs'); +const path = require('path'); +const jsonfile = require('jsonfile'); const {registerNeededEdit} = require('../leaderboardChannel'); const {localize} = require('../../../src/functions/localize'); const {formatDiscordUserName} = require('../../../src/functions/helpers'); +const {calculateLevelXP, displayLevel} = require('../events/messageCreate'); +const {reloadConfig} = require('../../../src/functions/configuration'); +const {getReplaceableRewardRoleIds} = require('../rewards'); + +function rewardsCommandsEnabled(client) { + const config = client.configurations?.levels?.config || {}; + return config.enableRewardCommands !== false; +} + +function getRewardsConfigPath(client) { + return path.join(client.configDir, 'levels', 'reward-roles.json'); +} + +function ensureRewardsDir(client) { + const dir = path.join(client.configDir, 'levels'); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, {recursive: true}); +} + +function readRewards(client) { + const filePath = getRewardsConfigPath(client); + try { + const data = jsonfile.readFileSync(filePath); + return Array.isArray(data) ? data : []; + } catch { + return []; + } +} + +function writeRewards(client, rewards) { + ensureRewardsDir(client); + jsonfile.writeFileSync(getRewardsConfigPath(client), rewards, {spaces: 2}); +} + +function collectRoles(interaction) { + const roles = [ + interaction.options.getRole('role', true), + interaction.options.getRole('role2'), + interaction.options.getRole('role3'), + interaction.options.getRole('role4'), + interaction.options.getRole('role5') + ].filter(Boolean).map(r => r.id); + return [...new Set(roles)]; +} + +function formatRoles(roleIds) { + if (!roleIds || roleIds.length === 0) return localize('levels', 'rewards-none'); + return roleIds.map(id => `<@&${id}>`).join(', '); +} + +function findEntry(rewards, level) { + return rewards.find(r => parseInt(r.level) === level); +} + +async function saveAndReload(interaction, rewards) { + writeRewards(interaction.client, rewards); + await reloadConfig(interaction.client); +} + +function ensureRewardsCommandsEnabled(interaction) { + if (rewardsCommandsEnabled(interaction.client)) return true; + interaction.reply({ephemeral: true, content: localize('levels', 'rewards-commands-disabled')}); + return false; +} 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,28 +87,20 @@ 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); + async function runXPCheck() { + 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()]); - } - runXPCheck(); + await fixLevelRoles(interaction, member, user.level); + await runXPCheck(); } } - runXPCheck(); + await runXPCheck(); await user.save(); @@ -54,8 +116,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 +125,87 @@ async function runXPAction(interaction, newXP) { }); } +async function fixLevelRoles(interaction, member, level) { + const moduleConfig = interaction.client.configurations['levels']['config']; + const adjustedLevel = level - (moduleConfig.startFromZero ? 1 : 0); + if (adjustedLevel < 0) return; + + const rewardEntries = Array.isArray(interaction.client.configurations?.levels?.['reward-roles']) + ? interaction.client.configurations.levels['reward-roles'] + : []; + if (rewardEntries.length > 0) { + const sorted = rewardEntries + .slice() + .sort((a, b) => parseInt(a.level) - parseInt(b.level)); + for (const entry of sorted) { + const entryLevel = parseInt(entry.level); + if (!Number.isFinite(entryLevel) || entryLevel > adjustedLevel) continue; + const roles = Array.isArray(entry.roles) ? entry.roles.filter(Boolean) : []; + if (roles.length === 0) continue; + if (entry.replacePrevious) { + for (const roleId of getReplaceableRewardRoleIds(interaction.client)) { + if (member.roles.cache.has(roleId)) { + await member.roles.remove(roleId, '[levels] ' + localize('levels', 'granted-rewards-audit-log')).catch(); + } + } + } + await member.roles.add(roles, '[levels] ' + localize('levels', 'granted-rewards-audit-log')).catch(); + } + return; + } + + let highest = null; + for (const key in moduleConfig.reward_roles) { + const role = moduleConfig.reward_roles[key]; + if (parseInt(key) <= adjustedLevel) { + if (highest && highest < parseInt(key) && moduleConfig.onlyTopLevelRole) { + await member.roles.remove(moduleConfig.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 }) @@ -115,6 +213,128 @@ async function runLevelAction(interaction, newLevel) { } module.exports.subcommands = { + 'rewards': { + 'add': async function (interaction) { + if (!ensureRewardsCommandsEnabled(interaction)) return; + const level = interaction.options.getInteger('level', true); + const roles = collectRoles(interaction); + const replacePrevious = interaction.options.getBoolean('replaceprevious'); + + const rewards = readRewards(interaction.client); + let entry = findEntry(rewards, level); + if (!entry) { + entry = {level, roles: [], replacePrevious: false}; + rewards.push(entry); + } + entry.roles = [...new Set([...(entry.roles || []), ...roles])]; + if (typeof replacePrevious === 'boolean') entry.replacePrevious = replacePrevious; + + await saveAndReload(interaction, rewards); + return interaction.reply({ + ephemeral: true, + content: localize('levels', 'rewards-added', { + l: level, + roles: formatRoles(entry.roles), + replace: entry.replacePrevious ? localize('levels', 'rewards-replace-on') : localize('levels', 'rewards-replace-off') + }) + }); + }, + 'set': async function (interaction) { + if (!ensureRewardsCommandsEnabled(interaction)) return; + const level = interaction.options.getInteger('level', true); + const roles = collectRoles(interaction); + const replacePrevious = interaction.options.getBoolean('replaceprevious'); + + const rewards = readRewards(interaction.client); + let entry = findEntry(rewards, level); + if (!entry) { + entry = {level, roles: [], replacePrevious: false}; + rewards.push(entry); + } + entry.roles = roles; + if (typeof replacePrevious === 'boolean') entry.replacePrevious = replacePrevious; + + await saveAndReload(interaction, rewards); + return interaction.reply({ + ephemeral: true, + content: localize('levels', 'rewards-set', { + l: level, + roles: formatRoles(entry.roles), + replace: entry.replacePrevious ? localize('levels', 'rewards-replace-on') : localize('levels', 'rewards-replace-off') + }) + }); + }, + 'remove': async function (interaction) { + if (!ensureRewardsCommandsEnabled(interaction)) return; + const level = interaction.options.getInteger('level', true); + const role = interaction.options.getRole('role', true); + + const rewards = readRewards(interaction.client); + const entry = findEntry(rewards, level); + if (!entry) { + return interaction.reply({ephemeral: true, content: localize('levels', 'rewards-level-not-found', {l: level})}); + } + entry.roles = (entry.roles || []).filter(r => r !== role.id); + if (entry.roles.length === 0) { + const idx = rewards.indexOf(entry); + if (idx >= 0) rewards.splice(idx, 1); + } + + await saveAndReload(interaction, rewards); + return interaction.reply({ + ephemeral: true, + content: localize('levels', 'rewards-removed', { + l: level, + role: role.toString() + }) + }); + }, + 'clear': async function (interaction) { + if (!ensureRewardsCommandsEnabled(interaction)) return; + const level = interaction.options.getInteger('level', true); + const rewards = readRewards(interaction.client); + const before = rewards.length; + const filtered = rewards.filter(r => parseInt(r.level) !== level); + if (filtered.length === before) { + return interaction.reply({ephemeral: true, content: localize('levels', 'rewards-level-not-found', {l: level})}); + } + await saveAndReload(interaction, filtered); + return interaction.reply({ephemeral: true, content: localize('levels', 'rewards-cleared', {l: level})}); + }, + 'list': async function (interaction) { + if (!ensureRewardsCommandsEnabled(interaction)) return; + const level = interaction.options.getInteger('level'); + const rewards = readRewards(interaction.client); + + if (level) { + const entry = findEntry(rewards, level); + if (!entry) { + return interaction.reply({ephemeral: true, content: localize('levels', 'rewards-level-not-found', {l: level})}); + } + return interaction.reply({ + ephemeral: true, + content: localize('levels', 'rewards-list-one', { + l: level, + roles: formatRoles(entry.roles || []), + replace: entry.replacePrevious ? localize('levels', 'rewards-replace-on') : localize('levels', 'rewards-replace-off') + }) + }); + } + + if (rewards.length === 0) { + return interaction.reply({ephemeral: true, content: localize('levels', 'rewards-list-empty')}); + } + const lines = rewards + .slice() + .sort((a, b) => parseInt(a.level) - parseInt(b.level)) + .map(r => localize('levels', 'rewards-list-line', { + l: r.level, + roles: formatRoles(r.roles || []), + replace: r.replacePrevious ? localize('levels', 'rewards-replace-on') : localize('levels', 'rewards-replace-off') + })); + return interaction.reply({ephemeral: true, content: lines.join('\n')}); + } + }, 'reset-xp': async function (interaction) { const type = interaction.options.getUser('user') ? 'user' : 'server'; if (!interaction.options.getBoolean('confirm')) return interaction.reply({ @@ -142,7 +362,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(); @@ -197,7 +417,160 @@ module.exports.config = { description: localize('levels', 'edit-xp-command-description'), options: function (client) { - const array = [{ + const array = []; + if (rewardsCommandsEnabled(client)) { + array.push({ + type: 'SUB_COMMAND_GROUP', + name: 'rewards', + description: localize('levels', 'rewards-command-description'), + options: [ + { + type: 'SUB_COMMAND', + name: 'add', + description: localize('levels', 'rewards-add-description'), + options: [ + { + type: 'INTEGER', + required: true, + name: 'level', + description: localize('levels', 'rewards-level-description') + }, + { + type: 'ROLE', + required: true, + name: 'role', + description: localize('levels', 'rewards-role-description') + }, + { + type: 'ROLE', + required: false, + name: 'role2', + description: localize('levels', 'rewards-role-description') + }, + { + type: 'ROLE', + required: false, + name: 'role3', + description: localize('levels', 'rewards-role-description') + }, + { + type: 'ROLE', + required: false, + name: 'role4', + description: localize('levels', 'rewards-role-description') + }, + { + type: 'ROLE', + required: false, + name: 'role5', + description: localize('levels', 'rewards-role-description') + }, + { + type: 'BOOLEAN', + required: false, + name: 'replaceprevious', + description: localize('levels', 'rewards-replace-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'set', + description: localize('levels', 'rewards-set-description'), + options: [ + { + type: 'INTEGER', + required: true, + name: 'level', + description: localize('levels', 'rewards-level-description') + }, + { + type: 'ROLE', + required: true, + name: 'role', + description: localize('levels', 'rewards-role-description') + }, + { + type: 'ROLE', + required: false, + name: 'role2', + description: localize('levels', 'rewards-role-description') + }, + { + type: 'ROLE', + required: false, + name: 'role3', + description: localize('levels', 'rewards-role-description') + }, + { + type: 'ROLE', + required: false, + name: 'role4', + description: localize('levels', 'rewards-role-description') + }, + { + type: 'ROLE', + required: false, + name: 'role5', + description: localize('levels', 'rewards-role-description') + }, + { + type: 'BOOLEAN', + required: false, + name: 'replaceprevious', + description: localize('levels', 'rewards-replace-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'remove', + description: localize('levels', 'rewards-remove-description'), + options: [ + { + type: 'INTEGER', + required: true, + name: 'level', + description: localize('levels', 'rewards-level-description') + }, + { + type: 'ROLE', + required: true, + name: 'role', + description: localize('levels', 'rewards-role-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'clear', + description: localize('levels', 'rewards-clear-description'), + options: [ + { + type: 'INTEGER', + required: true, + name: 'level', + description: localize('levels', 'rewards-level-description') + } + ] + }, + { + type: 'SUB_COMMAND', + name: 'list', + description: localize('levels', 'rewards-list-description'), + options: [ + { + type: 'INTEGER', + required: false, + name: 'level', + description: localize('levels', 'rewards-level-description') + } + ] + } + ] + }); + } + array.push({ type: 'SUB_COMMAND', name: 'reset-xp', description: localize('levels', 'reset-xp-description'), @@ -215,7 +588,7 @@ module.exports.config = { description: localize('levels', 'reset-xp-confirm-description') } ] - }]; + }); if (client.configurations['levels']['config']['allowCheats']) { array.push({ @@ -349,4 +722,4 @@ module.exports.config = { } return array; } -}; \ No newline at end of file +}; 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..da8b6dfe 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": { @@ -149,6 +269,22 @@ "value": "roleID" } }, + { + "name": "enableRewardCommands", + "humanName": { + "en": "Enable reward commands", + "de": "Belohnungs-Befehle aktivieren" + }, + "default": { + "en": true, + "de": true + }, + "description": { + "en": "If enabled, /manage-levels rewards subcommands are available to edit reward roles.", + "de": "Wenn aktiviert, sind /manage-levels rewards Unterbefehle verfuegbar, um Belohnungsrollen zu bearbeiten." + }, + "type": "boolean" + }, { "name": "multiplication_roles", "humanName": { @@ -169,6 +305,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 +365,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 +390,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/configs/reward-roles.json b/modules/levels/configs/reward-roles.json new file mode 100644 index 00000000..594ae7c3 --- /dev/null +++ b/modules/levels/configs/reward-roles.json @@ -0,0 +1,60 @@ +{ + "description": { + "en": "Configure reward roles per level", + "de": "Belohnungsrollen pro Level konfigurieren" + }, + "humanName": { + "en": "Reward roles", + "de": "Belohnungsrollen" + }, + "filename": "reward-roles.json", + "configElements": true, + "content": [ + { + "name": "level", + "humanName": { + "en": "Level", + "de": "Level" + }, + "default": { + "en": "" + }, + "description": { + "en": "Level at which the reward should be granted", + "de": "Level, bei dem die Belohnung vergeben wird" + }, + "type": "integer" + }, + { + "name": "roles", + "humanName": { + "en": "Reward roles", + "de": "Belohnungsrollen" + }, + "default": { + "en": [] + }, + "description": { + "en": "Roles that should be granted at this level", + "de": "Rollen, die bei diesem Level vergeben werden" + }, + "type": "array", + "content": "roleID" + }, + { + "name": "replacePrevious", + "humanName": { + "en": "Replace previous reward roles", + "de": "Vorherige Belohnungsrollen ersetzen" + }, + "default": { + "en": false + }, + "description": { + "en": "If enabled, previous reward roles will be removed when this reward is granted", + "de": "Wenn aktiviert, werden vorherige Belohnungsrollen entfernt" + }, + "type": "boolean" + } + ] +} 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..6fcffb49 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 {getReplaceableRewardRoleIds, getRewardForLevel} = require('../rewards'); const cooldown = new Set(); -let currentlyLevelingUp = []; +let currentlyLevelingUp = new Set(); function getMemberRoleFactor(member) { let roleFactor = 1; @@ -19,45 +59,45 @@ 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 rewardConfig = getRewardForLevel(client, calculatedLevel); + const isRewardMessage = !!rewardConfig; + 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; @@ -68,25 +108,28 @@ module.exports.run = async (client, msg) => { else if (randomMessages.length !== 0) messageToSend = randomElementFromArray(randomMessages).message; } - 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 (rewardConfig) { + if (rewardConfig.replacePrevious) { + for (const role of getReplaceableRewardRoleIds(client)) { + 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(rewardConfig.roles, '[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%': rewardConfig ? rewardConfig.roles.map(r => `<@&${r}>`).join(', ') : 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 +137,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/levels/module.json b/modules/levels/module.json index 4fbb00c6..ca46531b 100644 --- a/modules/levels/module.json +++ b/modules/levels/module.json @@ -14,6 +14,7 @@ "models-dir": "/models", "config-example-files": [ "configs/config.json", + "configs/reward-roles.json", "configs/strings.json", "configs/random-levelup-messages.json", "configs/special-levelup-messages.json" @@ -25,4 +26,4 @@ "en": "Easy to use levelsystem with a lot of customization!", "de": "Einfaches Level-System mit vielen Anpassungsmöglichkeiten!" } -} \ No newline at end of file +} diff --git a/modules/levels/rewards.js b/modules/levels/rewards.js new file mode 100644 index 00000000..714be139 --- /dev/null +++ b/modules/levels/rewards.js @@ -0,0 +1,45 @@ +function getRewardEntries(client) { + const rewardEntries = client.configurations?.levels?.['reward-roles']; + return Array.isArray(rewardEntries) ? rewardEntries : []; +} + +function getReplaceableRewardRoleIds(client) { + const moduleConfig = client.configurations['levels']['config']; + const rewardEntries = getRewardEntries(client); + const roles = new Set(); + if (rewardEntries.length !== 0) { + for (const entry of rewardEntries) { + if (!entry.replacePrevious) continue; + if (!Array.isArray(entry.roles)) continue; + for (const roleId of entry.roles) roles.add(roleId); + } + } else if (moduleConfig.reward_roles) { + for (const roleId of Object.values(moduleConfig.reward_roles)) roles.add(roleId); + } + return [...roles]; +} + +function getRewardForLevel(client, level) { + const moduleConfig = client.configurations['levels']['config']; + const rewardEntries = getRewardEntries(client); + const entry = rewardEntries.find(r => parseInt(r.level) === level); + if (entry) { + const roles = Array.isArray(entry.roles) ? entry.roles.filter(Boolean) : []; + if (roles.length === 0) return null; + return { + roles, + replacePrevious: !!entry.replacePrevious + }; + } + const legacyRole = moduleConfig.reward_roles ? moduleConfig.reward_roles[level.toString()] : null; + if (!legacyRole) return null; + return { + roles: [legacyRole], + replacePrevious: !!moduleConfig.onlyTopLevelRole + }; +} + +module.exports = { + getReplaceableRewardRoleIds, + getRewardForLevel +}; 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..2ddb557f 100644 --- a/modules/moderation/commands/moderate.js +++ b/modules/moderation/commands/moderate.js @@ -1,9 +1,15 @@ const {localize} = require('../../../src/functions/localize'); const { embedType, dateToDiscordTimestamp, lockChannel, unlockChannel, - sendMultipleSiteButtonMessage, truncate, formatDiscordUserName + sendMultipleSiteButtonMessage, + truncate, + formatDiscordUserName, + parseEmbedColor, + safeSetFooter } = require('../../../src/functions/helpers'); const {moderationAction} = require('../moderationActions'); +const {getLinkedGroup, linkAccounts, unlinkAccount, unlinkGroup} = require('../linkedAccounts'); +const {activateLockdown, liftLockdown, isLockdownActive} = require('../lockdown'); const durationParser = require('parse-duration'); const {MessageEmbed} = require('discord.js'); const {Op} = require('sequelize'); @@ -11,17 +17,26 @@ let guildBanCache; module.exports.beforeSubcommand = async function (interaction) { if (interaction.options.getUser('user')) { - interaction.memberToExecuteUpon = interaction.options.getMember('user'); + const targetUser = interaction.options.getUser('user'); + const sub = interaction.options.getSubcommand(false); + const group = interaction.options.getSubcommandGroup(false); + if (sub === 'actions' || sub === 'clear-punishments' || group === 'notes') { + interaction.memberToExecuteUpon = interaction.guild.members.cache.get(targetUser.id) || { + user: targetUser, + id: targetUser.id, + notFound: true + }; + } else 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') }); else { interaction.userNotOnServer = true; interaction.memberToExecuteUpon = { - user: interaction.options.getUser('user'), - id: interaction.options.getUser('user').id, + user: targetUser, + id: targetUser.id, notFound: true }; } @@ -65,6 +80,31 @@ async function fetchNotesUser(interaction) { return notesUser; } +function collectLinkedUsers(interaction) { + const users = [ + interaction.options.getUser('account'), + interaction.options.getUser('account2'), + interaction.options.getUser('account3'), + interaction.options.getUser('account4'), + interaction.options.getUser('account5') + ].filter(Boolean); + const unique = new Map(); + for (const user of users) unique.set(user.id, user); + return Array.from(unique.values()); +} + +function formatUserMentions(userIDs) { + if (!userIDs || userIDs.length === 0) return localize('moderation', 'linked-accounts-none'); + return userIDs.map(id => `<@${id}>`).join(' '); +} + +function formatNoteAuthor(userID, interaction) { + const user = (interaction.guild.members.cache.get(userID) || {user: {tag: userID}}).user; + let name = formatDiscordUserName(user); + if (name.startsWith('@')) name = name.slice(1); + return name; +} + module.exports.subcommands = { 'notes': { 'view': async function (interaction) { @@ -102,11 +142,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('GREEN') + .setColor(parseEmbedColor('GREEN')) .setAuthor({name: interaction.client.user.username, iconURL: interaction.client.user.avatarURL()}) .setFields(fields); + safeSetFooter(embed, interaction.client); interaction.editReply({ embeds: [embed] }); @@ -176,14 +216,98 @@ module.exports.subcommands = { }); } }, + 'accounts': { + 'link': async function (interaction) { + if (!checkRoles(interaction, 3)) return; + const config = interaction.client.configurations['moderation']['config']; + if (!config['linked_accounts_enabled']) return interaction.editReply({ + content: '⚠️ ' + localize('moderation', 'linked-accounts-disabled') + }); + const main = interaction.options.getUser('main', true); + const accounts = collectLinkedUsers(interaction); + if (accounts.length === 0) return interaction.editReply({ + content: '⚠️ ' + localize('moderation', 'linked-accounts-no-accounts') + }); + const userIDs = [main.id, ...accounts.map(a => a.id)]; + await linkAccounts(interaction.client, main.id, userIDs, interaction.user.id); + + if (config['linked_accounts_mode'] === 'single') { + const actionType = config['linked_accounts_single_action']; + if (actionType && actionType !== 'none') { + for (const account of accounts) { + if (account.id === main.id) continue; + let member = await interaction.guild.members.fetch(account.id).catch(() => null); + if (!member && actionType !== 'ban') continue; + if (!member && actionType === 'ban') member = {id: account.id, notFound: true, user: {id: account.id, tag: account.id}}; + let additionalData = {}; + if (actionType === 'quarantine' && member.roles) { + additionalData = {roles: Array.from(member.roles.cache.keys())}; + } + await moderationAction(interaction.client, actionType, interaction.member, member, localize('moderation', 'linked-accounts-single-reason', {m: formatDiscordUserName(main)}), additionalData); + } + } + } + + return interaction.editReply({ + content: localize('moderation', 'linked-accounts-linked', { + m: `<@${main.id}>`, + a: formatUserMentions(accounts.map(a => a.id)) + }) + }); + }, + 'unlink': async function (interaction) { + if (!checkRoles(interaction, 3)) return; + const config = interaction.client.configurations['moderation']['config']; + if (!config['linked_accounts_enabled']) return interaction.editReply({ + content: '⚠️ ' + localize('moderation', 'linked-accounts-disabled') + }); + const user = interaction.options.getUser('user', true); + await unlinkAccount(interaction.client, user.id); + return interaction.editReply({ + content: localize('moderation', 'linked-accounts-unlinked', {u: `<@${user.id}>`}) + }); + }, + 'clear': async function (interaction) { + if (!checkRoles(interaction, 3)) return; + const config = interaction.client.configurations['moderation']['config']; + if (!config['linked_accounts_enabled']) return interaction.editReply({ + content: '⚠️ ' + localize('moderation', 'linked-accounts-disabled') + }); + const main = interaction.options.getUser('main', true); + await unlinkGroup(interaction.client, main.id); + return interaction.editReply({ + content: localize('moderation', 'linked-accounts-cleared', {m: `<@${main.id}>`}) + }); + }, + 'list': async function (interaction) { + if (!checkRoles(interaction, 3)) return; + const config = interaction.client.configurations['moderation']['config']; + if (!config['linked_accounts_enabled']) return interaction.editReply({ + content: '⚠️ ' + localize('moderation', 'linked-accounts-disabled') + }); + const user = interaction.options.getUser('user', true); + const group = await getLinkedGroup(interaction.client, user.id); + if (!group) return interaction.editReply({ + content: localize('moderation', 'linked-accounts-none-for-user', {u: `<@${user.id}>`}) + }); + const linked = group.userIDs.filter(id => id !== user.id); + return interaction.editReply({ + content: localize('moderation', 'linked-accounts-list', { + m: `<@${group.mainID}>`, + a: formatUserMentions(linked) + }) + }); + } + }, 'ban': function (interaction) { if (interaction.replied) return; if (!interaction.userNotOnServer) if (!checkRoles(interaction, 4)) return; + const disableLinkedMirror = !!interaction.options.getBoolean('only-target') && interaction.client.configurations['moderation']['config']['linked_accounts_enabled']; const parseDuration = interaction.options.getString('duration') ? new Date(new Date().getTime() + durationParser(interaction.options.getString('duration'))) : null; if (interaction.options.getInteger('days')) if (interaction.options.getInteger('days') < 0 || interaction.options.getInteger('days') > 7) return interaction.editReply({ content: '⚠️ ' + localize('moderation', 'invalid-days') }); - moderationAction(interaction.client, 'ban', interaction.member, interaction.memberToExecuteUpon, interaction.options.getString('reason'), {days: interaction.options.getInteger('days')}, parseDuration, interaction.options.getAttachment('proof')).then(r => { + moderationAction(interaction.client, 'ban', interaction.member, interaction.memberToExecuteUpon, interaction.options.getString('reason'), {days: interaction.options.getInteger('days')}, parseDuration, interaction.options.getAttachment('proof'), {disableLinkedMirror}).then(r => { guildBanCache = null; if (r) { if (parseDuration) interaction.editReply({ @@ -203,7 +327,8 @@ module.exports.subcommands = { 'unban': async function (interaction) { if (interaction.replied) return; if (!checkRoles(interaction, 4)) return; - moderationAction(interaction.client, 'unban', interaction.member, interaction.options.getString('id'), interaction.options.getString('reason')).then(r => { + const disableLinkedMirror = !!interaction.options.getBoolean('only-target') && interaction.client.configurations['moderation']['config']['linked_accounts_enabled']; + moderationAction(interaction.client, 'unban', interaction.member, interaction.options.getString('id'), interaction.options.getString('reason'), {}, null, null, {disableLinkedMirror}).then(r => { guildBanCache = null; if (r) interaction.editReply({ content: localize('moderation', 'action-done', {i: r.actionID}) @@ -228,8 +353,11 @@ module.exports.subcommands = { 'quarantine': function (interaction) { if (interaction.replied) return; if (!checkRoles(interaction, 3)) return; + const disableLinkedMirror = !!interaction.options.getBoolean('only-target') && interaction.client.configurations['moderation']['config']['linked_accounts_enabled']; 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 => { + const quarantineRoleId = interaction.client.configurations['moderation']['config']['quarantine-role-id']; + const roles = Array.from(interaction.memberToExecuteUpon.roles.cache.filter(f => !f.managed).keys()).filter(r => r !== quarantineRoleId); + moderationAction(interaction.client, 'quarantine', interaction.member, interaction.memberToExecuteUpon, interaction.options.getString('reason'), {roles}, parseDuration, null, {disableLinkedMirror}).then(r => { if (r) { if (parseDuration) interaction.editReply({ content: localize('moderation', 'expiring-action-done', { @@ -248,6 +376,7 @@ module.exports.subcommands = { 'unquarantine': async function (interaction) { if (interaction.replied) return; if (!checkRoles(interaction, 3)) return; + const disableLinkedMirror = !!interaction.options.getBoolean('only-target') && interaction.client.configurations['moderation']['config']['linked_accounts_enabled']; const lastAction = await interaction.client.models['moderation']['ModerationAction'].findOne({ where: { victimID: interaction.memberToExecuteUpon.user.id, @@ -260,7 +389,7 @@ module.exports.subcommands = { content: '⚠️ ' + localize('moderation', 'no-quarantine-action-found') }); if (!(lastAction.additionalData.roles instanceof Array)) lastAction.additionalData.roles = []; - moderationAction(interaction.client, 'unquarantine', interaction.member, interaction.memberToExecuteUpon, interaction.options.getString('reason'), {roles: lastAction.additionalData.roles || []}).then(r => { + moderationAction(interaction.client, 'unquarantine', interaction.member, interaction.memberToExecuteUpon, interaction.options.getString('reason'), {roles: lastAction.additionalData.roles || []}, null, null, {disableLinkedMirror}).then(r => { if (r) { interaction.editReply({content: localize('moderation', 'action-done', {i: r.actionID})}); } else interaction.editReply({content: '⚠️ ' + r}); @@ -271,7 +400,8 @@ module.exports.subcommands = { 'kick': async function (interaction) { if (interaction.replied) return; if (!checkRoles(interaction, 3)) return; - moderationAction(interaction.client, 'kick', interaction.member, interaction.memberToExecuteUpon, interaction.options.getString('reason'), {}, null, interaction.options.getAttachment('proof')).then(r => { + const disableLinkedMirror = !!interaction.options.getBoolean('only-target') && interaction.client.configurations['moderation']['config']['linked_accounts_enabled']; + moderationAction(interaction.client, 'kick', interaction.member, interaction.memberToExecuteUpon, interaction.options.getString('reason'), {}, null, interaction.options.getAttachment('proof'), {disableLinkedMirror}).then(r => { if (r) interaction.editReply({ content: localize('moderation', 'action-done', {i: r.actionID}) }); @@ -283,12 +413,13 @@ module.exports.subcommands = { 'mute': async function (interaction) { if (interaction.replied) return; if (!checkRoles(interaction, 2)) return; + const disableLinkedMirror = !!interaction.options.getBoolean('only-target') && interaction.client.configurations['moderation']['config']['linked_accounts_enabled']; const parseDuration = new Date(new Date().getTime() + durationParser(interaction.options.getString('duration'))); if (durationParser(interaction.options.getString('duration')) > 2419200000) return interaction.editReply({ ephemeral: true, content: '⚠️ ' + localize('moderation', 'mute-max-duration') }); - moderationAction(interaction.client, 'mute', interaction.member, interaction.memberToExecuteUpon, interaction.options.getString('reason'), {}, parseDuration, interaction.options.getAttachment('proof')).then(r => { + moderationAction(interaction.client, 'mute', interaction.member, interaction.memberToExecuteUpon, interaction.options.getString('reason'), {}, parseDuration, interaction.options.getAttachment('proof'), {disableLinkedMirror}).then(r => { if (r) interaction.editReply({ content: localize('moderation', 'action-done', {i: r.actionID}) }); @@ -300,7 +431,8 @@ module.exports.subcommands = { 'unmute': async function (interaction) { if (interaction.replied) return; if (!checkRoles(interaction, 2)) return; - moderationAction(interaction.client, 'unmute', interaction.member, interaction.memberToExecuteUpon, interaction.options.getString('reason')).then(r => { + const disableLinkedMirror = !!interaction.options.getBoolean('only-target') && interaction.client.configurations['moderation']['config']['linked_accounts_enabled']; + moderationAction(interaction.client, 'unmute', interaction.member, interaction.memberToExecuteUpon, interaction.options.getString('reason'), {}, null, null, {disableLinkedMirror}).then(r => { if (r) interaction.editReply({ content: localize('moderation', 'action-done', {i: r.actionID}) }); @@ -312,7 +444,8 @@ module.exports.subcommands = { 'warn': async function (interaction) { if (interaction.replied) return; if (!checkRoles(interaction, 1)) return; - moderationAction(interaction.client, 'warn', interaction.member, interaction.memberToExecuteUpon, interaction.options.getString('reason'), {}, null, interaction.options.getAttachment('proof')).then(r => { + const disableLinkedMirror = !!interaction.options.getBoolean('only-target') && interaction.client.configurations['moderation']['config']['linked_accounts_enabled']; + moderationAction(interaction.client, 'warn', interaction.member, interaction.memberToExecuteUpon, interaction.options.getString('reason'), {}, null, interaction.options.getAttachment('proof'), {disableLinkedMirror}).then(r => { if (r) interaction.editReply({ content: localize('moderation', 'action-done', {i: r.actionID}) }); @@ -324,7 +457,8 @@ module.exports.subcommands = { 'channel-mute': async function (interaction) { if (interaction.replied) return; if (!checkRoles(interaction, 2)) return; - moderationAction(interaction.client, 'channel-mute', interaction.member, interaction.memberToExecuteUpon, interaction.options.getString('reason'), {channel: interaction.channel}, null, interaction.options.getAttachment('proof')).then(r => { + const disableLinkedMirror = !!interaction.options.getBoolean('only-target') && interaction.client.configurations['moderation']['config']['linked_accounts_enabled']; + moderationAction(interaction.client, 'channel-mute', interaction.member, interaction.memberToExecuteUpon, interaction.options.getString('reason'), {channel: interaction.channel}, null, interaction.options.getAttachment('proof'), {disableLinkedMirror}).then(r => { if (r) interaction.editReply({ content: localize('moderation', 'action-done', {i: r.actionID}) }); @@ -336,7 +470,8 @@ module.exports.subcommands = { 'remove-channel-mute': async function (interaction) { if (interaction.replied) return; if (!checkRoles(interaction, 2)) return; - moderationAction(interaction.client, 'unchannel-mute', interaction.member, interaction.memberToExecuteUpon, interaction.options.getString('reason'), {channel: interaction.channel}).then(r => { + const disableLinkedMirror = !!interaction.options.getBoolean('only-target') && interaction.client.configurations['moderation']['config']['linked_accounts_enabled']; + moderationAction(interaction.client, 'unchannel-mute', interaction.member, interaction.memberToExecuteUpon, interaction.options.getString('reason'), {channel: interaction.channel}, null, null, {disableLinkedMirror}).then(r => { if (r) interaction.editReply({ content: localize('moderation', 'action-done', {i: r.actionID}) }); @@ -345,6 +480,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; @@ -373,57 +533,180 @@ module.exports.subcommands = { 'actions': async function (interaction) { if (interaction.replied) return; if (!checkRoles(interaction, 1)) return; + const moduleConfig = interaction.client.configurations['moderation']['config']; + if (moduleConfig['actions_restrict_channels']) { + const allowed = moduleConfig['actions_allowed_channels'] || []; + if (!allowed.includes(interaction.channel.id)) { + return interaction.editReply({ + content: '⚠️ ' + localize('moderation', 'actions-channel-not-allowed') + }); + } + } + const targetMember = interaction.memberToExecuteUpon; + const targetUser = interaction.memberToExecuteUpon.user; + let linkedGroup = null; + if (moduleConfig['linked_accounts_enabled']) { + linkedGroup = await getLinkedGroup(interaction.client, targetUser.id); + } + const includeAltActions = !!moduleConfig['dossier_include_alt_actions']; + const victimIDs = (linkedGroup && includeAltActions) ? linkedGroup.userIDs : [targetUser.id]; const actions = await interaction.client.models['moderation']['ModerationAction'].findAll({ - where: { - victimID: interaction.memberToExecuteUpon.id - }, + where: {victimID: victimIDs}, order: [['createdAt', 'DESC']] }); - const sites = []; - let fieldCount = 0; - let fieldCache = []; - actions.forEach(action => { - fieldCount++; - fieldCache.push({ - name: `#${action.actionID}: ${action.type}`, - value: localize('moderation', 'action-description-format', { - reason: action.reason, - u: action.memberID, - t: dateToDiscordTimestamp(new Date(action.createdAt)) - }) - }); - if (fieldCount % 3 === 0) { - addSite(fieldCache); - fieldCache = []; + const autoModBatchIds = new Set( + actions + .filter(a => a.type === 'warn' && a.additionalData && a.additionalData.autoModBatchId) + .map(a => a.additionalData.autoModBatchId) + ); + const visibleActions = actions.filter(a => { + if (a.additionalData && a.additionalData.autoModBatchId && a.type !== 'warn') { + return !autoModBatchIds.has(a.additionalData.autoModBatchId); } + return true; }); - if (fieldCache.length !== 0) addSite(fieldCache); - if (sites.length === 0) addSite([{ - name: localize('moderation', 'no-actions-title'), - value: localize('moderation', 'no-actions-title', {u: formatDiscordUserName(interaction.memberToExecuteUpon.user)}) - }]); + const joinedAt = (targetMember && targetMember.joinedAt) ? dateToDiscordTimestamp(new Date(targetMember.joinedAt), 'D') : localize('moderation', 'unknown'); + const createdAt = targetUser.createdAt ? dateToDiscordTimestamp(new Date(targetUser.createdAt), 'D') : localize('moderation', 'unknown'); + const counts = { + ban: actions.filter(a => a.type === 'ban').length, + quarantine: actions.filter(a => a.type === 'quarantine').length, + mute: actions.filter(a => a.type === 'mute').length, + warn: actions.filter(a => a.type === 'warn').length + }; + const notesLines = []; + const notesLimit = 10; + const showNotes = moduleConfig['dossier_show_notes'] && (!moduleConfig['dossier_notes_require_opt_in'] || interaction.options.getBoolean('show-notes')); + if (showNotes) { + const notesRecord = await interaction.client.models['moderation']['UserNotes'].findOne({ + where: {userID: targetUser.id} + }); + const notes = (notesRecord ? notesRecord.notes : []).filter(n => n.content && n.content !== '[deleted]').sort((a, b) => b.lastUpdateAt - a.lastUpdateAt); + for (const note of notes) { + if (notesLines.length >= notesLimit) break; + notesLines.push(localize('moderation', 'dossier-note-line', { + i: note.id, + t: dateToDiscordTimestamp(new Date(note.lastUpdateAt), 'R'), + author: `<@${note.authorID}>`, + c: note.content.replaceAll('\n', ' '), + altInfo: '' + })); + } + } + + const showLinkedAccounts = moduleConfig['dossier_show_linked_accounts'] && showNotes; + let linkedText = null; + if (linkedGroup && showLinkedAccounts) { + const linked = linkedGroup.userIDs.filter(id => id !== targetUser.id); + if (linked.length !== 0) linkedText = formatUserMentions(linked); + } + if (linkedGroup && showNotes && moduleConfig['dossier_include_alt_notes']) { + const linked = linkedGroup.userIDs.filter(id => id !== targetUser.id); + for (const linkedID of linked) { + if (notesLines.length >= notesLimit) break; + const linkedNotes = await interaction.client.models['moderation']['UserNotes'].findOne({ + where: {userID: linkedID} + }); + const ln = (linkedNotes ? linkedNotes.notes : []).filter(n => n.content && n.content !== '[deleted]').sort((a, b) => b.lastUpdateAt - a.lastUpdateAt); + for (const note of ln) { + if (notesLines.length >= notesLimit) break; + notesLines.push(localize('moderation', 'dossier-note-line', { + i: note.id, + t: dateToDiscordTimestamp(new Date(note.lastUpdateAt), 'R'), + author: `<@${note.authorID}>`, + c: note.content.replaceAll('\n', ' '), + altInfo: `\n> ${localize('moderation', 'dossier-note-alt-inline', {u: `<@${linkedID}>`})}` + })); + } + } + } + const lines = [ + localize('moderation', 'dossier-subtitle', {u: formatDiscordUserName(targetUser), m: `<@${targetUser.id}>`}), + localize('moderation', 'dossier-joined', {d: joinedAt}), + localize('moderation', 'dossier-created', {d: createdAt}), + localize('moderation', 'dossier-counts', { + b: counts.ban, + q: counts.quarantine, + m: counts.mute, + w: counts.warn + }) + ]; + + if (showLinkedAccounts) { + lines.push(localize('moderation', 'dossier-separator')); + lines.push(localize('moderation', 'dossier-linked-title')); + if (linkedText) lines.push(linkedText); + else lines.push(localize('moderation', 'linked-accounts-none')); + } + + if (showNotes) { + lines.push(localize('moderation', 'dossier-separator')); + lines.push(localize('moderation', 'dossier-notes-title')); + if (notesLines.length === 0) lines.push(localize('moderation', 'dossier-notes-empty')); + else lines.push(...notesLines); + } + if (visibleActions.length === 0) { + lines.push(localize('moderation', 'dossier-separator')); + lines.push(localize('moderation', 'no-actions-value', {u: `<@${interaction.memberToExecuteUpon.user.id}>`})); + } else { + lines.push(localize('moderation', 'dossier-separator')); + lines.push(localize('moderation', 'dossier-actions-title')); + for (const action of visibleActions) { + const isAlt = action.victimID !== targetUser.id; + const actionLines = [ + localize('moderation', 'action-header', {i: action.actionID, t: action.type}), + localize('moderation', 'action-reason-line', {r: action.reason}), + localize('moderation', 'action-by-line', {u: action.memberID ? `<@${action.memberID}>` : localize('moderation', 'unknown')}), + localize('moderation', 'action-at-line', {t: dateToDiscordTimestamp(new Date(action.createdAt))}) + ]; + if (action.expiresOn) actionLines.push(localize('moderation', 'action-expires-line', {d: dateToDiscordTimestamp(new Date(action.expiresOn))})); + if (action.type === 'warn' && action.additionalData && action.additionalData.autoModActions && action.additionalData.autoModActions.length > 0) { + const autoMods = action.additionalData.autoModActions.map((entry) => { + if (typeof entry === 'string') return entry; + const d = entry.duration || localize('moderation', 'unknown'); + return localize('moderation', 'automod-log-line', {d, a: entry.type, r: entry.reason || ''}).trim(); + }); + actionLines.push(localize('moderation', 'action-automod-line', {a: autoMods.join(' | ')})); + } + if (isAlt) actionLines.push(localize('moderation', 'action-alt-line', {u: `<@${action.victimID}>`})); + lines.push(localize('moderation', 'action-block', {a: actionLines.join('\n')})); + } + lines.push(localize('moderation', 'dossier-separator')); + } + + const descriptionPages = []; + const maxLen = 1400; + let buffer = ''; + for (const line of lines) { + const add = (buffer.length === 0 ? line : `\n${line}`); + if ((buffer + add).length > maxLen) { + descriptionPages.push(buffer); + buffer = line; + } else buffer += add; + } + if (buffer.length !== 0) descriptionPages.push(buffer); + if (descriptionPages.length === 0) descriptionPages.push(localize('moderation', 'no-actions-value', {u: `<@${interaction.memberToExecuteUpon.user.id}>`})); /** * Adds a new site * @private * @param fs */ - function addSite(fs) { + function addSite(description, index, total) { 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), - i: sites.length + 1 + i: index + 1 })) - .setDescription(localize('moderation', 'actions-embed-description', {u: formatDiscordUserName(interaction.memberToExecuteUpon.user)})) + .setDescription(description) .setThumbnail(interaction.memberToExecuteUpon.user.avatarURL()) - .setFooter({text: interaction.client.strings.footer, iconURL: interaction.client.strings.footerImgUrl}) - .addFields(fs); - sites.push(embed); + safeSetFooter(embed, interaction.client); + return embed; } - sendMultipleSiteButtonMessage(interaction.channel, sites, [interaction.user.id], interaction); + const embedSites = descriptionPages.map((d, i) => addSite(d, i, descriptionPages.length)); + sendMultipleSiteButtonMessage(interaction.channel, embedSites, [interaction.user.id], interaction); }, 'revoke-warn': async function (interaction) { if (interaction.replied) return; @@ -449,6 +732,59 @@ module.exports.subcommands = { interaction.editReply({content: '⚠️ ' + r}); }); } + , + 'clear-punishments': async function (interaction) { + if (interaction.replied) return; + if (!checkRoles(interaction, 4)) return; + const moduleConfig = interaction.client.configurations['moderation']['config']; + if (!moduleConfig['debug_clear_punishments_enabled']) { + return interaction.editReply({ + content: '⚠️ ' + localize('moderation', 'clear-punishments-disabled') + }); + } + const confirm = interaction.options.getString('confirm', true); + if (confirm !== 'CONFIRM') { + return interaction.editReply({ + content: '⚠️ ' + localize('moderation', 'clear-punishments-confirm-required') + }); + } + const targetUser = interaction.options.getUser('user', true); + const targetMember = interaction.memberToExecuteUpon && !interaction.memberToExecuteUpon.notFound + ? interaction.memberToExecuteUpon + : null; + const reason = localize('moderation', 'clear-punishments-reason'); + const quarantineRoleId = moduleConfig['quarantine-role-id']; + + if (targetMember) { + if (targetMember.isCommunicationDisabled()) { + await moderationAction(interaction.client, 'unmute', interaction.member, targetMember, reason, {}, null, null, {suppressLog: true}); + } + if (quarantineRoleId && targetMember.roles.cache.get(quarantineRoleId)) { + const lastAction = await interaction.client.models['moderation']['ModerationAction'].findOne({ + where: { + victimID: targetUser.id, + type: 'quarantine' + }, + order: [['createdAt', 'DESC']] + }); + const roles = (lastAction && lastAction.additionalData && lastAction.additionalData.roles instanceof Array) + ? lastAction.additionalData.roles + : []; + await moderationAction(interaction.client, 'unquarantine', interaction.member, targetMember, reason, {roles}, null, null, {suppressLog: true}); + } + } + + await moderationAction(interaction.client, 'unban', interaction.member, targetUser.id, reason, {}, null, null, {suppressLog: true}).catch(() => { + }); + + const deleted = await interaction.client.models['moderation']['ModerationAction'].destroy({ + where: {victimID: targetUser.id} + }); + + return interaction.editReply({ + content: localize('moderation', 'clear-punishments-done', {u: `<@${targetUser.id}>`, n: deleted}) + }); + } }; module.exports.autoComplete = { @@ -513,7 +849,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 @@ -530,408 +866,606 @@ 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', + name: 'clear-punishments', + description: localize('moderation', 'moderate-clear-punishments-command-description'), + options: [ + { + type: 'USER', + name: 'user', + required: true, + description: localize('moderation', 'moderate-user-description') + }, + { + type: 'STRING', + name: 'confirm', + required: true, + description: localize('moderation', 'moderate-clear-punishments-confirm-description') + } + ] + }, + { + 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: '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: 'STRING', + name: 'duration', + required: false, + description: localize('moderation', 'moderate-duration-description') + }, + { + type: 'INTEGER', + name: 'days', + required: false, + description: localize('moderation', 'moderate-days-description') + }, + { + type: 'BOOLEAN', + name: 'only-target', + required: false, + description: localize('moderation', 'moderate-only-target-description') } - ] - }, - { - type: 'SUB_COMMAND', - name: 'create', - description: localize('moderation', 'moderate-notes-command-create'), - 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: 'STRING', - name: 'notes', - required: true, - description: localize('moderation', 'moderate-notes-description') + name: 'duration', + required: false, + description: localize('moderation', 'moderate-duration-description') + }, + { + type: 'BOOLEAN', + name: 'only-target', + required: false, + description: localize('moderation', 'moderate-only-target-description') } - ] - }, - { - type: 'SUB_COMMAND', - name: 'edit', - description: localize('moderation', 'moderate-notes-command-edit'), - options: [ + ]; + } + }, + { + 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: '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: 'BOOLEAN', + name: 'only-target', + required: false, + description: localize('moderation', 'moderate-only-target-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: 'BOOLEAN', + name: 'only-target', + required: false, + description: localize('moderation', 'moderate-only-target-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: 'notes', - required: true, - description: localize('moderation', 'moderate-notes-description') + 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: 'BOOLEAN', + name: 'only-target', + required: false, + description: localize('moderation', 'moderate-only-target-description') } - ] - }, - { - type: 'SUB_COMMAND', - name: 'delete', - description: localize('moderation', 'moderate-notes-command-delete'), - options: [ + ]; + } + }, + { + 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: 'USER', - name: 'user', + type: 'STRING', + name: 'duration', required: true, - description: localize('moderation', 'moderate-user-description') + description: localize('moderation', 'moderate-duration-description') }, { - type: 'INTEGER', - name: 'note-id', - required: true, - description: localize('moderation', 'moderate-note-id-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: 'BOOLEAN', + name: 'only-target', + required: false, + description: localize('moderation', 'moderate-only-target-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: 'STRING', - name: 'reason', - required: client.configurations['moderation']['config']['require_reason'], - description: localize('moderation', 'moderate-reason-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: 'STRING', + name: 'reason', + required: client.configurations['moderation']['config']['require_reason'], + description: localize('moderation', 'moderate-reason-description') + }, + { + type: 'BOOLEAN', + name: 'only-target', + required: false, + description: localize('moderation', 'moderate-only-target-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: '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: 'ATTACHMENT', + name: 'proof', + required: (client.configurations['moderation']['config']['require_proof'] && client.configurations['moderation']['config']['require_reason']), + description: localize('moderation', 'moderate-proof-description') + }, + { + type: 'BOOLEAN', + name: 'only-target', + required: false, + description: localize('moderation', 'moderate-only-target-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: '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: 'BOOLEAN', + name: 'only-target', + required: false, + description: localize('moderation', 'moderate-only-target-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: 'STRING', + name: 'reason', + required: client.configurations['moderation']['config']['require_reason'], + description: localize('moderation', 'moderate-reason-description') + }, + { + type: 'BOOLEAN', + name: 'only-target', + required: false, + description: localize('moderation', 'moderate-only-target-description') + } + ]; + } + }, + { + type: 'SUB_COMMAND_GROUP', + name: 'accounts', + description: localize('moderation', 'linked-accounts-command-description'), + options: [ { - 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: 'SUB_COMMAND', + name: 'link', + description: localize('moderation', 'linked-accounts-link-description'), + options: [ + { + type: 'USER', + name: 'main', + required: true, + description: localize('moderation', 'linked-accounts-main-description') + }, + { + type: 'USER', + name: 'account', + required: true, + description: localize('moderation', 'linked-accounts-account-description') + }, + { + type: 'USER', + name: 'account2', + required: false, + description: localize('moderation', 'linked-accounts-account-description') + }, + { + type: 'USER', + name: 'account3', + required: false, + description: localize('moderation', 'linked-accounts-account-description') + }, + { + type: 'USER', + name: 'account4', + required: false, + description: localize('moderation', 'linked-accounts-account-description') + }, + { + type: 'USER', + name: 'account5', + required: false, + description: localize('moderation', 'linked-accounts-account-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: true, - description: localize('moderation', 'moderate-duration-description') + type: 'SUB_COMMAND', + name: 'unlink', + description: localize('moderation', 'linked-accounts-unlink-description'), + options: [ + { + type: 'USER', + name: 'user', + required: true, + description: localize('moderation', 'linked-accounts-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', 'linked-accounts-clear-description'), + options: [ + { + type: 'USER', + name: 'main', + required: true, + description: localize('moderation', 'linked-accounts-main-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: 'list', + description: localize('moderation', 'linked-accounts-list-description'), + options: [ + { + type: 'USER', + name: 'user', + required: true, + description: localize('moderation', 'linked-accounts-user-description') + } + ] } - ]; - } - }, - { - type: 'SUB_COMMAND', - name: 'warn', - description: localize('moderation', 'moderate-warn-command-description'), - options: function (client) { - return [{ + ] + }, + { + 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: '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: 'BOOLEAN', + name: 'show-notes', + required: false, + description: localize('moderation', 'moderate-actions-show-notes') } - ]; - } - }, - { - 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: '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') + }] + }); } - ] -}; \ No newline at end of file + + return opts; + } +}; diff --git a/modules/moderation/commands/report.js b/modules/moderation/commands/report.js index f0da600a..1134b883 100644 --- a/modules/moderation/commands/report.js +++ b/modules/moderation/commands/report.js @@ -1,5 +1,10 @@ const {localize} = require('../../../src/functions/localize'); -const {embedType, messageLogToStringToPaste} = require('../../../src/functions/helpers'); +const { + embedType, + messageLogToStringToPaste, + parseEmbedColor, + safeSetFooter +} = require('../../../src/functions/helpers'); const {MessageEmbed} = require('discord.js'); module.exports.run = async function (interaction) { @@ -33,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('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 242a0472..ea9e16eb 100644 --- a/modules/moderation/configs/antiGrief.json +++ b/modules/moderation/configs/antiGrief.json @@ -31,22 +31,24 @@ "de": "Aktiviert oder deaktiviert das Anti-Join-Grief-System" }, "type": "boolean", - "elementToggle": true + "elementToggle": true, + "category": "settings" }, { "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" + "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 985e368b..aa7c6852 100644 --- a/modules/moderation/configs/antiJoinRaid.json +++ b/modules/moderation/configs/antiJoinRaid.json @@ -24,13 +24,14 @@ "de": "Aktiviert oder deaktiviert das Anti-Join-Raid-System" }, "type": "boolean", - "elementToggle": true + "elementToggle": true, + "category": "settings" }, { "name": "timeframe", "humanName": { - "de": "Zeitfenster", - "en": "Timeframe" + "de": "Zeitfenster (in Minuten)", + "en": "Timeframe (in minutes)" }, "default": { "en": 5, @@ -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 81a3189d..3542a71e 100644 --- a/modules/moderation/configs/antiSpam.json +++ b/modules/moderation/configs/antiSpam.json @@ -24,23 +24,25 @@ "de": "Aktiviert oder deaktiviert das Anti-Spam-System" }, "type": "boolean", - "elementToggle": true + "elementToggle": true, + "category": "settings" }, { "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" + "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 481ce819..b8da3b16 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,273 @@ "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": "linked_accounts_enabled", + "humanName": { + "en": "Linked Accounts", + "de": "Verknuepfte Accounts" + }, + "default": { + "en": false, + "de": false + }, + "description": { + "en": "Enable linking multiple accounts to a single dossier", + "de": "Erlaubt das Verknuepfen mehrerer Accounts zu einer Akte" + }, + "type": "boolean" + }, + { + "name": "linked_accounts_mode", + "dependsOn": "linked_accounts_enabled", + "humanName": { + "en": "Linked Account Mode", + "de": "Modus fuer verknuepfte Accounts" + }, + "default": { + "en": "mirror", + "de": "mirror" + }, + "description": { + "en": "single: only one account allowed; mirror: actions are mirrored across linked accounts", + "de": "single: nur ein Account erlaubt; mirror: Aktionen werden auf verknuepfte Accounts gespiegelt" + }, + "type": "select", + "content": [ + "single", + "mirror" + ] + }, + { + "name": "linked_accounts_single_action", + "dependsOn": "linked_accounts_enabled", + "humanName": { + "en": "Action for secondary accounts (single mode)", + "de": "Aktion fuer Zweitaccounts (single Modus)" + }, + "default": { + "en": "none", + "de": "none" + }, + "description": { + "en": "Action applied to non-main accounts when linking in single mode", + "de": "Aktion, die bei Verknuepfung im single Modus auf Zweitaccounts angewendet wird" + }, + "type": "select", + "content": [ + "none", + "warn", + "mute", + "kick", + "quarantine", + "ban" + ] + }, + { + "name": "linked_accounts_mirror_actions", + "dependsOn": "linked_accounts_enabled", + "humanName": { + "en": "Mirror actions", + "de": "Aktionen spiegeln" + }, + "default": { + "en": [ + "warn", + "mute", + "kick", + "quarantine", + "unquarantine", + "ban", + "channel-mute" + ], + "de": [ + "warn", + "mute", + "kick", + "quarantine", + "unquarantine", + "ban", + "channel-mute" + ] + }, + "description": { + "en": "Actions that should be mirrored to linked accounts in mirror mode", + "de": "Aktionen, die im mirror Modus auf verknuepfte Accounts uebertragen werden" + }, + "type": "array", + "content": "string" + }, + { + "name": "linked_accounts_suppress_log_channel", + "dependsOn": "linked_accounts_enabled", + "humanName": { + "en": "Suppress log channel for mirrored actions", + "de": "Log-Kanal fuer gespiegelte Aktionen unterdruecken" + }, + "default": { + "en": true, + "de": true + }, + "description": { + "en": "If enabled, mirrored actions won't be sent to the moderation log channel", + "de": "Wenn aktiviert, werden gespiegelte Aktionen nicht im Log-Kanal gesendet" + }, + "type": "boolean" + }, + { + "name": "linked_accounts_group_log", + "dependsOn": "linked_accounts_enabled", + "humanName": { + "en": "Group linked accounts in log", + "de": "Verknuepfte Accounts im Log gruppieren" + }, + "default": { + "en": true, + "de": true + }, + "description": { + "en": "If enabled, a single log entry includes all linked accounts for mirrored actions", + "de": "Wenn aktiviert, werden verknuepfte Accounts bei gespiegelten Aktionen in einem Log-Eintrag zusammengefasst" + }, + "type": "boolean" + }, + { + "name": "linked_accounts_group_log_show_linked", + "dependsOn": "linked_accounts_group_log", + "humanName": { + "en": "Show linked accounts in grouped log", + "de": "Verknuepfte Accounts im Gruppen-Log anzeigen" + }, + "default": { + "en": false, + "de": false + }, + "description": { + "en": "If enabled, the grouped log entry lists linked accounts", + "de": "Wenn aktiviert, listet der gruppierte Log-Eintrag verknuepfte Accounts" + }, + "type": "boolean" + }, + { + "name": "dossier_show_linked_accounts", + "dependsOn": "linked_accounts_enabled", + "humanName": { + "en": "Show linked accounts in dossier", + "de": "Verlinkte Accounts in Akte anzeigen" + }, + "default": { + "en": true, + "de": true + }, + "description": { + "en": "Show linked accounts section in /moderate actions", + "de": "Zeigt verlinkte Accounts in /moderate actions an" + }, + "type": "boolean" + }, + { + "name": "dossier_show_notes", + "humanName": { + "en": "Show notes in dossier", + "de": "Notizen in Akte anzeigen" + }, + "default": { + "en": true, + "de": true + }, + "description": { + "en": "Show notes section in /moderate actions", + "de": "Zeigt Notizen in /moderate actions an" + }, + "type": "boolean" + }, + { + "name": "dossier_notes_require_opt_in", + "dependsOn": "dossier_show_notes", + "humanName": { + "en": "Require show-notes parameter", + "de": "show-notes Parameter erforderlich" + }, + "default": { + "en": true, + "de": true + }, + "description": { + "en": "If enabled, notes are only shown when show-notes is set in /moderate actions", + "de": "Wenn aktiviert, werden Notizen nur angezeigt, wenn show-notes in /moderate actions gesetzt ist" + }, + "type": "boolean" + }, + { + "name": "dossier_include_alt_notes", + "dependsOn": "linked_accounts_enabled", + "humanName": { + "en": "Include alt account notes", + "de": "Notizen von Alt-Accounts einbeziehen" + }, + "default": { + "en": true, + "de": true + }, + "description": { + "en": "If enabled, notes from linked accounts are listed", + "de": "Wenn aktiviert, werden Notizen verlinkter Accounts angezeigt" + }, + "type": "boolean" + }, + { + "name": "actions_restrict_channels", + "humanName": { + "en": "Restrict /moderate actions to channels", + "de": "/moderate actions auf Kanaele begrenzen" + }, + "default": { + "en": false, + "de": false + }, + "description": { + "en": "If enabled, /moderate actions can only be used in allowed channels", + "de": "Wenn aktiviert, kann /moderate actions nur in erlaubten Kanaelen genutzt werden" + }, + "type": "boolean" + }, + { + "name": "actions_allowed_channels", + "dependsOn": "actions_restrict_channels", + "humanName": { + "en": "Allowed channels for /moderate actions", + "de": "Erlaubte Kanaele fuer /moderate actions" + }, + "default": { + "en": [], + "de": [] + }, + "description": { + "en": "Channels where /moderate actions is allowed", + "de": "Kanaele, in denen /moderate actions erlaubt ist" + }, + "type": "array", + "content": "channelID" + }, + { + "name": "dossier_include_alt_actions", + "dependsOn": "linked_accounts_enabled", + "humanName": { + "en": "Include alt account actions", + "de": "Aktionen von Alt-Accounts einbeziehen" + }, + "default": { + "en": false, + "de": false + }, + "description": { + "en": "If enabled, actions from linked accounts are listed in /moderate actions", + "de": "Wenn aktiviert, werden Aktionen verlinkter Accounts in /moderate actions angezeigt" + }, + "type": "boolean" }, { "name": "roles-to-ping-on-report", @@ -161,13 +434,14 @@ "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", "humanName": { "de": "Begründung erzwingen", - "en": "Fore moderators to set a reason" + "en": "Force moderators to set a reason" }, "default": { "en": true, @@ -177,13 +451,14 @@ "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", "humanName": { "de": "Beweis-Bild erzwingen", - "en": "Fore moderators to upload proof" + "en": "Force moderators to upload proof" }, "dependsOn": "require_reason", "default": { @@ -194,7 +469,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 +494,8 @@ "kick", "quarantine", "ban" - ] + ], + "category": "automod" }, { "name": "action_on_scam_link", @@ -242,7 +519,8 @@ "kick", "quarantine", "ban" - ] + ], + "category": "automod" }, { "name": "scam_link_level", @@ -262,7 +540,8 @@ "content": [ "confirmed", "suspicious" - ] + ], + "category": "automod" }, { "name": "whitelisted_channels_for_invite_blocking", @@ -279,7 +558,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 +576,8 @@ "de": "Rollen, welche die Invitesperre umgehen dürfen" }, "type": "array", - "content": "roleID" + "content": "roleID", + "category": "automod" }, { "name": "blacklisted_words", @@ -313,7 +594,8 @@ "de": "Wörter, die blockiert sind" }, "type": "array", - "content": "string" + "content": "string", + "category": "automod" }, { "name": "action_on_posting_blacklisted_word", @@ -337,7 +619,24 @@ "kick", "ban", "quarantine" - ] + ], + "category": "automod" + }, + { + "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." + }, + "category": "actions" }, { "name": "changeNicknames", @@ -353,7 +652,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", @@ -378,7 +678,8 @@ "en": "Original nickname of the user" } } - ] + ], + "category": "nicknames" }, { "name": "changeNicknameOnQuarantine", @@ -402,27 +703,83 @@ "en": "Original nickname of the user" } } - ] + ], + "category": "nicknames" + }, + { + "name": "debug_clear_punishments_enabled", + "humanName": { + "de": "Debug: Strafen loeschen", + "en": "Debug: Clear punishments" + }, + "default": { + "en": false, + "de": false + }, + "description": { + "en": "Enable the debug command to clear all moderation actions for a user", + "de": "Aktiviert den Debug-Befehl zum Loeschen aller Moderationsaktionen eines Nutzers" + }, + "type": "boolean" + }, + { + "name": "automod_enabled", + "humanName": { + "de": "Warn-Automod aktiv", + "en": "Warn automod enabled" + }, + "default": { + "en": true, + "de": true + }, + "description": { + "en": "If enabled, actions defined in Automod will be executed when warn thresholds are reached", + "de": "Wenn aktiviert, werden die in Automod definierten Aktionen beim Erreichen der Warn-Grenzen ausgefuehrt" + }, + "type": "boolean" }, { "name": "automod", + "dependsOn": "automod_enabled", "humanName": { "de": "Automod", "en": "Automod" }, "default": { - "en": {}, - "de": {} + "en": { + "7": "quarantine:2d" + }, + "de": { + "7": "quarantine:2d" + } }, "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." + "en": "Define what should happen (options: mute, kick, ban, quarantine) when someone reaches x warns. Specify duration by writing : after the action (e.g. 3:mute:5h).", + "de": "Lege fest, was passieren soll (Optionen: mute, kick, ban, quarantine), wenn jemand x Verwarnungen erreicht. Dauer mit : angeben (z.B. 3:mute:5h)." }, "type": "keyed", "content": { "key": "integer", "value": "string" - } + }, + "category": "automod" + }, + { + "name": "automod_reason", + "dependsOn": "automod_enabled", + "humanName": { + "de": "Automod Begruendung", + "en": "Automod reason" + }, + "default": { + "en": "User exceeded the warn limit of %w. Action: %a.", + "de": "User hat die Warn-Grenze von %w ueberschritten. Aktion: %a." + }, + "description": { + "en": "Reason template for automod actions. Placeholders: %w (warn count), %a (action).", + "de": "Begruendungstext fuer Automod-Aktionen. Platzhalter: %w (Warn-Anzahl), %a (Aktion)." + }, + "type": "string" }, { "name": "warnsExpire", @@ -437,7 +794,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", @@ -453,7 +811,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 12c018d2..c970d850 100644 --- a/modules/moderation/events/botReady.js +++ b/modules/moderation/events/botReady.js @@ -3,6 +3,8 @@ 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 {restoreLockdownState} = require('../lockdown'); const memberCache = {}; const durationParser = require('parse-duration'); @@ -33,11 +35,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 (!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 +72,19 @@ 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'])); + const guild = await client.guilds.fetch(client.guildID); + const roleId = moduleConfig['quarantine-role-id']; + let members; + if (guild.members && typeof guild.members.fetch === 'function') { + members = await guild.members.fetch().catch(() => null); + } + if (!members) { + memberCache['quarantine'] = new Map(); + return; + } + memberCache['quarantine'] = members.filter(m => !!m.roles.cache.get(roleId)); } -/** - * Removes expired warns - * @param {Client} client - * @return {Promise} - */ async function deleteExpiredWarns(client) { const aD = await client.models['moderation']['ModerationAction'].findAll({ where: { @@ -92,4 +101,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..44172322 100644 --- a/modules/moderation/events/guildMemberAdd.js +++ b/modules/moderation/events/guildMemberAdd.js @@ -1,18 +1,19 @@ 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 {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 +47,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}); } } @@ -62,12 +63,16 @@ 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); + } } } // 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 +89,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 +144,52 @@ 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})}`); } + await guildMember.roles.add(joinGateConfig.roleID, `[moderation] [${localize('moderation', 'join-gate')}] ${localize('moderation', 'join-gate-fail', {r: reason})}`); + } 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); + } } -}; +} + +module.exports.runJoinGate = runJoinGate; /** * Sends a user a DM about their verification @@ -190,7 +205,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 +311,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..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'); @@ -16,7 +17,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)) { @@ -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); + } } } @@ -86,11 +91,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 +107,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 +115,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/linkedAccounts.js b/modules/moderation/linkedAccounts.js new file mode 100644 index 00000000..ad49c268 --- /dev/null +++ b/modules/moderation/linkedAccounts.js @@ -0,0 +1,79 @@ +const {Op} = require('sequelize'); + +async function getLinkedGroup(client, userID) { + const record = await client.models['moderation']['LinkedAccount'].findOne({ + where: {userID} + }); + if (!record) return null; + const mainID = record.mainID || record.userID; + const entries = await client.models['moderation']['LinkedAccount'].findAll({ + where: {mainID} + }); + const userIDs = entries.map(e => e.userID); + return {mainID, userIDs, entries}; +} + +async function linkAccounts(client, mainID, userIDs, linkedBy) { + const now = new Date(); + const model = client.models['moderation']['LinkedAccount']; + const inputIDs = new Set([mainID, ...(userIDs || [])]); + const inputList = Array.from(inputIDs); + if (inputList.length === 0) return; + + const existing = await model.findAll({ + where: {userID: {[Op.in]: inputList}} + }); + const existingMainIDs = new Set(); + for (const entry of existing) { + existingMainIDs.add(entry.mainID || entry.userID); + } + + const groupIDs = new Set(inputIDs); + if (existingMainIDs.size > 0) { + const mainList = Array.from(existingMainIDs); + const groupEntries = await model.findAll({ + where: {mainID: {[Op.in]: mainList}} + }); + for (const entry of groupEntries) groupIDs.add(entry.userID); + } + + let canonicalMainID = mainID; + const preferred = existing.find(entry => entry.userID === mainID); + if (preferred) canonicalMainID = preferred.mainID || preferred.userID; + else if (existing.length > 0) canonicalMainID = existing[0].mainID || existing[0].userID; + + if (!groupIDs.has(canonicalMainID)) { + const first = groupIDs.values().next().value; + if (first) canonicalMainID = first; + } + + const upserts = []; + for (const userID of groupIDs) { + upserts.push(model.upsert({ + userID, + mainID: canonicalMainID, + linkedBy, + linkedAt: now + })); + } + await Promise.all(upserts); +} + +async function unlinkAccount(client, userID) { + await client.models['moderation']['LinkedAccount'].destroy({ + where: {userID} + }); +} + +async function unlinkGroup(client, mainID) { + await client.models['moderation']['LinkedAccount'].destroy({ + where: {mainID} + }); +} + +module.exports = { + getLinkedGroup, + linkAccounts, + unlinkAccount, + unlinkGroup +}; 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/LinkedAccount.js b/modules/moderation/models/LinkedAccount.js new file mode 100644 index 00000000..aa763465 --- /dev/null +++ b/modules/moderation/models/LinkedAccount.js @@ -0,0 +1,24 @@ +const { DataTypes, Model } = require('sequelize'); + +module.exports = class LinkedAccount extends Model { + static init(sequelize) { + return super.init({ + userID: { + type: DataTypes.STRING, + primaryKey: true + }, + mainID: DataTypes.STRING, + linkedBy: DataTypes.STRING, + linkedAt: DataTypes.DATE + }, { + tableName: 'moderation_LinkedAccounts', + timestamps: false, + sequelize + }); + } +}; + +module.exports.config = { + name: 'LinkedAccount', + module: 'moderation' +}; 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 a2ca1f9d..33ec89f4 100644 --- a/modules/moderation/moderationActions.js +++ b/modules/moderation/moderationActions.js @@ -1,9 +1,10 @@ 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'); const {Op} = require('sequelize'); +const {getLinkedGroup} = require('./linkedAccounts'); /** * Performs a mod action @@ -17,13 +18,48 @@ const {Op} = require('sequelize'); * @param {MessageAttachment} proof Message-Attachment containing proof * @return {Promise} */ -async function moderationAction(client, type, user, victim, reason, additionalData = {}, expiringAt = null, proof = null) { +async function moderationAction(client, type, user, victim, reason, additionalData = {}, expiringAt = null, proof = null, options = {}) { const moduleConfig = client.configurations['moderation']['config']; const moduleStrings = client.configurations['moderation']['strings']; const antiGriefConfig = client.configurations['moderation']['antiGrief']; if (!reason) reason = localize('moderation', 'no-reason'); return new Promise(async (resolve, reject) => { + try { const guild = await client.guilds.fetch(client.guildID); + const now = new Date(); + let activeAction = null; + if (expiringAt && victim && victim.id) { + activeAction = await client.models['moderation']['ModerationAction'].findOne({ + where: { + victimID: victim.id, + type, + expiresOn: { + [Op.gt]: now + } + }, + order: [['createdAt', 'DESC']] + }); + if (activeAction) { + const undone = await client.models['moderation']['ModerationAction'].findOne({ + where: { + victimID: victim.id, + type: 'un' + type, + createdAt: { + [Op.gte]: activeAction.createdAt + } + } + }); + if (undone) activeAction = null; + } + } + if (activeAction && expiringAt && activeAction.expiresOn) { + const extendMs = expiringAt.getTime() - now.getTime(); + if (extendMs > 0) expiringAt = new Date(new Date(activeAction.expiresOn).getTime() + extendMs); + if (type === 'quarantine') { + const savedRoles = (activeAction.additionalData || {}).roles; + if (savedRoles instanceof Array) additionalData = {...additionalData, roles: savedRoles}; + } + } const quarantineRole = await guild.roles.fetch(moduleConfig['quarantine-role-id']).catch(() => { }); if (!quarantineRole && (type === 'quarantine' || type === 'unquarantine')) { @@ -50,7 +86,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 +96,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['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,20 +111,29 @@ 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['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], '[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 +162,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['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,11 +187,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['changeNicknames'] && 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) })); @@ -195,11 +240,52 @@ async function moderationAction(client, type, user, victim, reason, additionalDa type: 'warn' } }); - if (moduleConfig['automod'][warns.length + 1]) { + const warnCount = warns.length + 1; + if (moduleConfig['automod_enabled'] && moduleConfig['automod'] && moduleConfig['automod'][warnCount]) { const roles = []; victim.roles.cache.forEach(role => roles.push(role.id)); - moderationAction(client, moduleConfig['automod'][warns.length + 1].split(':')[0], {user: client.user}, victim, `[${localize('moderation', 'auto-mod')}]: ${localize('moderation', 'reached-warns', {w: warns.length + 1})}`, {roles: roles}, moduleConfig['automod'][warns.length + 1].includes(':') ? new Date(new Date().getTime() + durationParser(moduleConfig['automod'][warns.length + 1].split(':')[1])) : null).then(() => { - }); + const actionConfig = String(moduleConfig['automod'][warnCount]); + const autoReasonTemplate = moduleConfig['automod_reason'] || `[${localize('moderation', 'auto-mod')}]: ${localize('moderation', 'reached-warns', {w: warnCount})}`; + const actionSpecs = actionConfig.split(/[|,]/).map(s => s.trim()).filter(Boolean); + const autoModBatchId = `${victim.id}-${Date.now()}-${warnCount}`; + additionalData.autoModBatchId = autoModBatchId; + additionalData.autoModActions = []; + for (const spec of actionSpecs) { + const parts = spec.split(':'); + let actionType = (parts.shift() || '').trim().toLowerCase(); + if (actionType === 'timeout') actionType = 'mute'; + const durationPart = parts.join(':').trim() || null; + if (!['mute', 'kick', 'ban', 'quarantine'].includes(actionType)) { + client.logger.warn(`[moderation] Invalid automod action "${actionType}" for warn ${warnCount}.`); + continue; + } + if (durationPart) { + const durationMs = durationParser(durationPart); + if (!durationMs || Number.isNaN(durationMs)) { + client.logger.warn(`[moderation] Invalid automod duration "${durationPart}" for warn ${warnCount}.`); + continue; + } + } + const autoReason = autoReasonTemplate + .split('%w').join(warnCount.toString()) + .split('%a').join(actionType); + additionalData.autoModActions.push({type: actionType, duration: durationPart, reason: autoReason}); + try { + await moderationAction( + client, + actionType, + {user: client.user}, + victim, + autoReason, + {roles: roles, autoModBatchId}, + durationPart ? new Date(new Date().getTime() + durationParser(durationPart)) : null, + null, + {suppressLog: true} + ); + } catch (e) { + client.logger.warn('[moderation] Automod action failed', e); + } + } } break; case 'channel-mute': @@ -256,19 +342,90 @@ async function moderationAction(client, type, user, victim, reason, additionalDa default: return reject('Option not found'); } + const memberID = user.id || (user.user ? user.user.id : null); const modAction = await client.models['moderation']['ModerationAction'].create({ victimID: victim.id, - memberID: user.id, + memberID, reason, type: type, additionalData: additionalData, expiresOn: expiringAt }); if (expiringAt) await planExpiringAction(expiringAt, modAction, guild); + + let logVictimIDs = [victim.id]; + let logLinkedIDs = []; + const groupLogEnabled = moduleConfig['linked_accounts_group_log'] !== false; + const showGroupedLinked = moduleConfig['linked_accounts_group_log_show_linked'] === true; + if (moduleConfig['linked_accounts_enabled'] && !options.isMirrored && !options.disableLinkedMirror && moduleConfig['linked_accounts_mode'] === 'mirror') { + const mirrorList = new Set(moduleConfig['linked_accounts_mirror_actions'] || []); + if (mirrorList.has('quarantine') && !mirrorList.has('unquarantine')) mirrorList.add('unquarantine'); + if (mirrorList.has(type)) { + const linkedGroup = await getLinkedGroup(client, victim.id); + if (linkedGroup && linkedGroup.userIDs.length > 1) { + const linkedIDs = linkedGroup.userIDs.filter(id => id !== victim.id); + if (groupLogEnabled && showGroupedLinked) { + logLinkedIDs = linkedIDs; + } + for (const linkedID of linkedGroup.userIDs) { + if (linkedID === victim.id) continue; + let linkedVictim = await guild.members.fetch(linkedID).catch(() => null); + if (!linkedVictim) { + if (type === 'ban') { + linkedVictim = {id: linkedID, notFound: true, user: {id: linkedID, tag: linkedID}}; + } else if (type === 'unban') { + linkedVictim = linkedID; + } else { + continue; + } + } + let mirrorAdditionalData = additionalData; + if (type === 'quarantine' && linkedVictim.roles) { + const quarantineRoleId = moduleConfig['quarantine-role-id']; + if (linkedVictim.roles.cache.get(quarantineRoleId)) { + const linkedLastAction = await client.models['moderation']['ModerationAction'].findOne({ + where: { + victimID: linkedID, + type: 'quarantine' + }, + order: [['createdAt', 'DESC']] + }); + const linkedRoles = (linkedLastAction && linkedLastAction.additionalData && linkedLastAction.additionalData.roles instanceof Array) + ? linkedLastAction.additionalData.roles + : []; + mirrorAdditionalData = {roles: linkedRoles}; + } else { + mirrorAdditionalData = {roles: Array.from(linkedVictim.roles.cache.keys()).filter(r => r !== quarantineRoleId)}; + } + } + if (type === 'unquarantine') { + const linkedLastAction = await client.models['moderation']['ModerationAction'].findOne({ + where: { + victimID: linkedID, + type: 'quarantine' + }, + order: [['createdAt', 'DESC']] + }); + const linkedRoles = (linkedLastAction && linkedLastAction.additionalData && linkedLastAction.additionalData.roles instanceof Array) + ? linkedLastAction.additionalData.roles + : []; + mirrorAdditionalData = {roles: linkedRoles}; + } + await moderationAction(client, type, user, linkedVictim, reason, mirrorAdditionalData, expiringAt, proof, { + isMirrored: true, + suppressLog: !!moduleConfig['linked_accounts_suppress_log_channel'] || groupLogEnabled, + skipCacheUpdate: true + }); + } + } + } + } let channel = guild.channels.cache.get(moduleConfig['logchannel-id']); if (!channel) channel = client.logChannel; - if (!channel) { - client.error('[moderation] ' + localize('moderation', 'missing-logchannel')); + if (options.suppressLog) { + // Skip log channel for mirrored actions if configured + } else if (!channel) { + client.logger.error('[moderation] ' + localize('moderation', 'missing-logchannel')); } else { const fields = []; if (expiringAt) fields.push({ @@ -286,22 +443,62 @@ 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({ + if (type === 'warn' && additionalData.autoModActions && additionalData.autoModActions.length > 0) { + const autoModLines = additionalData.autoModActions.map((entry) => { + if (typeof entry === 'string') { + const parts = entry.split(':'); + const t = parts[0]; + const d = parts.slice(1).join(':') || localize('moderation', 'unknown'); + return localize('moderation', 'automod-log-line', {d, a: t, r: ''}).trim(); + } + const t = entry.type; + const d = entry.duration || localize('moderation', 'unknown'); + return localize('moderation', 'automod-log-line', {d, a: t, r: entry.reason || ''}).trim(); + }); + fields.push({ + name: localize('moderation', 'automod-log-field'), + value: autoModLines.join('\n') + }); + } + const victimMentions = logVictimIDs.map(id => `<@${id}>`).join(', '); + if (logLinkedIDs.length > 0 && groupLogEnabled && showGroupedLinked) { + fields.push({ + name: localize('moderation', 'linked-accounts-log-field'), + value: logLinkedIDs.map(id => `<@${id}>`).join(', ') + }); + } + const victimFieldValue = victimMentions + (logVictimIDs.length === 1 ? `\n\`${victim.id}\`` : ''); + 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'), victimFieldValue || localize('moderation', 'unknown'), 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] + }); + } + if (!options.skipCacheUpdate) { + const {updateCache} = require('./events/botReady'); + updateCache(client).catch((e) => { + client.logger.warn('[moderation] updateCache failed', e); }); } - const {updateCache} = require('./events/botReady'); - await updateCache(client); resolve(modAction); + } catch (e) { + client.logger.error('[moderation] moderationAction failed', e); + reject(e); + } }); } @@ -313,8 +510,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(() => { }); } @@ -329,6 +526,25 @@ function sendMessage(user, content) { async function planExpiringAction(expiringDate, action, guild) { if (!expiringDate) return; guild.client.jobs.push(scheduleJob(expiringDate, async () => { + const now = new Date(); + const actionRecord = await guild.client.models['moderation']['ModerationAction'].findOne({ + where: {actionID: action.actionID} + }); + if (actionRecord && actionRecord.expiresOn && new Date(actionRecord.expiresOn) > now) return; + const newerAction = await guild.client.models['moderation']['ModerationAction'].findOne({ + where: { + victimID: action.victimID, + type: action.type, + createdAt: { + [Op.gt]: action.createdAt + }, + expiresOn: { + [Op.gt]: now + } + }, + order: [['createdAt', 'DESC']] + }); + if (newerAction) return; const undoAction = 'un' + action.type; const undoneModAction = await guild.client.models['moderation']['ModerationAction'].findOne({ where: { @@ -350,4 +566,4 @@ async function planExpiringAction(expiringDate, action, guild) { })); } -module.exports.planExpiringAction = planExpiringAction; \ No newline at end of file +module.exports.planExpiringAction = planExpiringAction; 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/ping-protection/commands/ping-protection.js b/modules/ping-protection/commands/ping-protection.js new file mode 100644 index 00000000..d8ac43c7 --- /dev/null +++ b/modules/ping-protection/commands/ping-protection.js @@ -0,0 +1,235 @@ +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-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 + }); +} + +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..acd5b7d0 --- /dev/null +++ b/modules/ping-protection/configs/configuration.json @@ -0,0 +1,273 @@ +{ + "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." + }, + "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" + }, + "description": { + "en": "Specific roles which are protected from pings." + }, + "type": "array", + "content": "roleID", + "default": { + "en": [] + } + }, + { + "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." + }, + "type": "boolean", + "default": { + "en": true + } + }, + { + "name": "protectedUsers", + "category": "protection", + "humanName": { + "en": "Protected Users" + }, + "description": { + "en": "Specific users who are protected from pings." + }, + "type": "array", + "content": "userID", + "default": { + "en": [] + } + }, + { + "name": "ignoredRoles", + "category": "whitelisted", + "humanName": { + "en": "Whitelisted Roles" + }, + "description": { + "en": "Roles allowed to ping protected members or roles." + }, + "type": "array", + "content": "roleID", + "default": { + "en": [] + } + }, + { + "name": "ignoredChannels", + "category": "whitelisted", + "humanName": { + "en": "Whitelisted Channels" + }, + "description": { + "en": "Pings in these channels are ignored." + }, + "type": "array", + "content": "channelID", + "default": { + "en": [] + } + }, + { + "name": "ignoredUsers", + "category": "whitelisted", + "humanName": { + "en": "Whitelisted Users" + }, + "description": { + "en": "Pings from these users are ignored." + }, + "type": "array", + "content": "userID", + "default": { + "en": [] + } + }, + { + "name": "allowReplyPings", + "category": "rules", + "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", + "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." + }, + "type": "select", + "content": [ + "Get punished like normal members", + "Ignored", + "Get fun easter eggs when pinging themselves" + ], + "default": { + "en": "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." + }, + "type": "boolean", + "default": { + "en": true + } + }, + { + "name": "autoModLogChannel", + "category": "automod", + "humanName": { + "en": "AutoMod Log Channel" + }, + "description": { + "en": "Channel where AutoMod alerts are sent." + }, + "type": "channelID", + "default": { + "en": [] + }, + "channelTypes": [ + "GUILD_TEXT" + ], + "dependsOn": "enableAutomod" + }, + { + "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)." + }, + "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." + }, + "dependsOn": "enableAutomod" + }, + { + "name": "pingWarningMessage", + "category": "messages", + "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" + } + } + } + ] +} \ No newline at end of file diff --git a/modules/ping-protection/configs/moderation.json b/modules/ping-protection/configs/moderation.json new file mode 100644 index 00000000..1c15ed63 --- /dev/null +++ b/modules/ping-protection/configs/moderation.json @@ -0,0 +1,157 @@ +{ + "filename": "moderation.json", + "humanName": { + "en": "Moderation Actions" + }, + "configElementName": { + "en": { + "one": "punishment", + "more": "punishment" + } + }, + "description": { + "en": "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." + }, + "type": "integer", + "default": { + "en": 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." + }, + "type": "boolean", + "default": { + "en": false + } + }, + { + "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..995a1ca1 --- /dev/null +++ b/modules/ping-protection/configs/storage.json @@ -0,0 +1,126 @@ +{ + "filename": "storage.json", + "humanName": { + "en": "Data Storage" + }, + "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" + }, + "description": { + "en": "If enabled, the bot will keep a history of pings to enforce moderation actions." + }, + "type": "boolean", + "default": { + "en": 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." + }, + "type": "integer", + "default": { + "en": 12 + }, + "minValue": "4", + "maxValue": "96", + "dependsOn": "enablePingHistory" + }, + { + "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." + }, + "type": "boolean", + "default": { + "en": 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." + }, + "type": "integer", + "default": { + "en": 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." + }, + "type": "boolean", + "default": { + "en": 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)." + }, + "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..042de12a --- /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', 'modal-success-data-deletion', {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..012143dd --- /dev/null +++ b/modules/ping-protection/ping-protection.js @@ -0,0 +1,671 @@ +/** + * 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-data-deletion', { + 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 || 12)); + 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.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); + 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 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/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/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..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}); - 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/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..ea4925b5 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; @@ -16,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); } @@ -56,15 +71,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 +92,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/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/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..36864556 100644 --- a/src/commands/help.js +++ b/src/commands/help.js @@ -1,139 +1,308 @@ -const {truncate, formatDate, sendMultipleSiteButtonMessage, formatDiscordUserName} = require('../functions/helpers'); -const {MessageEmbed} = require('discord.js'); +const { + truncate, + formatDate, + parseEmbedColor +} = require('../functions/helpers'); +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'), - - /* - *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}) - }); - } - 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('RANDOM') - .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; } - 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}) + const overviewComponents = buildOverviewComponents(currentSelectPage); + const m = await interaction.reply({ + components: overviewComponents, + flags: MessageFlags.IsComponentsV2, + fetchReply: true }); - sendMultipleSiteButtonMessage(interaction.channel, sites, [interaction.user.id], interaction); + 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/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..b70ef01d --- /dev/null +++ b/src/discordjs-fix.js @@ -0,0 +1,153 @@ +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; + 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); + 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..99f0554e 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,20 @@ 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 (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']; @@ -63,9 +74,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 +95,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 +106,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..2e500f74 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); @@ -80,29 +93,35 @@ 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) { 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..386dcc9e 100644 --- a/src/functions/helpers.js +++ b/src/functions/helpers.js @@ -3,10 +3,30 @@ * @module Helpers */ -const {MessageEmbed, MessageAttachment} = 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.net'); +const privatebin = new PrivatebinClient('https://paste.scootkit.com'); const isoCrypto = require('isomorphic-webcrypto'); const {encode} = require('bs58'); const crypto = require('crypto'); @@ -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,8 +102,70 @@ 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 = { + '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 @@ -69,6 +176,7 @@ module.exports.inputReplacer = inputReplacer; * @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'); @@ -79,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({ @@ -99,10 +215,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 +235,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 +252,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'])); @@ -150,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); @@ -161,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; }; @@ -207,7 +677,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 +776,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({ @@ -319,12 +789,14 @@ async function sendMultipleSiteButtonMessage(channel, sites = [], allowedUserIDs await interaction.update({ components: [{type: 'ACTION_ROW', components: getButtons(nextSite)}], embeds: [sites[nextSite - 1]] + }).catch(() => { }); }); c.on('end', () => { m.edit({ components: [{type: 'ACTION_ROW', components: getButtons(currentSite, true)}], embeds: [sites[currentSite - 1]] + }).catch(() => { }); }); @@ -453,29 +925,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 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); + } - for (const roleID of allowedRoles) { - await channel.permissionOverwrites.create(roleID, { - SEND_MESSAGES: true + 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 +967,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 +1033,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 +1043,12 @@ 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; +}; 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`) } /**