diff --git a/backend/src/data/GuildSavedMessages.ts b/backend/src/data/GuildSavedMessages.ts index 501229c0..c44b4df4 100644 --- a/backend/src/data/GuildSavedMessages.ts +++ b/backend/src/data/GuildSavedMessages.ts @@ -96,6 +96,17 @@ export class GuildSavedMessages extends BaseGuildRepository { .getMany(); } + getLatestByUser(userId, limit = 20) { + return this.messages + .createQueryBuilder() + .where("guild_id = :guild_id", { guild_id: this.guildId }) + .andWhere("user_id = :user_id", { user_id: userId }) + .andWhere("deleted_at IS NULL") + .orderBy("id", "DESC") + .limit(limit) + .getMany(); + } + getUserMessagesByChannelAfterId(userId, channelId, afterId, limit?: number) { let query = this.messages .createQueryBuilder() diff --git a/backend/src/plugins/Utility/UtilityPlugin.ts b/backend/src/plugins/Utility/UtilityPlugin.ts index 0ae876ec..fd0a88da 100644 --- a/backend/src/plugins/Utility/UtilityPlugin.ts +++ b/backend/src/plugins/Utility/UtilityPlugin.ts @@ -35,6 +35,7 @@ import { SnowflakeInfoCmd } from "./commands/SnowflakeInfoCmd"; import { discardRegExpRunner, getRegExpRunner } from "../../regExpRunners"; import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin"; import { VcdisconnectCmd } from "./commands/VcdisconnectCmd"; +import { ShowMessagesCmd } from "./commands/ShowMessagesCmd"; const defaultOptions: PluginOptions = { config: { @@ -48,6 +49,7 @@ const defaultOptions: PluginOptions = { can_channelinfo: false, can_messageinfo: false, can_userinfo: false, + can_showmessage: false, can_snowflake: false, can_reload_guild: false, can_nickname: false, @@ -77,6 +79,7 @@ const defaultOptions: PluginOptions = { can_channelinfo: true, can_messageinfo: true, can_userinfo: true, + can_showmessage: true, can_snowflake: true, can_nickname: true, can_vcmove: true, @@ -136,6 +139,7 @@ export const UtilityPlugin = zeppelinGuildPlugin()("utility", MessageInfoCmd, InfoCmd, SnowflakeInfoCmd, + ShowMessagesCmd, ], onLoad(pluginData) { diff --git a/backend/src/plugins/Utility/commands/ShowMessagesCmd.ts b/backend/src/plugins/Utility/commands/ShowMessagesCmd.ts new file mode 100644 index 00000000..5babc9f8 --- /dev/null +++ b/backend/src/plugins/Utility/commands/ShowMessagesCmd.ts @@ -0,0 +1,82 @@ +import { utilityCmd } from "../types"; +import { commandTypeHelpers as ct } from "../../../commandTypes"; +import { chunkMessageLines, convertMSToDelayString, EmbedWith } from "../../../utils"; +import { SavedMessage } from "../../../data/entities/SavedMessage"; +import moment from "moment"; + +export const ShowMessagesCmd = utilityCmd({ + trigger: ["show_messages", "messages", "recent_messages", "showmessages"], + description: "Shows recent messages of a user", + usage: "!show_messages 108552944961454080", + permission: "can_showmessage", + + signature: { + user: ct.resolvedUser(), + amount: ct.number({ required: false }), + fullMessage: ct.switchOption({ shortcut: "f" }), + }, + + async run({ message, args, pluginData }) { + const latestMessages = (await pluginData.state.savedMessages.getLatestByUser(args.user.id, args.amount)).reverse(); + if (latestMessages.length === 0) { + message.channel.createMessage(`No recent messages by user!`); + return; + } + const messagesToChannelMap: Map = new Map(); + for (const msg of latestMessages) { + if (messagesToChannelMap[msg.channel_id] == null) messagesToChannelMap[msg.channel_id] = []; + messagesToChannelMap[msg.channel_id].push(msg); + } + + const embed: EmbedWith<"fields"> = { + fields: [], + }; + + let description = ""; + for (const channel in messagesToChannelMap) { + const messages: SavedMessage[] = messagesToChannelMap[channel]; + description += `**Messages in <#${channel}>:**`; + + for (const msg of messages) { + // Trim preview down to 50 (47 with ...) characters as to not overflow chat (unless -f switch is set) + const formattedContent = + msg.data.content.length > 47 && !args.fullMessage + ? msg.data.content.substring(0, 47) + "..." + : msg.data.content; + // We remove milliseconds here as to not show 3 decimal points (which appears on convertMSToDelayString and humanizeDurationShort both) + const timeAgo = convertMSToDelayString( + moment() + .milliseconds(0) + .valueOf() - + moment(msg.posted_at) + .milliseconds(0) + .valueOf(), + ); + + description += `\n\`${formattedContent}\`, ${timeAgo} ago \[[Link](https://discord.com/channels/${msg.guild_id}/${msg.channel_id}/${msg.id})\]`; + } + description += `\n\n`; + } + + embed.author = { + name: args.user.username + "#" + args.user.discriminator, + icon_url: args.user.avatarURL, + }; + + // Chunking to 1950 max length to accomodate footer and/or header + const chunkedDescription = chunkMessageLines(description.trim(), 1950); + for (let i = 0; i < chunkedDescription.length; i++) { + embed.description = chunkedDescription[i]; + + // Only show author on top-most embed and footer on bottom-most embed to keep visual throughline + if (i > 0) { + embed.author = undefined; + } + if (i === chunkedDescription.length - 1) { + embed.footer = { text: `Showing ${latestMessages.length} recent messages` }; + } + + await message.channel.createMessage({ embed }); + } + }, +}); diff --git a/backend/src/plugins/Utility/types.ts b/backend/src/plugins/Utility/types.ts index 8d6de6b4..e15814fe 100644 --- a/backend/src/plugins/Utility/types.ts +++ b/backend/src/plugins/Utility/types.ts @@ -18,6 +18,7 @@ export const ConfigSchema = t.type({ can_channelinfo: t.boolean, can_messageinfo: t.boolean, can_userinfo: t.boolean, + can_showmessage: t.boolean, can_snowflake: t.boolean, can_reload_guild: t.boolean, can_nickname: t.boolean,