diff --git a/migrations/20180730230000_create_persisted_data_table.js b/migrations/20180730230000_create_persisted_data_table.js new file mode 100644 index 00000000..2da9737e --- /dev/null +++ b/migrations/20180730230000_create_persisted_data_table.js @@ -0,0 +1,17 @@ +exports.up = async function(knex, Promise) { + if (! await knex.schema.hasTable('persisted_data')) { + await knex.schema.createTable('persisted_data', table => { + table.string('guild_id', 20).notNullable(); + table.string('user_id', 20).notNullable(); + table.string('roles', 1024).nullable().defaultTo(null); + table.string('nickname', 255).nullable().defaultTo(null); + table.integer('is_voice_muted').notNullable().defaultTo(0); + + table.primary(['guild_id', 'user_id']); + }); + } +}; + +exports.down = async function(knex, Promise) { + await knex.schema.dropTableIfExists('persisted_data'); +}; diff --git a/package-lock.json b/package-lock.json index 08acac55..09ed19bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2490,6 +2490,11 @@ "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=" }, + "lodash.intersection": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.intersection/-/lodash.intersection-4.4.0.tgz", + "integrity": "sha1-ChG6Yx0OlcI8fy9Mu5ppLtF45wU=" + }, "lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", diff --git a/package.json b/package.json index 89f72124..3be1fcfe 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "knub": "^9.4.13", "lodash.at": "^4.6.0", "lodash.difference": "^4.5.0", + "lodash.intersection": "^4.4.0", "lodash.isequal": "^4.5.0", "mariasql": "^0.2.6", "moment-timezone": "^0.5.21", diff --git a/src/data/DefaultLogMessages.json b/src/data/DefaultLogMessages.json index 13f3e2aa..3967876d 100644 --- a/src/data/DefaultLogMessages.json +++ b/src/data/DefaultLogMessages.json @@ -13,7 +13,7 @@ "MEMBER_ROLE_REMOVE": "🔑 **{member.user.username}#{member.user.discriminator}** (`{member.id}`) role removed **{role.name}** by {mod.username}#{mod.discriminator}", "MEMBER_NICK_CHANGE": "✏ **{member.user.username}#{member.user.discriminator}** (`{member.id}`) changed their nickname from **{oldNick}** to **{newNick}**", "MEMBER_USERNAME_CHANGE": "✏ **{member.user.username}#{member.user.discriminator}** (`{member.id}`) changed their username from **{oldName}** to **{newName}**", - "MEMBER_ROLES_RESTORE": "💿 **{member.user.username}#{member.user.discriminator}** (`{member.id}`) roles were restored", + "MEMBER_RESTORE": "💿 **{member.user.username}#{member.user.discriminator}** (`{member.id}`) was restored", "CHANNEL_CREATE": "🖊 Channel **#{channel.name}** was created", "CHANNEL_DELETE": "🗑 Channel **#{channel.name}** was deleted", diff --git a/src/data/GuildPersistedData.ts b/src/data/GuildPersistedData.ts new file mode 100644 index 00000000..2126847c --- /dev/null +++ b/src/data/GuildPersistedData.ts @@ -0,0 +1,53 @@ +import knex from "../knex"; +import PersistedData from "../models/PersistedData"; + +export interface IPartialPersistData { + roles?: string[]; + nickname?: string; + is_voice_muted?: boolean; +} + +export class GuildPersistedData { + protected guildId: string; + + constructor(guildId) { + this.guildId = guildId; + } + + async find(userId: string) { + const result = await knex("persisted_data") + .where("guild_id", this.guildId) + .where("user_id", userId) + .first(); + + return result ? new PersistedData(result) : null; + } + + async set(userId: string, data: IPartialPersistData = {}) { + const finalData: any = {}; + if (data.roles) finalData.roles = data.roles.join(","); + if (data.nickname) finalData.nickname = data.nickname; + if (data.is_voice_muted) finalData.is_voice_muted = data.is_voice_muted ? 1 : 0; + + const existing = await this.find(userId); + if (existing) { + await knex("persisted_data") + .where("guild_id", this.guildId) + .where("user_id", userId) + .update(finalData); + } else { + await knex("persisted_data").insert({ + ...finalData, + guild_id: this.guildId, + user_id: userId + }); + } + } + + async clear(userId: string) { + await knex("persisted_data") + .where("guild_id", this.guildId) + .where("user_id", userId) + .delete(); + } +} diff --git a/src/data/LogType.ts b/src/data/LogType.ts index 017695e5..1a0eeaca 100644 --- a/src/data/LogType.ts +++ b/src/data/LogType.ts @@ -13,7 +13,7 @@ export enum LogType { MEMBER_ROLE_REMOVE, MEMBER_NICK_CHANGE, MEMBER_USERNAME_CHANGE, - MEMBER_ROLES_RESTORE, + MEMBER_RESTORE, CHANNEL_CREATE, CHANNEL_DELETE, diff --git a/src/index.ts b/src/index.ts index cae27e00..e20f5410 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,6 +20,7 @@ import { LogsPlugin } from "./plugins/Logs"; import { PostPlugin } from "./plugins/Post"; import { ReactionRolesPlugin } from "./plugins/ReactionRoles"; import { CensorPlugin } from "./plugins/Censor"; +import { PersistPlugin } from "./plugins/Persist"; import knex from "./knex"; // Run latest database migrations @@ -34,7 +35,8 @@ knex.migrate.latest().then(() => { logs: LogsPlugin, post: PostPlugin, reaction_roles: ReactionRolesPlugin, - censor: CensorPlugin + censor: CensorPlugin, + persist: PersistPlugin }, globalPlugins: { bot_control: BotControlPlugin diff --git a/src/models/PersistedData.ts b/src/models/PersistedData.ts new file mode 100644 index 00000000..b4b1e2e0 --- /dev/null +++ b/src/models/PersistedData.ts @@ -0,0 +1,26 @@ +import Model from "./Model"; + +export default class PersistedData extends Model { + private _roles; + private _isVoiceMuted; + + public guild_id: string; + public user_id: string; + public nickname: string; + + set roles(v) { + this._roles = v ? v.split(",") : []; + } + + get roles() { + return this._roles; + } + + set is_voice_muted(v) { + this._isVoiceMuted = v === 1; + } + + get is_voice_muted() { + return this._isVoiceMuted; + } +} diff --git a/src/plugins/Logs.ts b/src/plugins/Logs.ts index 19f2dfbb..1724af5a 100644 --- a/src/plugins/Logs.ts +++ b/src/plugins/Logs.ts @@ -141,7 +141,7 @@ export class LogsPlugin extends Plugin { if (member.nick !== oldMember.nick) { this.serverLogs.log(LogType.MEMBER_NICK_CHANGE, { member, - oldNick: oldMember.nick, + oldNick: oldMember.nick || "", newNick: member.nick }); } diff --git a/src/plugins/Persist.ts b/src/plugins/Persist.ts new file mode 100644 index 00000000..00903270 --- /dev/null +++ b/src/plugins/Persist.ts @@ -0,0 +1,91 @@ +import { Plugin, decorators as d } from "knub"; +import { GuildPersistedData, IPartialPersistData } from "../data/GuildPersistedData"; +import intersection from "lodash.intersection"; +import { Member, MemberOptions } from "eris"; +import { GuildLogs } from "../data/GuildLogs"; +import { LogType } from "../data/LogType"; +import { stripObjectToScalars } from "../utils"; + +export class PersistPlugin extends Plugin { + protected persistedData: GuildPersistedData; + protected logs: GuildLogs; + + getDefaultOptions() { + return { + config: { + persisted_roles: [], + persist_nicknames: false, + persist_voice_mutes: false + } + }; + } + + onLoad() { + this.persistedData = new GuildPersistedData(this.guildId); + this.logs = new GuildLogs(this.guildId); + } + + @d.event("guildMemberRemove") + onGuildMemberRemove(_, member: Member) { + let persist = false; + const persistData: IPartialPersistData = {}; + + const persistedRoles = this.configValue("persisted_roles"); + if (persistedRoles.length) { + const rolesToPersist = intersection(persistedRoles, member.roles); + if (rolesToPersist.length) { + persist = true; + persistData.roles = rolesToPersist; + } + } + + if (this.configValue("persist_nicknames") && member.nick) { + persist = true; + persistData.nickname = member.nick; + } + + if (this.configValue("persist_voice_mutes")) { + persist = true; + persistData.is_voice_muted = member.voiceState.mute; + } + + if (persist) { + this.persistedData.set(member.id, persistData); + } + } + + @d.event("guildMemberAdd") + async onGuildMemberAdd(_, member: Member) { + const persistedData = await this.persistedData.find(member.id); + if (!persistedData) return; + + let restore = false; + const toRestore: MemberOptions = {}; + + const persistedRoles = this.configValue("persisted_roles"); + if (persistedRoles.length) { + const rolesToRestore = intersection(persistedRoles, persistedData.roles); + if (rolesToRestore.length) { + restore = true; + toRestore.roles = rolesToRestore; + } + } + + if (this.configValue("persist_nicknames") && persistedData.nickname) { + restore = true; + toRestore.nick = persistedData.nickname; + } + + if (this.configValue("persist_voice_mutes") && persistedData.is_voice_muted) { + restore = true; + toRestore.mute = true; + } + + if (restore) { + await member.edit(toRestore, "Restored upon rejoin"); + this.logs.log(LogType.MEMBER_RESTORE, { + member: stripObjectToScalars(member, ["user"]) + }); + } + } +}