diff --git a/bun.lockb b/bun.lockb index 2c35139..6ce7546 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/countries.json b/countries.json new file mode 100644 index 0000000..18927c8 --- /dev/null +++ b/countries.json @@ -0,0 +1,254 @@ +{ + "AD": "ca", + "AE": "ar-AE,fa,en,hi,ur", + "AF": "fa-AF,ps,uz-AF,tk", + "AG": "en-AG", + "AI": "en-AI", + "AL": "sq,el", + "AM": "hy", + "AN": "nl-AN,en,es", + "AO": "pt-AO", + "AQ": "", + "AR": "es-AR,en,it,de,fr,gn", + "AS": "en-AS,sm,to", + "AT": "de-AT,hr,hu,sl", + "AU": "en-AU", + "AW": "nl-AW,pap,es,en", + "AX": "sv-AX", + "AZ": "az,ru,hy", + "BA": "bs,hr-BA,sr-BA", + "BB": "en-BB", + "BD": "bn-BD,en", + "BE": "nl-BE,fr-BE,de-BE", + "BF": "fr-BF,mos", + "BG": "bg,tr-BG,rom", + "BH": "ar-BH,en,fa,ur", + "BI": "fr-BI,rn", + "BJ": "fr-BJ", + "BL": "fr", + "BM": "en-BM,pt", + "BN": "ms-BN,en-BN", + "BO": "es-BO,qu,ay", + "BQ": "nl,pap,en", + "BR": "pt-BR,es,en,fr", + "BS": "en-BS", + "BT": "dz", + "BV": "", + "BW": "en-BW,tn-BW", + "BY": "be,ru", + "BZ": "en-BZ,es", + "CA": "en-CA,fr-CA,iu", + "CC": "ms-CC,en", + "CD": "fr-CD,ln,ktu,kg,sw,lua", + "CF": "fr-CF,sg,ln,kg", + "CG": "fr-CG,kg,ln-CG", + "CH": "de-CH,fr-CH,it-CH,rm", + "CI": "fr-CI", + "CK": "en-CK,mi", + "CL": "es-CL", + "CM": "en-CM,fr-CM", + "CN": "zh-CN,yue,wuu,dta,ug,za", + "CO": "es-CO", + "CR": "es-CR,en", + "CS": "cu,hu,sq,sr", + "CU": "es-CU,pap", + "CV": "pt-CV", + "CW": "nl,pap", + "CX": "en,zh,ms-CX", + "CY": "el-CY,tr-CY,en", + "CZ": "cs,sk", + "DE": "de", + "DJ": "fr-DJ,ar,so-DJ,aa", + "DK": "da-DK,en,fo,de-DK", + "DM": "en-DM", + "DO": "es-DO", + "DZ": "ar-DZ", + "EC": "es-EC", + "EE": "et,ru", + "EG": "ar-EG,en,fr", + "EH": "ar,mey", + "ER": "aa-ER,ar,tig,kun,ti-ER", + "ES": "es-ES,ca,gl,eu,oc", + "ET": "am,en-ET,om-ET,ti-ET,so-ET,sid", + "FI": "fi-FI,sv-FI,smn", + "FJ": "en-FJ,fj", + "FK": "en-FK", + "FM": "en-FM,chk,pon,yap,kos,uli,woe,nkr,kpg", + "FO": "fo,da-FO", + "FR": "fr-FR,frp,br,co,ca,eu,oc", + "GA": "fr-GA", + "GB": "en-GB,cy-GB,gd", + "GD": "en-GD", + "GE": "ka,ru,hy,az", + "GF": "fr-GF", + "GG": "en,nrf", + "GH": "en-GH,ak,ee,tw", + "GI": "en-GI,es,it,pt", + "GL": "kl,da-GL,en", + "GM": "en-GM,mnk,wof,wo,ff", + "GN": "fr-GN", + "GP": "fr-GP", + "GQ": "es-GQ,fr,pt", + "GR": "el-GR,en,fr", + "GS": "en", + "GT": "es-GT", + "GU": "en-GU,ch-GU", + "GW": "pt-GW,pov", + "GY": "en-GY", + "HK": "zh-HK,yue,zh,en", + "HM": "", + "HN": "es-HN,cab,miq", + "HR": "hr-HR,sr", + "HT": "ht,fr-HT", + "HU": "hu-HU", + "ID": "id,en,nl,jv", + "IE": "en-IE,ga-IE", + "IL": "he,ar-IL,en-IL,", + "IM": "en,gv", + "IN": "en-IN,hi,bn,te,mr,ta,ur,gu,kn,ml,or,pa,as,bh,sat,ks,ne,sd,kok,doi,mni,sit,sa,fr,lus,inc", + "IO": "en-IO", + "IQ": "ar-IQ,ku,hy", + "IR": "fa-IR,ku", + "IS": "is,en,de,da,sv,no", + "IT": "it-IT,de-IT,fr-IT,sc,ca,co,sl", + "JE": "en,fr,nrf", + "JM": "en-JM", + "JO": "ar-JO,en", + "JP": "ja", + "KE": "en-KE,sw-KE", + "KG": "ky,uz,ru", + "KH": "km,fr,en", + "KI": "en-KI,gil", + "KM": "ar,fr-KM", + "KN": "en-KN", + "KP": "ko-KP", + "KR": "ko-KR,en", + "KW": "ar-KW,en", + "KY": "en-KY", + "KZ": "kk,ru", + "LA": "lo,fr,en", + "LB": "ar-LB,fr-LB,en,hy", + "LC": "en-LC", + "LI": "de-LI", + "LK": "si,ta,en", + "LR": "en-LR", + "LS": "en-LS,st,zu,xh", + "LT": "lt,ru,pl", + "LU": "lb,de-LU,fr-LU", + "LV": "lv,ru,lt", + "LY": "ar-LY,it,en", + "MA": "ar-MA,ber,fr", + "MC": "fr-MC,en,it", + "MD": "ro,ru,gag,tr", + "ME": "sr,hu,bs,sq,hr,rom", + "MF": "fr", + "MG": "fr-MG,mg", + "MH": "mh,en-MH", + "MK": "mk,sq,tr,rmm,sr", + "ML": "fr-ML,bm", + "MM": "my", + "MN": "mn,ru", + "MO": "zh,zh-MO,pt", + "MP": "fil,tl,zh,ch-MP,en-MP", + "MQ": "fr-MQ", + "MR": "ar-MR,fuc,snk,fr,mey,wo", + "MS": "en-MS", + "MT": "mt,en-MT", + "MU": "en-MU,bho,fr", + "MV": "dv,en", + "MW": "ny,yao,tum,swk", + "MX": "es-MX", + "MY": "ms-MY,en,zh,ta,te,ml,pa,th", + "MZ": "pt-MZ,vmw", + "NA": "en-NA,af,de,hz,naq", + "NC": "fr-NC", + "NE": "fr-NE,ha,kr,dje", + "NF": "en-NF", + "NG": "en-NG,ha,yo,ig,ff", + "NI": "es-NI,en", + "NL": "nl-NL,fy-NL", + "NO": "no,nb,nn,se,fi", + "NP": "ne,en", + "NR": "na,en-NR", + "NU": "niu,en-NU", + "NZ": "en-NZ,mi", + "OM": "ar-OM,en,bal,ur", + "PA": "es-PA,en", + "PE": "es-PE,qu,ay", + "PF": "fr-PF,ty", + "PG": "en-PG,ho,meu,tpi", + "PH": "tl,en-PH,fil,ceb,ilo,hil,war,pam,bik,bcl,pag,mrw,tsg,mdh,cbk,krj,sgd,msb,akl,ibg,yka,mta,abx", + "PK": "ur-PK,en-PK,pa,sd,ps,brh", + "PL": "pl", + "PM": "fr-PM", + "PN": "en-PN", + "PR": "en-PR,es-PR", + "PS": "ar-PS", + "PT": "pt-PT,mwl", + "PW": "pau,sov,en-PW,tox,ja,fil,zh", + "PY": "es-PY,gn", + "QA": "ar-QA,es", + "RE": "fr-RE", + "RO": "ro,hu,rom", + "RS": "sr,hu,bs,rom", + "RU": "ru,tt,xal,cau,ady,kv,ce,tyv,cv,udm,tut,mns,bua,myv,mdf,chm,ba,inh,kbd,krc,av,sah,nog", + "RW": "rw,en-RW,fr-RW,sw", + "SA": "ar-SA", + "SB": "en-SB,tpi", + "SC": "en-SC,fr-SC", + "SD": "ar-SD,en,fia", + "SE": "sv-SE,se,sma,fi-SE", + "SG": "cmn,en-SG,ms-SG,ta-SG,zh-SG", + "SH": "en-SH", + "SI": "sl,sh", + "SJ": "no,ru", + "SK": "sk,hu", + "SL": "en-SL,men,tem", + "SM": "it-SM", + "SN": "fr-SN,wo,fuc,mnk", + "SO": "so-SO,ar-SO,it,en-SO", + "SR": "nl-SR,en,srn,hns,jv", + "SS": "en", + "ST": "pt-ST", + "SV": "es-SV", + "SX": "nl,en", + "SY": "ar-SY,ku,hy,arc,fr,en", + "SZ": "en-SZ,ss-SZ", + "TC": "en-TC", + "TD": "fr-TD,ar-TD,sre", + "TF": "fr", + "TG": "fr-TG,ee,hna,kbp,dag,ha", + "TH": "th,en", + "TJ": "tg,ru", + "TK": "tkl,en-TK", + "TL": "tet,pt-TL,id,en", + "TM": "tk,ru,uz", + "TN": "ar-TN,fr", + "TO": "to,en-TO", + "TR": "tr-TR,ku,diq,az,av", + "TT": "en-TT,hns,fr,es,zh", + "TV": "tvl,en,sm,gil", + "TW": "zh-TW,zh,nan,hak", + "TZ": "sw-TZ,en,ar", + "UA": "uk,ru-UA,rom,pl,hu", + "UG": "en-UG,lg,sw,ar", + "UM": "en-UM", + "US": "en-US,es-US,haw,fr", + "UY": "es-UY", + "UZ": "uz,ru,tg", + "VA": "la,it,fr", + "VC": "en-VC,fr", + "VE": "es-VE", + "VG": "en-VG", + "VI": "en-VI", + "VN": "vi,en,fr,zh,km", + "VU": "bi,en-VU,fr-VU", + "WF": "wls,fud,fr-WF", + "WS": "sm,en-WS", + "XK": "sq,sr", + "YE": "ar-YE", + "YT": "fr-YT", + "ZA": "zu,xh,af,nso,en-ZA,tn,st,ts,ss,ve,nr", + "ZM": "en-ZM,bem,loz,lun,lue,ny,toi", + "ZW": "en-ZW,sn,nr,nd" +} diff --git a/package.json b/package.json index 56ca3cf..c09d942 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,9 @@ "dependencies": { "@baselime/pino-transport": "^0.1.5", "@biomejs/biome": "^1.8.2", + "@google-cloud/translate": "^8.5.0", "@prisma/client": "^6.0.1", + "country-code-emoji": "^2.3.0", "discord.js": "^14.15.3", "elysia": "^1.1.5", "pino": "^9.5.0", diff --git a/src/handlers/command.ts b/src/handlers/command.ts index d49e9b7..ed92832 100644 --- a/src/handlers/command.ts +++ b/src/handlers/command.ts @@ -49,8 +49,17 @@ export async function handleCommand(interaction: CommandInteraction) { `../commands/${interaction.commandName}.ts` ); const command = commandModule.default; - const limited = await checkCommandRatelimit("cmd", interaction, command.name); - if (limited) return interaction.reply({ content: "You are being ratelimited! Please wait a bit before using this command again.", ephemeral: true }); + const limited = await checkCommandRatelimit( + "cmd", + interaction, + command.name, + ); + if (limited) + return interaction.reply({ + content: + "You are being ratelimited! Please wait a bit before using this command again.", + ephemeral: true, + }); if (!command.slashCommand.enabled) return interaction.reply("This command is not enabled!"); if (command.isPremium && !premium) @@ -59,10 +68,20 @@ export async function handleCommand(interaction: CommandInteraction) { ); if (command.slashCommand.enabled) command.interactionRun(interaction); if (premium) { - setCommandRatelimit("cmd", interaction, command.PremiumCooldown || command.cooldown, command.name); + setCommandRatelimit( + "cmd", + interaction, + command.PremiumCooldown || command.cooldown, + command.name, + ); } if (!premium) { - setCommandRatelimit("cmd", interaction, command.cooldown, command.name); + setCommandRatelimit( + "cmd", + interaction, + command.cooldown, + command.name, + ); } } catch (error) { const logId = await sendLog(error.message); diff --git a/src/handlers/flagTranslation.ts b/src/handlers/flagTranslation.ts new file mode 100644 index 0000000..2b78f02 --- /dev/null +++ b/src/handlers/flagTranslation.ts @@ -0,0 +1,72 @@ +import { Translate } from "@google-cloud/translate/build/src/v2"; +import { emojiCountryCode } from "country-code-emoji"; +import { MessageReaction, Message, User, EmbedBuilder } from "discord.js"; +import { + setTranslationRatelimit, + checkTranslationRatelimit, +} from "./ratelimit"; +import { dmUser } from "../index.ts"; +import { PrismaClient } from "@prisma/client"; +import fs from "node:fs"; + +function countryToLanguage(country: string): string | undefined { + const languages = JSON.parse(fs.readFileSync("./countries.json", "utf-8")); + const languageList = languages[country]; + if (!languageList) return undefined; + return languageList.split(",")[0]; +} + +const translate = new Translate({ + key: process.env.GOOGLE_API_KEY as string, +}); + +const prisma = new PrismaClient(); + +export async function translateMessage(reaction: MessageReaction, user: User) { + const country = emojiCountryCode(reaction.emoji.name as string); + const language = countryToLanguage(country); + if (!language) { + console.error("Unsupported country code:", country); + return; + } + const message = await reaction.message.fetch(); + const limited = await checkTranslationRatelimit("translate", user.id); + if (limited) { + message.reactions.removeAll(); + await dmUser( + user.id, + "MikanBot", + "You are being ratelimited! Please wait a bit before translating another message. You can speed this up by becoming a premium user.", + ); + } + if (!message.content) { + console.error("Message content is empty or undefined."); + return; + } + message.react(""); + const result = await translate + .translate(message.content, language) + .catch((error) => { + console.error("Translation error: ", error); + return null; + }); + if (!result) return; + const [translation] = result; + const embed = new EmbedBuilder() + .setTitle("Translation to " + reaction.emoji.name) + .setDescription(translation) + .setColor("#FF7700") + .setTimestamp(); + await message.reply({ embeds: [embed] }); + message.reactions.removeAll(); + const userDb = await prisma.user.findUnique({ + where: { + id: user.id, + }, + }); + if (userDb?.premium) { + setTranslationRatelimit("translate", user.id, 5); + } else { + setTranslationRatelimit("translate", user.id, 30); + } +} diff --git a/src/handlers/ratelimit.ts b/src/handlers/ratelimit.ts index 2c6306d..1948ebf 100644 --- a/src/handlers/ratelimit.ts +++ b/src/handlers/ratelimit.ts @@ -30,7 +30,12 @@ export async function checkMessageRatelimit(type: string, message: Message) { } } -export function setCommandRatelimit(type: string, interaction: CommandInteraction, time: number, name: string) { +export function setCommandRatelimit( + type: string, + interaction: CommandInteraction, + time: number, + name: string, +) { if (type == "cmd") { redis.set( `command:${interaction.guildId}:${interaction.user.id}:${name}`, @@ -40,7 +45,11 @@ export function setCommandRatelimit(type: string, interaction: CommandInteractio } } -export async function checkCommandRatelimit(type: string, interaction: CommandInteraction, name: string) { +export async function checkCommandRatelimit( + type: string, + interaction: CommandInteraction, + name: string, +) { if (type == "cmd") { const isLimited = await redis.get( `command:${interaction.guildId}:${interaction.user.id}:${name}`, @@ -52,3 +61,24 @@ export async function checkCommandRatelimit(type: string, interaction: CommandIn } } } + +export async function setTranslationRatelimit( + type: string, + user: string, + time: number, +) { + if (type == "translate") { + redis.set(`translation:${user}`, `${user}`, { EX: time }); + } +} + +export async function checkTranslationRatelimit(type: string, user: string) { + if (type == "translate") { + const isLimited = await redis.get(`translation:${user}`); + if (isLimited == `${user}`) { + return true; + } else { + return false; + } + } +} diff --git a/src/index.ts b/src/index.ts index d75efbd..ee977d1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,14 +3,26 @@ import { deployCommands } from "./deploy"; import { setPresence } from "./presence"; import { handleCommand } from "./handlers/command"; import { handleLevel } from "./handlers/lvl"; -import { Client, GatewayIntentBits, EmbedBuilder } from "discord.js"; +import { translateMessage } from "./handlers/flagTranslation"; +import { emojiCountryCode } from "country-code-emoji"; +import { + Client, + GatewayIntentBits, + Partials, + EmbedBuilder, + MessageReaction, +} from "discord.js"; const client = new Client({ intents: [ GatewayIntentBits.Guilds, GatewayIntentBits.DirectMessages, GatewayIntentBits.GuildMessages, + GatewayIntentBits.GuildMessageReactions, + GatewayIntentBits.GuildMembers, + GatewayIntentBits.DirectMessageReactions, ], + partials: [Partials.Message, Partials.Channel, Partials.Reaction], }); export async function dmUser(id: string, provider: string, message: string) { @@ -36,6 +48,28 @@ client.on("ready", () => { setPresence(client); }); +client.on("messageReactionAdd", async (reaction: MessageReaction, user) => { + if (user.bot) return; + if (reaction.partial) { + try { + await reaction.fetch(); + } catch (error) { + console.error( + "Something went wrong when fetching the message: ", + error, + ); + return; + } + } + try { + if (emojiCountryCode(reaction.emoji.name)) { + await translateMessage(reaction, user); + } + } catch (error) { + return; + } +}); + client.on("interactionCreate", async (interaction) => { if (interaction.isCommand()) { console.log(`Received command: ${interaction.commandName}`); diff --git a/src/types/country-code-emoji.d.ts b/src/types/country-code-emoji.d.ts new file mode 100644 index 0000000..ff84aec --- /dev/null +++ b/src/types/country-code-emoji.d.ts @@ -0,0 +1,3 @@ +declare module "country-code-emoji" { + export function emojiCountryCode(emoji: string): string; +}