diff --git a/backend/src/plugins/Starboard/events/StarboardReactionAddEvt.ts b/backend/src/plugins/Starboard/events/StarboardReactionAddEvt.ts index a4fe7cb2..0de8af2f 100644 --- a/backend/src/plugins/Starboard/events/StarboardReactionAddEvt.ts +++ b/backend/src/plugins/Starboard/events/StarboardReactionAddEvt.ts @@ -2,6 +2,7 @@ import { starboardEvt } from "../types"; import { Message, TextChannel } from "eris"; import { UnknownUser, resolveMember, noop, resolveUser } from "../../../utils"; import { saveMessageToStarboard } from "../util/saveMessageToStarboard"; +import { updateStarboardMessageStarCount } from "../util/updateStarboardMessageStarCount"; export const StarboardReactionAddEvt = starboardEvt({ event: "messageReactionAdd", @@ -59,21 +60,39 @@ export const StarboardReactionAddEvt = starboardEvt({ for (const starboard of applicableStarboards) { const boardLock = await pluginData.locks.acquire(`starboards-channel-${starboard.channel_id}`); + // Save reaction into the database await pluginData.state.starboardReactions.createStarboardReaction(msg.id, userId).catch(noop); - // If the message has already been posted to this starboard, we don't need to do anything else + const reactions = await pluginData.state.starboardReactions.getAllReactionsForMessageId(msg.id); + const reactionsCount = reactions.length; + const starboardMessages = await pluginData.state.starboardMessages.getMatchingStarboardMessages( starboard.channel_id, msg.id, ); - if (starboardMessages.length > 0) continue; - - const reactions = await pluginData.state.starboardReactions.getAllReactionsForMessageId(msg.id); - const reactionsCount = reactions.length; - if (reactionsCount >= starboard.stars_required) { + if (starboardMessages.length > 0) { + // If the message has already been posted to this starboard, update star counts + if (starboard.show_star_count) { + for (const starboardMessage of starboardMessages) { + const realStarboardMessage = await pluginData.client.getMessage( + starboardMessage.starboard_channel_id, + starboardMessage.starboard_message_id, + ); + await updateStarboardMessageStarCount( + starboard, + msg, + realStarboardMessage, + starboard.star_emoji![0]!, + reactionsCount, + ); + } + } + } else if (reactionsCount >= starboard.stars_required) { + // Otherwise, if the star count exceeds the required star count, save the message to the starboard await saveMessageToStarboard(pluginData, msg, starboard); } + boardLock.unlock(); } }, diff --git a/backend/src/plugins/Starboard/types.ts b/backend/src/plugins/Starboard/types.ts index eb2c3c40..8c94f415 100644 --- a/backend/src/plugins/Starboard/types.ts +++ b/backend/src/plugins/Starboard/types.ts @@ -11,6 +11,7 @@ const StarboardOpts = t.type({ star_emoji: tNullable(t.array(t.string)), copy_full_embed: tNullable(t.boolean), enabled: tNullable(t.boolean), + show_star_count: t.boolean, }); export type TStarboardOpts = t.TypeOf; @@ -25,6 +26,7 @@ export const PartialConfigSchema = tDeepPartial(ConfigSchema); export const defaultStarboardOpts: Partial = { star_emoji: ["⭐"], enabled: true, + show_star_count: true, }; export interface StarboardPluginType extends BasePluginType { diff --git a/backend/src/plugins/Starboard/util/createStarboardEmbedFromMessage.ts b/backend/src/plugins/Starboard/util/createStarboardEmbedFromMessage.ts new file mode 100644 index 00000000..d63b519c --- /dev/null +++ b/backend/src/plugins/Starboard/util/createStarboardEmbedFromMessage.ts @@ -0,0 +1,79 @@ +import { EmbedWith, EMPTY_CHAR, messageLink } from "../../../utils"; +import { EmbedOptions, GuildChannel, Message } from "eris"; +import path from "path"; + +const imageAttachmentExtensions = ["jpeg", "jpg", "png", "gif", "webp"]; +const audioAttachmentExtensions = ["wav", "mp3", "m4a"]; +const videoAttachmentExtensions = ["mp4", "mkv", "mov"]; + +type StarboardEmbed = EmbedWith<"footer" | "author" | "fields" | "timestamp">; + +export function createStarboardEmbedFromMessage(msg: Message, copyFullEmbed: boolean): StarboardEmbed { + const embed: StarboardEmbed = { + footer: { + text: `#${(msg.channel as GuildChannel).name}`, + }, + author: { + name: `${msg.author.username}#${msg.author.discriminator}`, + }, + fields: [], + timestamp: new Date(msg.timestamp).toISOString(), + }; + + if (msg.author.avatarURL) { + embed.author.icon_url = msg.author.avatarURL; + } + + // The second condition here checks for messages with only an image link that is then embedded. + // The message content in that case is hidden by the Discord client, so we hide it here too. + if (msg.content && msg.embeds[0]?.thumbnail?.url !== msg.content) { + embed.description = msg.content; + } + + // Merge media and - if copy_full_embed is enabled - fields and title from the first embed in the original message + if (msg.embeds.length > 0) { + if (msg.embeds[0].image) { + embed.image = msg.embeds[0].image; + } else if (msg.embeds[0].thumbnail) { + embed.image = { url: msg.embeds[0].thumbnail.url }; + } + + if (copyFullEmbed) { + if (msg.embeds[0].title) { + const titleText = msg.embeds[0].url ? `[${msg.embeds[0].title}](${msg.embeds[0].url})` : msg.embeds[0].title; + embed.fields.push({ name: EMPTY_CHAR, value: titleText }); + } + + if (msg.embeds[0].fields) { + embed.fields.push(...msg.embeds[0].fields); + } + } + } + + // If there are no embeds, add the first image attachment explicitly + else if (msg.attachments.length) { + for (const attachment of msg.attachments) { + const ext = path + .extname(attachment.filename) + .slice(1) + .toLowerCase(); + + if (imageAttachmentExtensions.includes(ext)) { + embed.image = { url: attachment.url }; + break; + } + + if (audioAttachmentExtensions.includes(ext)) { + embed.fields.push({ name: EMPTY_CHAR, value: `*Message contains an audio clip*` }); + break; + } + + if (videoAttachmentExtensions.includes(ext)) { + embed.fields.push({ name: EMPTY_CHAR, value: `*Message contains a video*` }); + break; + } + } + } + + return embed; +} diff --git a/backend/src/plugins/Starboard/util/createStarboardPseudoFooterForMessage.ts b/backend/src/plugins/Starboard/util/createStarboardPseudoFooterForMessage.ts new file mode 100644 index 00000000..08172831 --- /dev/null +++ b/backend/src/plugins/Starboard/util/createStarboardPseudoFooterForMessage.ts @@ -0,0 +1,27 @@ +import { EmbedField, EmojiOptions, GuildChannel, Message } from "eris"; +import { EMPTY_CHAR, messageLink } from "../../../utils"; +import { TStarboardOpts } from "../types"; + +export function createStarboardPseudoFooterForMessage( + starboard: TStarboardOpts, + msg: Message, + starEmoji: string, + starCount: number, +): EmbedField { + const jumpLink = `[Jump to message](${messageLink(msg)})`; + + let content; + if (starboard.show_star_count) { + content = + starCount > 1 + ? `${starEmoji} **${starCount}** \u200B \u200B \u200B ${jumpLink}` + : `${starEmoji} \u200B ${jumpLink}`; + } else { + content = jumpLink; + } + + return { + name: EMPTY_CHAR, + value: content, + }; +} diff --git a/backend/src/plugins/Starboard/util/saveMessageToStarboard.ts b/backend/src/plugins/Starboard/util/saveMessageToStarboard.ts index bf161366..f30beffc 100644 --- a/backend/src/plugins/Starboard/util/saveMessageToStarboard.ts +++ b/backend/src/plugins/Starboard/util/saveMessageToStarboard.ts @@ -4,10 +4,8 @@ import { Message, GuildChannel, TextChannel, Embed } from "eris"; import moment from "moment-timezone"; import { EmbedWith, EMPTY_CHAR, messageLink } from "../../../utils"; import path from "path"; - -const imageAttachmentExtensions = ["jpeg", "jpg", "png", "gif", "webp"]; -const audioAttachmentExtensions = ["wav", "mp3", "m4a"]; -const videoAttachmentExtensions = ["mp4", "mkv", "mov"]; +import { createStarboardEmbedFromMessage } from "./createStarboardEmbedFromMessage"; +import { createStarboardPseudoFooterForMessage } from "./createStarboardPseudoFooterForMessage"; export async function saveMessageToStarboard( pluginData: GuildPluginData, @@ -17,73 +15,9 @@ export async function saveMessageToStarboard( const channel = pluginData.guild.channels.get(starboard.channel_id); if (!channel) return; - const embed: EmbedWith<"footer" | "author" | "fields" | "timestamp"> = { - footer: { - text: `#${(msg.channel as GuildChannel).name}`, - }, - author: { - name: `${msg.author.username}#${msg.author.discriminator}`, - }, - fields: [], - timestamp: new Date(msg.timestamp).toISOString(), - }; - - if (msg.author.avatarURL) { - embed.author.icon_url = msg.author.avatarURL; - } - - // The second condition here checks for messages with only an image link that is then embedded. - // The message content in that case is hidden by the Discord client, so we hide it here too. - if (msg.content && msg.embeds[0]?.thumbnail?.url !== msg.content) { - embed.description = msg.content; - } - - // Merge media and - if copy_full_embed is enabled - fields and title from the first embed in the original message - if (msg.embeds.length > 0) { - if (msg.embeds[0].image) { - embed.image = msg.embeds[0].image; - } else if (msg.embeds[0].thumbnail) { - embed.image = { url: msg.embeds[0].thumbnail.url }; - } - - if (starboard.copy_full_embed) { - if (msg.embeds[0].title) { - const titleText = msg.embeds[0].url ? `[${msg.embeds[0].title}](${msg.embeds[0].url})` : msg.embeds[0].title; - embed.fields.push({ name: EMPTY_CHAR, value: titleText }); - } - - if (msg.embeds[0].fields) { - embed.fields.push(...msg.embeds[0].fields); - } - } - } - - // If there are no embeds, add the first image attachment explicitly - else if (msg.attachments.length) { - for (const attachment of msg.attachments) { - const ext = path - .extname(attachment.filename) - .slice(1) - .toLowerCase(); - - if (imageAttachmentExtensions.includes(ext)) { - embed.image = { url: attachment.url }; - break; - } - - if (audioAttachmentExtensions.includes(ext)) { - embed.fields.push({ name: EMPTY_CHAR, value: `*Message contains an audio clip*` }); - break; - } - - if (videoAttachmentExtensions.includes(ext)) { - embed.fields.push({ name: EMPTY_CHAR, value: `*Message contains a video*` }); - break; - } - } - } - - embed.fields.push({ name: EMPTY_CHAR, value: `[Jump to message](${messageLink(msg)})` }); + const starCount = (await pluginData.state.starboardReactions.getAllReactionsForMessageId(msg.id)).length; + const embed = createStarboardEmbedFromMessage(msg, Boolean(starboard.copy_full_embed)); + embed.fields!.push(createStarboardPseudoFooterForMessage(starboard, msg, starboard.star_emoji![0], starCount)); const starboardMessage = await (channel as TextChannel).createMessage({ embed }); await pluginData.state.starboardMessages.createStarboardMessage(channel.id, msg.id, starboardMessage.id); diff --git a/backend/src/plugins/Starboard/util/updateStarboardMessageStarCount.ts b/backend/src/plugins/Starboard/util/updateStarboardMessageStarCount.ts new file mode 100644 index 00000000..61ce2bd5 --- /dev/null +++ b/backend/src/plugins/Starboard/util/updateStarboardMessageStarCount.ts @@ -0,0 +1,17 @@ +import { Client, GuildTextableChannel, Message } from "eris"; +import { noop } from "../../../utils"; +import { createStarboardPseudoFooterForMessage } from "./createStarboardPseudoFooterForMessage"; +import { TStarboardOpts } from "../types"; + +export async function updateStarboardMessageStarCount( + starboard: TStarboardOpts, + originalMessage: Message, + starboardMessage: Message, + starEmoji: string, + starCount: number, +) { + const embed = starboardMessage.embeds[0]!; + embed.fields!.shift(); // Remove pseudo footer + embed.fields!.push(createStarboardPseudoFooterForMessage(starboard, originalMessage, starEmoji, starCount)); // Create new pseudo footer + await starboardMessage.edit({ embed }); +}