diff --git a/backend/src/plugins/NameHistory/NameHistoryPlugin.ts b/backend/src/plugins/NameHistory/NameHistoryPlugin.ts new file mode 100644 index 00000000..19dabb02 --- /dev/null +++ b/backend/src/plugins/NameHistory/NameHistoryPlugin.ts @@ -0,0 +1,46 @@ +import { PluginOptions } from "knub"; +import { NameHistoryPluginType, ConfigSchema } from "./types"; +import { zeppelinPlugin } from "../ZeppelinPluginBlueprint"; +import { GuildNicknameHistory } from "src/data/GuildNicknameHistory"; +import { UsernameHistory } from "src/data/UsernameHistory"; +import { Queue } from "src/Queue"; +import { NamesCmd } from "./commands/NamesCmd"; +import { ChannelJoinEvt, MessageCreateEvt } from "./events/UpdateNameEvts"; + +const defaultOptions: PluginOptions = { + config: { + can_view: false, + }, + overrides: [ + { + level: ">=50", + config: { + can_view: true, + }, + }, + ], +}; + +export const NameHistoryPlugin = zeppelinPlugin()("name_history", { + configSchema: ConfigSchema, + defaultOptions, + + // prettier-ignore + commands: [ + NamesCmd, + ], + + // prettier-ignore + events: [ + ChannelJoinEvt, + MessageCreateEvt, + ], + + onLoad(pluginData) { + const { state, guild } = pluginData; + + state.nicknameHistory = GuildNicknameHistory.getGuildInstance(guild.id); + state.usernameHistory = new UsernameHistory(); + state.updateQueue = new Queue(); + }, +}); diff --git a/backend/src/plugins/NameHistory/commands/NamesCmd.ts b/backend/src/plugins/NameHistory/commands/NamesCmd.ts new file mode 100644 index 00000000..3e5889c3 --- /dev/null +++ b/backend/src/plugins/NameHistory/commands/NamesCmd.ts @@ -0,0 +1,51 @@ +import { nameHistoryCmd } from "../types"; +import { commandTypeHelpers as ct } from "../../../commandTypes"; +import { disableCodeBlocks, createChunkedMessage } from "knub/dist/helpers"; +import { NICKNAME_RETENTION_PERIOD } from "src/data/cleanup/nicknames"; +import { DAYS } from "src/utils"; +import { MAX_NICKNAME_ENTRIES_PER_USER } from "src/data/GuildNicknameHistory"; +import { MAX_USERNAME_ENTRIES_PER_USER } from "src/data/UsernameHistory"; +import { sendErrorMessage } from "src/pluginUtils"; + +export const NamesCmd = nameHistoryCmd({ + trigger: "names", + permission: "can_view", + + signature: { + userId: ct.userId(), + }, + + async run({ message: msg, args, pluginData }) { + const nicknames = await pluginData.state.nicknameHistory.getByUserId(args.userId); + const usernames = await pluginData.state.usernameHistory.getByUserId(args.userId); + + if (nicknames.length === 0 && usernames.length === 0) { + return sendErrorMessage(pluginData, msg.channel, "No name history found"); + } + + const nicknameRows = nicknames.map( + r => `\`[${r.timestamp}]\` ${r.nickname ? `**${disableCodeBlocks(r.nickname)}**` : "*None*"}`, + ); + const usernameRows = usernames.map(r => `\`[${r.timestamp}]\` **${disableCodeBlocks(r.username)}**`); + + const user = pluginData.client.users.get(args.userId); + const currentUsername = user ? `${user.username}#${user.discriminator}` : args.userId; + + const nicknameDays = Math.round(NICKNAME_RETENTION_PERIOD / DAYS); + const usernameDays = Math.round(NICKNAME_RETENTION_PERIOD / DAYS); + + let message = `Name history for **${currentUsername}**:`; + if (nicknameRows.length) { + message += `\n\n__Last ${MAX_NICKNAME_ENTRIES_PER_USER} nicknames within ${nicknameDays} days:__\n${nicknameRows.join( + "\n", + )}`; + } + if (usernameRows.length) { + message += `\n\n__Last ${MAX_USERNAME_ENTRIES_PER_USER} usernames within ${usernameDays} days:__\n${usernameRows.join( + "\n", + )}`; + } + + createChunkedMessage(msg.channel, message); + }, +}); diff --git a/backend/src/plugins/NameHistory/events/UpdateNameEvts.ts b/backend/src/plugins/NameHistory/events/UpdateNameEvts.ts new file mode 100644 index 00000000..3c6ca433 --- /dev/null +++ b/backend/src/plugins/NameHistory/events/UpdateNameEvts.ts @@ -0,0 +1,18 @@ +import { nameHistoryEvt } from "../types"; +import { updateNickname } from "../updateNickname"; + +export const ChannelJoinEvt = nameHistoryEvt({ + event: "voiceChannelJoin", + + async listener(meta) { + meta.pluginData.state.updateQueue.add(() => updateNickname(meta.pluginData, meta.args.member)); + }, +}); + +export const MessageCreateEvt = nameHistoryEvt({ + event: "messageCreate", + + async listener(meta) { + meta.pluginData.state.updateQueue.add(() => updateNickname(meta.pluginData, meta.args.message.member)); + }, +}); diff --git a/backend/src/plugins/NameHistory/types.ts b/backend/src/plugins/NameHistory/types.ts new file mode 100644 index 00000000..ad9f6833 --- /dev/null +++ b/backend/src/plugins/NameHistory/types.ts @@ -0,0 +1,22 @@ +import * as t from "io-ts"; +import { BasePluginType, command, eventListener } from "knub"; +import { GuildNicknameHistory } from "src/data/GuildNicknameHistory"; +import { UsernameHistory } from "src/data/UsernameHistory"; +import { Queue } from "src/Queue"; + +export const ConfigSchema = t.type({ + can_view: t.boolean, +}); +export type TConfigSchema = t.TypeOf; + +export interface NameHistoryPluginType extends BasePluginType { + config: TConfigSchema; + state: { + nicknameHistory: GuildNicknameHistory; + usernameHistory: UsernameHistory; + updateQueue: Queue; + }; +} + +export const nameHistoryCmd = command(); +export const nameHistoryEvt = eventListener(); diff --git a/backend/src/plugins/NameHistory/updateNickname.ts b/backend/src/plugins/NameHistory/updateNickname.ts new file mode 100644 index 00000000..b00e01fa --- /dev/null +++ b/backend/src/plugins/NameHistory/updateNickname.ts @@ -0,0 +1,12 @@ +import { Member } from "eris"; +import { PluginData } from "knub"; +import { NameHistoryPluginType } from "./types"; + +export async function updateNickname(pluginData: PluginData, member: Member) { + if (!member) return; + const latestEntry = await pluginData.state.nicknameHistory.getLastEntry(member.id); + if (!latestEntry || latestEntry.nickname !== member.nick) { + if (!latestEntry && member.nick == null) return; // No need to save "no nickname" if there's no previous data + await pluginData.state.nicknameHistory.addEntry(member.id, member.nick); + } +} diff --git a/backend/src/plugins/availablePlugins.ts b/backend/src/plugins/availablePlugins.ts index 0dce5a2c..51be85ce 100644 --- a/backend/src/plugins/availablePlugins.ts +++ b/backend/src/plugins/availablePlugins.ts @@ -1,10 +1,12 @@ import { UtilityPlugin } from "./Utility/UtilityPlugin"; import { LocateUserPlugin } from "./LocateUser/LocateUserPlugin"; import { ZeppelinPluginBlueprint } from "./ZeppelinPluginBlueprint"; +import { NameHistoryPlugin } from "./NameHistory/NameHistoryPlugin"; // prettier-ignore export const guildPlugins: Array> = [ LocateUserPlugin, + NameHistoryPlugin, UtilityPlugin, ];