From ff305769fca68d086299670b836a72d14401088f Mon Sep 17 00:00:00 2001 From: Shadow Date: Mon, 5 Aug 2024 14:18:23 +0000 Subject: [PATCH 1/2] Add translation feature to message context menu (#810) * feat(translate): Adds translate feature * cleanup(translate): cleaned up code and comments --------- Co-authored-by: LunaUrsa <1836049+LunaUrsa@users.noreply.github.com> --- src/discord/commands/global/m.translate.ts | 43 ++++++++++++++++ src/global/commands/g.ai.ts | 57 ++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 src/discord/commands/global/m.translate.ts diff --git a/src/discord/commands/global/m.translate.ts b/src/discord/commands/global/m.translate.ts new file mode 100644 index 000000000..59416b6f5 --- /dev/null +++ b/src/discord/commands/global/m.translate.ts @@ -0,0 +1,43 @@ +import { + ContextMenuCommandBuilder, + Colors, +} from 'discord.js'; +import { + ApplicationCommandType, +} from 'discord-api-types/v10'; +import OpenAI from 'openai'; +import { MessageCommand } from '../../@types/commandDef'; +import { embedTemplate } from '../../utils/embedTemplate'; +import { aiTranslate } from '../../../global/commands/g.ai'; + +const F = f(__filename); + +export const mTranslate: MessageCommand = { + data: new ContextMenuCommandBuilder() + .setName('Translate') + .setType(ApplicationCommandType.Message), + async execute(interaction) { + if (!interaction.guild) return false; + log.info(F, await commandContext(interaction)); + await interaction.deferReply({ ephemeral: true }); + + const targetMessage = interaction.targetMessage.content; + + const messageList = [{ + role: 'user', + content: targetMessage, + }] as OpenAI.Chat.ChatCompletionMessageParam[]; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { response, promptTokens, completionTokens } = await aiTranslate('English', messageList); + await interaction.editReply({ + embeds: [embedTemplate() + .setTitle('Here\'s your translation!') + .setDescription(response) + .setColor(Colors.Blurple)], + }); + return true; + }, +}; + +export default mTranslate; diff --git a/src/global/commands/g.ai.ts b/src/global/commands/g.ai.ts index aa7d41caf..aee01a454 100644 --- a/src/global/commands/g.ai.ts +++ b/src/global/commands/g.ai.ts @@ -1022,3 +1022,60 @@ export async function aiModerate( return moderationAlerts; } + +export async function aiTranslate( + target_language: string, + messages: OpenAI.Chat.ChatCompletionMessageParam [], +):Promise<{ + response: string, + promptTokens: number, + completionTokens: number, + }> { + let response = ''; + let promptTokens = 0; + let completionTokens = 0; + if (!env.OPENAI_API_ORG || !env.OPENAI_API_KEY) return { response, promptTokens, completionTokens }; + + const model = 'gpt-3.5-turbo-1106'; + const chatCompletionMessages = [{ + role: 'system', + content: `You will translate whatever the user sends to their desired language. Their desired language or language code is: ${target_language}.`, + }] as OpenAI.Chat.ChatCompletionMessageParam[]; + chatCompletionMessages.push(...messages); + + const payload = { + model, + messages: chatCompletionMessages, + } as OpenAI.Chat.ChatCompletionCreateParamsNonStreaming; + + // log.debug(F, `payload: ${JSON.stringify(payload, null, 2)}`); + let responseMessage = {} as OpenAI.Chat.ChatCompletionMessageParam; + + const chatCompletion = await openAi.chat.completions + .create(payload) + .catch(err => { + if (err instanceof OpenAI.APIError) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + log.error(F, `${err.name} - ${err.status} - ${err.type} - ${(err.error as any).message} `); // 400 + // log.error (F, `${JSON.stringify(err.headers, null, 2)}`); // {server: 'nginx', ...} + // log.error(F, `${JSON.stringify(err, null, 2)}`); // {server: 'nginx', ...} + } else { + throw err; + } + }); + // log.debug(F, `chatCompletion: ${JSON.stringify(chatCompletion, null, 2)}`); + + if (chatCompletion?.choices[0].message) { + responseMessage = chatCompletion.choices[0].message; + + // Sum up the existing tokens + promptTokens = chatCompletion.usage?.prompt_tokens ?? 0; + completionTokens = chatCompletion.usage?.completion_tokens ?? 0; + + response = responseMessage.content ?? 'Sorry, I\'m not sure how to respond to that.'; + } + + // log.debug(F, `response: ${response}`); + + return { response, promptTokens, completionTokens }; +} From c943672fc773c0b9549d937ebba519abe71f4053 Mon Sep 17 00:00:00 2001 From: Shadow Date: Mon, 5 Aug 2024 14:30:03 +0000 Subject: [PATCH 2/2] Fix null label in d.moderate.ts preventing mod actions on users with long names (#825) * fix(moderate): apps->report * fix(moderate): fixed reporting and responses * fix(moderate): label failing to set on long names --------- Co-authored-by: LunaUrsa <1836049+LunaUrsa@users.noreply.github.com> --- src/discord/commands/guild/d.moderate.ts | 39 ++++++++++++++++++------ 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/src/discord/commands/guild/d.moderate.ts b/src/discord/commands/guild/d.moderate.ts index fd4fdcf4a..525ad1466 100644 --- a/src/discord/commands/guild/d.moderate.ts +++ b/src/discord/commands/guild/d.moderate.ts @@ -789,7 +789,7 @@ export async function modResponse( } let targetString = ''; - let target = {} as GuildMember; + let target = {} as GuildMember | User; const modEmbedObj = embedTemplate(); const { embedColor } = embedVariables[command as keyof typeof embedVariables]; @@ -903,13 +903,15 @@ export async function modResponse( // log.debug(F, `Assigning target from string: ${targets}`); [target] = targets; } - if (interaction.isUserContextMenuCommand() && interaction.targetMember) { + + if (interaction.isUserContextMenuCommand() && (interaction.targetMember || interaction.targetUser)) { // log.debug(F, `User context target member: ${interaction.targetMember}`); - target = interaction.targetMember as GuildMember; + target = interaction.targetMember ? interaction.targetMember as GuildMember : interaction.targetUser as User; } else if (interaction.isMessageContextMenuCommand() && interaction.targetMessage) { // log.debug(F, `Message context target message member: ${interaction.targetMessage.member}`); - target = interaction.targetMessage.member as GuildMember; + target = interaction.targetMessage.member ? interaction.targetMessage.member as GuildMember : interaction.targetMessage.author as User; } + const targetData = await db.users.upsert({ where: { discord_id: target.id, @@ -937,7 +939,10 @@ export async function modResponse( // Determine if the actor is a mod // const actorIsMod = (!!guildData.role_moderator && actor.roles.cache.has(guildData.role_moderator)); - const timeoutTime = target.communicationDisabledUntilTimestamp; + let timeoutTime = null; + if (target instanceof GuildMember) { + timeoutTime = target.communicationDisabledUntilTimestamp; + } if (showModButtons) { if (isInfo(command) || isReport(command)) { @@ -1859,7 +1864,11 @@ export async function modModal( .setCustomId('internalNote'); try { - modalInputComponent.setLabel(`Why are you ${verb} ${target}?`); + // Ensure the label text is within the limit + const label = `Why are you ${verb} ${target}?`; + const truncatedLabelText = label.length > 45 ? `${label.substring(0, 41)}...?` : label; + + modalInputComponent.setLabel(truncatedLabelText); } catch (err) { log.error(F, `Error: ${err}`); log.error(F, `Verb: ${verb}, Target: ${target}`); @@ -1939,8 +1948,19 @@ export async function modModal( const filter = (i: ModalSubmitInteraction) => i.customId.startsWith('modModal'); await interaction.awaitModalSubmit({ filter, time: disableButtonTime }) .then(async i => { - if (i.customId.split('~')[2] !== interaction.id) return; + if (i.customId.split('~')[2] !== interaction.id) { + return; + } await i.deferReply({ ephemeral: true }); + if (isReport(command)) { + const reportResponseEmbed = embedTemplate() + .setColor(Colors.Yellow) + .setTitle('Report sent!') + .setDescription('The moderators have received your report and will look into it. Thanks!'); + await i.editReply({ + embeds: [reportResponseEmbed], + }); + } // const internalNote = i.fields.getTextInputValue('internalNote'); // eslint-disable-line // // Only these commands actually have the description input, so only pull it if it exists @@ -2032,8 +2052,9 @@ export async function modModal( // log.error(F, `Error: ${err}`); } } - - await i.editReply(await moderate(interaction, i)); + if (!isReport) { + await i.editReply(await moderate(interaction, i)); + } }) .catch(async err => { // log.error(F, `Error: ${JSON.stringify(err as DiscordErrorData, null, 2)}`);