_=!)E
z!X~7KO+ibgeZnRc52`Xy(1*@D%rdY+aE`89Am`wc12>gebXK3fXwzea;=1d^CfJ|8
z$y28gZmxUlQlZx7j9Lxm4kr4I>+QAA=G7@79cy1X7^wq}KH>V&k2yB6{VKbBc2EzQ
z@$jl#UoAY5`~-2MUi!EszR{l%k~*dMs^j
zjT?@awN6~x*VUkUhkeN($(ELC5QR>5Xe(#jjZY#YqrIq{uy(dy+xf2N)zZxg>OrI@
zC38jN@OqulwdQ4pDZR-do@Xa3h4p9~nRoAH>G~SYZ*k{cWnM-}
zTE+tP*B&bFU|xOJsxIIjc>`H?M$shiGiYYH6y%Xwov^=Uq^9`~;5|CgY8Z`aU64Pk
zmFm+x@8u-oFfns1T9m_%O{tZGGmw0DooiG_cAo`qLTXV?g@+z38BGh`)w%}&y3ykx50qzV9
zwYhF?@5QFg&RMx#E_)-cvAx0V=8lU`%;-AoDPgCJBd#yDhN~cJoYXL<;|ZD58B7jl
zn}e3BOF?o+o3nByes;8m*HNTZl2c7*^OkNN&g9TA%pmu6v!q8!v8@6i)EUF^u3O7M
zLd3}0y2Vt>j5~dOBiN~-&4#H<5`Z~E5qd#68|%sZU#3yA+Bo>}mhsxd-@mnJLz7SX
zJ6^hJ^!5;ajyb;U2Nhfp=M|O=iL8(|qAsOqesbW*wJ=6;gI9U8po~QCw%O%Zj^)lw
zmUy1aU;Bzn_1ZW%ra>=IS3D$IBiD~1`f*{&U)P1tmaLv`O3y|MkL?m5gmRgN%&o-P
zy?u`-I-VP_P&W>;)1Ly_tbFo%gr9sYq8KW85uYv;w-d!oG4kWhnnO$_X`DK-Lx}n%
zVpFecMVqo)H8crWX#e=+SE5oObr}%^A9muslPGjbvQlHNb>C=@Z`~?*5F~GHXx+u5
zdzHw2XKzu_Nw1&`{ERU(?uaG_N@}v?nu=0ln!}3@@Xid`#-2Z@Dw(UL(ZUx9iy`gV
z6*mkYvN#hg3b1EOK75+33ur!U6Ee_R5Nlq(dc3ZNyg>^@Thso`X547o%Wik{Sz>mRihVH*DV<*Bs1r}#Bh=2N((^Bp?Q8qG!_1kZ1hty^`e%ugmz-IztMHf(
z00jFp-(L6^n>br-nH+X}(V1eKUwHmzG@8|KgpiGyT8t&nVO?fsX7_Zd0xBLSa;lAx
zkW8Ui;$Ya4s58arp!ucVNB4F=hW7UHsme;jWI~LLI0nup<<;_1hT}-AR{hV*z2aX6
zne{SXjg{WF5ZJ%h$=bTC`Gdm+DpBdoHK+(tRA+#MjccTpzJ5c$vT7Ql?K^tjOFx>A
z#{;x7z31f6svdui6bup)w1N1bMUp_XiKikC{*UZ!PQy*g?3P(=8n
zx+(tXCDAOs(%t$yT=Q+LPCrP`vSA7tVpfDws)}*#BLv(&IijVIP;<}gKC@-W2x#b;
zXns!yt{NR&PESzP_c}ilW1IrlEuE}CBdji1~;ATS%Mm!mPY*6ZP%35sw5u>r4(0>F6ZIov+yEC??VPtU~|Eeu47g7AE-ag|3P+;;t5cpnDvji@}!K^3F78LzLMC)J=VZ0m*XVuWY}!l5YL^Y?>;?
z9V;&B;P7zyO(UCVW?T97C>XuCyzxtP&y&1Q4K(siWZ`tXmVyV##nbGwfF-MoGw$jNU^Q;BbyQA#>1B
zUh1wHR1_nlRzra7k7vB@e^OU%c1G;l(aH8~bHZ@*H56;tSQ=2#pKpt~eQWtR1sZsL
z|MvXqezEd>*V-EtzZjfKm%XrDQCr-L-V9MW{1Q|Z>0-@{X@f-Sh3Sy6#GDmBO@o%h
z{Bm*E=
z5IvqOXvNW?j(6B|2WQFk#`g*1>q`v1`0~XG!AD+rTqmi!t+^)^@0`%kp{N^B
z)bG#790{x@
+
+
diff --git a/backend/src/plugins/Utility/UtilityPlugin.ts b/backend/src/plugins/Utility/UtilityPlugin.ts
index 3b909040..ad0fbd37 100644
--- a/backend/src/plugins/Utility/UtilityPlugin.ts
+++ b/backend/src/plugins/Utility/UtilityPlugin.ts
@@ -29,6 +29,7 @@ import { CleanCmd } from "./commands/CleanCmd";
import { Message } from "eris";
import { InviteInfoCmd } from "./commands/InviteInfoCmd";
import { ChannelInfoCmd } from "./commands/ChannelInfoCmd";
+import { MessageInfoCmd } from "./commands/MessageInfoCmd";
const defaultOptions: PluginOptions = {
config: {
@@ -40,6 +41,7 @@ const defaultOptions: PluginOptions = {
can_server: false,
can_invite: false,
can_channel: false,
+ can_message: false,
can_reload_guild: false,
can_nickname: false,
can_ping: false,
@@ -65,6 +67,7 @@ const defaultOptions: PluginOptions = {
can_server: true,
can_invite: true,
can_channel: true,
+ can_message: true,
can_nickname: true,
can_vcmove: true,
can_help: true,
@@ -116,6 +119,7 @@ export const UtilityPlugin = zeppelinPlugin()("utility", {
CleanCmd,
InviteInfoCmd,
ChannelInfoCmd,
+ MessageInfoCmd,
],
onLoad(pluginData) {
diff --git a/backend/src/plugins/Utility/commands/MessageInfoCmd.ts b/backend/src/plugins/Utility/commands/MessageInfoCmd.ts
new file mode 100644
index 00000000..7072c72c
--- /dev/null
+++ b/backend/src/plugins/Utility/commands/MessageInfoCmd.ts
@@ -0,0 +1,31 @@
+import { utilityCmd } from "../types";
+import { commandTypeHelpers as ct } from "../../../commandTypes";
+import { sendErrorMessage } from "../../../pluginUtils";
+import { getMessageInfoEmbed } from "../functions/getMessageInfoEmbed";
+import { canReadChannel } from "../../../utils/canReadChannel";
+
+export const MessageInfoCmd = utilityCmd({
+ trigger: ["message", "messageinfo"],
+ description: "Show information about a message",
+ usage: "!message 534722016549404673-534722219696455701",
+ permission: "can_message",
+
+ signature: {
+ message: ct.messageTarget(),
+ },
+
+ async run({ message, args, pluginData }) {
+ if (!canReadChannel(args.message.channel, message.author.id)) {
+ sendErrorMessage(pluginData, message.channel, "Unknown message");
+ return;
+ }
+
+ const embed = await getMessageInfoEmbed(pluginData, args.message.channel.id, args.message.messageId);
+ if (!embed) {
+ sendErrorMessage(pluginData, message.channel, "Unknown message");
+ return;
+ }
+
+ message.channel.createMessage({ embed });
+ },
+});
diff --git a/backend/src/plugins/Utility/functions/getMessageInfoEmbed.ts b/backend/src/plugins/Utility/functions/getMessageInfoEmbed.ts
new file mode 100644
index 00000000..083f6cfd
--- /dev/null
+++ b/backend/src/plugins/Utility/functions/getMessageInfoEmbed.ts
@@ -0,0 +1,129 @@
+import { PluginData } from "knub";
+import { UtilityPluginType } from "../types";
+import { Constants, EmbedOptions } from "eris";
+import moment from "moment-timezone";
+import humanizeDuration from "humanize-duration";
+import { chunkMessageLines, preEmbedPadding, trimEmptyLines, trimLines } from "../../../utils";
+import { getDefaultPrefix } from "knub/dist/commands/commandUtils";
+
+const MESSAGE_ICON = "https://cdn.discordapp.com/attachments/740650744830623756/740685652152025088/message.png";
+
+export async function getMessageInfoEmbed(
+ pluginData: PluginData,
+ channelId: string,
+ messageId: string,
+): Promise {
+ const message = await pluginData.client.getMessage(channelId, messageId).catch(() => null);
+ if (!message) {
+ return null;
+ }
+
+ const embed: EmbedOptions = {
+ fields: [],
+ };
+
+ embed.author = {
+ name: `Message: ${message.id}`,
+ icon_url: MESSAGE_ICON,
+ };
+
+ const createdAt = moment(message.createdAt, "x");
+ const messageAge = humanizeDuration(Date.now() - message.createdAt, {
+ largest: 2,
+ round: true,
+ });
+
+ const editedAt = message.editedTimestamp && moment(message.editedTimestamp, "x");
+ const editAge =
+ message.editedTimestamp &&
+ humanizeDuration(Date.now() - message.editedTimestamp, {
+ largest: 2,
+ round: true,
+ });
+
+ const type =
+ {
+ [Constants.MessageTypes.DEFAULT]: "Regular message",
+ [Constants.MessageTypes.CHANNEL_PINNED_MESSAGE]: "System message",
+ [Constants.MessageTypes.GUILD_MEMBER_JOIN]: "System message",
+ [Constants.MessageTypes.USER_PREMIUM_GUILD_SUBSCRIPTION]: "System message",
+ [Constants.MessageTypes.USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_1]: "System message",
+ [Constants.MessageTypes.USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_2]: "System message",
+ [Constants.MessageTypes.USER_PREMIUM_GUILD_SUBSCRIPTION_TIER_3]: "System message",
+ [Constants.MessageTypes.CHANNEL_FOLLOW_ADD]: "System message",
+ [Constants.MessageTypes.GUILD_DISCOVERY_DISQUALIFIED]: "System message",
+ [Constants.MessageTypes.GUILD_DISCOVERY_REQUALIFIED]: "System message",
+ }[message.type] || "Unknown";
+
+ embed.fields.push({
+ name: preEmbedPadding + "Message information",
+ value: trimEmptyLines(
+ trimLines(`
+ ID: \`${message.id}\`
+ Channel: <#${message.channel.id}>
+ Channel ID: \`${message.channel.id}\`
+ Created: **${messageAge} ago** (\`${createdAt.format("MMM D, YYYY [at] H:mm [UTC]")}\`)
+ ${editedAt ? `Edited at: **${editAge} ago** (\`${editedAt.format("MMM D, YYYY [at] H:mm [UTC]")}\`)` : ""}
+ Type: **${type}**
+ Link: [**Go to message ➔**](https://discord.com/channels/${pluginData.guild.id}/${message.channel.id}/${
+ message.id
+ })
+ `),
+ ),
+ });
+
+ const authorCreatedAt = moment(message.author.createdAt);
+ const authorAccountAge = humanizeDuration(Date.now() - message.author.createdAt, {
+ largest: 2,
+ round: true,
+ });
+
+ const authorJoinedAt = message.member && moment(message.member.joinedAt);
+ const authorServerAge =
+ message.member &&
+ humanizeDuration(Date.now() - message.member.joinedAt, {
+ largest: 2,
+ round: true,
+ });
+
+ embed.fields.push({
+ name: preEmbedPadding + "Author information",
+ value: trimLines(`
+ Name: **${message.author.username}#${message.author.discriminator}**
+ ID: \`${message.author.id}\`
+ Created: **${authorAccountAge} ago** (\`${authorCreatedAt.format("MMM D, YYYY [at] H:mm [UTC]")}\`)
+ ${
+ authorJoinedAt
+ ? `Joined: **${authorServerAge} ago** (\`${authorJoinedAt.format("MMM D, YYYY [at] H:mm [UTC]")}\`)`
+ : ""
+ }
+ Mention: <@!${message.author.id}>
+ `),
+ });
+
+ const textContent = message.content || "";
+ const chunked = chunkMessageLines(textContent, 1014);
+ for (const [i, chunk] of chunked.entries()) {
+ embed.fields.push({
+ name: i === 0 ? preEmbedPadding + "Text content" : "[...]",
+ value: chunk,
+ });
+ }
+
+ if (message.attachments.length) {
+ embed.fields.push({
+ name: preEmbedPadding + "Attachments",
+ value: message.attachments[0].url,
+ });
+ }
+
+ if (message.embeds.length) {
+ const prefix = pluginData.guildConfig.prefix || getDefaultPrefix(pluginData.client);
+ embed.fields.push({
+ name: preEmbedPadding + "Embeds",
+ value: `Message contains an embed, use \`${prefix}source\` to see the embed source`,
+ });
+ }
+
+ return embed;
+}
diff --git a/backend/src/plugins/Utility/types.ts b/backend/src/plugins/Utility/types.ts
index 5efbadc3..e085e2da 100644
--- a/backend/src/plugins/Utility/types.ts
+++ b/backend/src/plugins/Utility/types.ts
@@ -15,6 +15,7 @@ export const ConfigSchema = t.type({
can_server: t.boolean,
can_invite: t.boolean,
can_channel: t.boolean,
+ can_message: t.boolean,
can_reload_guild: t.boolean,
can_nickname: t.boolean,
can_ping: t.boolean,
diff --git a/backend/src/utils.ts b/backend/src/utils.ts
index c6c91546..8bda4bda 100644
--- a/backend/src/utils.ts
+++ b/backend/src/utils.ts
@@ -492,6 +492,13 @@ export function trimLines(str: string) {
.trim();
}
+export function trimEmptyLines(str: string) {
+ return str
+ .split("\n")
+ .filter(l => l.trim() !== "")
+ .join("\n");
+}
+
export function asSingleLine(str: string) {
return trimLines(str).replace(/\n/g, " ");
}
|