diff --git a/backend/package-lock.json b/backend/package-lock.json index 461ff917..984a6020 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -14,7 +14,7 @@ "cors": "^2.8.5", "cross-env": "^5.2.0", "deep-diff": "^1.0.2", - "discord.js": "^13.0.0-dev.a7c6678.1625427504", + "discord.js": "^13.0.0-dev.2e078e4.1625529824", "dotenv": "^4.0.0", "emoji-regex": "^8.0.0", "erlpack": "github:discord/erlpack", @@ -82,7 +82,7 @@ "license": "MIT", "dependencies": { "discord-api-types": "^0.18.1", - "discord.js": "^13.0.0-dev.a7c6678.1625427504", + "discord.js": "^13.0.0-dev.2e078e4.1625529824", "knub-command-manager": "^9.1.0", "ts-essentials": "^6.0.7" }, @@ -2159,9 +2159,9 @@ } }, "node_modules/discord.js": { - "version": "13.0.0-dev.a7c6678.1625427504", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-13.0.0-dev.a7c6678.1625427504.tgz", - "integrity": "sha512-S4ntnGVUCRyyOtz8YvoWNpSZyZEFwXTkTjXbU0shveD/QVT+0cBCcMX6vp0zPt3lCkMUgdW+crrOfEYKMGwKoQ==", + "version": "13.0.0-dev.2e078e4.1625529824", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-13.0.0-dev.2e078e4.1625529824.tgz", + "integrity": "sha512-NKNnoEmh3+4eCG44BOIBjlJS0bKQEH8OgWoEjdJ9K53MYa5lfqbSho4NKXe5+hVpMr+U43wJ8ByUvw96ogqu8w==", "dependencies": { "@discordjs/builders": "^0.2.0", "@discordjs/collection": "^0.1.6", @@ -8034,9 +8034,9 @@ "integrity": "sha512-hNC38R9ZF4uaujaZQtQfm5CdQO58uhdkoHQAVvMfIL0LgOSZeW575W8H6upngQOuoxWd8tiRII3LLJm9zuQKYg==" }, "discord.js": { - "version": "13.0.0-dev.a7c6678.1625427504", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-13.0.0-dev.a7c6678.1625427504.tgz", - "integrity": "sha512-S4ntnGVUCRyyOtz8YvoWNpSZyZEFwXTkTjXbU0shveD/QVT+0cBCcMX6vp0zPt3lCkMUgdW+crrOfEYKMGwKoQ==", + "version": "13.0.0-dev.2e078e4.1625529824", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-13.0.0-dev.2e078e4.1625529824.tgz", + "integrity": "sha512-NKNnoEmh3+4eCG44BOIBjlJS0bKQEH8OgWoEjdJ9K53MYa5lfqbSho4NKXe5+hVpMr+U43wJ8ByUvw96ogqu8w==", "requires": { "@discordjs/builders": "^0.2.0", "@discordjs/collection": "^0.1.6", @@ -8878,7 +8878,7 @@ "@typescript-eslint/parser": "^4.23.0", "chai": "^4.3.4", "discord-api-types": "^0.18.1", - "discord.js": "^13.0.0-dev.a7c6678.1625427504", + "discord.js": "^13.0.0-dev.2e078e4.1625529824", "eslint": "^7.2.0", "husky": "^4.3.8", "knub-command-manager": "^9.1.0", diff --git a/backend/package.json b/backend/package.json index 75169098..792d8126 100644 --- a/backend/package.json +++ b/backend/package.json @@ -29,7 +29,7 @@ "cors": "^2.8.5", "cross-env": "^5.2.0", "deep-diff": "^1.0.2", - "discord.js": "^13.0.0-dev.a7c6678.1625427504", + "discord.js": "^13.0.0-dev.2e078e4.1625529824", "dotenv": "^4.0.0", "emoji-regex": "^8.0.0", "erlpack": "github:discord/erlpack", diff --git a/backend/src/plugins/Automod/functions/getTextMatchPartialSummary.ts b/backend/src/plugins/Automod/functions/getTextMatchPartialSummary.ts index f88f9b69..fe0466e4 100644 --- a/backend/src/plugins/Automod/functions/getTextMatchPartialSummary.ts +++ b/backend/src/plugins/Automod/functions/getTextMatchPartialSummary.ts @@ -29,6 +29,6 @@ export function getTextMatchPartialSummary( const visibleName = context.member?.nickname || context.user!.username; return `visible name: ${visibleName}`; } else if (type === "customstatus") { - return `custom status: ${context.member!.presence.activities.find(a => a.type === "CUSTOM")?.name}`; + return `custom status: ${context.member!.presence?.activities.find(a => a.type === "CUSTOM")?.name}`; } } diff --git a/backend/src/plugins/Automod/functions/matchMultipleTextTypesOnMessage.ts b/backend/src/plugins/Automod/functions/matchMultipleTextTypesOnMessage.ts index 6b2e1038..10188f7e 100644 --- a/backend/src/plugins/Automod/functions/matchMultipleTextTypesOnMessage.ts +++ b/backend/src/plugins/Automod/functions/matchMultipleTextTypesOnMessage.ts @@ -52,7 +52,7 @@ export async function* matchMultipleTextTypesOnMessage( yield ["nickname", member.nickname]; } - for (const activity of member.presence.activities) { + for (const activity of member.presence?.activities ?? []) { if (activity.type === Constants.ActivityTypes[4]) { yield ["customstatus", `${activity.emoji} ${activity.name}`]; break; diff --git a/backend/src/plugins/CompanionChannels/CompanionChannelsPlugin.ts b/backend/src/plugins/CompanionChannels/CompanionChannelsPlugin.ts index 64bda1c1..8565c67b 100644 --- a/backend/src/plugins/CompanionChannels/CompanionChannelsPlugin.ts +++ b/backend/src/plugins/CompanionChannels/CompanionChannelsPlugin.ts @@ -1,4 +1,5 @@ import { CooldownManager } from "knub"; +import { GuildLogs } from "../../../data/GuildLogs"; import { trimPluginDescription } from "../../utils"; import { LogsPlugin } from "../Logs/LogsPlugin"; import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint"; @@ -32,4 +33,8 @@ export const CompanionChannelsPlugin = zeppelinGuildPlugin pluginData.guild.roles.cache.get(roleId) || { id: roleId, name: `Unknown (${roleId})` }) .map(r => r.name) .join(", "), - mod: stripObjectToScalars(mod), + mod: mod ? userToConfigAccessibleUser(mod) : {}, }, member.id, ); @@ -82,7 +82,7 @@ export const LogsGuildMemberUpdateEvt = logsEvt({ .map(roleId => pluginData.guild.roles.cache.get(roleId) || { id: roleId, name: `Unknown (${roleId})` }) .map(r => r.name) .join(", "), - mod: stripObjectToScalars(mod), + mod: mod ? userToConfigAccessibleUser(mod) : {}, }, member.id, ); @@ -96,7 +96,7 @@ export const LogsGuildMemberUpdateEvt = logsEvt({ .map(roleId => pluginData.guild.roles.cache.get(roleId) || { id: roleId, name: `Unknown (${roleId})` }) .map(r => r.name) .join(", "), - mod: stripObjectToScalars(mod), + mod: mod ? userToConfigAccessibleUser(mod) : {}, }, member.id, ); diff --git a/backend/src/plugins/Logs/events/LogsVoiceChannelEvts.ts b/backend/src/plugins/Logs/events/LogsVoiceChannelEvts.ts index 50d31bfa..ae19eb53 100644 --- a/backend/src/plugins/Logs/events/LogsVoiceChannelEvts.ts +++ b/backend/src/plugins/Logs/events/LogsVoiceChannelEvts.ts @@ -1,3 +1,4 @@ +import { channelToConfigAccessibleChannel, memberToConfigAccessibleMember } from "src/utils/configAccessibleObjects"; import { LogType } from "../../../data/LogType"; import { stripObjectToScalars } from "../../../utils"; import { logsEvt } from "../types"; @@ -10,25 +11,23 @@ export const LogsVoiceStateUpdateEvt = logsEvt({ const newChannel = meta.args.newState.channel; const member = meta.args.newState.member ?? meta.args.oldState.member!; - if (!newChannel) { + if (!newChannel && oldChannel) { // Leave evt meta.pluginData.state.guildLogs.log(LogType.VOICE_CHANNEL_LEAVE, { - member: stripObjectToScalars(member, ["user", "roles"]), - oldChannel: stripObjectToScalars(oldChannel), - newChannel: stripObjectToScalars(newChannel), + member: memberToConfigAccessibleMember(member), + oldChannel: channelToConfigAccessibleChannel(oldChannel!), }); - } else if (!oldChannel) { + } else if (!oldChannel && newChannel) { // Join Evt meta.pluginData.state.guildLogs.log(LogType.VOICE_CHANNEL_JOIN, { - member: stripObjectToScalars(member, ["user", "roles"]), - oldChannel: stripObjectToScalars(oldChannel), - newChannel: stripObjectToScalars(newChannel), + member: memberToConfigAccessibleMember(member), + newChannel: channelToConfigAccessibleChannel(newChannel), }); } else { meta.pluginData.state.guildLogs.log(LogType.VOICE_CHANNEL_MOVE, { - member: stripObjectToScalars(member, ["user", "roles"]), - oldChannel: stripObjectToScalars(oldChannel), - newChannel: stripObjectToScalars(newChannel), + member: memberToConfigAccessibleMember(member), + oldChannel: channelToConfigAccessibleChannel(oldChannel!), + newChannel: channelToConfigAccessibleChannel(newChannel!), }); } }, diff --git a/backend/src/plugins/Slowmode/SlowmodePlugin.ts b/backend/src/plugins/Slowmode/SlowmodePlugin.ts index 5dda25c9..f7dd06a3 100644 --- a/backend/src/plugins/Slowmode/SlowmodePlugin.ts +++ b/backend/src/plugins/Slowmode/SlowmodePlugin.ts @@ -66,6 +66,7 @@ export const SlowmodePlugin = zeppelinGuildPlugin()({ afterLoad(pluginData) { const { state } = pluginData; + state.serverLogs = new GuildLogs(pluginData.guild.id); state.clearInterval = setInterval(() => clearExpiredSlowmodes(pluginData), BOT_SLOWMODE_CLEAR_INTERVAL); state.onMessageCreateFn = msg => onMessageCreate(pluginData, msg); diff --git a/backend/src/plugins/Slowmode/types.ts b/backend/src/plugins/Slowmode/types.ts index 3c3824da..7679df34 100644 --- a/backend/src/plugins/Slowmode/types.ts +++ b/backend/src/plugins/Slowmode/types.ts @@ -19,6 +19,7 @@ export interface SlowmodePluginType extends BasePluginType { savedMessages: GuildSavedMessages; logs: GuildLogs; clearInterval: NodeJS.Timeout; + serverLogs: GuildLogs; onMessageCreateFn; }; diff --git a/backend/src/plugins/Slowmode/util/applyBotSlowmodeToUserId.ts b/backend/src/plugins/Slowmode/util/applyBotSlowmodeToUserId.ts index bcc32d55..2bcb7c9d 100644 --- a/backend/src/plugins/Slowmode/util/applyBotSlowmodeToUserId.ts +++ b/backend/src/plugins/Slowmode/util/applyBotSlowmodeToUserId.ts @@ -13,6 +13,7 @@ export async function applyBotSlowmodeToUserId( // Deny sendMessage permission from the user. If there are existing permission overwrites, take those into account. const existingOverride = channel.permissionOverwrites.resolve(userId as Snowflake); try { + pluginData.state.serverLogs.ignoreLog(LogType.CHANNEL_UPDATE, channel.id, 5 * 1000); if (existingOverride) { await existingOverride.edit({ SEND_MESSAGES: false }); } else { diff --git a/backend/src/plugins/Slowmode/util/clearBotSlowmodeFromUserId.ts b/backend/src/plugins/Slowmode/util/clearBotSlowmodeFromUserId.ts index 733a2cdd..381fa930 100644 --- a/backend/src/plugins/Slowmode/util/clearBotSlowmodeFromUserId.ts +++ b/backend/src/plugins/Slowmode/util/clearBotSlowmodeFromUserId.ts @@ -1,5 +1,6 @@ import { GuildChannel, Snowflake, TextChannel } from "discord.js"; import { GuildPluginData } from "knub"; +import { LogType } from "../../../data/LogType"; import { SlowmodePluginType } from "../types"; export async function clearBotSlowmodeFromUserId( @@ -13,6 +14,7 @@ export async function clearBotSlowmodeFromUserId( // Previously we diffed the overrides so we could clear the "send messages" override without touching other // overrides. Unfortunately, it seems that was a bit buggy - we didn't always receive the event for the changed // overrides and then we also couldn't diff against them. For consistency's sake, we just delete the override now. + pluginData.state.serverLogs.ignoreLog(LogType.CHANNEL_UPDATE, channel.id, 3 * 1000); await channel.permissionOverwrites.resolve(userId as Snowflake)?.delete(); } catch (e) { if (!force) { diff --git a/backend/src/plugins/Slowmode/util/clearExpiredSlowmodes.ts b/backend/src/plugins/Slowmode/util/clearExpiredSlowmodes.ts index dd7ced57..6c5fe46d 100644 --- a/backend/src/plugins/Slowmode/util/clearExpiredSlowmodes.ts +++ b/backend/src/plugins/Slowmode/util/clearExpiredSlowmodes.ts @@ -1,5 +1,6 @@ import { GuildChannel, Snowflake, TextChannel } from "discord.js"; import { GuildPluginData } from "knub"; +import { channelToConfigAccessibleChannel, userToConfigAccessibleUser } from "src/utils/configAccessibleObjects"; import { LogType } from "../../../data/LogType"; import { logger } from "../../../logger"; import { stripObjectToScalars, UnknownUser } from "../../../utils"; @@ -22,10 +23,11 @@ export async function clearExpiredSlowmodes(pluginData: GuildPluginData()({ // prettier-ignore events: [ AutoJoinThreadEvt, + AutoJoinThreadSyncEvt, ], beforeLoad(pluginData) { diff --git a/backend/src/plugins/Utility/events/AutoJoinThreadEvt.ts b/backend/src/plugins/Utility/events/AutoJoinThreadEvt.ts index c7d49d70..6789073c 100644 --- a/backend/src/plugins/Utility/events/AutoJoinThreadEvt.ts +++ b/backend/src/plugins/Utility/events/AutoJoinThreadEvt.ts @@ -10,3 +10,18 @@ export const AutoJoinThreadEvt = utilityEvt({ } }, }); + +export const AutoJoinThreadSyncEvt = utilityEvt({ + event: "threadListSync", + + async listener(meta) { + const config = meta.pluginData.config.get(); + if (config.autojoin_threads) { + for (const thread of meta.args.threads.values()) { + if (!thread.joined && thread.joinable) { + await thread.join(); + } + } + } + }, +}); diff --git a/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts b/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts index f6a00847..2eb04a60 100644 --- a/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts +++ b/backend/src/plugins/Utility/functions/getServerInfoEmbed.ts @@ -126,7 +126,7 @@ export async function getServerInfoEmbed( } if (!onlineMemberCount && thisServer) { - onlineMemberCount = thisServer.members.cache.filter(m => m.presence.status !== "offline").size; // Extremely inaccurate fallback + onlineMemberCount = thisServer.members.cache.filter(m => m.presence?.status !== "offline").size; // Extremely inaccurate fallback } const offlineMemberCount = totalMembers - onlineMemberCount; diff --git a/backend/src/utils.ts b/backend/src/utils.ts index f9d9094a..be9c6767 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -36,6 +36,7 @@ import { sendDM } from "./utils/sendDM"; import { waitForButtonConfirm } from "./utils/waitForInteraction"; import { decodeAndValidateStrict, StrictValidationError } from "./validatorUtils"; import { isEqual } from "lodash"; +import humanizeDuration from "humanize-duration"; const fsp = fs.promises; @@ -184,8 +185,28 @@ export function getScalarDifference( return diff; } +// This is a stupid, messy solution that is not extendable at all. +// If anyone plans on adding anything to this, they should rewrite this first. +// I just want to get this done and this works for now :) +export function prettyDifference(diff: Map): Map { + const toReturn = new Map(); + + for (let [key, difference] of diff) { + if (key === "rateLimitPerUser") { + difference.is = humanizeDuration(difference.is * 1000); + difference.was = humanizeDuration(difference.was * 1000); + key = "slowmode"; + } + + toReturn.set(key, { was: difference.was, is: difference.is }); + } + + return toReturn; +} + export function differenceToString(diff: Map): string { let toReturn = ""; + diff = prettyDifference(diff); for (const [key, difference] of diff) { toReturn += `${key[0].toUpperCase() + key.slice(1)}: \`${difference.was}\` ➜ \`${difference.is}\`\n`; } diff --git a/backend/src/utils/configAccessibleObjects.ts b/backend/src/utils/configAccessibleObjects.ts new file mode 100644 index 00000000..92dc17c0 --- /dev/null +++ b/backend/src/utils/configAccessibleObjects.ts @@ -0,0 +1,116 @@ +import { + GuildChannel, + GuildMember, + PartialGuildMember, + Role, + Snowflake, + StageInstance, + ThreadChannel, + User, +} from "discord.js"; + +export interface IConfigAccessibleUser { + id: Snowflake; + username: string; + discriminator: string; + mention: string; + avatarURL: string; + bot: boolean; + createdAt: number; +} + +export interface IConfigAccessibleRole { + id: Snowflake; + name: string; + createdAt: number; + hexColor: string; + hoist: boolean; +} + +export interface IConfigAccessibleMember extends IConfigAccessibleUser { + user: IConfigAccessibleUser; + nick: string; + roles: IConfigAccessibleRole[]; + joinedAt?: number; + // guildAvatarURL: string, Once DJS supports per-server avatars + guildName: string; +} + +export function userToConfigAccessibleUser(user: User): IConfigAccessibleUser { + const toReturn: IConfigAccessibleUser = { + id: user.id, + username: user.username, + discriminator: user.discriminator, + mention: `<@${user.id}>`, + avatarURL: user.displayAvatarURL({ dynamic: true }), + bot: user.bot, + createdAt: user.createdTimestamp, + }; + + return toReturn; +} + +export function roleToConfigAccessibleRole(role: Role): IConfigAccessibleRole { + const toReturn: IConfigAccessibleRole = { + id: role.id, + name: role.name, + createdAt: role.createdTimestamp, + hexColor: role.hexColor, + hoist: role.hoist, + }; + + return toReturn; +} + +export function memberToConfigAccessibleMember(member: GuildMember | PartialGuildMember): IConfigAccessibleMember { + const user = userToConfigAccessibleUser(member.user!); + + const toReturn: IConfigAccessibleMember = { + ...user, + user, + nick: member.nickname ?? "*None*", + roles: member.roles.cache.mapValues(r => roleToConfigAccessibleRole(r)).array(), + joinedAt: member.joinedTimestamp ?? undefined, + guildName: member.guild.name, + }; + + return toReturn; +} + +export interface IConfigAccessibleChannel { + id: Snowflake; + name: string; + mention: string; + parentId?: Snowflake; +} + +export function channelToConfigAccessibleChannel(channel: GuildChannel | ThreadChannel): IConfigAccessibleChannel { + const toReturn: IConfigAccessibleChannel = { + id: channel.id, + name: channel.name, + mention: `<#${channel.id}>`, + parentId: channel.parentId ?? undefined, + }; + + return toReturn; +} + +export interface IConfigAccessibleStage { + channelId: Snowflake; + channelMention: string; + createdAt: number; + discoverable: boolean; + topic: string; +} + +export function stageToConfigAccessibleStage(stage: StageInstance): IConfigAccessibleStage { + const toReturn: IConfigAccessibleStage = { + channelId: stage.channelId, + channelMention: `<#${stage.channelId}>`, + createdAt: stage.createdTimestamp, + discoverable: !stage.discoverableDisabled, + topic: stage.topic, + }; + + return toReturn; +}