diff --git a/backend/package-lock.json b/backend/package-lock.json index ab298c68..79e1da28 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -24,7 +24,7 @@ "humanize-duration": "^3.15.0", "io-ts": "^2.0.0", "js-yaml": "^3.13.1", - "knub": "^30.0.0-beta.42", + "knub": "^30.0.0-beta.45", "knub-command-manager": "^9.1.0", "last-commit-log": "^2.1.0", "lodash.chunk": "^4.2.0", @@ -3043,9 +3043,9 @@ } }, "node_modules/knub": { - "version": "30.0.0-beta.42", - "resolved": "https://registry.npmjs.org/knub/-/knub-30.0.0-beta.42.tgz", - "integrity": "sha512-y7nqQh1bzQniYwEftdv6S8Jp2qBvT5a7vn+3JeA0s0ADXobI+/rRVznpq8o0x2m0+E+EeKxo1Ch8F8Hy+VMX6w==", + "version": "30.0.0-beta.45", + "resolved": "https://registry.npmjs.org/knub/-/knub-30.0.0-beta.45.tgz", + "integrity": "sha512-r1jtHBYthOn8zjgyILh418/Qnw8f/cUMzz5aky7+T5HLFV0BAiBzeg5TOb0UFMkn8ewIPSy8GTG1x/CIAv3s8Q==", "dependencies": { "discord-api-types": "^0.22.0", "discord.js": "^13.0.1", @@ -8290,9 +8290,9 @@ } }, "knub": { - "version": "30.0.0-beta.42", - "resolved": "https://registry.npmjs.org/knub/-/knub-30.0.0-beta.42.tgz", - "integrity": "sha512-y7nqQh1bzQniYwEftdv6S8Jp2qBvT5a7vn+3JeA0s0ADXobI+/rRVznpq8o0x2m0+E+EeKxo1Ch8F8Hy+VMX6w==", + "version": "30.0.0-beta.45", + "resolved": "https://registry.npmjs.org/knub/-/knub-30.0.0-beta.45.tgz", + "integrity": "sha512-r1jtHBYthOn8zjgyILh418/Qnw8f/cUMzz5aky7+T5HLFV0BAiBzeg5TOb0UFMkn8ewIPSy8GTG1x/CIAv3s8Q==", "requires": { "discord-api-types": "^0.22.0", "discord.js": "^13.0.1", diff --git a/backend/package.json b/backend/package.json index 5ef83427..685637b7 100644 --- a/backend/package.json +++ b/backend/package.json @@ -39,7 +39,7 @@ "humanize-duration": "^3.15.0", "io-ts": "^2.0.0", "js-yaml": "^3.13.1", - "knub": "^30.0.0-beta.42", + "knub": "^30.0.0-beta.45", "knub-command-manager": "^9.1.0", "last-commit-log": "^2.1.0", "lodash.chunk": "^4.2.0", diff --git a/backend/src/data/GuildSavedMessages.ts b/backend/src/data/GuildSavedMessages.ts index 0e7db243..462df642 100644 --- a/backend/src/data/GuildSavedMessages.ts +++ b/backend/src/data/GuildSavedMessages.ts @@ -226,9 +226,6 @@ export class GuildSavedMessages extends BaseGuildRepository { } async createFromMsg(msg: Message, overrides = {}) { - const existingSavedMsg = await this.find(msg.id); - if (existingSavedMsg) return; - // FIXME: Hotfix if (!msg.channel) { return; diff --git a/backend/src/index.ts b/backend/src/index.ts index 2ab3417c..108a3cbe 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -300,6 +300,22 @@ connect().then(async () => { startUptimeCounter(); }); + const debugGuilds = ["877581055920603238", "348468156597010432", "134286179121102848"]; + bot.on("guildLoaded", guildId => { + if (!debugGuilds.includes(guildId)) { + return; + } + + console.log(`[!! DEBUG !!] LOADED GUILD ${guildId}`); + }); + bot.on("guildUnloaded", guildId => { + if (!debugGuilds.includes(guildId)) { + return; + } + + console.log(`[!! DEBUG !!] UNLOADED GUILD ${guildId}`); + }); + bot.initialize(); logger.info("Bot Initialized"); logger.info("Logging in..."); diff --git a/backend/src/pluginUtils.ts b/backend/src/pluginUtils.ts index 14dcf85e..309914f1 100644 --- a/backend/src/pluginUtils.ts +++ b/backend/src/pluginUtils.ts @@ -232,7 +232,7 @@ export function getBaseUrl(pluginData: AnyPluginData) { export function isOwner(pluginData: AnyPluginData, userId: string) { const knub = pluginData.getKnubInstance() as TZeppelinKnub; - const owners = knub.getGlobalConfig().owners; + const owners = knub.getGlobalConfig()?.owners; if (!owners) { return false; } diff --git a/backend/src/plugins/Automod/AutomodPlugin.ts b/backend/src/plugins/Automod/AutomodPlugin.ts index 7748b1b2..d8c81512 100644 --- a/backend/src/plugins/Automod/AutomodPlugin.ts +++ b/backend/src/plugins/Automod/AutomodPlugin.ts @@ -114,6 +114,21 @@ const configPreprocessor: ConfigPreprocessorFn = options => { ]); } } + + if (triggerObj[triggerName].match_mime_type) { + const white = triggerObj[triggerName].match_mime_type.whitelist_enabled; + const black = triggerObj[triggerName].match_mime_type.blacklist_enabled; + + if (white && black) { + throw new StrictValidationError([ + `Cannot have both blacklist and whitelist enabled at rule <${rule.name}/match_mime_type>`, + ]); + } else if (!white && !black) { + throw new StrictValidationError([ + `Must have either blacklist or whitelist enabled at rule <${rule.name}/match_mime_type>`, + ]); + } + } } } } diff --git a/backend/src/plugins/Automod/actions/archiveThread.ts b/backend/src/plugins/Automod/actions/archiveThread.ts new file mode 100644 index 00000000..7c7aa218 --- /dev/null +++ b/backend/src/plugins/Automod/actions/archiveThread.ts @@ -0,0 +1,20 @@ +import { ThreadChannel } from "discord.js"; +import * as t from "io-ts"; +import { noop } from "../../../utils"; +import { automodAction } from "../helpers"; + +export const ArchiveThreadAction = automodAction({ + configType: t.type({}), + defaultConfig: {}, + + async apply({ pluginData, contexts }) { + const threads = contexts + .filter(c => c.message?.channel_id) + .map(c => pluginData.guild.channels.cache.get(c.message!.channel_id)) + .filter((c): c is ThreadChannel => c?.isThread() ?? false); + + for (const thread of threads) { + await thread.setArchived().catch(noop); + } + }, +}); diff --git a/backend/src/plugins/Automod/actions/availableActions.ts b/backend/src/plugins/Automod/actions/availableActions.ts index f6596914..c37f3a39 100644 --- a/backend/src/plugins/Automod/actions/availableActions.ts +++ b/backend/src/plugins/Automod/actions/availableActions.ts @@ -3,6 +3,7 @@ import { AutomodActionBlueprint } from "../helpers"; import { AddRolesAction } from "./addRoles"; import { AddToCounterAction } from "./addToCounter"; import { AlertAction } from "./alert"; +import { ArchiveThreadAction } from "./archiveThread"; import { BanAction } from "./ban"; import { ChangeNicknameAction } from "./changeNickname"; import { CleanAction } from "./clean"; @@ -34,6 +35,7 @@ export const availableActions: Record> = { set_counter: SetCounterAction, set_slowmode: SetSlowmodeAction, start_thread: StartThreadAction, + archive_thread: ArchiveThreadAction, }; export const AvailableActions = t.type({ @@ -53,4 +55,5 @@ export const AvailableActions = t.type({ set_counter: SetCounterAction.configType, set_slowmode: SetSlowmodeAction.configType, start_thread: StartThreadAction.configType, + archive_thread: ArchiveThreadAction.configType, }); diff --git a/backend/src/plugins/Automod/actions/reply.ts b/backend/src/plugins/Automod/actions/reply.ts index d40681e3..70527f0b 100644 --- a/backend/src/plugins/Automod/actions/reply.ts +++ b/backend/src/plugins/Automod/actions/reply.ts @@ -1,7 +1,6 @@ import { MessageOptions, Permissions, Snowflake, TextChannel, ThreadChannel, User } from "discord.js"; import * as t from "io-ts"; import { userToTemplateSafeUser } from "../../../utils/templateSafeObjects"; -import { LogType } from "../../../data/LogType"; import { renderTemplate, TemplateSafeValueContainer } from "../../../templateFormatter"; import { convertDelayStringToMS, @@ -25,6 +24,7 @@ export const ReplyAction = automodAction({ t.type({ text: tMessageContent, auto_delete: tNullable(t.union([tDelayString, t.number])), + use_inline_reply: tNullable(t.boolean), }), ]), @@ -51,7 +51,7 @@ export const ReplyAction = automodAction({ const users = unique(Array.from(new Set(_contexts.map(c => c.user).filter(Boolean)))) as User[]; const user = users[0]; - const renderReplyText = async str => + const renderReplyText = async (str: string) => renderTemplate( str, new TemplateSafeValueContainer({ @@ -94,16 +94,26 @@ export const ReplyAction = automodAction({ } const messageContent = validateAndParseMessageContent(formatted); - const replyMsg = await channel.send({ + + const messageOpts: MessageOptions = { ...messageContent, allowedMentions: { users: [user.id], }, - }); + }; + + if (typeof actionConfig !== "string" && actionConfig.use_inline_reply) { + messageOpts.reply = { + failIfNotExists: false, + messageReference: _contexts[0].message!.id, + }; + } + + const replyMsg = await channel.send(messageOpts); if (typeof actionConfig === "object" && actionConfig.auto_delete) { const delay = convertDelayStringToMS(String(actionConfig.auto_delete))!; - setTimeout(() => replyMsg.delete().catch(noop), delay); + setTimeout(() => !replyMsg.deleted && replyMsg.delete().catch(noop), delay); } } } diff --git a/backend/src/plugins/Automod/events/RunAutomodOnMemberUpdate.ts b/backend/src/plugins/Automod/events/RunAutomodOnMemberUpdate.ts index a3636118..cf4563ca 100644 --- a/backend/src/plugins/Automod/events/RunAutomodOnMemberUpdate.ts +++ b/backend/src/plugins/Automod/events/RunAutomodOnMemberUpdate.ts @@ -14,8 +14,8 @@ export const RunAutomodOnMemberUpdate = typedGuildEventListener match_invites: MatchInvitesTrigger, match_links: MatchLinksTrigger, match_attachment_type: MatchAttachmentTypeTrigger, + match_mime_type: MatchMimeTypeTrigger, member_join: MemberJoinTrigger, role_added: RoleAddedTrigger, role_removed: RoleRemovedTrigger, @@ -72,6 +74,7 @@ export const AvailableTriggers = t.type({ match_invites: MatchInvitesTrigger.configType, match_links: MatchLinksTrigger.configType, match_attachment_type: MatchAttachmentTypeTrigger.configType, + match_mime_type: MatchMimeTypeTrigger.configType, member_join: MemberJoinTrigger.configType, member_leave: MemberLeaveTrigger.configType, role_added: RoleAddedTrigger.configType, diff --git a/backend/src/plugins/Automod/triggers/matchMimeType.ts b/backend/src/plugins/Automod/triggers/matchMimeType.ts new file mode 100644 index 00000000..f38bb8fa --- /dev/null +++ b/backend/src/plugins/Automod/triggers/matchMimeType.ts @@ -0,0 +1,79 @@ +import { automodTrigger } from "../helpers"; +import * as t from "io-ts"; +import { asSingleLine, messageSummary, verboseChannelMention } from "../../../utils"; +import { GuildChannel, Util } from "discord.js"; + +interface MatchResultType { + matchedType: string; + mode: "blacklist" | "whitelist"; +} + +export const MatchMimeTypeTrigger = automodTrigger()({ + configType: t.type({ + mime_type_blacklist: t.array(t.string), + blacklist_enabled: t.boolean, + mime_type_whitelist: t.array(t.string), + whitelist_enabled: t.boolean, + }), + + defaultConfig: { + mime_type_blacklist: [], + blacklist_enabled: false, + mime_type_whitelist: [], + whitelist_enabled: false, + }, + + async match({ context, triggerConfig: trigger }) { + if (!context.message) return; + + const { attachments } = context.message.data; + if (!attachments) return null; + + for (const attachment of attachments) { + const { contentType } = attachment; + + const blacklist = trigger.blacklist_enabled + ? (trigger.mime_type_blacklist ?? []).map(_t => _t.toLowerCase()) + : null; + + if (contentType && blacklist?.includes(contentType)) { + return { + extra: { + matchedType: contentType, + mode: "blacklist", + }, + }; + } + + const whitelist = trigger.whitelist_enabled + ? (trigger.mime_type_whitelist ?? []).map(_t => _t.toLowerCase()) + : null; + + if (whitelist && (!contentType || !whitelist.includes(contentType))) { + return { + extra: { + matchedType: contentType || "", + mode: "whitelist", + }, + }; + } + + return null; + } + }, + + renderMatchInformation({ pluginData, contexts, matchResult }) { + const { message } = contexts[0]; + const channel = pluginData.guild.channels.resolve(message!.channel_id); + const prettyChannel = verboseChannelMention(channel as GuildChannel); + const { matchedType, mode } = matchResult.extra; + + return ( + asSingleLine(` + Matched MIME type \`${Util.escapeInlineCode(matchedType)}\` + (${mode === "blacklist" ? "blacklisted" : "not in whitelist"}) + in message (\`${message!.id}\`) in ${prettyChannel} + `) + messageSummary(message!) + ); + }, +}); diff --git a/backend/src/plugins/BotControl/BotControlPlugin.ts b/backend/src/plugins/BotControl/BotControlPlugin.ts index dd53d72c..6770b8ef 100644 --- a/backend/src/plugins/BotControl/BotControlPlugin.ts +++ b/backend/src/plugins/BotControl/BotControlPlugin.ts @@ -18,11 +18,13 @@ import { ReloadServerCmd } from "./commands/ReloadServerCmd"; import { RemoveDashboardUserCmd } from "./commands/RemoveDashboardUserCmd"; import { ServersCmd } from "./commands/ServersCmd"; import { BotControlPluginType, ConfigSchema } from "./types"; +import { PerformanceCmd } from "./commands/PerformanceCmd"; const defaultOptions = { config: { can_use: false, can_eligible: false, + can_performance: false, update_cmd: null, }, }; @@ -45,6 +47,7 @@ export const BotControlPlugin = zeppelinGlobalPlugin()({ ListDashboardUsersCmd, ListDashboardPermsCmd, EligibleCmd, + PerformanceCmd, ], async afterLoad(pluginData) { diff --git a/backend/src/plugins/BotControl/commands/PerformanceCmd.ts b/backend/src/plugins/BotControl/commands/PerformanceCmd.ts new file mode 100644 index 00000000..6e4c857a --- /dev/null +++ b/backend/src/plugins/BotControl/commands/PerformanceCmd.ts @@ -0,0 +1,23 @@ +import { TextChannel } from "discord.js"; +import { commandTypeHelpers as ct } from "../../../commandTypes"; +import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils"; +import { createChunkedMessage, formatNumber, resolveInvite, sorter, verboseUserMention } from "../../../utils"; +import { botControlCmd } from "../types"; + +export const PerformanceCmd = botControlCmd({ + trigger: ["performance"], + permission: "can_performance", + + signature: {}, + + async run({ pluginData, message: msg, args }) { + const stats = pluginData.getKnubInstance().getPluginPerformanceStats(); + const averageLoadTimeEntries = Object.entries(stats.averageLoadTimes); + averageLoadTimeEntries.sort(sorter(v => v[1].time, "DESC")); + const lines = averageLoadTimeEntries.map( + ([pluginName, { time }]) => `${pluginName}: **${formatNumber(Math.round(time))}ms**`, + ); + const fullStats = `Average plugin load times:\n\n${lines.join("\n")}`; + createChunkedMessage(msg.channel as TextChannel, fullStats); + }, +}); diff --git a/backend/src/plugins/BotControl/types.ts b/backend/src/plugins/BotControl/types.ts index 1acac3a4..44e57cf8 100644 --- a/backend/src/plugins/BotControl/types.ts +++ b/backend/src/plugins/BotControl/types.ts @@ -9,6 +9,7 @@ import { tNullable } from "../../utils"; export const ConfigSchema = t.type({ can_use: t.boolean, can_eligible: t.boolean, + can_performance: t.boolean, update_cmd: tNullable(t.string), }); export type TConfigSchema = t.TypeOf; diff --git a/backend/src/plugins/CustomEvents/CustomEventsPlugin.ts b/backend/src/plugins/CustomEvents/CustomEventsPlugin.ts index 009bf5a6..391ee191 100644 --- a/backend/src/plugins/CustomEvents/CustomEventsPlugin.ts +++ b/backend/src/plugins/CustomEvents/CustomEventsPlugin.ts @@ -51,7 +51,7 @@ export const CustomEventsPlugin = zeppelinGuildPlugin()( } const values = createTypedTemplateSafeValueContainer({ - ...args, + ...safeArgs, msg: messageToTemplateSafeMessage(message), }); diff --git a/backend/src/plugins/CustomEvents/actions/addRoleAction.ts b/backend/src/plugins/CustomEvents/actions/addRoleAction.ts index 4fb21bf1..6b83770a 100644 --- a/backend/src/plugins/CustomEvents/actions/addRoleAction.ts +++ b/backend/src/plugins/CustomEvents/actions/addRoleAction.ts @@ -28,9 +28,11 @@ export async function addRoleAction( if (event.trigger.type === "command" && !canActOn(pluginData, eventData.msg.member, target)) { throw new ActionError("Missing permissions"); } - - const rolesToAdd = Array.isArray(action.role) ? action.role : [action.role]; - await target.edit({ - roles: Array.from(new Set([...target.roles.cache.values(), ...rolesToAdd])) as Snowflake[], - }); + const rolesToAdd = (Array.isArray(action.role) ? action.role : [action.role]).filter( + id => !target.roles.cache.has(id), + ); + if (rolesToAdd.length === 0) { + throw new ActionError("Target already has the role(s) specified"); + } + await target.roles.add(rolesToAdd); } diff --git a/backend/src/plugins/CustomEvents/actions/setChannelPermissionOverrides.ts b/backend/src/plugins/CustomEvents/actions/setChannelPermissionOverrides.ts index 6c345f05..6b9e9b1b 100644 --- a/backend/src/plugins/CustomEvents/actions/setChannelPermissionOverrides.ts +++ b/backend/src/plugins/CustomEvents/actions/setChannelPermissionOverrides.ts @@ -1,4 +1,4 @@ -import { Permissions, Snowflake, TextChannel } from "discord.js"; +import { Permissions, Snowflake, TextChannel, PermissionString } from "discord.js"; import * as t from "io-ts"; import { GuildPluginData } from "knub"; import { ActionError } from "../ActionError"; @@ -32,10 +32,17 @@ export async function setChannelPermissionOverridesAction( } for (const override of action.overrides) { - channel.permissionOverwrites.create( - override.id as Snowflake, - new Permissions(BigInt(override.allow)).remove(BigInt(override.deny)).serialize(), - ); + const allow = new Permissions(BigInt(override.allow)).serialize(); + const deny = new Permissions(BigInt(override.deny)).serialize(); + const perms: Partial> = {}; + for (const key in allow) { + if (allow[key]) { + perms[key] = true; + } else if (deny[key]) { + perms[key] = false; + } + } + channel.permissionOverwrites.create(override.id as Snowflake, perms); /* await channel.permissionOverwrites overwritePermissions( diff --git a/backend/src/plugins/Logs/events/LogsUserUpdateEvts.ts b/backend/src/plugins/Logs/events/LogsUserUpdateEvts.ts index 06b8a516..07e0165f 100644 --- a/backend/src/plugins/Logs/events/LogsUserUpdateEvts.ts +++ b/backend/src/plugins/Logs/events/LogsUserUpdateEvts.ts @@ -17,6 +17,8 @@ export const LogsGuildMemberUpdateEvt = logsEvt({ const pluginData = meta.pluginData; const oldMember = meta.args.oldMember; const member = meta.args.newMember; + const oldRoles = [...oldMember.roles.cache.keys()]; + const currentRoles = [...member.roles.cache.keys()]; if (!oldMember || oldMember.partial) { return; @@ -30,9 +32,9 @@ export const LogsGuildMemberUpdateEvt = logsEvt({ }); } - if (!isEqual(oldMember.roles, member.roles)) { - const addedRoles = diff([...member.roles.cache.keys()], [...oldMember.roles.cache.keys()]); - const removedRoles = diff([...oldMember.roles.cache.keys()], [...member.roles.cache.keys()]); + if (!isEqual(oldRoles, currentRoles)) { + const addedRoles = diff(currentRoles, oldRoles); + const removedRoles = diff(oldRoles, currentRoles); let skip = false; if ( diff --git a/backend/src/plugins/MessageSaver/MessageSaverPlugin.ts b/backend/src/plugins/MessageSaver/MessageSaverPlugin.ts index 0f316ad2..202f03dd 100644 --- a/backend/src/plugins/MessageSaver/MessageSaverPlugin.ts +++ b/backend/src/plugins/MessageSaver/MessageSaverPlugin.ts @@ -21,6 +21,9 @@ const defaultOptions: PluginOptions = { ], }; +let debugId = 0; +const debugGuilds = ["877581055920603238", "348468156597010432", "134286179121102848"]; + export const MessageSaverPlugin = zeppelinGuildPlugin()({ name: "message_saver", showInDocs: false, @@ -45,6 +48,18 @@ export const MessageSaverPlugin = zeppelinGuildPlugin()( beforeLoad(pluginData) { const { state, guild } = pluginData; state.savedMessages = GuildSavedMessages.getGuildInstance(guild.id); - state.queue = new Queue(); + state.debugId = ++debugId; + + if (debugGuilds.includes(pluginData.guild.id)) { + console.log(`[!! DEBUG !!] MessageSaverPlugin::beforeLoad (${state.debugId}): ${pluginData.guild.id}`); + } + }, + + beforeUnload(pluginData) { + if (debugGuilds.includes(pluginData.guild.id)) { + console.log( + `[!! DEBUG !!] MessageSaverPlugin::beforeUnload (${pluginData.state.debugId}): ${pluginData.guild.id}`, + ); + } }, }); diff --git a/backend/src/plugins/MessageSaver/events/SaveMessagesEvts.ts b/backend/src/plugins/MessageSaver/events/SaveMessagesEvts.ts index 386549e6..3e1e91a6 100644 --- a/backend/src/plugins/MessageSaver/events/SaveMessagesEvts.ts +++ b/backend/src/plugins/MessageSaver/events/SaveMessagesEvts.ts @@ -1,14 +1,21 @@ import { Constants, Message, MessageType, Snowflake } from "discord.js"; import { messageSaverEvt } from "../types"; import { SECONDS } from "../../../utils"; +import moment from "moment-timezone"; -const recentlyCreatedMessages: Snowflake[] = []; +const recentlyCreatedMessages: Map = new Map(); const recentlyCreatedMessagesToKeep = 100; setInterval(() => { - const toDelete = recentlyCreatedMessages.length - recentlyCreatedMessagesToKeep; - if (toDelete > 0) { - recentlyCreatedMessages.splice(0, toDelete); + let toDelete = recentlyCreatedMessages.size - recentlyCreatedMessagesToKeep; + for (const key of recentlyCreatedMessages.keys()) { + if (toDelete === 0) { + break; + } + + recentlyCreatedMessages.delete(key); + + toDelete--; } }, 60 * SECONDS); @@ -29,17 +36,25 @@ export const MessageCreateEvt = messageSaverEvt({ return; } - meta.pluginData.state.queue.add(async () => { - if (recentlyCreatedMessages.includes(meta.args.message.id)) { - console.warn( - `Tried to save duplicate message from messageCreate event: ${meta.args.message.guildId} / ${meta.args.message.channelId} / ${meta.args.message.id}`, - ); - return; - } - recentlyCreatedMessages.push(meta.args.message.id); + // Don't save the bot's own messages + if (meta.args.message.author.id === meta.pluginData.client.user?.id) { + return; + } - await meta.pluginData.state.savedMessages.createFromMsg(meta.args.message); - }); + // FIXME: Remove debug code + if (recentlyCreatedMessages.has(meta.args.message.id)) { + const ourDebugId = meta.pluginData.state.debugId; + const oldDebugId = recentlyCreatedMessages.get(meta.args.message.id)![0]; + const oldGuildId = recentlyCreatedMessages.get(meta.args.message.id)![2]; + const context = `${ourDebugId} : ${oldDebugId} / ${meta.pluginData.guild.id} : ${oldGuildId} : ${meta.args.message.guildId} / ${meta.args.message.channelId} / ${meta.args.message.id}`; + const timestamp = moment(recentlyCreatedMessages.get(meta.args.message.id)![1]).format("HH:mm:ss.SSS"); + // tslint:disable-next-line:no-console + console.warn(`Tried to save duplicate message from messageCreate event: ${context} / saved at: ${timestamp}`); + return; + } + recentlyCreatedMessages.set(meta.args.message.id, [meta.pluginData.state.debugId, Date.now(), meta.pluginData.guild.id]); + + await meta.pluginData.state.savedMessages.createFromMsg(meta.args.message); }, }); @@ -57,9 +72,7 @@ export const MessageUpdateEvt = messageSaverEvt({ return; } - meta.pluginData.state.queue.add(async () => { - await meta.pluginData.state.savedMessages.saveEditFromMsg(meta.args.newMessage as Message); - }); + await meta.pluginData.state.savedMessages.saveEditFromMsg(meta.args.newMessage as Message); }, }); @@ -74,9 +87,7 @@ export const MessageDeleteEvt = messageSaverEvt({ return; } - meta.pluginData.state.queue.add(async () => { - await meta.pluginData.state.savedMessages.markAsDeleted(msg.id); - }); + await meta.pluginData.state.savedMessages.markAsDeleted(msg.id); }, }); @@ -87,8 +98,6 @@ export const MessageDeleteBulkEvt = messageSaverEvt({ async listener(meta) { const ids = meta.args.messages.map(m => m.id); - meta.pluginData.state.queue.add(async () => { - await meta.pluginData.state.savedMessages.markBulkAsDeleted(ids); - }); + await meta.pluginData.state.savedMessages.markBulkAsDeleted(ids); }, }); diff --git a/backend/src/plugins/MessageSaver/types.ts b/backend/src/plugins/MessageSaver/types.ts index f8b481f7..694a0b0a 100644 --- a/backend/src/plugins/MessageSaver/types.ts +++ b/backend/src/plugins/MessageSaver/types.ts @@ -12,7 +12,7 @@ export interface MessageSaverPluginType extends BasePluginType { config: TConfigSchema; state: { savedMessages: GuildSavedMessages; - queue: Queue; + debugId: number; }; } diff --git a/backend/src/plugins/Persist/events/StoreDataEvt.ts b/backend/src/plugins/Persist/events/StoreDataEvt.ts index aad0c48e..45dbda4b 100644 --- a/backend/src/plugins/Persist/events/StoreDataEvt.ts +++ b/backend/src/plugins/Persist/events/StoreDataEvt.ts @@ -16,7 +16,7 @@ export const StoreDataEvt = persistEvt({ const persistedRoles = config.persisted_roles; if (persistedRoles.length && member.roles) { - const rolesToPersist = intersection(persistedRoles, member.roles); + const rolesToPersist = intersection(persistedRoles, [...member.roles.cache.keys()]); if (rolesToPersist.length) { persist = true; persistData.roles = rolesToPersist; diff --git a/backend/src/plugins/ReactionRoles/util/applyReactionRoleReactionsToMessage.ts b/backend/src/plugins/ReactionRoles/util/applyReactionRoleReactionsToMessage.ts index 86c81a29..0c6334e9 100644 --- a/backend/src/plugins/ReactionRoles/util/applyReactionRoleReactionsToMessage.ts +++ b/backend/src/plugins/ReactionRoles/util/applyReactionRoleReactionsToMessage.ts @@ -25,7 +25,7 @@ export async function applyReactionRoleReactionsToMessage( let targetMessage; try { - targetMessage = channel.messages.fetch(messageId, { force: true }); + targetMessage = await channel.messages.fetch(messageId, { force: true }); } catch (e) { if (isDiscordAPIError(e)) { if (e.code === 10008) { diff --git a/backend/src/plugins/Roles/commands/RemoveRoleCmd.ts b/backend/src/plugins/Roles/commands/RemoveRoleCmd.ts index d6e36356..63a843c8 100644 --- a/backend/src/plugins/Roles/commands/RemoveRoleCmd.ts +++ b/backend/src/plugins/Roles/commands/RemoveRoleCmd.ts @@ -63,7 +63,7 @@ export const RemoveRoleCmd = rolesCmd({ sendSuccessMessage( pluginData, msg.channel, - `Removed role **${role.name}** removed from ${verboseUserMention(args.member.user)}!`, + `Removed role **${role.name}** from ${verboseUserMention(args.member.user)}!`, ); }, }); diff --git a/backend/src/plugins/Starboard/events/StarboardReactionAddEvt.ts b/backend/src/plugins/Starboard/events/StarboardReactionAddEvt.ts index 22c5761c..99dbbaa0 100644 --- a/backend/src/plugins/Starboard/events/StarboardReactionAddEvt.ts +++ b/backend/src/plugins/Starboard/events/StarboardReactionAddEvt.ts @@ -9,9 +9,6 @@ export const StarboardReactionAddEvt = starboardEvt({ event: "messageReactionAdd", async listener(meta) { - // FIXME: Temporarily disabled - return; - const pluginData = meta.pluginData; let msg = meta.args.reaction.message as Message; diff --git a/backend/src/plugins/Starboard/events/StarboardReactionRemoveEvts.ts b/backend/src/plugins/Starboard/events/StarboardReactionRemoveEvts.ts index d2cc72da..203c4bde 100644 --- a/backend/src/plugins/Starboard/events/StarboardReactionRemoveEvts.ts +++ b/backend/src/plugins/Starboard/events/StarboardReactionRemoveEvts.ts @@ -5,9 +5,6 @@ export const StarboardReactionRemoveEvt = starboardEvt({ event: "messageReactionRemove", async listener(meta) { - // FIXME: Temporarily disabled - return; - const boardLock = await meta.pluginData.locks.acquire(allStarboardsLock()); await meta.pluginData.state.starboardReactions.deleteStarboardReaction( meta.args.reaction.message.id, @@ -21,9 +18,6 @@ export const StarboardReactionRemoveAllEvt = starboardEvt({ event: "messageReactionRemoveAll", async listener(meta) { - // FIXME: Temporarily disabled - return; - const boardLock = await meta.pluginData.locks.acquire(allStarboardsLock()); await meta.pluginData.state.starboardReactions.deleteAllStarboardReactionsForMessageId(meta.args.message.id); boardLock.unlock(); diff --git a/backend/src/plugins/Starboard/util/removeMessageFromStarboard.ts b/backend/src/plugins/Starboard/util/removeMessageFromStarboard.ts index 1a331358..f4635371 100644 --- a/backend/src/plugins/Starboard/util/removeMessageFromStarboard.ts +++ b/backend/src/plugins/Starboard/util/removeMessageFromStarboard.ts @@ -1,6 +1,19 @@ +import { GuildPluginData } from "knub"; import { StarboardMessage } from "../../../data/entities/StarboardMessage"; import { noop } from "../../../utils"; +import { StarboardPluginType } from "../types"; -export async function removeMessageFromStarboard(pluginData, msg: StarboardMessage) { - await pluginData.client.deleteMessage(msg.starboard_channel_id, msg.starboard_message_id).catch(noop); +export async function removeMessageFromStarboard( + pluginData: GuildPluginData, + msg: StarboardMessage, +) { + // fixes stuck entries on starboard_reactions table after messages being deleted, probably should add a cleanup script for this as well, i.e. DELETE FROM starboard_reactions WHERE message_id NOT IN (SELECT id FROM starboard_messages) + await pluginData.state.starboardReactions.deleteAllStarboardReactionsForMessageId(msg.message_id).catch(noop); + + // this code is now Almeida-certified and no longer ugly :ok_hand: :cake: + const channel = pluginData.client.channels.cache.find(c => c.id === msg.starboard_channel_id); + if (!channel?.isText()) return; + const message = await channel.messages.fetch(msg.starboard_message_id).catch(noop); + if (!message?.deletable) return; + await message.delete().catch(noop); } diff --git a/backend/src/plugins/Utility/functions/getInviteInfoEmbed.ts b/backend/src/plugins/Utility/functions/getInviteInfoEmbed.ts index a95de14b..4a7215ed 100644 --- a/backend/src/plugins/Utility/functions/getInviteInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getInviteInfoEmbed.ts @@ -65,32 +65,33 @@ export async function getInviteInfoEmbed( `), inline: true, }); + if (invite.channel) { + const channelName = + invite.channel.type === ChannelTypeStrings.VOICE ? `🔉 ${invite.channel.name}` : `#${invite.channel.name}`; - const channelName = - invite.channel.type === ChannelTypeStrings.VOICE ? `🔉 ${invite.channel.name}` : `#${invite.channel.name}`; + const channelCreatedAtTimestamp = snowflakeToTimestamp(invite.channel.id); + const channelCreatedAt = moment.utc(channelCreatedAtTimestamp, "x"); + const channelAge = humanizeDuration(Date.now() - channelCreatedAtTimestamp, { + largest: 2, + round: true, + }); - const channelCreatedAtTimestamp = snowflakeToTimestamp(invite.channel.id); - const channelCreatedAt = moment.utc(channelCreatedAtTimestamp, "x"); - const channelAge = humanizeDuration(Date.now() - channelCreatedAtTimestamp, { - largest: 2, - round: true, - }); - - let channelInfo = trimLines(` + let channelInfo = trimLines(` Name: **${channelName}** ID: \`${invite.channel.id}\` Created: **${channelAge} ago** `); - if (invite.channel.type !== ChannelTypeStrings.VOICE) { - channelInfo += `\nMention: <#${invite.channel.id}>`; - } + if (invite.channel.type !== ChannelTypeStrings.VOICE) { + channelInfo += `\nMention: <#${invite.channel.id}>`; + } - embed.fields.push({ - name: preEmbedPadding + "Channel information", - value: channelInfo, - inline: true, - }); + embed.fields.push({ + name: preEmbedPadding + "Channel information", + value: channelInfo, + inline: true, + }); + } if (invite.inviter) { embed.fields.push({ diff --git a/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts b/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts index a0922399..91728e04 100644 --- a/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts @@ -1,4 +1,4 @@ -import { MessageEmbedOptions, Snowflake } from "discord.js"; +import { MessageEmbedOptions, PremiumTier, Snowflake } from "discord.js"; import humanizeDuration from "humanize-duration"; import { GuildPluginData } from "knub"; import moment from "moment-timezone"; @@ -19,6 +19,13 @@ import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin"; import { UtilityPluginType } from "../types"; import { getGuildPreview } from "./getGuildPreview"; +const PremiumTiers: Record = { + NONE: 0, + TIER_1: 1, + TIER_2: 2, + TIER_3: 3, +}; + export async function getServerInfoEmbed( pluginData: GuildPluginData, serverId: string, @@ -179,20 +186,22 @@ export async function getServerInfoEmbed( } if (restGuild) { + const premiumTierValue = PremiumTiers[restGuild.premiumTier]; + const maxEmojis = { 0: 50, 1: 100, 2: 150, 3: 250, - }[restGuild.premiumTier] || 50; + }[premiumTierValue] ?? 50; const maxStickers = { 0: 0, 1: 15, 2: 30, 3: 60, - }[restGuild.premiumTier] || 0; + }[premiumTierValue] ?? 0; otherStats.push(`Emojis: **${restGuild.emojis.cache.size}** / ${maxEmojis * 2}`); otherStats.push(`Stickers: **${restGuild.stickers.cache.size}** / ${maxStickers}`); @@ -202,7 +211,9 @@ export async function getServerInfoEmbed( } if (thisServer) { - otherStats.push(`Boosts: **${thisServer.premiumSubscriptionCount ?? 0}** (level ${thisServer.premiumTier})`); + otherStats.push( + `Boosts: **${thisServer.premiumSubscriptionCount ?? 0}** (level ${PremiumTiers[thisServer.premiumTier]})`, + ); } embed.fields.push({ diff --git a/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts b/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts index dde541d7..d4e74637 100644 --- a/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getUserInfoEmbed.ts @@ -36,7 +36,7 @@ export async function getUserInfoEmbed( const timeAndDate = pluginData.getPlugin(TimeAndDatePlugin); embed.author = { - name: `User: ${user.tag}`, + name: `${user.bot ? "Bot" : "User"}: ${user.tag}`, }; const avatarURL = user.displayAvatarURL(); @@ -54,7 +54,7 @@ export async function getUserInfoEmbed( if (compact) { embed.fields.push({ - name: preEmbedPadding + "User information", + name: preEmbedPadding + `${user.bot ? "Bot" : "User"} information`, value: trimLines(` Profile: <@!${user.id}> Created: **${accountAge} ago** (\`${prettyCreatedAt}\`) @@ -70,11 +70,12 @@ export async function getUserInfoEmbed( largest: 2, round: true, }); - embed.fields[0].value += `\nJoined: **${joinAge} ago** (\`${prettyJoinedAt}\`)`; + + embed.fields[0].value += `\n${user.bot ? "Added" : "Joined"}: **${joinAge} ago** (\`${prettyJoinedAt}\`)`; } else { embed.fields.push({ name: preEmbedPadding + "!! NOTE !!", - value: "User is not on the server", + value: `${user.bot ? "Bot" : "User"} is not on the server`, }); } @@ -82,7 +83,7 @@ export async function getUserInfoEmbed( } embed.fields.push({ - name: preEmbedPadding + "User information", + name: preEmbedPadding + `${user.bot ? "Bot" : "User"} information`, value: trimLines(` Name: **${user.tag}** ID: \`${user.id}\` @@ -107,7 +108,7 @@ export async function getUserInfoEmbed( embed.fields.push({ name: preEmbedPadding + "Member information", value: trimLines(` - Joined: **${joinAge} ago** (\`${prettyJoinedAt}\`) + ${user.bot ? "Added" : "Joined"}: **${joinAge} ago** (\`${prettyJoinedAt}\`) ${roles.length > 0 ? "Roles: " + roles.map(r => `<@&${r.id}>`).join(", ") : ""} `), }); @@ -126,7 +127,7 @@ export async function getUserInfoEmbed( } else { embed.fields.push({ name: preEmbedPadding + "Member information", - value: "⚠ User is not on the server", + value: `⚠ ${user.bot ? "Bot" : "User"} is not on the server`, }); } const cases = (await pluginData.state.cases.getByUserId(user.id)).filter(c => !c.is_hidden); diff --git a/backend/src/plugins/WelcomeMessage/events/SendWelcomeMessageEvt.ts b/backend/src/plugins/WelcomeMessage/events/SendWelcomeMessageEvt.ts index 750c6c29..772f27a9 100644 --- a/backend/src/plugins/WelcomeMessage/events/SendWelcomeMessageEvt.ts +++ b/backend/src/plugins/WelcomeMessage/events/SendWelcomeMessageEvt.ts @@ -68,7 +68,9 @@ export const SendWelcomeMessageEvt = welcomeMessageEvt({ if (!channel || !(channel instanceof TextChannel)) return; try { - await createChunkedMessage(channel, formatted); + await createChunkedMessage(channel, formatted, { + parse: ["users"], + }); } catch { pluginData.getPlugin(LogsPlugin).logBotAlert({ body: `Failed send a welcome message for ${verboseUserMention(member.user)} to ${verboseChannelMention( diff --git a/backend/src/templateFormatter.ts b/backend/src/templateFormatter.ts index 7b61af1e..2ed902d9 100644 --- a/backend/src/templateFormatter.ts +++ b/backend/src/templateFormatter.ts @@ -388,6 +388,10 @@ const baseValues = { ucfirst(arg) { return baseValues.upperFirst(arg); }, + strlen(arg) { + if (typeof arg !== "string") return 0; + return [...arg].length; + }, rand(from, to, seed = null) { if (isNaN(from)) return 0; @@ -406,6 +410,10 @@ const baseValues = { return Math.round(randValue * (to - from) + from); }, + round(arg, decimals = 0) { + if (isNaN(arg)) return 0; + return decimals === 0 ? Math.round(arg) : arg.toFixed(decimals); + }, add(...args) { return args.reduce((result, arg) => { if (isNaN(arg)) return result; diff --git a/backend/src/utils.ts b/backend/src/utils.ts index 8a7f7c55..6e40d666 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -743,10 +743,11 @@ export function isNotNull(value): value is Exclude { // discordapp.com/invite/ // discord.gg/invite/ // discord.gg/ -const quickInviteDetection = /(?:discord.com|discordapp.com)\/invite\/([a-z0-9\-]+)|discord.gg\/(?:\S+\/)?([a-z0-9\-]+)/gi; +// discord.com/friend-invite/ +const quickInviteDetection = /discord(?:app)?\.com\/(?:friend-)?invite\/([a-z0-9\-]+)|discord\.gg\/(?:\S+\/)?([a-z0-9\-]+)/gi; const isInviteHostRegex = /(?:^|\.)(?:discord.gg|discord.com|discordapp.com)$/i; -const longInvitePathRegex = /^\/invite\/([a-z0-9\-]+)$/i; +const longInvitePathRegex = /^\/(?:friend-)?invite\/([a-z0-9\-]+)$/i; export function getInviteCodesInString(str: string): string[] { const inviteCodes: string[] = []; @@ -778,6 +779,8 @@ export function getInviteCodesInString(str: string): string[] { // discord.com/invite/[/anything] // discordapp.com/invite/[/anything] + // discord.com/friend-invite/[/anything] + // discordapp.com/friend-invite/[/anything] const longInviteMatch = url.pathname.match(longInvitePathRegex); if (longInviteMatch) { return longInviteMatch[1]; diff --git a/process-api.json b/process-api.json index 99b5a8a3..2feaf75f 100644 --- a/process-api.json +++ b/process-api.json @@ -5,7 +5,8 @@ "cwd": "./backend", "script": "npm", "args": "run start-api-prod", - "log_date_format": "YYYY-MM-DD HH:mm:ss" + "log_date_format": "YYYY-MM-DD HH:mm:ss.SSS", + "exp_backoff_restart_delay": 2500 } ] } diff --git a/process-bot.json b/process-bot.json index 9c552b90..b8667e2d 100644 --- a/process-bot.json +++ b/process-bot.json @@ -5,7 +5,8 @@ "cwd": "./backend", "script": "npm", "args": "run start-bot-prod", - "log_date_format": "YYYY-MM-DD HH:mm:ss" + "log_date_format": "YYYY-MM-DD HH:mm:ss.SSS", + "exp_backoff_restart_delay": 2500 } ] } diff --git a/update-backend-hotfix.sh b/update-backend-hotfix.sh new file mode 100755 index 00000000..e692d7a9 --- /dev/null +++ b/update-backend-hotfix.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# Load nvm +. ~/.nvm/nvm.sh + +# Run hotfix update +cd backend +nvm use +git pull +npm run build + +# Restart processes +cd .. +nvm use +pm2 restart process-bot.json +pm2 restart process-api.json diff --git a/update-backend.sh b/update-backend.sh new file mode 100755 index 00000000..4da51186 --- /dev/null +++ b/update-backend.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# Load nvm +. ~/.nvm/nvm.sh + +# Stop current processes +nvm use +pm2 delete process-bot.json +pm2 delete process-api.json + +# Run update +nvm use +git pull +npm ci + +cd backend +npm ci +npm run build +npm run migrate-prod + +# Start processes again +cd .. +nvm use +pm2 start process-bot.json +pm2 start process-api.json diff --git a/update-dashboard.sh b/update-dashboard.sh new file mode 100755 index 00000000..7a336905 --- /dev/null +++ b/update-dashboard.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +TARGET_DIR=/var/www/zeppelin.gg + +# Load nvm +. ~/.nvm/nvm.sh + +# Update dashboard +cd dashboard +git pull +nvm use +npm ci +npm run build +rm -r "$TARGET_DIR/*" +cp -R dist/* "$TARGET_DIR" diff --git a/update.sh b/update.sh index 5e910c60..bf615aa5 100755 --- a/update.sh +++ b/update.sh @@ -1,11 +1,4 @@ #!/bin/bash -# Load nvm -. ~/.nvm/nvm.sh - -# Run update -nvm use -git pull -npm ci -npm run build -pm2 restart process.json +. ./update-backend.sh +. ./update-dashboard.sh