diff --git a/language/brazilianPortuguese.json b/language/brazilianPortuguese.json index eda133c..ffb566d 100644 --- a/language/brazilianPortuguese.json +++ b/language/brazilianPortuguese.json @@ -71,5 +71,43 @@ "errorRoleAssignTitle": "Erro de Atribuição de Role", "errorRoleAssignMessage": "Falha ao atribuir a role verificado/não verificado a um usuário. Por favor verifique:\n• A role do bot está acima das roles verificado/não verificado\n• As roles ainda existem\n• O bot tem a permissão 'Gerenciar Cargos'\n\n💡 **Dica:** Mova a role do EmailBot acima das roles verificado/não verificado em Configurações do Servidor → Cargos, ou atribua a role verificado/não verificado diretamente ao bot.\n[Ver exemplo](https://raw.githubusercontent.com/lkaesberg/EmailBot/main/images/bothierarchy.png)", "errorBotNotConfiguredTitle": "Bot Não Configurado", - "errorBotNotConfiguredMessage": "Um usuário tentou se verificar mas o bot não está configurado corretamente.\n\n**Configuração necessária:**\n• Definir domínios de email permitidos com `/domain add`\n• Definir role verificado com `/role verified`\n\nExecute `/status` para verificar a configuração atual." + "errorBotNotConfiguredMessage": "Um usuário tentou se verificar mas o bot não está configurado corretamente.\n\n**Configuração necessária:**\n• Definir domínios de email permitidos com `/domain add`\n• Definir role verificado com `/role verified`\n\nExecute `/status` para verificar a configuração atual.", + "emaillistEmpty": "📧 **Nenhum endereço de email permitido configurado.**\n\nEnvie um arquivo CSV com `/emaillist upload` para permitir endereços de email específicos para verificação.", + "emaillistListHeader": "📧 **Endereços de email permitidos (%VAR% no total):**", + "emaillistListMore": "e mais %VAR%...", + "emaillistAlreadyEmpty": "⚠️ **A lista de emails já está vazia.**", + "emaillistCleared": "🗑️ **Lista de emails limpa!**\n\n%VAR% endereço(s) de email removido(s).", + "emaillistInvalidFile": "❌ **Tipo de arquivo inválido.**\n\nPor favor envie um arquivo `.csv` ou `.txt` com um endereço de email por linha.", + "emaillistFileTooLarge": "❌ **Arquivo muito grande.**\n\nO arquivo deve ser menor que 1MB.", + "emaillistNoValidEmails": "❌ **Nenhum endereço de email válido encontrado no arquivo.**\n\nCertifique-se de que o arquivo contém um endereço de email por linha (ex. `usuario@exemplo.com`).", + "emaillistUploaded": "✅ **Lista de emails enviada!**\n\n%VAR% endereço(s) de email único(s) adicionado(s). Usuários com esses endereços podem agora se verificar.", + "emaillistUploadError": "❌ **Falha ao processar o arquivo.**\n\nCertifique-se de que o arquivo é um arquivo CSV/texto válido e tente novamente.", + "emailModalAllowedEmails": "📋 Além disso, **%VAR%** endereços de email específicos são permitidos.", + "premiumMailLimitTitle": "📬 Limite Mensal de Emails Atingido", + "premiumMailLimitDescription": "Este servidor enviou **%VAR%/%VAR%** emails de verificação gratuitos este mês.\n\n**Como continuar verificando:**\n• Assine para verificações **ilimitadas**\n• Compre um pacote de créditos (100 / 500 / 2.000 verificações)\n\nOs créditos nunca expiram e acumulam de mês a mês. Use `/premium status` para verificar seu plano.", + "premiumCsvRequiredTitle": "🔒 Recurso Premium", + "premiumCsvRequiredDescription": "Os recursos de importação e exportação CSV requerem um plano premium.\n\n**Como desbloquear:**\n• Assine o plano **Pro** (inclui CSV + emails ilimitados)\n• Compre um **desbloqueio CSV** único\n\nUse `/premium status` para verificar seu plano.", + "premiumStatusTitle": "💳 Status Premium", + "premiumPlanFree": "🆓 Gratuito", + "premiumPlanStandard": "⭐ Padrão", + "premiumPlanPro": "💎 Pro", + "premiumMailsUnlimited": "**%VAR%** enviados este mês (ilimitado)", + "premiumMailsLimited": "**%VAR%** / **%VAR%** gratuitos este mês (%VAR% restantes)", + "premiumCreditsRemaining": "**%VAR%** créditos restantes", + "premiumCsvUnlocked": "✅ Desbloqueado", + "premiumCsvLocked": "🔒 Bloqueado", + "premiumStatusFooter": "Assine ou compre créditos para desbloquear mais recursos", + "premiumRedeemTitle": "🎁 Resgatar Compras", + "premiumCreditsAdded": "Créditos Adicionados", + "premiumNotEnabled": "A monetização não está habilitada nesta instância do bot.", + "premiumNoRedeemable": "Nenhuma compra resgatável encontrada.", + "premiumRedeemCredits": "✅ **%VAR% créditos** resgatados", + "premiumRedeemCreditsFailed": "❌ Falha ao resgatar pacote de %VAR% créditos", + "premiumRedeemCsv": "✅ Recursos de **importação e exportação CSV** desbloqueados", + "premiumRedeemCsvFailed": "❌ Falha ao desbloquear recursos CSV", + "premiumFetchFailed": "Falha ao buscar suas compras no Discord.", + "premiumFieldPlan": "📋 Plano", + "premiumFieldEmails": "📧 Emails", + "premiumFieldCredits": "🎟️ Créditos Bônus", + "premiumFieldCsv": "📁 Recursos CSV" } diff --git a/language/english.json b/language/english.json index 1f7b3f5..a545a6e 100644 --- a/language/english.json +++ b/language/english.json @@ -71,5 +71,43 @@ "errorRoleAssignTitle": "Role Assignment Error", "errorRoleAssignMessage": "Failed to assign the verified/unverified role to a user. Please ensure:\n• The bot's role is higher than the verified/unverified roles\n• The roles still exist\n• The bot has 'Manage Roles' permission\n\n💡 **Tip:** Either move the EmailBot role above the verified/unverified roles in Server Settings → Roles, or assign the verified/unverified role to the bot directly.\n[See example](https://raw.githubusercontent.com/lkaesberg/EmailBot/main/images/bothierarchy.png)", "errorBotNotConfiguredTitle": "Bot Not Configured", - "errorBotNotConfiguredMessage": "A user tried to verify but the bot is not properly configured.\n\n**Required setup:**\n• Set allowed email domains with `/domain add`\n• Set default roles with `/role add` or domain-specific roles with `/domainrole add`\n\nRun `/status` to check the current configuration." + "errorBotNotConfiguredMessage": "A user tried to verify but the bot is not properly configured.\n\n**Required setup:**\n• Set allowed email domains with `/domain add`\n• Set default roles with `/role add` or domain-specific roles with `/domainrole add`\n\nRun `/status` to check the current configuration.", + "emaillistEmpty": "📧 **No allowed email addresses configured.**\n\nUpload a CSV file with `/emaillist upload` to allow specific email addresses to verify.", + "emaillistListHeader": "📧 **Allowed email addresses (%VAR% total):**", + "emaillistListMore": "and %VAR% more...", + "emaillistAlreadyEmpty": "⚠️ **Email list is already empty.**", + "emaillistCleared": "🗑️ **Email list cleared!**\n\nRemoved %VAR% email address(es).", + "emaillistInvalidFile": "❌ **Invalid file type.**\n\nPlease upload a `.csv` or `.txt` file with one email address per row.", + "emaillistFileTooLarge": "❌ **File too large.**\n\nThe file must be smaller than 1MB.", + "emaillistNoValidEmails": "❌ **No valid email addresses found in the file.**\n\nMake sure the file contains one email address per row (e.g. `user@example.com`).", + "emaillistUploaded": "✅ **Email list uploaded!**\n\nAdded %VAR% unique email address(es). Users with these addresses can now verify.", + "emaillistUploadError": "❌ **Failed to process the file.**\n\nPlease make sure the file is a valid CSV/text file and try again.", + "emailModalAllowedEmails": "📋 Additionally, **%VAR%** specific email addresses are allowed.", + "premiumMailLimitTitle": "📬 Monthly Email Limit Reached", + "premiumMailLimitDescription": "This server has sent **%VAR%/%VAR%** free verification emails this month.\n\n**How to continue verifying:**\n• Subscribe for **unlimited** verifications\n• Purchase a credit pack (100 / 500 / 2,000 verifications)\n\nCredits never expire and roll over month to month. Use `/premium status` to check your plan.", + "premiumCsvRequiredTitle": "🔒 Premium Feature", + "premiumCsvRequiredDescription": "CSV import and export features require a premium plan.\n\n**How to unlock:**\n• Subscribe to the **Pro** plan (includes CSV + unlimited mails)\n• Purchase a one-time **CSV unlock**\n\nUse `/premium status` to check your plan.", + "premiumStatusTitle": "💳 Premium Status", + "premiumPlanFree": "🆓 Free", + "premiumPlanStandard": "⭐ Standard", + "premiumPlanPro": "💎 Pro", + "premiumMailsUnlimited": "**%VAR%** sent this month (unlimited)", + "premiumMailsLimited": "**%VAR%** / **%VAR%** free this month (%VAR% remaining)", + "premiumCreditsRemaining": "**%VAR%** credits remaining", + "premiumCsvUnlocked": "✅ Unlocked", + "premiumCsvLocked": "🔒 Locked", + "premiumStatusFooter": "Subscribe or purchase credits to unlock more features", + "premiumRedeemTitle": "🎁 Redeem Purchases", + "premiumCreditsAdded": "Credits Added", + "premiumNotEnabled": "Monetization is not enabled on this bot instance.", + "premiumNoRedeemable": "No unredeemed purchases found.", + "premiumRedeemCredits": "✅ Redeemed **%VAR% credits**", + "premiumRedeemCreditsFailed": "❌ Failed to redeem %VAR% credit pack", + "premiumRedeemCsv": "✅ Unlocked **CSV import & export** features", + "premiumRedeemCsvFailed": "❌ Failed to unlock CSV features", + "premiumFetchFailed": "Failed to fetch your purchases from Discord.", + "premiumFieldPlan": "📋 Plan", + "premiumFieldEmails": "📧 Emails", + "premiumFieldCredits": "🎟️ Bonus Credits", + "premiumFieldCsv": "📁 CSV Features" } diff --git a/language/french.json b/language/french.json index 19edbf8..3a4417c 100644 --- a/language/french.json +++ b/language/french.json @@ -71,6 +71,44 @@ "errorRoleAssignTitle": "Erreur d'Attribution de Rôle", "errorRoleAssignMessage": "Impossible d'attribuer le rôle vérifié/non-vérifié à un utilisateur. Veuillez vérifier :\n• Le rôle du bot est plus haut que les rôles vérifié/non-vérifié\n• Les rôles existent toujours\n• Le bot a la permission 'Gérer les Rôles'\n\n💡 **Astuce :** Déplacez le rôle EmailBot au-dessus des rôles vérifié/non-vérifié dans Paramètres du serveur → Rôles, ou attribuez le rôle vérifié/non-vérifié directement au bot.\n[Voir l'exemple](https://raw.githubusercontent.com/lkaesberg/EmailBot/main/images/bothierarchy.png)", "errorBotNotConfiguredTitle": "Bot Non Configuré", - "errorBotNotConfiguredMessage": "Un utilisateur a tenté de se vérifier mais le bot n'est pas correctement configuré.\n\n**Configuration requise :**\n• Définir les domaines email autorisés avec `/domain add`\n• Définir le rôle vérifié avec `/role verified`\n\nExécutez `/status` pour vérifier la configuration actuelle." - } + "errorBotNotConfiguredMessage": "Un utilisateur a tenté de se vérifier mais le bot n'est pas correctement configuré.\n\n**Configuration requise :**\n• Définir les domaines email autorisés avec `/domain add`\n• Définir le rôle vérifié avec `/role verified`\n\nExécutez `/status` pour vérifier la configuration actuelle.", + "emaillistEmpty": "📧 **Aucune adresse email autorisée configurée.**\n\nTéléchargez un fichier CSV avec `/emaillist upload` pour autoriser des adresses email spécifiques à se vérifier.", + "emaillistListHeader": "📧 **Adresses email autorisées (%VAR% au total) :**", + "emaillistListMore": "et %VAR% de plus...", + "emaillistAlreadyEmpty": "⚠️ **La liste d'emails est déjà vide.**", + "emaillistCleared": "🗑️ **Liste d'emails effacée !**\n\n%VAR% adresse(s) email supprimée(s).", + "emaillistInvalidFile": "❌ **Type de fichier invalide.**\n\nVeuillez télécharger un fichier `.csv` ou `.txt` avec une adresse email par ligne.", + "emaillistFileTooLarge": "❌ **Fichier trop volumineux.**\n\nLe fichier doit être inférieur à 1 Mo.", + "emaillistNoValidEmails": "❌ **Aucune adresse email valide trouvée dans le fichier.**\n\nAssurez-vous que le fichier contient une adresse email par ligne (ex. `utilisateur@exemple.com`).", + "emaillistUploaded": "✅ **Liste d'emails téléchargée !**\n\n%VAR% adresse(s) email unique(s) ajoutée(s). Les utilisateurs avec ces adresses peuvent maintenant se vérifier.", + "emaillistUploadError": "❌ **Échec du traitement du fichier.**\n\nVeuillez vous assurer que le fichier est un fichier CSV/texte valide et réessayer.", +"emailModalAllowedEmails": "📋 De plus, **%VAR%** adresses email spécifiques sont autorisées.", + "premiumMailLimitTitle": "📬 Limite Mensuelle d'Emails Atteinte", + "premiumMailLimitDescription": "Ce serveur a envoyé **%VAR%/%VAR%** emails de vérification gratuits ce mois-ci.\n\n**Comment continuer à vérifier :**\n• Abonnez-vous pour des vérifications **illimitées**\n• Achetez un pack de crédits (100 / 500 / 2 000 vérifications)\n\nLes crédits n'expirent jamais et sont reportés de mois en mois. Utilisez `/premium status` pour vérifier votre plan.", + "premiumCsvRequiredTitle": "🔒 Fonctionnalité Premium", + "premiumCsvRequiredDescription": "Les fonctionnalités d'import et d'export CSV nécessitent un plan premium.\n\n**Comment débloquer :**\n• Abonnez-vous au plan **Pro** (inclut CSV + mails illimités)\n• Achetez un **déblocage CSV** unique\n\nUtilisez `/premium status` pour vérifier votre plan.", + "premiumStatusTitle": "💳 Statut Premium", + "premiumPlanFree": "🆓 Gratuit", + "premiumPlanStandard": "⭐ Standard", + "premiumPlanPro": "💎 Pro", + "premiumMailsUnlimited": "**%VAR%** envoyés ce mois-ci (illimité)", + "premiumMailsLimited": "**%VAR%** / **%VAR%** gratuits ce mois-ci (%VAR% restants)", + "premiumCreditsRemaining": "**%VAR%** crédits restants", + "premiumCsvUnlocked": "✅ Débloqué", + "premiumCsvLocked": "🔒 Verrouillé", + "premiumStatusFooter": "Abonnez-vous ou achetez des crédits pour débloquer plus de fonctionnalités", + "premiumRedeemTitle": "🎁 Échanger les Achats", + "premiumCreditsAdded": "Crédits Ajoutés", + "premiumNotEnabled": "La monétisation n'est pas activée sur cette instance du bot.", + "premiumNoRedeemable": "Aucun achat à échanger trouvé.", + "premiumRedeemCredits": "✅ **%VAR% crédits** échangés", + "premiumRedeemCreditsFailed": "❌ Échec de l'échange du pack de %VAR% crédits", + "premiumRedeemCsv": "✅ Fonctionnalités **import & export CSV** débloquées", + "premiumRedeemCsvFailed": "❌ Échec du déblocage des fonctionnalités CSV", + "premiumFetchFailed": "Impossible de récupérer vos achats depuis Discord.", + "premiumFieldPlan": "📋 Plan", + "premiumFieldEmails": "📧 Emails", + "premiumFieldCredits": "🎟️ Crédits Bonus", + "premiumFieldCsv": "📁 Fonctionnalités CSV" +} \ No newline at end of file diff --git a/language/german.json b/language/german.json index 3db50ae..dcf81d9 100644 --- a/language/german.json +++ b/language/german.json @@ -71,5 +71,43 @@ "errorRoleAssignTitle": "Rollenzuweisungs-Fehler", "errorRoleAssignMessage": "Die Verifiziert/Unverifiziert-Rolle konnte einem Benutzer nicht zugewiesen werden. Bitte stelle sicher:\n• Die Bot-Rolle ist höher als die Verifiziert/Unverifiziert-Rollen\n• Die Rollen existieren noch\n• Der Bot hat die 'Rollen verwalten'-Berechtigung\n\n💡 **Tipp:** Verschiebe die EmailBot-Rolle über die Verifiziert/Unverifiziert-Rollen in Servereinstellungen → Rollen, oder weise dem Bot die Verifiziert/Unverifiziert-Rolle direkt zu.\n[Beispiel ansehen](https://raw.githubusercontent.com/lkaesberg/EmailBot/main/images/bothierarchy.png)", "errorBotNotConfiguredTitle": "Bot Nicht Konfiguriert", - "errorBotNotConfiguredMessage": "Ein Benutzer hat versucht sich zu verifizieren, aber der Bot ist nicht richtig konfiguriert.\n\n**Erforderliche Einrichtung:**\n• Erlaubte E-Mail-Domains mit `/domain add` festlegen\n• Verifiziert-Rolle mit `/role verified` festlegen\n\nFühre `/status` aus, um die aktuelle Konfiguration zu überprüfen." + "errorBotNotConfiguredMessage": "Ein Benutzer hat versucht sich zu verifizieren, aber der Bot ist nicht richtig konfiguriert.\n\n**Erforderliche Einrichtung:**\n• Erlaubte E-Mail-Domains mit `/domain add` festlegen\n• Verifiziert-Rolle mit `/role verified` festlegen\n\nFühre `/status` aus, um die aktuelle Konfiguration zu überprüfen.", + "emaillistEmpty": "📧 **Keine erlaubten E-Mail-Adressen konfiguriert.**\n\nLade eine CSV-Datei mit `/emaillist upload` hoch, um bestimmte E-Mail-Adressen zur Verifizierung zuzulassen.", + "emaillistListHeader": "📧 **Erlaubte E-Mail-Adressen (%VAR% insgesamt):**", + "emaillistListMore": "und %VAR% weitere...", + "emaillistAlreadyEmpty": "⚠️ **E-Mail-Liste ist bereits leer.**", + "emaillistCleared": "🗑️ **E-Mail-Liste gelöscht!**\n\n%VAR% E-Mail-Adresse(n) entfernt.", + "emaillistInvalidFile": "❌ **Ungültiger Dateityp.**\n\nBitte lade eine `.csv`- oder `.txt`-Datei mit einer E-Mail-Adresse pro Zeile hoch.", + "emaillistFileTooLarge": "❌ **Datei zu groß.**\n\nDie Datei muss kleiner als 1MB sein.", + "emaillistNoValidEmails": "❌ **Keine gültigen E-Mail-Adressen in der Datei gefunden.**\n\nStelle sicher, dass die Datei eine E-Mail-Adresse pro Zeile enthält (z.B. `benutzer@beispiel.com`).", + "emaillistUploaded": "✅ **E-Mail-Liste hochgeladen!**\n\n%VAR% eindeutige E-Mail-Adresse(n) hinzugefügt. Benutzer mit diesen Adressen können sich jetzt verifizieren.", + "emaillistUploadError": "❌ **Fehler beim Verarbeiten der Datei.**\n\nBitte stelle sicher, dass die Datei eine gültige CSV-/Textdatei ist und versuche es erneut.", + "emailModalAllowedEmails": "📋 Zusätzlich sind **%VAR%** bestimmte E-Mail-Adressen erlaubt.", + "premiumMailLimitTitle": "📬 Monatliches E-Mail-Limit erreicht", + "premiumMailLimitDescription": "Dieser Server hat **%VAR%/%VAR%** kostenlose Verifizierungs-E-Mails diesen Monat gesendet.\n\n**So kannst du weiter verifizieren:**\n• Abonniere für **unbegrenzte** Verifizierungen\n• Kaufe ein Guthaben-Paket (100 / 500 / 2.000 Verifizierungen)\n\nGuthaben verfällt nie und wird von Monat zu Monat übertragen. Nutze `/premium status` um deinen Plan zu prüfen.", + "premiumCsvRequiredTitle": "🔒 Premium-Funktion", + "premiumCsvRequiredDescription": "CSV-Import und -Export erfordern einen Premium-Plan.\n\n**So schaltest du frei:**\n• Abonniere den **Pro**-Plan (inkl. CSV + unbegrenzte Mails)\n• Kaufe eine einmalige **CSV-Freischaltung**\n\nNutze `/premium status` um deinen Plan zu prüfen.", + "premiumStatusTitle": "💳 Premium-Status", + "premiumPlanFree": "🆓 Kostenlos", + "premiumPlanStandard": "⭐ Standard", + "premiumPlanPro": "💎 Pro", + "premiumMailsUnlimited": "**%VAR%** diesen Monat gesendet (unbegrenzt)", + "premiumMailsLimited": "**%VAR%** / **%VAR%** kostenlos diesen Monat (%VAR% verbleibend)", + "premiumCreditsRemaining": "**%VAR%** Guthaben verbleibend", + "premiumCsvUnlocked": "✅ Freigeschaltet", + "premiumCsvLocked": "🔒 Gesperrt", + "premiumStatusFooter": "Abonniere oder kaufe Guthaben um weitere Funktionen freizuschalten", + "premiumRedeemTitle": "🎁 Käufe einlösen", + "premiumCreditsAdded": "Guthaben hinzugefügt", + "premiumNotEnabled": "Monetarisierung ist auf dieser Bot-Instanz nicht aktiviert.", + "premiumNoRedeemable": "Keine einlösbaren Käufe gefunden.", + "premiumRedeemCredits": "✅ **%VAR% Guthaben** eingelöst", + "premiumRedeemCreditsFailed": "❌ Einlösen von %VAR% Guthaben-Paket fehlgeschlagen", + "premiumRedeemCsv": "✅ **CSV-Import & -Export** freigeschaltet", + "premiumRedeemCsvFailed": "❌ CSV-Freischaltung fehlgeschlagen", + "premiumFetchFailed": "Käufe konnten nicht von Discord abgerufen werden.", + "premiumFieldPlan": "📋 Plan", + "premiumFieldEmails": "📧 E-Mails", + "premiumFieldCredits": "🎟️ Bonus-Guthaben", + "premiumFieldCsv": "📁 CSV-Funktionen" } diff --git a/language/hebrew.json b/language/hebrew.json index cb98d38..4d60334 100644 --- a/language/hebrew.json +++ b/language/hebrew.json @@ -71,5 +71,43 @@ "errorRoleAssignTitle": "שגיאת הקצאת תפקיד", "errorRoleAssignMessage": "נכשל בהקצאת תפקיד מאומת/לא מאומת למשתמש. אנא ודא:\n• תפקיד הבוט גבוה יותר מתפקידי מאומת/לא מאומת\n• התפקידים עדיין קיימים\n• לבוט יש הרשאת 'ניהול תפקידים'\n\n💡 **טיפ:** העבר את תפקיד EmailBot מעל תפקידי מאומת/לא מאומת בהגדרות שרת → תפקידים, או הקצה את התפקיד מאומת/לא מאומת ישירות לבוט.\n[צפה בדוגמה](https://raw.githubusercontent.com/lkaesberg/EmailBot/main/images/bothierarchy.png)", "errorBotNotConfiguredTitle": "הבוט לא מוגדר", - "errorBotNotConfiguredMessage": "משתמש ניסה לאמת אבל הבוט לא מוגדר כראוי.\n\n**הגדרה נדרשת:**\n• הגדר דומיינים מותרים עם `/domain add`\n• הגדר תפקיד מאומת עם `/role verified`\n\nהרץ `/status` כדי לבדוק את ההגדרה הנוכחית." + "errorBotNotConfiguredMessage": "משתמש ניסה לאמת אבל הבוט לא מוגדר כראוי.\n\n**הגדרה נדרשת:**\n• הגדר דומיינים מותרים עם `/domain add`\n• הגדר תפקיד מאומת עם `/role verified`\n\nהרץ `/status` כדי לבדוק את ההגדרה הנוכחית.", + "emaillistEmpty": "📧 **לא הוגדרו כתובות מייל מותרות.**\n\nהעלה קובץ CSV עם `/emaillist upload` כדי לאפשר כתובות מייל ספציפיות לאימות.", + "emaillistListHeader": "📧 **כתובות מייל מותרות (%VAR% בסך הכל):**", + "emaillistListMore": "ועוד %VAR%...", + "emaillistAlreadyEmpty": "⚠️ **רשימת המיילים כבר ריקה.**", + "emaillistCleared": "🗑️ **רשימת המיילים נמחקה!**\n\n%VAR% כתובת/ות מייל הוסרו.", + "emaillistInvalidFile": "❌ **סוג קובץ לא חוקי.**\n\nאנא העלה קובץ `.csv` או `.txt` עם כתובת מייל אחת בכל שורה.", + "emaillistFileTooLarge": "❌ **הקובץ גדול מדי.**\n\nהקובץ חייב להיות קטן מ-1MB.", + "emaillistNoValidEmails": "❌ **לא נמצאו כתובות מייל תקינות בקובץ.**\n\nוודא שהקובץ מכיל כתובת מייל אחת בכל שורה (לדוגמה `user@example.com`).", + "emaillistUploaded": "✅ **רשימת המיילים הועלתה!**\n\n%VAR% כתובת/ות מייל ייחודיות נוספו. משתמשים עם כתובות אלו יכולים כעת לאמת.", + "emaillistUploadError": "❌ **נכשל בעיבוד הקובץ.**\n\nאנא ודא שהקובץ הוא קובץ CSV/טקסט תקין ונסה שוב.", + "emailModalAllowedEmails": "📋 בנוסף, **%VAR%** כתובות מייל ספציפיות מותרות.", + "premiumMailLimitTitle": "📬 הגעת למגבלת הדוא\"ל החודשית", + "premiumMailLimitDescription": "שרת זה שלח **%VAR%/%VAR%** הודעות אימות בחינם החודש.\n\n**איך להמשיך לאמת:**\n• הירשם למנוי לאימותים **ללא הגבלה**\n• רכוש חבילת קרדיטים (100 / 500 / 2,000 אימותים)\n\nקרדיטים לא פגים לעולם ומצטברים מחודש לחודש. השתמש ב-`/premium status` כדי לבדוק את התוכנית שלך.", + "premiumCsvRequiredTitle": "🔒 תכונת פרימיום", + "premiumCsvRequiredDescription": "תכונות ייבוא וייצוא CSV דורשות תוכנית פרימיום.\n\n**איך לפתוח:**\n• הירשם לתוכנית **Pro** (כולל CSV + דוא\"ל ללא הגבלה)\n• רכוש **פתיחת CSV** חד-פעמית\n\nהשתמש ב-`/premium status` כדי לבדוק את התוכנית שלך.", + "premiumStatusTitle": "💳 סטטוס פרימיום", + "premiumPlanFree": "🆓 חינם", + "premiumPlanStandard": "⭐ רגיל", + "premiumPlanPro": "💎 פרו", + "premiumMailsUnlimited": "**%VAR%** נשלחו החודש (ללא הגבלה)", + "premiumMailsLimited": "**%VAR%** / **%VAR%** בחינם החודש (%VAR% נותרו)", + "premiumCreditsRemaining": "**%VAR%** קרדיטים נותרו", + "premiumCsvUnlocked": "✅ פתוח", + "premiumCsvLocked": "🔒 נעול", + "premiumStatusFooter": "הירשם למנוי או רכוש קרדיטים כדי לפתוח תכונות נוספות", + "premiumRedeemTitle": "🎁 מימוש רכישות", + "premiumCreditsAdded": "קרדיטים נוספו", + "premiumNotEnabled": "מוניטיזציה לא מופעלת במופע בוט זה.", + "premiumNoRedeemable": "לא נמצאו רכישות למימוש.", + "premiumRedeemCredits": "✅ מומשו **%VAR% קרדיטים**", + "premiumRedeemCreditsFailed": "❌ נכשל מימוש חבילת %VAR% קרדיטים", + "premiumRedeemCsv": "✅ נפתחו תכונות **ייבוא וייצוא CSV**", + "premiumRedeemCsvFailed": "❌ נכשל פתיחת תכונות CSV", + "premiumFetchFailed": "נכשל שליפת הרכישות שלך מ-Discord.", + "premiumFieldPlan": "📋 תוכנית", + "premiumFieldEmails": "📧 דוא\"ל", + "premiumFieldCredits": "🎟️ קרדיטים בונוס", + "premiumFieldCsv": "📁 תכונות CSV" } diff --git a/language/korean.json b/language/korean.json index 7db41b5..25fa005 100644 --- a/language/korean.json +++ b/language/korean.json @@ -71,5 +71,43 @@ "errorRoleAssignTitle": "역할 할당 오류", "errorRoleAssignMessage": "사용자에게 인증/미인증 역할을 할당하지 못했습니다. 확인해주세요:\n• 봇의 역할이 인증/미인증 역할보다 높은지\n• 역할이 여전히 존재하는지\n• 봇에 '역할 관리' 권한이 있는지\n\n💡 **팁:** 서버 설정 → 역할에서 EmailBot 역할을 인증/미인증 역할 위로 이동하거나, 인증/미인증 역할을 봇에 직접 할당하세요.\n[예시 보기](https://raw.githubusercontent.com/lkaesberg/EmailBot/main/images/bothierarchy.png)", "errorBotNotConfiguredTitle": "봇 미설정", - "errorBotNotConfiguredMessage": "사용자가 인증을 시도했지만 봇이 올바르게 구성되지 않았습니다.\n\n**필요한 설정:**\n• `/domain add`로 허용된 이메일 도메인 설정\n• `/role verified`로 인증된 역할 설정\n\n`/status`를 실행하여 현재 구성을 확인하세요." + "errorBotNotConfiguredMessage": "사용자가 인증을 시도했지만 봇이 올바르게 구성되지 않았습니다.\n\n**필요한 설정:**\n• `/domain add`로 허용된 이메일 도메인 설정\n• `/role verified`로 인증된 역할 설정\n\n`/status`를 실행하여 현재 구성을 확인하세요.", + "emaillistEmpty": "📧 **허용된 이메일 주소가 설정되지 않았습니다.**\n\n`/emaillist upload`로 CSV 파일을 업로드하여 특정 이메일 주소의 인증을 허용하세요.", + "emaillistListHeader": "📧 **허용된 이메일 주소 (총 %VAR%개):**", + "emaillistListMore": "그리고 %VAR%개 더...", + "emaillistAlreadyEmpty": "⚠️ **이메일 목록이 이미 비어있습니다.**", + "emaillistCleared": "🗑️ **이메일 목록이 삭제되었습니다!**\n\n%VAR%개의 이메일 주소가 제거되었습니다.", + "emaillistInvalidFile": "❌ **잘못된 파일 형식입니다.**\n\n한 줄에 하나의 이메일 주소가 포함된 `.csv` 또는 `.txt` 파일을 업로드해주세요.", + "emaillistFileTooLarge": "❌ **파일이 너무 큽니다.**\n\n파일은 1MB보다 작아야 합니다.", + "emaillistNoValidEmails": "❌ **파일에서 유효한 이메일 주소를 찾을 수 없습니다.**\n\n파일에 한 줄에 하나의 이메일 주소가 포함되어 있는지 확인하세요 (예: `user@example.com`).", + "emaillistUploaded": "✅ **이메일 목록이 업로드되었습니다!**\n\n%VAR%개의 고유한 이메일 주소가 추가되었습니다. 이 주소를 가진 사용자는 이제 인증할 수 있습니다.", + "emaillistUploadError": "❌ **파일 처리에 실패했습니다.**\n\n파일이 유효한 CSV/텍스트 파일인지 확인하고 다시 시도해주세요.", + "emailModalAllowedEmails": "📋 추가로, **%VAR%**개의 특정 이메일 주소가 허용됩니다.", + "premiumMailLimitTitle": "📬 월간 이메일 한도 도달", + "premiumMailLimitDescription": "이 서버는 이번 달 **%VAR%/%VAR%**개의 무료 인증 이메일을 발송했습니다.\n\n**인증을 계속하려면:**\n• **무제한** 인증을 위해 구독하세요\n• 크레딧 팩 구매 (100 / 500 / 2,000 인증)\n\n크레딧은 만료되지 않으며 매월 이월됩니다. `/premium status`로 플랜을 확인하세요.", + "premiumCsvRequiredTitle": "🔒 프리미엄 기능", + "premiumCsvRequiredDescription": "CSV 가져오기 및 내보내기 기능은 프리미엄 플랜이 필요합니다.\n\n**잠금 해제 방법:**\n• **Pro** 플랜 구독 (CSV + 무제한 메일 포함)\n• 일회성 **CSV 잠금 해제** 구매\n\n`/premium status`로 플랜을 확인하세요.", + "premiumStatusTitle": "💳 프리미엄 상태", + "premiumPlanFree": "🆓 무료", + "premiumPlanStandard": "⭐ 스탠다드", + "premiumPlanPro": "💎 프로", + "premiumMailsUnlimited": "이번 달 **%VAR%**개 발송 (무제한)", + "premiumMailsLimited": "이번 달 **%VAR%** / **%VAR%** 무료 (%VAR%개 남음)", + "premiumCreditsRemaining": "**%VAR%** 크레딧 남음", + "premiumCsvUnlocked": "✅ 잠금 해제됨", + "premiumCsvLocked": "🔒 잠금됨", + "premiumStatusFooter": "구독하거나 크레딧을 구매하여 더 많은 기능을 잠금 해제하세요", + "premiumRedeemTitle": "🎁 구매 교환", + "premiumCreditsAdded": "크레딧 추가됨", + "premiumNotEnabled": "이 봇 인스턴스에서는 수익화가 활성화되지 않았습니다.", + "premiumNoRedeemable": "교환 가능한 구매가 없습니다.", + "premiumRedeemCredits": "✅ **%VAR% 크레딧** 교환됨", + "premiumRedeemCreditsFailed": "❌ %VAR% 크레딧 팩 교환 실패", + "premiumRedeemCsv": "✅ **CSV 가져오기 및 내보내기** 기능 잠금 해제됨", + "premiumRedeemCsvFailed": "❌ CSV 기능 잠금 해제 실패", + "premiumFetchFailed": "Discord에서 구매 정보를 가져오지 못했습니다.", + "premiumFieldPlan": "📋 플랜", + "premiumFieldEmails": "📧 이메일", + "premiumFieldCredits": "🎟️ 보너스 크레딧", + "premiumFieldCsv": "📁 CSV 기능" } diff --git a/language/polish.json b/language/polish.json index 4896596..960ea9c 100644 --- a/language/polish.json +++ b/language/polish.json @@ -71,5 +71,43 @@ "errorRoleAssignTitle": "Błąd Przypisania Roli", "errorRoleAssignMessage": "Nie udało się przypisać roli zweryfikowany/niezweryfikowany do użytkownika. Upewnij się, że:\n• Rola bota jest wyżej niż role zweryfikowany/niezweryfikowany\n• Role nadal istnieją\n• Bot ma uprawnienie 'Zarządzaj Rolami'\n\n💡 **Wskazówka:** Przenieś rolę EmailBot powyżej ról zweryfikowany/niezweryfikowany w Ustawienia serwera → Role, lub przypisz rolę zweryfikowany/niezweryfikowany bezpośrednio do bota.\n[Zobacz przykład](https://raw.githubusercontent.com/lkaesberg/EmailBot/main/images/bothierarchy.png)", "errorBotNotConfiguredTitle": "Bot Nie Skonfigurowany", - "errorBotNotConfiguredMessage": "Użytkownik próbował się zweryfikować, ale bot nie jest poprawnie skonfigurowany.\n\n**Wymagana konfiguracja:**\n• Ustaw dozwolone domeny email za pomocą `/domain add`\n• Ustaw rolę zweryfikowanego za pomocą `/role verified`\n\nUruchom `/status` aby sprawdzić aktualną konfigurację." + "errorBotNotConfiguredMessage": "Użytkownik próbował się zweryfikować, ale bot nie jest poprawnie skonfigurowany.\n\n**Wymagana konfiguracja:**\n• Ustaw dozwolone domeny email za pomocą `/domain add`\n• Ustaw rolę zweryfikowanego za pomocą `/role verified`\n\nUruchom `/status` aby sprawdzić aktualną konfigurację.", + "emaillistEmpty": "📧 **Brak skonfigurowanych dozwolonych adresów email.**\n\nPrześlij plik CSV za pomocą `/emaillist upload` aby zezwolić na weryfikację określonych adresów email.", + "emaillistListHeader": "📧 **Dozwolone adresy email (%VAR% łącznie):**", + "emaillistListMore": "i jeszcze %VAR%...", + "emaillistAlreadyEmpty": "⚠️ **Lista email jest już pusta.**", + "emaillistCleared": "🗑️ **Lista email wyczyszczona!**\n\nUsunięto %VAR% adres(ów) email.", + "emaillistInvalidFile": "❌ **Nieprawidłowy typ pliku.**\n\nPrześlij plik `.csv` lub `.txt` z jednym adresem email w każdym wierszu.", + "emaillistFileTooLarge": "❌ **Plik zbyt duży.**\n\nPlik musi być mniejszy niż 1MB.", + "emaillistNoValidEmails": "❌ **Nie znaleziono prawidłowych adresów email w pliku.**\n\nUpewnij się, że plik zawiera jeden adres email w każdym wierszu (np. `uzytkownik@przyklad.com`).", + "emaillistUploaded": "✅ **Lista email przesłana!**\n\nDodano %VAR% unikalnych adres(ów) email. Użytkownicy z tymi adresami mogą teraz się zweryfikować.", + "emaillistUploadError": "❌ **Nie udało się przetworzyć pliku.**\n\nUpewnij się, że plik jest prawidłowym plikiem CSV/tekstowym i spróbuj ponownie.", + "emailModalAllowedEmails": "📋 Dodatkowo, **%VAR%** konkretnych adresów email jest dozwolonych.", + "premiumMailLimitTitle": "📬 Osiągnięto Miesięczny Limit Emaili", + "premiumMailLimitDescription": "Ten serwer wysłał **%VAR%/%VAR%** darmowych emaili weryfikacyjnych w tym miesiącu.\n\n**Jak kontynuować weryfikację:**\n• Subskrybuj dla **nieograniczonych** weryfikacji\n• Kup pakiet kredytów (100 / 500 / 2000 weryfikacji)\n\nKredyty nigdy nie wygasają i przenoszą się z miesiąca na miesiąc. Użyj `/premium status` aby sprawdzić swój plan.", + "premiumCsvRequiredTitle": "🔒 Funkcja Premium", + "premiumCsvRequiredDescription": "Funkcje importu i eksportu CSV wymagają planu premium.\n\n**Jak odblokować:**\n• Subskrybuj plan **Pro** (w tym CSV + nieograniczone maile)\n• Kup jednorazowe **odblokowanie CSV**\n\nUżyj `/premium status` aby sprawdzić swój plan.", + "premiumStatusTitle": "💳 Status Premium", + "premiumPlanFree": "🆓 Darmowy", + "premiumPlanStandard": "⭐ Standard", + "premiumPlanPro": "💎 Pro", + "premiumMailsUnlimited": "**%VAR%** wysłanych w tym miesiącu (bez limitu)", + "premiumMailsLimited": "**%VAR%** / **%VAR%** darmowych w tym miesiącu (%VAR% pozostało)", + "premiumCreditsRemaining": "**%VAR%** kredytów pozostało", + "premiumCsvUnlocked": "✅ Odblokowane", + "premiumCsvLocked": "🔒 Zablokowane", + "premiumStatusFooter": "Subskrybuj lub kup kredyty aby odblokować więcej funkcji", + "premiumRedeemTitle": "🎁 Wymień Zakupy", + "premiumCreditsAdded": "Dodano Kredyty", + "premiumNotEnabled": "Monetyzacja nie jest włączona na tej instancji bota.", + "premiumNoRedeemable": "Nie znaleziono zakupów do wymiany.", + "premiumRedeemCredits": "✅ Wymieniono **%VAR% kredytów**", + "premiumRedeemCreditsFailed": "❌ Nie udało się wymienić pakietu %VAR% kredytów", + "premiumRedeemCsv": "✅ Odblokowano funkcje **importu i eksportu CSV**", + "premiumRedeemCsvFailed": "❌ Nie udało się odblokować funkcji CSV", + "premiumFetchFailed": "Nie udało się pobrać zakupów z Discorda.", + "premiumFieldPlan": "📋 Plan", + "premiumFieldEmails": "📧 Emaile", + "premiumFieldCredits": "🎟️ Kredyty Bonus", + "premiumFieldCsv": "📁 Funkcje CSV" } diff --git a/language/spanish.json b/language/spanish.json index a02eef5..d503444 100644 --- a/language/spanish.json +++ b/language/spanish.json @@ -71,5 +71,43 @@ "errorRoleAssignTitle": "Error de Asignación de Rol", "errorRoleAssignMessage": "No se pudo asignar el rol verificado/no verificado a un usuario. Por favor asegúrese de:\n• El rol del bot está más alto que los roles verificado/no verificado\n• Los roles todavía existen\n• El bot tiene el permiso 'Gestionar Roles'\n\n💡 **Consejo:** Mueva el rol de EmailBot por encima de los roles verificado/no verificado en Configuración del servidor → Roles, o asigne el rol verificado/no verificado directamente al bot.\n[Ver ejemplo](https://raw.githubusercontent.com/lkaesberg/EmailBot/main/images/bothierarchy.png)", "errorBotNotConfiguredTitle": "Bot No Configurado", - "errorBotNotConfiguredMessage": "Un usuario intentó verificarse pero el bot no está configurado correctamente.\n\n**Configuración requerida:**\n• Establecer dominios de email permitidos con `/domain add`\n• Establecer rol verificado con `/role verified`\n\nEjecute `/status` para verificar la configuración actual." + "errorBotNotConfiguredMessage": "Un usuario intentó verificarse pero el bot no está configurado correctamente.\n\n**Configuración requerida:**\n• Establecer dominios de email permitidos con `/domain add`\n• Establecer rol verificado con `/role verified`\n\nEjecute `/status` para verificar la configuración actual.", + "emaillistEmpty": "📧 **No hay direcciones de email permitidas configuradas.**\n\nSube un archivo CSV con `/emaillist upload` para permitir la verificación de direcciones de email específicas.", + "emaillistListHeader": "📧 **Direcciones de email permitidas (%VAR% en total):**", + "emaillistListMore": "y %VAR% más...", + "emaillistAlreadyEmpty": "⚠️ **La lista de emails ya está vacía.**", + "emaillistCleared": "🗑️ **¡Lista de emails eliminada!**\n\n%VAR% dirección(es) de email eliminada(s).", + "emaillistInvalidFile": "❌ **Tipo de archivo inválido.**\n\nPor favor sube un archivo `.csv` o `.txt` con una dirección de email por línea.", + "emaillistFileTooLarge": "❌ **Archivo demasiado grande.**\n\nEl archivo debe ser menor de 1MB.", + "emaillistNoValidEmails": "❌ **No se encontraron direcciones de email válidas en el archivo.**\n\nAsegúrate de que el archivo contenga una dirección de email por línea (ej. `usuario@ejemplo.com`).", + "emaillistUploaded": "✅ **¡Lista de emails subida!**\n\n%VAR% dirección(es) de email única(s) añadida(s). Los usuarios con estas direcciones ahora pueden verificarse.", + "emaillistUploadError": "❌ **Error al procesar el archivo.**\n\nAsegúrate de que el archivo sea un archivo CSV/texto válido e inténtalo de nuevo.", + "emailModalAllowedEmails": "📋 Además, **%VAR%** direcciones de email específicas están permitidas.", + "premiumMailLimitTitle": "📬 Límite Mensual de Emails Alcanzado", + "premiumMailLimitDescription": "Este servidor ha enviado **%VAR%/%VAR%** emails de verificación gratuitos este mes.\n\n**Cómo seguir verificando:**\n• Suscríbete para verificaciones **ilimitadas**\n• Compra un paquete de créditos (100 / 500 / 2.000 verificaciones)\n\nLos créditos nunca expiran y se acumulan mes a mes. Usa `/premium status` para revisar tu plan.", + "premiumCsvRequiredTitle": "🔒 Función Premium", + "premiumCsvRequiredDescription": "Las funciones de importación y exportación CSV requieren un plan premium.\n\n**Cómo desbloquear:**\n• Suscríbete al plan **Pro** (incluye CSV + emails ilimitados)\n• Compra un **desbloqueo CSV** único\n\nUsa `/premium status` para revisar tu plan.", + "premiumStatusTitle": "💳 Estado Premium", + "premiumPlanFree": "🆓 Gratis", + "premiumPlanStandard": "⭐ Estándar", + "premiumPlanPro": "💎 Pro", + "premiumMailsUnlimited": "**%VAR%** enviados este mes (ilimitado)", + "premiumMailsLimited": "**%VAR%** / **%VAR%** gratis este mes (%VAR% restantes)", + "premiumCreditsRemaining": "**%VAR%** créditos restantes", + "premiumCsvUnlocked": "✅ Desbloqueado", + "premiumCsvLocked": "🔒 Bloqueado", + "premiumStatusFooter": "Suscríbete o compra créditos para desbloquear más funciones", + "premiumRedeemTitle": "🎁 Canjear Compras", + "premiumCreditsAdded": "Créditos Añadidos", + "premiumNotEnabled": "La monetización no está habilitada en esta instancia del bot.", + "premiumNoRedeemable": "No se encontraron compras canjeables.", + "premiumRedeemCredits": "✅ **%VAR% créditos** canjeados", + "premiumRedeemCreditsFailed": "❌ Error al canjear el paquete de %VAR% créditos", + "premiumRedeemCsv": "✅ Funciones de **importación y exportación CSV** desbloqueadas", + "premiumRedeemCsvFailed": "❌ Error al desbloquear las funciones CSV", + "premiumFetchFailed": "No se pudieron obtener tus compras de Discord.", + "premiumFieldPlan": "📋 Plan", + "premiumFieldEmails": "📧 Emails", + "premiumFieldCredits": "🎟️ Créditos Bonus", + "premiumFieldCsv": "📁 Funciones CSV" } diff --git a/language/thai.json b/language/thai.json index dc5d9ba..d01d306 100644 --- a/language/thai.json +++ b/language/thai.json @@ -71,5 +71,43 @@ "errorRoleAssignTitle": "ข้อผิดพลาดการกำหนดบทบาท", "errorRoleAssignMessage": "ไม่สามารถกำหนดบทบาท verified/unverified ให้ผู้ใช้ได้ กรุณาตรวจสอบ:\n• บทบาทของบอทอยู่สูงกว่าบทบาท verified/unverified\n• บทบาทยังคงมีอยู่\n• บอทมีสิทธิ์ 'จัดการบทบาท'\n\n💡 **เคล็ดลับ:** ย้ายบทบาท EmailBot ให้อยู่เหนือบทบาท verified/unverified ในการตั้งค่าเซิร์ฟเวอร์ → บทบาท หรือกำหนดบทบาท verified/unverified ให้บอทโดยตรง\n[ดูตัวอย่าง](https://raw.githubusercontent.com/lkaesberg/EmailBot/main/images/bothierarchy.png)", "errorBotNotConfiguredTitle": "บอทไม่ได้ถูกตั้งค่า", - "errorBotNotConfiguredMessage": "ผู้ใช้พยายามยืนยันตัวตนแต่บอทไม่ได้ถูกตั้งค่าอย่างถูกต้อง\n\n**การตั้งค่าที่ต้องการ:**\n• ตั้งค่าโดเมนอีเมลที่อนุญาตด้วย `/domain add`\n• ตั้งค่าบทบาท verified ด้วย `/role verified`\n\nรัน `/status` เพื่อตรวจสอบการตั้งค่าปัจจุบัน" + "errorBotNotConfiguredMessage": "ผู้ใช้พยายามยืนยันตัวตนแต่บอทไม่ได้ถูกตั้งค่าอย่างถูกต้อง\n\n**การตั้งค่าที่ต้องการ:**\n• ตั้งค่าโดเมนอีเมลที่อนุญาตด้วย `/domain add`\n• ตั้งค่าบทบาท verified ด้วย `/role verified`\n\nรัน `/status` เพื่อตรวจสอบการตั้งค่าปัจจุบัน", + "emaillistEmpty": "📧 **ไม่มีที่อยู่อีเมลที่อนุญาตถูกตั้งค่า**\n\nอัปโหลดไฟล์ CSV ด้วย `/emaillist upload` เพื่ออนุญาตที่อยู่อีเมลเฉพาะในการยืนยัน", + "emaillistListHeader": "📧 **ที่อยู่อีเมลที่อนุญาต (ทั้งหมด %VAR%):**", + "emaillistListMore": "และอีก %VAR%...", + "emaillistAlreadyEmpty": "⚠️ **รายการอีเมลว่างเปล่าอยู่แล้ว**", + "emaillistCleared": "🗑️ **รายการอีเมลถูกล้างแล้ว!**\n\nลบ %VAR% ที่อยู่อีเมลแล้ว", + "emaillistInvalidFile": "❌ **ประเภทไฟล์ไม่ถูกต้อง**\n\nกรุณาอัปโหลดไฟล์ `.csv` หรือ `.txt` ที่มีที่อยู่อีเมลหนึ่งรายการต่อแถว", + "emaillistFileTooLarge": "❌ **ไฟล์ใหญ่เกินไป**\n\nไฟล์ต้องมีขนาดเล็กกว่า 1MB", + "emaillistNoValidEmails": "❌ **ไม่พบที่อยู่อีเมลที่ถูกต้องในไฟล์**\n\nตรวจสอบว่าไฟล์มีที่อยู่อีเมลหนึ่งรายการต่อแถว (เช่น `user@example.com`)", + "emaillistUploaded": "✅ **อัปโหลดรายการอีเมลแล้ว!**\n\nเพิ่ม %VAR% ที่อยู่อีเมลที่ไม่ซ้ำกัน ผู้ใช้ที่มีที่อยู่เหล่านี้สามารถยืนยันได้แล้ว", + "emaillistUploadError": "❌ **ไม่สามารถประมวลผลไฟล์ได้**\n\nกรุณาตรวจสอบว่าไฟล์เป็นไฟล์ CSV/ข้อความที่ถูกต้องและลองอีกครั้ง", + "emailModalAllowedEmails": "📋 นอกจากนี้ ที่อยู่อีเมลเฉพาะ **%VAR%** รายการได้รับอนุญาต", + "premiumMailLimitTitle": "📬 ถึงขีดจำกัดอีเมลรายเดือนแล้ว", + "premiumMailLimitDescription": "เซิร์ฟเวอร์นี้ส่งอีเมลยืนยันฟรี **%VAR%/%VAR%** ฉบับในเดือนนี้แล้ว\n\n**วิธีดำเนินการยืนยันต่อ:**\n• สมัครสมาชิกเพื่อการยืนยัน**ไม่จำกัด**\n• ซื้อแพ็คเครดิต (100 / 500 / 2,000 การยืนยัน)\n\nเครดิตไม่มีวันหมดอายุและสะสมได้ทุกเดือน ใช้ `/premium status` เพื่อตรวจสอบแผนของคุณ", + "premiumCsvRequiredTitle": "🔒 ฟีเจอร์พรีเมียม", + "premiumCsvRequiredDescription": "ฟีเจอร์นำเข้าและส่งออก CSV ต้องใช้แผนพรีเมียม\n\n**วิธีปลดล็อค:**\n• สมัครสมาชิกแผน **Pro** (รวม CSV + อีเมลไม่จำกัด)\n• ซื้อ**ปลดล็อค CSV** ครั้งเดียว\n\nใช้ `/premium status` เพื่อตรวจสอบแผนของคุณ", + "premiumStatusTitle": "💳 สถานะพรีเมียม", + "premiumPlanFree": "🆓 ฟรี", + "premiumPlanStandard": "⭐ มาตรฐาน", + "premiumPlanPro": "💎 โปร", + "premiumMailsUnlimited": "ส่ง **%VAR%** ฉบับในเดือนนี้ (ไม่จำกัด)", + "premiumMailsLimited": "**%VAR%** / **%VAR%** ฟรีในเดือนนี้ (เหลือ %VAR%)", + "premiumCreditsRemaining": "เหลือ **%VAR%** เครดิต", + "premiumCsvUnlocked": "✅ ปลดล็อคแล้ว", + "premiumCsvLocked": "🔒 ล็อคอยู่", + "premiumStatusFooter": "สมัครสมาชิกหรือซื้อเครดิตเพื่อปลดล็อคฟีเจอร์เพิ่มเติม", + "premiumRedeemTitle": "🎁 แลกการซื้อ", + "premiumCreditsAdded": "เพิ่มเครดิตแล้ว", + "premiumNotEnabled": "การสร้างรายได้ไม่ได้เปิดใช้งานในอินสแตนซ์บอทนี้", + "premiumNoRedeemable": "ไม่พบการซื้อที่แลกได้", + "premiumRedeemCredits": "✅ แลก **%VAR% เครดิต** แล้ว", + "premiumRedeemCreditsFailed": "❌ แลกแพ็คเครดิต %VAR% ไม่สำเร็จ", + "premiumRedeemCsv": "✅ ปลดล็อคฟีเจอร์ **นำเข้าและส่งออก CSV** แล้ว", + "premiumRedeemCsvFailed": "❌ ปลดล็อคฟีเจอร์ CSV ไม่สำเร็จ", + "premiumFetchFailed": "ไม่สามารถดึงข้อมูลการซื้อจาก Discord ได้", + "premiumFieldPlan": "📋 แผน", + "premiumFieldEmails": "📧 อีเมล", + "premiumFieldCredits": "🎟️ เครดิตโบนัส", + "premiumFieldCsv": "📁 ฟีเจอร์ CSV" } diff --git a/language/turkish.json b/language/turkish.json index c234eed..8d8fece 100644 --- a/language/turkish.json +++ b/language/turkish.json @@ -71,5 +71,43 @@ "errorRoleAssignTitle": "Rol Atama Hatası", "errorRoleAssignMessage": "Bir kullanıcıya doğrulanmış/doğrulanmamış rol atanamadı. Lütfen kontrol edin:\n• Bot rolü doğrulanmış/doğrulanmamış rollerden daha yüksekte\n• Roller hala mevcut\n• Bot 'Rolleri Yönet' iznine sahip\n\n💡 **İpucu:** Sunucu Ayarları → Roller'de EmailBot rolünü doğrulanmış/doğrulanmamış rollerin üzerine taşıyın veya doğrulanmış/doğrulanmamış rolü doğrudan bota atayın.\n[Örneğe bakın](https://raw.githubusercontent.com/lkaesberg/EmailBot/main/images/bothierarchy.png)", "errorBotNotConfiguredTitle": "Bot Yapılandırılmamış", - "errorBotNotConfiguredMessage": "Bir kullanıcı doğrulamaya çalıştı ancak bot düzgün yapılandırılmamış.\n\n**Gerekli kurulum:**\n• `/domain add` ile izin verilen e-posta domainlerini ayarlayın\n• `/role verified` ile doğrulanmış rolü ayarlayın\n\nMevcut yapılandırmayı kontrol etmek için `/status` çalıştırın." + "errorBotNotConfiguredMessage": "Bir kullanıcı doğrulamaya çalıştı ancak bot düzgün yapılandırılmamış.\n\n**Gerekli kurulum:**\n• `/domain add` ile izin verilen e-posta domainlerini ayarlayın\n• `/role verified` ile doğrulanmış rolü ayarlayın\n\nMevcut yapılandırmayı kontrol etmek için `/status` çalıştırın.", + "emaillistEmpty": "📧 **İzin verilen e-posta adresi yapılandırılmamış.**\n\nBelirli e-posta adreslerinin doğrulanmasına izin vermek için `/emaillist upload` ile bir CSV dosyası yükleyin.", + "emaillistListHeader": "📧 **İzin verilen e-posta adresleri (toplam %VAR%):**", + "emaillistListMore": "ve %VAR% daha...", + "emaillistAlreadyEmpty": "⚠️ **E-posta listesi zaten boş.**", + "emaillistCleared": "🗑️ **E-posta listesi temizlendi!**\n\n%VAR% e-posta adresi kaldırıldı.", + "emaillistInvalidFile": "❌ **Geçersiz dosya türü.**\n\nLütfen her satırda bir e-posta adresi olan bir `.csv` veya `.txt` dosyası yükleyin.", + "emaillistFileTooLarge": "❌ **Dosya çok büyük.**\n\nDosya 1MB'den küçük olmalıdır.", + "emaillistNoValidEmails": "❌ **Dosyada geçerli e-posta adresi bulunamadı.**\n\nDosyanın her satırda bir e-posta adresi içerdiğinden emin olun (örn. `kullanici@ornek.com`).", + "emaillistUploaded": "✅ **E-posta listesi yüklendi!**\n\n%VAR% benzersiz e-posta adresi eklendi. Bu adreslere sahip kullanıcılar artık doğrulanabilir.", + "emaillistUploadError": "❌ **Dosya işlenemedi.**\n\nDosyanın geçerli bir CSV/metin dosyası olduğundan emin olun ve tekrar deneyin.", + "emailModalAllowedEmails": "📋 Ayrıca, **%VAR%** belirli e-posta adresine izin verilmektedir.", + "premiumMailLimitTitle": "📬 Aylık E-posta Limiti Doldu", + "premiumMailLimitDescription": "Bu sunucu bu ay **%VAR%/%VAR%** ücretsiz doğrulama e-postası gönderdi.\n\n**Doğrulamaya devam etmek için:**\n• **Sınırsız** doğrulama için abone olun\n• Kredi paketi satın alın (100 / 500 / 2.000 doğrulama)\n\nKrediler asla süresi dolmaz ve her ay aktarılır. Planınızı kontrol etmek için `/premium status` kullanın.", + "premiumCsvRequiredTitle": "🔒 Premium Özellik", + "premiumCsvRequiredDescription": "CSV içe ve dışa aktarma özellikleri premium plan gerektirir.\n\n**Nasıl açılır:**\n• **Pro** plana abone olun (CSV + sınırsız e-posta dahil)\n• Tek seferlik **CSV kilidi açma** satın alın\n\nPlanınızı kontrol etmek için `/premium status` kullanın.", + "premiumStatusTitle": "💳 Premium Durumu", + "premiumPlanFree": "🆓 Ücretsiz", + "premiumPlanStandard": "⭐ Standart", + "premiumPlanPro": "💎 Pro", + "premiumMailsUnlimited": "Bu ay **%VAR%** gönderildi (sınırsız)", + "premiumMailsLimited": "Bu ay **%VAR%** / **%VAR%** ücretsiz (%VAR% kalan)", + "premiumCreditsRemaining": "**%VAR%** kredi kaldı", + "premiumCsvUnlocked": "✅ Açık", + "premiumCsvLocked": "🔒 Kilitli", + "premiumStatusFooter": "Daha fazla özellik açmak için abone olun veya kredi satın alın", + "premiumRedeemTitle": "🎁 Satın Alımları Kullan", + "premiumCreditsAdded": "Kredi Eklendi", + "premiumNotEnabled": "Bu bot örneğinde para kazanma etkin değil.", + "premiumNoRedeemable": "Kullanılabilir satın alım bulunamadı.", + "premiumRedeemCredits": "✅ **%VAR% kredi** kullanıldı", + "premiumRedeemCreditsFailed": "❌ %VAR% kredi paketi kullanılamadı", + "premiumRedeemCsv": "✅ **CSV içe ve dışa aktarma** özellikleri açıldı", + "premiumRedeemCsvFailed": "❌ CSV özellikleri açılamadı", + "premiumFetchFailed": "Discord'dan satın alımlarınız getirilemedi.", + "premiumFieldPlan": "📋 Plan", + "premiumFieldEmails": "📧 E-postalar", + "premiumFieldCredits": "🎟️ Bonus Kredi", + "premiumFieldCsv": "📁 CSV Özellikleri" } diff --git a/src/EmailBot.js b/src/EmailBot.js index 348f284..8c87c78 100644 --- a/src/EmailBot.js +++ b/src/EmailBot.js @@ -20,9 +20,10 @@ const UserTimeout = require("./UserTimeout"); const md5hash = require("./crypto/Crypto"); const EmailUser = require("./database/EmailUser"); const { MessageFlags } = require('discord.js'); -const { createSessionExpiredEmbed, createInvalidCodeEmbed, createInvalidEmailEmbed, createVerificationSuccessEmbed, createCodeSentEmbed } = require('./utils/embeds'); +const { createSessionExpiredEmbed, createInvalidCodeEmbed, createInvalidEmailEmbed, createVerificationSuccessEmbed, createCodeSentEmbed, createMailLimitReachedEmbed } = require('./utils/embeds'); const ErrorNotifier = require('./utils/ErrorNotifier'); const { emailMatchesDomains, emailIsBlacklisted, getMatchingDomainPatterns } = require('./utils/wildcardMatch'); +const premiumManager = require('./premium/PremiumManager'); const bot = new Discord.Client({ intents: [ @@ -406,10 +407,13 @@ bot.on('interactionCreate', async interaction => { return } // Domain allowlist check (supports wildcards, e.g., @*.edu, @*.harvard.edu) + // Also checks against uploaded email list const hasValidFormat = emailText.split("@").length - 1 === 1 && !emailText.includes(' ') const matchesDomain = emailMatchesDomains(emailText, serverSettings.domains) + const allowedEmails = serverSettings.allowedEmails || [] + const isInAllowedList = allowedEmails.includes(emailText.toLowerCase()) - if (!hasValidFormat || !matchesDomain) { + if (!hasValidFormat || (!matchesDomain && !isInAllowedList)) { await interaction.followUp({ embeds: [createInvalidEmailEmbed(serverSettings.language)], flags: MessageFlags.Ephemeral }).catch(() => {}) return } @@ -431,6 +435,27 @@ bot.on('interactionCreate', async interaction => { userTimeout.timestamp = Date.now() userTimeout.increaseWaitTime() + // Premium check: verify the guild hasn't exceeded its free monthly limit + const premiumCheck = await premiumManager.canSendMail(userGuild.id, interaction.entitlements) + if (!premiumCheck.allowed) { + const limitEmbed = createMailLimitReachedEmbed(serverSettings.language, premiumCheck.mailsSentMonth, premiumCheck.freeLimit) + const { monetization } = require('../config/config.json') + const skus = monetization?.skus || {} + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setStyle(ButtonStyle.Premium) + .setSKUId(skus.subscriptionTier1) + ) + try { + await interaction.followUp({ embeds: [limitEmbed], components: [row], flags: MessageFlags.Ephemeral }) + } catch (err) { + if (err.code === 50035) { + await interaction.followUp({ embeds: [limitEmbed], components: [], flags: MessageFlags.Ephemeral }).catch(() => {}) + } + } + return + } + const code = Math.floor((Math.random() + 1) * 100000).toString() // Send email and store code on success await mailSender.sendEmail(emailText.toLowerCase(), code, userGuild.name, interaction, emailNotify, async (email) => { @@ -648,7 +673,7 @@ bot.on('interactionCreate', async interaction => { try { // Allow all users to use /verify and /data (delete-user subcommand is user-accessible) // Allow /globalstats for owner check to happen inside the command - if (interaction.member.permissions.has(PermissionsBitField.Flags.Administrator) || interaction.commandName === "data" || interaction.commandName === "verify" || interaction.commandName === "globalstats") { + if (interaction.member.permissions.has(PermissionsBitField.Flags.Administrator) || interaction.commandName === "data" || interaction.commandName === "verify" || interaction.commandName === "globalstats" || interaction.commandName === "premium") { await command.execute(interaction); } else { await interaction.reply({ diff --git a/src/bot/showEmailModal.js b/src/bot/showEmailModal.js index 5d91bd5..2f745e0 100644 --- a/src/bot/showEmailModal.js +++ b/src/bot/showEmailModal.js @@ -93,6 +93,7 @@ async function showEmailModal(interaction, guild, userGuilds) { const domains = serverSettings.domains || [] const domainRoles = serverSettings.domainRoles || {} const defaultRoles = serverSettings.defaultRoles || [] + const allowedEmails = serverSettings.allowedEmails || [] // Helper to get role names from IDs const getRoleNames = (roleIds) => { @@ -110,7 +111,9 @@ async function showEmailModal(interaction, guild, userGuilds) { // Check if all domains are accepted (if ANY domain is a full wildcard, all emails are accepted) const hasNoDomains = domains.length === 0 const hasAnyFullWildcard = domains.some(d => isFullWildcard(d)) - const allDomainsAccepted = hasNoDomains || hasAnyFullWildcard + const hasAllowedEmails = allowedEmails.length > 0 + const allDomainsAccepted = (!hasNoDomains && hasAnyFullWildcard) || (hasNoDomains && !hasAllowedEmails) + const onlyEmailList = hasNoDomains && hasAllowedEmails // Get default role names for display const defaultRoleNames = getRoleNames(defaultRoles) @@ -150,6 +153,12 @@ async function showEmailModal(interaction, guild, userGuilds) { } }) } + } else if (onlyEmailList) { + // Only email list, no domains - show default roles + const defaultRoleNamesDisplay = getRoleNames(defaultRoles) + if (defaultRoleNamesDisplay.length > 0) { + headerText += `\n\n${getLocale(language, "emailModalRolesAssigned")}: ${defaultRoleNamesDisplay.join(', ')}` + } } else { // Show formatted domain list with roles headerText += `\n\n${getLocale(language, "emailModalAcceptedDomains")}` @@ -164,6 +173,11 @@ async function showEmailModal(interaction, guild, userGuilds) { }) } + // Show allowed email list info if present + if (hasAllowedEmails) { + headerText += `\n\n${getLocale(language, "emailModalAllowedEmails", allowedEmails.length.toString())}` + } + // Add custom verify message if set if (serverSettings.verifyMessage !== "") { headerText += `\n\n${serverSettings.verifyMessage}` diff --git a/src/commands/emaillist.js b/src/commands/emaillist.js new file mode 100644 index 0000000..88fa4e6 --- /dev/null +++ b/src/commands/emaillist.js @@ -0,0 +1,172 @@ +const { SlashCommandBuilder } = require("@discordjs/builders"); +const { MessageFlags, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); +const database = require("../database/Database.js"); +const { getLocale } = require("../Language"); +const premiumManager = require("../premium/PremiumManager"); +const { createCSVPremiumRequiredEmbed } = require("../utils/embeds"); + +module.exports = { + data: new SlashCommandBuilder() + .setName('emaillist') + .setDescription('Manage allowed email addresses via CSV upload') + .addSubcommand(subcommand => + subcommand + .setName('upload') + .setDescription('Upload a CSV file with allowed email addresses (one per row)') + .addAttachmentOption(option => + option + .setName('file') + .setDescription('CSV file with one email address per row') + .setRequired(true) + ) + ) + .addSubcommand(subcommand => + subcommand + .setName('list') + .setDescription('View all currently allowed email addresses') + ) + .addSubcommand(subcommand => + subcommand + .setName('clear') + .setDescription('Remove all allowed email addresses from the list') + ) + .setDefaultMemberPermissions(0), + + async execute(interaction) { + const subcommand = interaction.options.getSubcommand(); + + await database.getServerSettings(interaction.guildId, async serverSettings => { + const language = serverSettings.language || 'english'; + + if (subcommand === 'list') { + const emails = serverSettings.allowedEmails || []; + if (emails.length === 0) { + await interaction.reply({ + content: getLocale(language, "emaillistEmpty"), + flags: MessageFlags.Ephemeral + }); + } else { + const displayLimit = 20; + const shown = emails.slice(0, displayLimit).map(e => `\`${e}\``).join('\n• '); + const remaining = emails.length - displayLimit; + let content = getLocale(language, "emaillistListHeader", emails.length.toString()) + `\n• ${shown}`; + if (remaining > 0) { + content += `\n\n... ${getLocale(language, "emaillistListMore", remaining.toString())}`; + } + await interaction.reply({ + content: content, + flags: MessageFlags.Ephemeral + }); + } + return; + } + + if (subcommand === 'clear') { + const count = (serverSettings.allowedEmails || []).length; + if (count === 0) { + await interaction.reply({ + content: getLocale(language, "emaillistAlreadyEmpty"), + flags: MessageFlags.Ephemeral + }); + return; + } + serverSettings.allowedEmails = []; + database.updateServerSettings(interaction.guildId, serverSettings); + await interaction.reply({ + content: getLocale(language, "emaillistCleared", count.toString()), + flags: MessageFlags.Ephemeral + }); + return; + } + + if (subcommand === 'upload') { + // Premium gate: CSV upload requires tier 2 subscription or CSV unlock + const csvCheck = await premiumManager.canUseCSVFeature(interaction.guildId, interaction.entitlements) + if (!csvCheck.allowed) { + const { monetization } = require('../../config/config.json') + const skus = monetization?.skus || {} + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setStyle(ButtonStyle.Premium) + .setSKUId(skus.subscriptionTier2) + ) + try { + await interaction.reply({ embeds: [createCSVPremiumRequiredEmbed(language)], components: [row], flags: MessageFlags.Ephemeral }) + } catch (err) { + if (err.code === 50035) { + await interaction.reply({ embeds: [createCSVPremiumRequiredEmbed(language)], components: [], flags: MessageFlags.Ephemeral }) + } else { + throw err + } + } + return + } + + const attachment = interaction.options.getAttachment('file', true); + + // Validate file type + if (!attachment.name.endsWith('.csv') && !attachment.name.endsWith('.txt')) { + await interaction.reply({ + content: getLocale(language, "emaillistInvalidFile"), + flags: MessageFlags.Ephemeral + }); + return; + } + + // Limit file size (1MB max) + if (attachment.size > 1024 * 1024) { + await interaction.reply({ + content: getLocale(language, "emaillistFileTooLarge"), + flags: MessageFlags.Ephemeral + }); + return; + } + + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); + + try { + const response = await fetch(attachment.url); + const text = await response.text(); + + // Parse emails: one per row, handle CSV with commas, trim whitespace + const emails = []; + const lines = text.split(/\r?\n/); + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + + for (const line of lines) { + // Split by comma to handle CSV with multiple columns + const parts = line.split(','); + for (const part of parts) { + const trimmed = part.trim().toLowerCase().replace(/^["']|["']$/g, ''); + if (trimmed && emailRegex.test(trimmed)) { + emails.push(trimmed); + } + } + } + + if (emails.length === 0) { + await interaction.editReply({ + content: getLocale(language, "emaillistNoValidEmails") + }); + return; + } + + // Deduplicate + const uniqueEmails = [...new Set(emails)]; + + serverSettings.allowedEmails = uniqueEmails; + database.updateServerSettings(interaction.guildId, serverSettings); + + await interaction.editReply({ + content: getLocale(language, "emaillistUploaded", uniqueEmails.length.toString()) + }); + } catch (error) { + console.error('Error processing email list:', error); + await interaction.editReply({ + content: getLocale(language, "emaillistUploadError") + }); + } + } + }); + } +}; diff --git a/src/commands/export.js b/src/commands/export.js new file mode 100644 index 0000000..b122d4b --- /dev/null +++ b/src/commands/export.js @@ -0,0 +1,267 @@ +const { SlashCommandBuilder } = require("@discordjs/builders"); +const { MessageFlags, AttachmentBuilder, PermissionFlagsBits, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); +const database = require("../database/Database.js"); +const premiumManager = require("../premium/PremiumManager"); +const { createCSVPremiumRequiredEmbed } = require("../utils/embeds"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("export") + .setDescription("Export verification logs as CSV") + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + .addSubcommand(subcommand => + subcommand + .setName('logs') + .setDescription('Export verification log channel messages as CSV') + .addIntegerOption(option => + option + .setName('limit') + .setDescription('Maximum number of messages to fetch (default: 1000, max: 10000)') + .setRequired(false) + .setMinValue(1) + .setMaxValue(10000) + ) + ), + + async execute(interaction) { + const subcommand = interaction.options.getSubcommand(); + + if (subcommand === 'logs') { + // Premium gate: CSV export requires tier 2 subscription or CSV unlock + const csvCheck = await premiumManager.canUseCSVFeature(interaction.guildId, interaction.entitlements) + if (!csvCheck.allowed) { + const { monetization } = require('../../config/config.json') + const skus = monetization?.skus || {} + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setStyle(ButtonStyle.Premium) + .setSKUId(skus.subscriptionTier2) + ) + await database.getServerSettings(interaction.guildId, async serverSettings => { + const language = serverSettings.language || 'english' + try { + await interaction.reply({ embeds: [createCSVPremiumRequiredEmbed(language)], components: [row], flags: MessageFlags.Ephemeral }) + } catch (err) { + if (err.code === 50035) { + await interaction.reply({ embeds: [createCSVPremiumRequiredEmbed(language)], components: [], flags: MessageFlags.Ephemeral }) + } else { + throw err + } + } + }) + return + } + + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); + + await database.getServerSettings(interaction.guildId, async serverSettings => { + if (!serverSettings.logChannel || serverSettings.logChannel === "") { + await interaction.editReply({ + content: "❌ **No log channel configured!**\n\nUse `/settings logchannel` to set up a verification log channel first.", + }); + return; + } + + const logChannel = interaction.guild.channels.cache.get(serverSettings.logChannel); + if (!logChannel) { + await interaction.editReply({ + content: "❌ **Log channel not found!**\n\nThe configured log channel may have been deleted.", + }); + return; + } + + const limit = interaction.options.getInteger('limit') || 1000; + + try { + // Fetch messages in batches (Discord API limit is 100 per request) + let allMessages = []; + let lastMessageId = null; + let remaining = limit; + + await interaction.editReply({ + content: `⏳ Fetching messages from <#${serverSettings.logChannel}>...`, + }); + + while (remaining > 0) { + const fetchLimit = Math.min(remaining, 100); + const options = { limit: fetchLimit }; + if (lastMessageId) { + options.before = lastMessageId; + } + + const messages = await logChannel.messages.fetch(options); + if (messages.size === 0) break; + + // Filter to only bot's own messages + const botMessages = messages.filter(msg => msg.author.id === interaction.client.user.id); + allMessages.push(...botMessages.values()); + + lastMessageId = messages.last().id; + remaining -= messages.size; + + // Break if we got fewer messages than requested (end of channel) + if (messages.size < fetchLimit) break; + } + + if (allMessages.length === 0) { + await interaction.editReply({ + content: "❌ **No verification logs found!**\n\nThe bot hasn't logged any verifications yet, or the messages have been deleted.", + }); + return; + } + + // Parse messages and build CSV + const csvRows = ['timestamp,user_id,username,email,type,verified_by,tags']; + let successCount = 0; + let parseErrors = 0; + + for (const message of allMessages) { + const parsed = parseLogMessage(message.content); + if (parsed) { + // Try to get username from mention + let username = ''; + try { + const member = await interaction.guild.members.fetch(parsed.userId).catch(() => null); + username = member ? member.user.username : ''; + } catch { + username = ''; + } + + // Replace commas in tags with semicolons to avoid CSV issues + const tags = parsed.tags ? parsed.tags.replace(/,/g, ';') : ''; + + csvRows.push([ + message.createdAt.toISOString(), + parsed.userId, + escapeCsvField(username), + escapeCsvField(parsed.email), + parsed.type, + parsed.verifiedBy || '', + tags + ].join(',')); + successCount++; + } else { + parseErrors++; + } + } + + if (successCount === 0) { + await interaction.editReply({ + content: "❌ **Could not parse any log messages!**\n\nThe log format may have changed or messages are in an unexpected format.", + }); + return; + } + + // Create CSV file + const csvContent = csvRows.join('\n'); + const buffer = Buffer.from(csvContent, 'utf-8'); + const attachment = new AttachmentBuilder(buffer, { + name: `verification-logs-${interaction.guildId}-${Date.now()}.csv` + }); + + let summaryMessage = `✅ **Export complete!**\n\n`; + summaryMessage += `📊 **Entries exported:** ${successCount}\n`; + if (parseErrors > 0) { + summaryMessage += `⚠️ **Parse errors:** ${parseErrors} (unrecognized format)\n`; + } + summaryMessage += `📅 **Date range:** ${allMessages[allMessages.length - 1].createdAt.toLocaleDateString()} - ${allMessages[0].createdAt.toLocaleDateString()}`; + + await interaction.editReply({ + content: summaryMessage, + files: [attachment] + }); + + } catch (error) { + console.error('Export error:', error); + await interaction.editReply({ + content: "❌ **Export failed!**\n\nMake sure the bot has permission to read the log channel.", + }); + } + }); + } + }, +}; + +/** + * Parse a log message and extract user ID and email + * Formats: + * - Current: ✅ <@123456789> → `email@example.com` + * - With tags: ✅ <@123456789> → `email@example.com` [Ver, TEST] + * - Manual: 🔧 <@123456789> → `email@example.com` (by <@987654321>) + * - Legacy: Authorized: <@123456789> → email@example.com + */ +function parseLogMessage(content) { + // Current format with optional tags: ✅ <@userId> → `email` [tags] + const regularMatchWithTags = content.match(/^✅\s*<@!?(\d+)>\s*→\s*`([^`]+)`\s*\[([^\]]+)\]$/); + if (regularMatchWithTags) { + return { + userId: regularMatchWithTags[1], + email: regularMatchWithTags[2], + type: 'auto', + verifiedBy: null, + tags: regularMatchWithTags[3] + }; + } + + // Current format without tags: ✅ <@userId> → `email` + const regularMatch = content.match(/^✅\s*<@!?(\d+)>\s*→\s*`([^`]+)`$/); + if (regularMatch) { + return { + userId: regularMatch[1], + email: regularMatch[2], + type: 'auto', + verifiedBy: null, + tags: null + }; + } + + // Manual verification with optional tags: 🔧 <@userId> → `email` (by <@adminId>) [tags] + const manualMatchWithTags = content.match(/^🔧\s*<@!?(\d+)>\s*→\s*`([^`]+)`\s*\(by\s*<@!?(\d+)>\)\s*\[([^\]]+)\]$/); + if (manualMatchWithTags) { + return { + userId: manualMatchWithTags[1], + email: manualMatchWithTags[2], + type: 'manual', + verifiedBy: manualMatchWithTags[3], + tags: manualMatchWithTags[4] + }; + } + + // Manual verification without tags: 🔧 <@userId> → `email` (by <@adminId>) + const manualMatch = content.match(/^🔧\s*<@!?(\d+)>\s*→\s*`([^`]+)`\s*\(by\s*<@!?(\d+)>\)$/); + if (manualMatch) { + return { + userId: manualMatch[1], + email: manualMatch[2], + type: 'manual', + verifiedBy: manualMatch[3], + tags: null + }; + } + + // Legacy format: Authorized: <@userId> → email + const legacyMatch = content.match(/^Authorized:\s*<@!?(\d+)>\s*→\s*(\S+@\S+)$/); + if (legacyMatch) { + return { + userId: legacyMatch[1], + email: legacyMatch[2].trim(), + type: 'auto', + verifiedBy: null, + tags: null + }; + } + + return null; +} + +/** + * Escape a field for CSV (handle commas, quotes, newlines) + */ +function escapeCsvField(field) { + if (!field) return ''; + const str = String(field); + if (str.includes(',') || str.includes('"') || str.includes('\n')) { + return `"${str.replace(/"/g, '""')}"`; + } + return str; +} diff --git a/src/commands/premium.js b/src/commands/premium.js new file mode 100644 index 0000000..acc1f70 --- /dev/null +++ b/src/commands/premium.js @@ -0,0 +1,114 @@ +const { SlashCommandBuilder } = require("@discordjs/builders"); +const { MessageFlags, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); +const premiumManager = require("../premium/PremiumManager"); +const database = require("../database/Database"); +const { getLocale } = require("../Language"); +const { monetization } = require('../../config/config.json'); + +const skus = monetization?.skus || {} + +module.exports = { + data: new SlashCommandBuilder() + .setName('premium') + .setDescription('Manage premium features and view subscription status') + .addSubcommand(subcommand => + subcommand + .setName('status') + .setDescription('View this server\'s premium plan, credits, and usage') + ) + .addSubcommand(subcommand => + subcommand + .setName('redeem') + .setDescription('Redeem your purchased credit packs or CSV unlock to this server') + ) + .setDefaultMemberPermissions(null), + + async execute(interaction) { + const subcommand = interaction.options.getSubcommand(); + + await database.getServerSettings(interaction.guildId, async serverSettings => { + const language = serverSettings.language || 'english' + + if (subcommand === 'status') { + if (!premiumManager.enabled) { + await interaction.reply({ + content: getLocale(language, 'premiumNotEnabled'), + flags: MessageFlags.Ephemeral + }) + return + } + + const status = await premiumManager.getPremiumStatus(interaction.guildId, interaction.entitlements) + + const tierName = status.subscriptionTier + ? getLocale(language, status.subscriptionTier === 'tier2' ? 'premiumPlanPro' : 'premiumPlanStandard') + : getLocale(language, 'premiumPlanFree') + + const mailsValue = status.hasUnlimitedMails + ? getLocale(language, 'premiumMailsUnlimited', status.mailsSentMonth.toString()) + : getLocale(language, 'premiumMailsLimited', status.mailsSentMonth.toString(), status.freeLimit.toString(), status.freeRemaining.toString()) + + const embed = new EmbedBuilder() + .setTitle(getLocale(language, 'premiumStatusTitle')) + .setColor(status.subscriptionTier ? 0x5865F2 : 0x99AAB5) + .addFields( + { name: getLocale(language, 'premiumFieldPlan'), value: tierName, inline: true }, + { name: getLocale(language, 'premiumFieldEmails'), value: mailsValue, inline: false }, + { name: getLocale(language, 'premiumFieldCredits'), value: getLocale(language, 'premiumCreditsRemaining', status.bonusCredits.toString()), inline: true }, + { name: getLocale(language, 'premiumFieldCsv'), value: status.csvUnlocked || status.subscriptionTier === 'tier2' ? getLocale(language, 'premiumCsvUnlocked') : getLocale(language, 'premiumCsvLocked'), inline: true }, + ) + + if (!status.subscriptionTier) { + embed.setFooter({ text: getLocale(language, 'premiumStatusFooter') }) + } + + const components = [] + if (!status.subscriptionTier && skus.subscriptionTier1) { + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setStyle(ButtonStyle.Premium) + .setSKUId(skus.subscriptionTier1) + ) + components.push(row) + } + + try { + await interaction.reply({ embeds: [embed], components, flags: MessageFlags.Ephemeral }) + } catch (err) { + // SKU may be unavailable/unpublished – retry without premium button + if (components.length > 0 && err.code === 50035) { + await interaction.reply({ embeds: [embed], components: [], flags: MessageFlags.Ephemeral }) + } else { + throw err + } + } + return + } + + if (subcommand === 'redeem') { + if (!premiumManager.enabled) { + await interaction.reply({ + content: getLocale(language, 'premiumNotEnabled'), + flags: MessageFlags.Ephemeral + }) + return + } + + await interaction.deferReply({ flags: MessageFlags.Ephemeral }) + + const results = await premiumManager.redeemEntitlements(interaction, interaction.guildId, language) + + const embed = new EmbedBuilder() + .setTitle(getLocale(language, 'premiumRedeemTitle')) + .setDescription(results.details.join('\n')) + .setColor(results.creditsAdded > 0 || results.csvUnlocked ? 0x57F287 : 0x99AAB5) + + if (results.creditsAdded > 0) { + embed.addFields({ name: getLocale(language, 'premiumCreditsAdded'), value: `+${results.creditsAdded}`, inline: true }) + } + + await interaction.editReply({ embeds: [embed] }) + } + }) + } +} diff --git a/src/commands/status.js b/src/commands/status.js index b121185..b4a2408 100644 --- a/src/commands/status.js +++ b/src/commands/status.js @@ -1,7 +1,8 @@ const {SlashCommandBuilder} = require('@discordjs/builders'); const database = require("../database/Database"); const { MessageFlags, EmbedBuilder } = require('discord.js'); - +const premiumManager = require("../premium/PremiumManager"); +const { getLocale } = require("../Language"); module.exports = { data: new SlashCommandBuilder().setDefaultPermission(true).setName('status').setDescription('View bot configuration, verification statistics, and check setup issues').setDefaultMemberPermissions(0), @@ -89,7 +90,7 @@ module.exports = { const issues = [] const hasAnyRoles = validDefaultRoles.length > 0 || domainRoleEntries.length > 0 if (!hasAnyRoles) issues.push('• No verified roles configured (use `/role add` or `/domainrole add`)') - if (serverSettings.domains.length === 0) issues.push('• No email domains configured') + if (serverSettings.domains.length === 0 && (serverSettings.allowedEmails || []).length === 0) issues.push('• No email domains or allowed emails configured') // Get current month name for display const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', @@ -130,6 +131,12 @@ module.exports = { name: '📧 Allowed Domains', value: domainsDisplay }, + { + name: '📋 Allowed Email List', + value: (serverSettings.allowedEmails || []).length > 0 + ? `${serverSettings.allowedEmails.length} email address(es)` + : '*None uploaded*' + }, { name: '🚫 Blacklisted Emails', value: blacklistDisplay @@ -174,6 +181,27 @@ module.exports = { }) } + // Add premium info when monetization is enabled + if (premiumManager.enabled) { + const premiumStatus = await premiumManager.getPremiumStatus(interaction.guildId, interaction.entitlements) + const lang = serverSettings.language || 'english' + const tierName = premiumStatus.subscriptionTier + ? getLocale(lang, premiumStatus.subscriptionTier === 'tier2' ? 'premiumPlanPro' : 'premiumPlanStandard') + : getLocale(lang, 'premiumPlanFree') + const mailsInfo = premiumStatus.hasUnlimitedMails + ? getLocale(lang, 'premiumMailsUnlimited', premiumStatus.mailsSentMonth.toString()) + : getLocale(lang, 'premiumMailsLimited', premiumStatus.mailsSentMonth.toString(), premiumStatus.freeLimit.toString(), premiumStatus.freeRemaining.toString()) + + statusEmbed.addFields({ + name: '💳 Premium', + value: + `**${getLocale(lang, 'premiumFieldPlan')}:** ${tierName}\n` + + `**${getLocale(lang, 'premiumFieldEmails')}:** ${mailsInfo}\n` + + `**${getLocale(lang, 'premiumFieldCredits')}:** ${premiumStatus.bonusCredits}\n` + + `**${getLocale(lang, 'premiumFieldCsv')}:** ${premiumStatus.csvUnlocked || premiumStatus.subscriptionTier === 'tier2' ? getLocale(lang, 'premiumCsvUnlocked') : getLocale(lang, 'premiumCsvLocked')}` + }) + } + statusEmbed.setFooter({ text: `Server: ${interaction.guild.name}`, iconURL: interaction.guild.iconURL({ dynamic: true }) diff --git a/src/database/Database.js b/src/database/Database.js index 7c55bf3..631a2ac 100644 --- a/src/database/Database.js +++ b/src/database/Database.js @@ -98,6 +98,18 @@ class Database { } }) }) + this.runMigration(12, () => { + // Add allowed emails list for CSV upload feature + this.db.run("ALTER TABLE guilds ADD allowedEmails TEXT DEFAULT '[]'") + }) + this.runMigration(13, () => { + // Premium / monetization: per-guild purchased credits and one-time CSV unlock + this.db.run(`CREATE TABLE IF NOT EXISTS guild_premium( + guildID TEXT PRIMARY KEY, + bonusCredits INTEGER DEFAULT 0, + csvUnlocked INTEGER DEFAULT 0 + );`) + }) } runMigration(version, migration) { @@ -128,8 +140,8 @@ class Database { updateServerSettings(guildID, serverSettings) { this.db.run( - "INSERT OR REPLACE INTO guilds (guildid, domains, blacklist, verifiedrole, unverifiedrole, channelid, messageid, language, autoVerify, autoAddUnverified, verifyMessage, logChannel, errorNotifyType, errorNotifyTarget, defaultRoles, domainRoles) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - [guildID, JSON.stringify(serverSettings.domains), JSON.stringify(serverSettings.blacklist), serverSettings.verifiedRoleName, serverSettings.unverifiedRoleName, serverSettings.channelID, serverSettings.messageID, serverSettings.language, serverSettings.autoVerify, serverSettings.autoAddUnverified, serverSettings.verifyMessage, serverSettings.logChannel, serverSettings.errorNotifyType, serverSettings.errorNotifyTarget, JSON.stringify(serverSettings.defaultRoles), JSON.stringify(serverSettings.domainRoles)]) + "INSERT OR REPLACE INTO guilds (guildid, domains, blacklist, verifiedrole, unverifiedrole, channelid, messageid, language, autoVerify, autoAddUnverified, verifyMessage, logChannel, errorNotifyType, errorNotifyTarget, defaultRoles, domainRoles, allowedEmails) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + [guildID, JSON.stringify(serverSettings.domains), JSON.stringify(serverSettings.blacklist), serverSettings.verifiedRoleName, serverSettings.unverifiedRoleName, serverSettings.channelID, serverSettings.messageID, serverSettings.language, serverSettings.autoVerify, serverSettings.autoAddUnverified, serverSettings.verifyMessage, serverSettings.logChannel, serverSettings.errorNotifyType, serverSettings.errorNotifyTarget, JSON.stringify(serverSettings.defaultRoles), JSON.stringify(serverSettings.domainRoles), JSON.stringify(serverSettings.allowedEmails)]) } async getServerSettings(guildID, callback) { @@ -181,6 +193,13 @@ class Database { serverSettings.domainRoles = {} } + // Parse allowedEmails (JSON array) + try { + serverSettings.allowedEmails = result.allowedEmails ? JSON.parse(result.allowedEmails) : [] + } catch { + serverSettings.allowedEmails = [] + } + // Legacy migration: if defaultRoles is empty but verifiedRoleName exists, use it if (serverSettings.defaultRoles.length === 0 && serverSettings.verifiedRoleName) { serverSettings.defaultRoles = [serverSettings.verifiedRoleName] @@ -315,6 +334,74 @@ class Database { }) }) } + + getGuildPremium(guildID) { + return new Promise((resolve, reject) => { + this.db.get("SELECT * FROM guild_premium WHERE guildID = ?", [guildID], (err, result) => { + if (err) { + console.error('Error getting guild premium:', err) + resolve({ bonusCredits: 0, csvUnlocked: false }) + return + } + if (result === undefined) { + resolve({ bonusCredits: 0, csvUnlocked: false }) + } else { + resolve({ bonusCredits: result.bonusCredits, csvUnlocked: !!result.csvUnlocked }) + } + }) + }) + } + + addGuildCredits(guildID, amount) { + return new Promise((resolve, reject) => { + this.db.run( + "INSERT INTO guild_premium (guildID, bonusCredits) VALUES (?, ?) ON CONFLICT(guildID) DO UPDATE SET bonusCredits = bonusCredits + ?", + [guildID, amount, amount], + (err) => { + if (err) { + console.error('Error adding guild credits:', err) + reject(err) + return + } + resolve() + } + ) + }) + } + + consumeGuildCredit(guildID) { + return new Promise((resolve, reject) => { + this.db.run( + "UPDATE guild_premium SET bonusCredits = bonusCredits - 1 WHERE guildID = ? AND bonusCredits > 0", + [guildID], + function (err) { + if (err) { + console.error('Error consuming guild credit:', err) + resolve(false) + return + } + resolve(this.changes > 0) + } + ) + }) + } + + unlockGuildCSV(guildID) { + return new Promise((resolve, reject) => { + this.db.run( + "INSERT INTO guild_premium (guildID, csvUnlocked) VALUES (?, 1) ON CONFLICT(guildID) DO UPDATE SET csvUnlocked = 1", + [guildID], + (err) => { + if (err) { + console.error('Error unlocking guild CSV:', err) + reject(err) + return + } + resolve() + } + ) + }) + } } const database = new Database() diff --git a/src/database/ServerSettings.js b/src/database/ServerSettings.js index 15cc974..e67999f 100644 --- a/src/database/ServerSettings.js +++ b/src/database/ServerSettings.js @@ -19,14 +19,17 @@ class ServerSettings { this.defaultRoles = [] // Domain-specific roles: { "@domain.com": ["roleId1", "roleId2"], "@*.edu": ["roleId3"] } this.domainRoles = {} + // Allowed email addresses uploaded via CSV (array of lowercase email strings) + this.allowedEmails = [] } get status() { - // Bot is configured if domains exist AND at least one role is configured (default or domain-specific) + // Bot is configured if (domains exist OR allowedEmails exist) AND at least one role is configured const hasRoles = this.defaultRoles.length > 0 || Object.keys(this.domainRoles).length > 0 || this.verifiedRoleName !== "" // Legacy support - return this.domains.length !== 0 && hasRoles + const hasEmailSource = this.domains.length !== 0 || this.allowedEmails.length !== 0 + return hasEmailSource && hasRoles } } diff --git a/src/premium/PremiumManager.js b/src/premium/PremiumManager.js new file mode 100644 index 0000000..887a5ca --- /dev/null +++ b/src/premium/PremiumManager.js @@ -0,0 +1,165 @@ +const database = require('../database/Database') +const config = require('../../config/config.json') +const { getLocale } = require('../Language') + +const monetization = config.monetization || { enabled: false } +const skus = monetization.skus || {} + +class PremiumManager { + get enabled() { + return !!monetization.enabled + } + + get freeMonthlyLimit() { + return monetization.freeMonthlyLimit ?? 50 + } + + /** + * Checks if a guild has an active subscription entitlement. + * Returns 'tier2', 'tier1', or null. + */ + getSubscriptionTier(entitlements) { + if (!entitlements || entitlements.size === 0) return null + if (skus.subscriptionTier2 && entitlements.some(e => e.skuId === skus.subscriptionTier2)) return 'tier2' + if (skus.subscriptionTier1 && entitlements.some(e => e.skuId === skus.subscriptionTier1)) return 'tier1' + return null + } + + /** + * Determines whether the guild is allowed to send an email. + * If credits are consumed, the credit is decremented atomically. + * Returns { allowed: boolean, reason: string|null, source: string } + */ + async canSendMail(guildID, entitlements) { + if (!this.enabled) return { allowed: true, source: 'disabled' } + + // 1. Check subscription (unlimited) + const tier = this.getSubscriptionTier(entitlements) + if (tier) return { allowed: true, source: 'subscription' } + + // 2. Check free monthly allowance + const stats = await new Promise(resolve => { + database.getGuildStats(guildID, resolve) + }) + if (stats.mailsSentMonth < this.freeMonthlyLimit) { + return { allowed: true, source: 'free' } + } + + // 3. Check bonus credits + const consumed = await database.consumeGuildCredit(guildID) + if (consumed) return { allowed: true, source: 'credits' } + + // 4. Denied + return { + allowed: false, + reason: 'limit_reached', + mailsSentMonth: stats.mailsSentMonth, + freeLimit: this.freeMonthlyLimit + } + } + + /** + * Determines whether the guild can use CSV features (export / emaillist upload). + * Returns { allowed: boolean } + */ + async canUseCSVFeature(guildID, entitlements) { + if (!this.enabled) return { allowed: true } + + // Tier 2 subscription includes CSV + const tier = this.getSubscriptionTier(entitlements) + if (tier === 'tier2') return { allowed: true } + + // One-time CSV purchase stored in DB + const premium = await database.getGuildPremium(guildID) + if (premium.csvUnlocked) return { allowed: true } + + return { allowed: false } + } + + /** + * Redeems the invoking user's unconsumed entitlements for the given guild. + * Consumes credit packs and processes CSV durable unlocks. + * Returns { creditsAdded: number, csvUnlocked: boolean, details: string[] } + */ + async redeemEntitlements(interaction, guildID, language) { + const lang = language || 'english' + const results = { creditsAdded: 0, csvUnlocked: false, details: [] } + + const creditSkuMap = {} + if (skus.credits100) creditSkuMap[skus.credits100] = 100 + if (skus.credits500) creditSkuMap[skus.credits500] = 500 + if (skus.credits2000) creditSkuMap[skus.credits2000] = 2000 + + let entitlements + try { + entitlements = await interaction.client.application.entitlements.fetch({ + user: interaction.user.id, + excludeEnded: true + }) + } catch (err) { + console.error('Failed to fetch entitlements:', err) + results.details.push(getLocale(lang, 'premiumFetchFailed')) + return results + } + + for (const [, entitlement] of entitlements) { + // Credit packs (consumable) + if (creditSkuMap[entitlement.skuId] && !entitlement.consumed) { + const amount = creditSkuMap[entitlement.skuId] + try { + await entitlement.consume() + await database.addGuildCredits(guildID, amount) + results.creditsAdded += amount + results.details.push(getLocale(lang, 'premiumRedeemCredits', amount.toString())) + } catch (err) { + console.error('Failed to consume entitlement:', err) + results.details.push(getLocale(lang, 'premiumRedeemCreditsFailed', amount.toString())) + } + } + + // CSV one-time unlock (consumable, per-server) + if (entitlement.skuId === skus.csvUnlock && !entitlement.consumed) { + try { + await entitlement.consume() + await database.unlockGuildCSV(guildID) + results.csvUnlocked = true + results.details.push(getLocale(lang, 'premiumRedeemCsv')) + } catch (err) { + console.error('Failed to consume/unlock CSV:', err) + results.details.push(getLocale(lang, 'premiumRedeemCsvFailed')) + } + } + } + + if (results.details.length === 0) { + results.details.push(getLocale(lang, 'premiumNoRedeemable')) + } + + return results + } + + /** + * Returns a full premium status object for display. + */ + async getPremiumStatus(guildID, entitlements) { + const tier = this.getSubscriptionTier(entitlements) + const premium = await database.getGuildPremium(guildID) + const stats = await new Promise(resolve => { + database.getGuildStats(guildID, resolve) + }) + + return { + enabled: this.enabled, + subscriptionTier: tier, + freeLimit: this.freeMonthlyLimit, + mailsSentMonth: stats.mailsSentMonth, + freeRemaining: Math.max(0, this.freeMonthlyLimit - stats.mailsSentMonth), + bonusCredits: premium.bonusCredits, + csvUnlocked: premium.csvUnlocked, + hasUnlimitedMails: !!tier + } + } +} + +const premiumManager = new PremiumManager() +module.exports = premiumManager diff --git a/src/utils/embeds.js b/src/utils/embeds.js index 7455552..bca1b94 100644 --- a/src/utils/embeds.js +++ b/src/utils/embeds.js @@ -161,5 +161,21 @@ module.exports = { createVerificationSuccessEmbed, createCodeSentEmbed, createVerificationLogEmbed, - createVerificationFailedLogEmbed + createVerificationFailedLogEmbed, + createMailLimitReachedEmbed, + createCSVPremiumRequiredEmbed, }; + +function createMailLimitReachedEmbed(language, mailsSentMonth, freeLimit) { + return new EmbedBuilder() + .setTitle(getLocale(language, 'premiumMailLimitTitle')) + .setDescription(getLocale(language, 'premiumMailLimitDescription', mailsSentMonth.toString(), freeLimit.toString())) + .setColor(0xFFA500); +} + +function createCSVPremiumRequiredEmbed(language) { + return new EmbedBuilder() + .setTitle(getLocale(language, 'premiumCsvRequiredTitle')) + .setDescription(getLocale(language, 'premiumCsvRequiredDescription')) + .setColor(0x5865F2); +}