diff --git a/src/data/DefaultLogMessages.json b/src/data/DefaultLogMessages.json index fe101feb..90945c45 100644 --- a/src/data/DefaultLogMessages.json +++ b/src/data/DefaultLogMessages.json @@ -16,7 +16,7 @@ "MEMBER_ROLE_REMOVE": "🔑 {userMention(member)}: role(s) **{roles}** removed by {userMention(mod)}", "MEMBER_ROLE_CHANGES": "🔑 {userMention(member)}: roles changed: added **{addedRoles}**, removed **{removedRoles}** by {userMention(mod)}", "MEMBER_NICK_CHANGE": "✏ {userMention(member)}: nickname changed from **{oldNick}** to **{newNick}**", - "MEMBER_USERNAME_CHANGE": "✏ {userMention(member)}: username changed from **{oldName}** to **{newName}**", + "MEMBER_USERNAME_CHANGE": "✏ {userMention(user)}: username changed from **{oldName}** to **{newName}**", "MEMBER_RESTORE": "💿 Restored {restoredData} for {userMention(member)} on rejoin", "CHANNEL_CREATE": "🖊 Channel {channelMention(channel)} was created", diff --git a/src/data/GuildNameHistory.ts b/src/data/GuildNameHistory.ts deleted file mode 100644 index be0af1c2..00000000 --- a/src/data/GuildNameHistory.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { BaseRepository } from "./BaseRepository"; -import { getRepository, Repository } from "typeorm"; -import { NameHistoryEntry } from "./entities/NameHistoryEntry"; - -const MAX_ENTRIES_PER_USER = 10; - -export class GuildNameHistory extends BaseRepository { - private nameHistory: Repository; - - constructor(guildId) { - super(guildId); - this.nameHistory = getRepository(NameHistoryEntry); - } - - async getByUserId(userId): Promise { - return this.nameHistory.find({ - where: { - guild_id: this.guildId, - user_id: userId - }, - order: { - id: "DESC" - }, - take: MAX_ENTRIES_PER_USER - }); - } - - getLastEntryByType(userId, type): Promise { - return this.nameHistory.findOne({ - where: { - guild_id: this.guildId, - user_id: userId, - type - }, - order: { - id: "DESC" - } - }); - } - - async addEntry(userId, type, value) { - await this.nameHistory.insert({ - guild_id: this.guildId, - user_id: userId, - type, - value - }); - - // Cleanup (leave only the last MAX_ENTRIES_PER_USER entries) - const lastEntries = await this.getByUserId(userId); - if (lastEntries.length > MAX_ENTRIES_PER_USER) { - const earliestEntry = lastEntries[lastEntries.length - 1]; - if (!earliestEntry) return; - - this.nameHistory - .createQueryBuilder() - .where("guild_id = :guildId", { guildId: this.guildId }) - .andWhere("user_id = :userId", { userId }) - .andWhere("id < :id", { id: earliestEntry.id }) - .delete() - .execute(); - } - } -} diff --git a/src/data/GuildNicknameHistory.ts b/src/data/GuildNicknameHistory.ts new file mode 100644 index 00000000..1909f234 --- /dev/null +++ b/src/data/GuildNicknameHistory.ts @@ -0,0 +1,68 @@ +import { BaseRepository } from "./BaseRepository"; +import { getRepository, Repository } from "typeorm"; +import { NicknameHistoryEntry } from "./entities/NicknameHistoryEntry"; +import { sorter } from "../utils"; + +export const MAX_NICKNAME_ENTRIES_PER_USER = 10; + +export class GuildNicknameHistory extends BaseRepository { + private nicknameHistory: Repository; + + constructor(guildId) { + super(guildId); + this.nicknameHistory = getRepository(NicknameHistoryEntry); + } + + async getByUserId(userId): Promise { + return this.nicknameHistory.find({ + where: { + guild_id: this.guildId, + user_id: userId, + }, + order: { + id: "DESC", + }, + }); + } + + getLastEntry(userId): Promise { + return this.nicknameHistory.findOne({ + where: { + guild_id: this.guildId, + user_id: userId, + }, + order: { + id: "DESC", + }, + }); + } + + async addEntry(userId, nickname) { + await this.nicknameHistory.insert({ + guild_id: this.guildId, + user_id: userId, + nickname, + }); + + // Cleanup (leave only the last MAX_NICKNAME_ENTRIES_PER_USER entries) + const lastEntries = await this.getByUserId(userId); + if (lastEntries.length > MAX_NICKNAME_ENTRIES_PER_USER) { + const earliestEntry = lastEntries + .sort(sorter("timestamp", "DESC")) + .slice(0, 10) + .reduce((earliest, entry) => { + if (earliest == null) return entry; + if (entry.id < earliest.id) return entry; + return earliest; + }, null); + + this.nicknameHistory + .createQueryBuilder() + .where("guild_id = :guildId", { guildId: this.guildId }) + .andWhere("user_id = :userId", { userId }) + .andWhere("id < :id", { id: earliestEntry.id }) + .delete() + .execute(); + } + } +} diff --git a/src/data/NameHistoryEntryTypes.ts b/src/data/NameHistoryEntryTypes.ts deleted file mode 100644 index a64287f8..00000000 --- a/src/data/NameHistoryEntryTypes.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum NameHistoryEntryTypes { - Username = 1, - Nickname -} diff --git a/src/data/UsernameHistory.ts b/src/data/UsernameHistory.ts new file mode 100644 index 00000000..31cee860 --- /dev/null +++ b/src/data/UsernameHistory.ts @@ -0,0 +1,67 @@ +import { BaseRepository } from "./BaseRepository"; +import { getRepository, Repository } from "typeorm"; +import { UsernameHistoryEntry } from "./entities/UsernameHistoryEntry"; +import { sorter } from "../utils"; + +export const MAX_USERNAME_ENTRIES_PER_USER = 10; + +export class UsernameHistory extends BaseRepository { + private usernameHistory: Repository; + + constructor(guildId) { + super(guildId); + this.usernameHistory = getRepository(UsernameHistoryEntry); + } + + async getByUserId(userId): Promise { + return this.usernameHistory.find({ + where: { + guild_id: this.guildId, + user_id: userId, + }, + order: { + id: "DESC", + }, + take: MAX_USERNAME_ENTRIES_PER_USER, + }); + } + + getLastEntry(userId): Promise { + return this.usernameHistory.findOne({ + where: { + guild_id: this.guildId, + user_id: userId, + }, + order: { + id: "DESC", + }, + }); + } + + async addEntry(userId, username) { + await this.usernameHistory.insert({ + user_id: userId, + username, + }); + + // Cleanup (leave only the last MAX_USERNAME_ENTRIES_PER_USER entries) + const lastEntries = await this.getByUserId(userId); + if (lastEntries.length > MAX_USERNAME_ENTRIES_PER_USER) { + const earliestEntry = lastEntries + .sort(sorter("timestamp", "DESC")) + .slice(0, 10) + .reduce((earliest, entry) => { + if (earliest == null) return entry; + if (entry.id < earliest.id) return entry; + return earliest; + }, null); + + this.usernameHistory + .createQueryBuilder() + .andWhere("user_id = :userId", { userId }) + .andWhere("id < :id", { id: earliestEntry.id }) + .delete() + .execute(); + } + } +} diff --git a/src/data/entities/NameHistoryEntry.ts b/src/data/entities/NicknameHistoryEntry.ts similarity index 64% rename from src/data/entities/NameHistoryEntry.ts rename to src/data/entities/NicknameHistoryEntry.ts index 3be1c5a9..b3eed3b0 100644 --- a/src/data/entities/NameHistoryEntry.ts +++ b/src/data/entities/NicknameHistoryEntry.ts @@ -1,7 +1,7 @@ import { Entity, Column, PrimaryColumn } from "typeorm"; -@Entity("name_history") -export class NameHistoryEntry { +@Entity("nickname_history") +export class NicknameHistoryEntry { @Column() @PrimaryColumn() id: string; @@ -10,9 +10,7 @@ export class NameHistoryEntry { @Column() user_id: string; - @Column() type: number; - - @Column() value: string; + @Column() nickname: string; @Column() timestamp: string; } diff --git a/src/data/entities/UsernameHistoryEntry.ts b/src/data/entities/UsernameHistoryEntry.ts new file mode 100644 index 00000000..1774335f --- /dev/null +++ b/src/data/entities/UsernameHistoryEntry.ts @@ -0,0 +1,14 @@ +import { Entity, Column, PrimaryColumn } from "typeorm"; + +@Entity("username_history") +export class UsernameHistoryEntry { + @Column() + @PrimaryColumn() + id: string; + + @Column() user_id: string; + + @Column() username: string; + + @Column() timestamp: string; +} diff --git a/src/index.ts b/src/index.ts index 19ede04c..2a479924 100644 --- a/src/index.ts +++ b/src/index.ts @@ -91,11 +91,12 @@ import { errorMessage, successMessage } from "./utils"; import { ZeppelinPlugin } from "./plugins/ZeppelinPlugin"; import { customArgumentTypes } from "./customArgumentTypes"; import { startUptimeCounter } from "./uptime"; +import { UsernameSaver } from "./plugins/UsernameSaver"; // Run latest database migrations logger.info("Running database migrations"); connect().then(async conn => { - // await conn.runMigrations(); + await conn.runMigrations(); const client = new Client(`Bot ${process.env.TOKEN}`, { getAllUsers: true, @@ -137,7 +138,7 @@ connect().then(async conn => { RemindersPlugin, ], - globalPlugins: [BotControlPlugin, LogServerPlugin], + globalPlugins: [BotControlPlugin, LogServerPlugin, UsernameSaver], options: { getEnabledPlugins(guildId, guildConfig): string[] { diff --git a/src/migrations/1556913287547-TurnNameHistoryToNicknameHistory.ts b/src/migrations/1556913287547-TurnNameHistoryToNicknameHistory.ts new file mode 100644 index 00000000..d6263258 --- /dev/null +++ b/src/migrations/1556913287547-TurnNameHistoryToNicknameHistory.ts @@ -0,0 +1,37 @@ +import { MigrationInterface, QueryRunner, TableColumn } from "typeorm"; + +export class TurnNameHistoryToNicknameHistory1556913287547 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.dropColumn("name_history", "type"); + + // As a raw query because of some bug with renameColumn that generated an invalid query + await queryRunner.query(` + ALTER TABLE \`name_history\` + CHANGE COLUMN \`value\` \`nickname\` VARCHAR(160) NULL DEFAULT 'NULL' COLLATE 'utf8mb4_swedish_ci' AFTER \`user_id\`; + `); + + // Drop unneeded timestamp column index + await queryRunner.dropIndex("name_history", "IDX_6bd0600f9d55d4e4a08b508999"); + + await queryRunner.renameTable("name_history", "nickname_history"); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.addColumn( + "nickname_history", + new TableColumn({ + name: "type", + type: "tinyint", + unsigned: true, + }), + ); + + // As a raw query because of some bug with renameColumn that generated an invalid query + await queryRunner.query(` + ALTER TABLE \`nickname_history\` + CHANGE COLUMN \`nickname\` \`value\` VARCHAR(160) NULL DEFAULT 'NULL' COLLATE 'utf8mb4_swedish_ci' AFTER \`user_id\` + `); + + await queryRunner.renameTable("nickname_history", "name_history"); + } +} diff --git a/src/plugins/Logs.ts b/src/plugins/Logs.ts index 75e5a8c7..65264404 100644 --- a/src/plugins/Logs.ts +++ b/src/plugins/Logs.ts @@ -397,14 +397,13 @@ export class LogsPlugin extends ZeppelinPlugin { } } - @d.event("userUpdate") + @d.event("userUpdate", null, false) async onUserUpdate(user: User, oldUser: User) { if (!oldUser) return; if (user.username !== oldUser.username || user.discriminator !== oldUser.discriminator) { - const member = (await this.getMember(user.id)) || { id: user.id, user }; this.guildLogs.log(LogType.MEMBER_USERNAME_CHANGE, { - member: stripObjectToScalars(member, ["user", "roles"]), + user: stripObjectToScalars(user), oldName: `${oldUser.username}#${oldUser.discriminator}`, newName: `${user.username}#${user.discriminator}`, }); diff --git a/src/plugins/NameHistory.ts b/src/plugins/NameHistory.ts index a6129212..53394a9d 100644 --- a/src/plugins/NameHistory.ts +++ b/src/plugins/NameHistory.ts @@ -1,9 +1,9 @@ import { decorators as d, IPluginOptions } from "knub"; -import { GuildNameHistory } from "../data/GuildNameHistory"; -import { Member, Message, User } from "eris"; -import { NameHistoryEntryTypes } from "../data/NameHistoryEntryTypes"; -import { createChunkedMessage, errorMessage, trimLines } from "../utils"; +import { GuildNicknameHistory, MAX_NICKNAME_ENTRIES_PER_USER } from "../data/GuildNicknameHistory"; +import { Member, Message } from "eris"; +import { createChunkedMessage, disableCodeBlocks } from "../utils"; import { ZeppelinPlugin } from "./ZeppelinPlugin"; +import { MAX_USERNAME_ENTRIES_PER_USER, UsernameHistory } from "../data/UsernameHistory"; interface INameHistoryPluginConfig { can_view: boolean; @@ -12,7 +12,8 @@ interface INameHistoryPluginConfig { export class NameHistoryPlugin extends ZeppelinPlugin { public static pluginName = "name_history"; - protected nameHistory: GuildNameHistory; + protected nicknameHistory: GuildNicknameHistory; + protected usernameHistory: UsernameHistory; getDefaultOptions(): IPluginOptions { return { @@ -32,57 +33,45 @@ export class NameHistoryPlugin extends ZeppelinPlugin } onLoad() { - this.nameHistory = GuildNameHistory.getInstance(this.guildId); + this.nicknameHistory = GuildNicknameHistory.getInstance(this.guildId); + this.usernameHistory = UsernameHistory.getInstance(null); } @d.command("names", "") @d.permission("can_view") async namesCmd(msg: Message, args: { userId: string }) { - const names = await this.nameHistory.getByUserId(args.userId); - if (!names) { - msg.channel.createMessage(errorMessage("No name history found for that user!")); - return; + const nicknames = await this.nicknameHistory.getByUserId(args.userId); + const usernames = await this.usernameHistory.getByUserId(args.userId); + + if (nicknames.length === 0 && usernames.length === 0) { + return this.sendErrorMessage(msg.channel, "No name history found"); } - const rows = names.map(entry => { - const type = entry.type === NameHistoryEntryTypes.Username ? "Username" : "Nickname"; - const value = entry.value || ""; - return `\`[${entry.timestamp}]\` ${type} **${value}**`; - }); + 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 = this.bot.users.get(args.userId); const currentUsername = user ? `${user.username}#${user.discriminator}` : args.userId; - const message = trimLines(` - Name history for **${currentUsername}**: - - ${rows.join("\n")} - `); + let message = `Name history for **${currentUsername}**:`; + if (nicknameRows.length) { + message += `\n\n__Last ${MAX_NICKNAME_ENTRIES_PER_USER} nicknames:__\n${nicknameRows.join("\n")}`; + } + if (usernameRows.length) { + message += `\n\n__Last ${MAX_USERNAME_ENTRIES_PER_USER} usernames:__\n${usernameRows.join("\n")}`; + } + createChunkedMessage(msg.channel, message); } - // @d.event("userUpdate", null, false) - // async onUserUpdate(user: User, oldUser: { username: string; discriminator: string; avatar: string }) { - // if (user.username !== oldUser.username || user.discriminator !== oldUser.discriminator) { - // const newUsername = `${user.username}#${user.discriminator}`; - // await this.nameHistory.addEntry(user.id, NameHistoryEntryTypes.Username, newUsername); - // } - // } - @d.event("guildMemberUpdate") async onGuildMemberUpdate(_, member: Member) { - const latestEntry = await this.nameHistory.getLastEntryByType(member.id, NameHistoryEntryTypes.Nickname); - if (!latestEntry || latestEntry.value !== member.nick) { - await this.nameHistory.addEntry(member.id, NameHistoryEntryTypes.Nickname, member.nick); + const latestEntry = await this.nicknameHistory.getLastEntry(member.id); + if (!latestEntry || latestEntry.nickname != member.nick) { + // tslint:disable-line + await this.nicknameHistory.addEntry(member.id, member.nick); } } - - // @d.event("guildMemberAdd") - // async onGuildMemberAdd(_, member: Member) { - // const latestEntry = await this.nameHistory.getLastEntryByType(member.id, NameHistoryEntryTypes.Username); - // const username = `${member.user.username}#${member.user.discriminator}`; - // if (!latestEntry || latestEntry.value !== username) { - // await this.nameHistory.addEntry(member.id, NameHistoryEntryTypes.Username, username); - // } - // } } diff --git a/src/plugins/UsernameSaver.ts b/src/plugins/UsernameSaver.ts new file mode 100644 index 00000000..1a3de06d --- /dev/null +++ b/src/plugins/UsernameSaver.ts @@ -0,0 +1,31 @@ +import { decorators as d, GlobalPlugin } from "knub"; +import { UsernameHistory } from "../data/UsernameHistory"; +import { Member, User } from "eris"; + +export class UsernameSaver extends GlobalPlugin { + public static pluginName = "username_saver"; + + protected usernameHistory: UsernameHistory; + + async onLoad() { + this.usernameHistory = UsernameHistory.getInstance(null); + } + + protected async updateUsername(user: User) { + const newUsername = `${user.username}#${user.discriminator}`; + const latestEntry = await this.usernameHistory.getLastEntry(user.id); + if (newUsername !== latestEntry.username) { + await this.usernameHistory.addEntry(user.id, newUsername); + } + } + + @d.event("userUpdate", null, false) + async onUserUpdate(user: User) { + this.updateUsername(user); + } + + @d.event("guildMemberAdd", null, false) + async onGuildMemberAdd(_, member: Member) { + this.updateUsername(member.user); + } +}