diff --git a/assets/localisations/events/eng-US.json b/assets/localisations/events/eng-US.json index f6d276b91..2580f34ff 100644 --- a/assets/localisations/events/eng-US.json +++ b/assets/localisations/events/eng-US.json @@ -14,6 +14,8 @@ "events.messageUpdate.description": "{user} has updated their message in {channel}.", "events.messageUpdate.fields.before": "Before", "events.messageUpdate.fields.after": "After", + "events.memberKick.title": "User kicked", + "events.memberKick.description": "{user} has been kicked by {moderator}.", "events.entryRequestAccept.title": "Entry request accepted", "events.entryRequestAccept.description": "{user}'s entry request has been accepted by {moderator}.", "events.entryRequestReject.title": "Entry request rejected", diff --git a/source/constants/contexts.ts b/source/constants/contexts.ts index 89ce381a9..a1d9ee02b 100644 --- a/source/constants/contexts.ts +++ b/source/constants/contexts.ts @@ -1214,6 +1214,10 @@ export default Object.freeze({ after: localise("events.messageUpdate.fields.after", locale)(), }, }), + memberKick: ({ localise, locale }) => ({ + title: localise("events.memberKick.title", locale)(), + description: localise("events.memberKick.description", locale), + }), entryRequestAccept: ({ localise, locale }) => ({ title: localise("events.entryRequestAccept.title", locale)(), description: localise("events.entryRequestAccept.description", locale), diff --git a/source/constants/emojis.ts b/source/constants/emojis.ts index 79574ed03..f2d23944d 100644 --- a/source/constants/emojis.ts +++ b/source/constants/emojis.ts @@ -20,6 +20,7 @@ export default Object.freeze({ unbanned: "😇", joined: "😁", left: "😔", + kicked: "🚪", }, message: { updated: "⬆️", diff --git a/source/constants/parameters.ts b/source/constants/parameters.ts index 8e0f4b375..cb4b9606b 100644 --- a/source/constants/parameters.ts +++ b/source/constants/parameters.ts @@ -1,4 +1,3 @@ -import * as Discord from "@discordeno/bot"; import type { OptionTemplate } from "logos/commands/commands"; export default Object.freeze({ diff --git a/source/library/commands/commands.ts b/source/library/commands/commands.ts index c7b955b15..12be85713 100644 --- a/source/library/commands/commands.ts +++ b/source/library/commands/commands.ts @@ -1,4 +1,3 @@ -import * as Discord from "@discordeno/bot"; import { handleDisplayAcknowledgements } from "logos/commands/handlers/acknowledgements"; import { handleAnswer } from "logos/commands/handlers/answer"; import { handleDisplayCefrGuide } from "logos/commands/handlers/cefr"; diff --git a/source/library/stores/journalling.ts b/source/library/stores/journalling.ts index 3f7e55acb..29446149c 100644 --- a/source/library/stores/journalling.ts +++ b/source/library/stores/journalling.ts @@ -1,5 +1,4 @@ import { getLocaleByLocalisationLanguage } from "logos:constants/languages"; -import type * as Discord from "@discordeno/bot"; import type { Client } from "logos/client"; import { Collector } from "logos/collectors"; import { Logger } from "logos/logger"; @@ -33,34 +32,12 @@ class JournallingStore { async setup(): Promise { this.log.info("Setting up journalling store..."); - this.#guildBanAddCollector.onCollect((user, guildId) => - this.tryLog("guildBanAdd", { guildId, args: [user, guildId] }), - ); - this.#guildBanRemoveCollector.onCollect((user, guildId) => - this.tryLog("guildBanRemove", { guildId, args: [user, guildId] }), - ); - this.#guildMemberAddCollector.onCollect((member, user) => - this.tryLog("guildMemberAdd", { guildId: member.guildId, args: [member, user] }), - ); - this.#guildMemberRemoveCollector.onCollect((user, guildId) => - this.tryLog("guildMemberRemove", { guildId, args: [user, guildId] }), - ); - this.#messageDeleteCollector.onCollect((payload, message) => { - const guildId = payload.guildId; - if (guildId === undefined) { - return; - } - - this.tryLog("messageDelete", { guildId, args: [payload, message] }); - }); - this.#messageUpdateCollector.onCollect((message, oldMessage) => { - const guildId = message.guildId; - if (guildId === undefined) { - return; - } - - this.tryLog("messageUpdate", { guildId, args: [message, oldMessage] }); - }); + this.#guildBanAddCollector.onCollect(this.#guildBanAdd.bind(this)); + this.#guildBanRemoveCollector.onCollect(this.#guildBanRemove.bind(this)); + this.#guildMemberAddCollector.onCollect(this.#guildMemberAdd.bind(this)); + this.#guildMemberRemoveCollector.onCollect(this.#guildMemberRemove.bind(this)); + this.#messageDeleteCollector.onCollect(this.#messageDelete.bind(this)); + this.#messageUpdateCollector.onCollect(this.#messageUpdate.bind(this)); await this.#client.registerCollector("guildBanAdd", this.#guildBanAddCollector); await this.#client.registerCollector("guildBanRemove", this.#guildBanRemoveCollector); @@ -139,6 +116,79 @@ class JournallingStore { this.log.warn(`Failed to log '${event}' event on ${this.#client.diagnostics.guild(guildId)}:`, reason), ); } + + async #guildBanAdd(user: Discord.User, guildId: bigint): Promise { + await this.tryLog("guildBanAdd", { guildId, args: [user, guildId] }); + } + + async #guildBanRemove(user: Discord.User, guildId: bigint): Promise { + await this.tryLog("guildBanRemove", { guildId, args: [user, guildId] }); + } + + async #guildMemberAdd(member: Discord.Member, user: Discord.User): Promise { + await this.tryLog("guildMemberAdd", { guildId: member.guildId, args: [member, user] }); + } + + async #guildMemberRemove(user: Discord.User, guildId: bigint): Promise { + const kickInformation = await this.#getKickInformation({ user, guildId }); + if (kickInformation !== undefined) { + if (kickInformation.userId === null) { + return; + } + + const authorMember = this.#client.entities.members.get(guildId)?.get(BigInt(kickInformation.userId)); + if (authorMember === undefined) { + return; + } + + await this.tryLog("guildMemberKick", { guildId, args: [user, authorMember] }); + return; + } + + await this.tryLog("guildMemberRemove", { guildId, args: [user, guildId] }); + } + + async #messageDelete( + payload: Discord.Events["messageDelete"][0], + message: Discord.Message | undefined, + ): Promise { + const guildId = payload.guildId; + if (guildId === undefined) { + return; + } + + await this.tryLog("messageDelete", { guildId, args: [payload, message] }); + } + + async #messageUpdate(message: Discord.Message, oldMessage: Discord.Message | undefined): Promise { + const guildId = message.guildId; + if (guildId === undefined) { + return; + } + + await this.tryLog("messageUpdate", { guildId, args: [message, oldMessage] }); + } + + async #getKickInformation({ + user, + guildId, + }: { user: Logos.User; guildId: bigint }): Promise { + const now = Date.now(); + + const auditLog = await this.#client.bot.helpers + .getAuditLog(guildId, { actionType: Discord.AuditLogEvents.MemberKick }) + .catch((reason) => { + this.log.warn(`Could not get audit log for ${this.#client.diagnostics.guild(guildId)}:`, reason); + return undefined; + }); + if (auditLog === undefined) { + return undefined; + } + + return auditLog.auditLogEntries + .filter((entry) => Discord.snowflakeToTimestamp(BigInt(entry.id)) >= now - constants.time.second * 5) + .find((entry) => entry.targetId === user.id.toString()); + } } export { JournallingStore }; diff --git a/source/library/stores/journalling/loggers.ts b/source/library/stores/journalling/loggers.ts index 8797499c8..f5cf830b8 100644 --- a/source/library/stores/journalling/loggers.ts +++ b/source/library/stores/journalling/loggers.ts @@ -1,5 +1,4 @@ import type { FeatureLanguage, Locale } from "logos:constants/languages"; -import type * as Discord from "@discordeno/bot"; import type { Client } from "logos/client"; import guildBanAdd from "logos/stores/journalling/discord/guild-ban-add"; import guildBanRemove from "logos/stores/journalling/discord/guild-ban-remove"; @@ -10,6 +9,7 @@ import messageUpdate from "logos/stores/journalling/discord/message-update"; import entryRequestAccept from "logos/stores/journalling/logos/entry-request-accept"; import entryRequestReject from "logos/stores/journalling/logos/entry-request-reject"; import entryRequestSubmit from "logos/stores/journalling/logos/entry-request-submit"; +import guildMemberKick from "logos/stores/journalling/logos/guild-member-kick"; import inquiryOpen from "logos/stores/journalling/logos/inquiry-open"; import memberTimeoutAdd from "logos/stores/journalling/logos/member-timeout-add"; import memberTimeoutRemove from "logos/stores/journalling/logos/member-timeout-remove"; @@ -36,6 +36,7 @@ const loggers = Object.freeze({ guildMemberRemove, messageDelete, messageUpdate, + guildMemberKick, entryRequestSubmit, entryRequestAccept, entryRequestReject, diff --git a/source/library/stores/journalling/logos/guild-member-kick.ts b/source/library/stores/journalling/logos/guild-member-kick.ts new file mode 100644 index 000000000..109099cdc --- /dev/null +++ b/source/library/stores/journalling/logos/guild-member-kick.ts @@ -0,0 +1,22 @@ +import type { EventLogger } from "logos/stores/journalling/loggers"; + +const logger: EventLogger<"guildMemberKick"> = (client, [user, author], { guildLocale }) => { + const strings = constants.contexts.memberKick({ + localise: client.localise.bind(client), + locale: guildLocale, + }); + return { + embeds: [ + { + title: `${constants.emojis.events.user.kicked} ${strings.title}`, + color: constants.colours.warning, + description: strings.description({ + user: client.diagnostics.user(user), + moderator: client.diagnostics.member(author), + }), + }, + ], + }; +}; + +export default logger; diff --git a/source/types.d.ts b/source/types.d.ts index 0fddde8be..0598014b8 100644 --- a/source/types.d.ts +++ b/source/types.d.ts @@ -3,7 +3,6 @@ import type { FeatureLanguage, LearningLanguage, Locale, LocalisationLanguage } import type { Properties } from "logos:constants/properties"; import type { SlowmodeLevel } from "logos:constants/slowmode"; import type { WithRequired } from "logos:core/utilities.ts"; -import type * as Discord from "@discordeno/bot"; import type { EntryRequest } from "logos/models/entry-request"; import type { Praise } from "logos/models/praise"; import type { Report } from "logos/models/report"; @@ -99,6 +98,9 @@ declare global { /** Type representing events that occur within a guild. */ type Events = { + /** Fill-in Discord event for a member having been kicked. */ + guildMemberKick: [user: Logos.User, by: Logos.Member]; + } & { /** An entry request has been submitted. */ entryRequestSubmit: [user: Logos.User, entryRequest: EntryRequest];