diff --git a/assets/icons/LICENSE b/assets/icons/LICENSE
new file mode 100644
index 00000000..8518a347
--- /dev/null
+++ b/assets/icons/LICENSE
@@ -0,0 +1,4 @@
+# TWEMOJI
+Copyright 2020 Twitter, Inc and other contributors
+Code licensed under the MIT License: http://opensource.org/licenses/MIT
+Graphics licensed under CC-BY 4.0: https://creativecommons.org/licenses/by/4.0/
diff --git a/assets/icons/memo.svg b/assets/icons/memo.svg
new file mode 100644
index 00000000..1697ffbe
--- /dev/null
+++ b/assets/icons/memo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/icons/message.png b/assets/icons/message.png
new file mode 100644
index 00000000..e10d5af8
Binary files /dev/null and b/assets/icons/message.png differ
diff --git a/assets/icons/message.svg b/assets/icons/message.svg
new file mode 100644
index 00000000..62cf2b78
--- /dev/null
+++ b/assets/icons/message.svg
@@ -0,0 +1,33 @@
+
+
+
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, " ");
}