diff --git a/src/structure/client/wrappers/GuildWrapper.js b/src/structure/client/wrappers/GuildWrapper.js index 794e86d..0ca9ee5 100644 --- a/src/structure/client/wrappers/GuildWrapper.js +++ b/src/structure/client/wrappers/GuildWrapper.js @@ -134,6 +134,7 @@ class GuildWrapper { return this._settings; } + // TODO Move settings to a settings object insteadd of being in a mess with everything else in the data object async updateSettings(settings) { if (!this._settings) await this.settings(); try { diff --git a/src/structure/components/commands/administration/Import.js b/src/structure/components/commands/administration/Import.js index 8981233..88876f7 100644 --- a/src/structure/components/commands/administration/Import.js +++ b/src/structure/components/commands/administration/Import.js @@ -1,3 +1,4 @@ +const { InfractionMigrator, Util } = require("../../../../utilities"); const SettingsMigrator = require("../../../../utilities/SettingsMigrator"); const { SlashCommand } = require("../../../interfaces"); @@ -6,6 +7,7 @@ const dbs = { '3': 'newgbot' }; +const { MONGODB_HOST, MONGODB_USERNAME, MONGODB_PASSWORD, MONGODB_V2_HOST } = process.env; class ImportCommand extends SlashCommand { constructor(client) { @@ -17,7 +19,7 @@ class ImportCommand extends SlashCommand { guildOnly: true, memberPermissions: ['ADMINISTRATOR'], options: [{ - name: ['settings', 'modlogs'], + name: ['settings'], type: 'SUB_COMMAND', options: [{ name: 'version', @@ -27,24 +29,49 @@ class ImportCommand extends SlashCommand { { name: 'v3', value: '3' } ] }] + }, { + name: 'modlogs', + type: 'SUB_COMMAND', + options: [{ + name: 'version', + description: 'Which version do you want to import from', + choices: [ + { name: 'v2', value: '2' }, + { name: 'v3', value: '3' } + ] + }, { + name: 'overwrite', + description: 'Whether any existing logs should be overwritten by the imports. By default new ones are bumped', + type: 'BOOLEAN' + }] }] }); } - async execute(invoker, { version }) { + async execute(invoker, { version, overwrite }) { const { subcommand, guild } = invoker; - const settings = await guild.settings(); - if (settings._imported); + let settings = await guild.settings(); + if (settings._imported[subcommand.name]) return { emoji: 'failure', index: 'COMMAND_IMPORT_IMPORTED', params: { thing: Util.capitalise(subcommand.name) } }; version = version?.value || '3'; - if(subcommand.name === 'modlogs') return { content: 'Not supported yet' }; + await invoker.reply({ index: 'COMMAND_IMPORT_WORKING', emoji: 'loading' }); + const result = await this[subcommand.name](guild, version, overwrite.value); - // TODO split into settings and modlogs + // This looks ridiculous but it's to keep track of what's been imported + settings = guild._settings; + if (!settings.imported) settings.imported = {}; + settings.imported[subcommand.name] = true; - const { MONGODB_HOST, MONGODB_USERNAME, MONGODB_PASSWORD } = process.env; - const migrator = new SettingsMigrator(this.client, guild, { - host: MONGODB_HOST, + result._edit = true; + return result; + + } + + async modlogs(guild, version, overwrite = false) { + + const migrator = new InfractionMigrator(this.client, guild, { + host: version === '2' ? MONGODB_V2_HOST : MONGODB_HOST, username: MONGODB_USERNAME, password: MONGODB_PASSWORD, database: dbs[version], // Default to v3 @@ -54,7 +81,48 @@ class ImportCommand extends SlashCommand { await migrator.connect(); let imported = null; try { - imported = await migrator.migrate(); + imported = await migrator.import(); + imported.sort((a, b) => a.case - b.case); + } catch (err) { + this.client.logger.error(err.stack); + return { index: 'COMMAND_IMPORT_ERROR', params: { message: err.message }, emoji: 'failure' }; + } + await migrator.end(); + console.log(imported); + + if (overwrite) { // Overwrite any existing logs with the imported ones + await this.client.mongodb.infractions.deleteMany({ guild: guild.id }); + await this.client.mongodb.infractions.insertMany(imported); + } else { // Bump existing logs by the highest case id from imported logs + const highestOldId = imported[imported.length - 1]; + const existingLogs = await this.client.mongodb.infractions.find({ guild: guild.id }); + for (const log of existingLogs) { + log.case += highestOldId; + await this.client.mongodb.infractions.updateOne({ _id: log._id }, { case: log.case }); + } + guild._settings.caseId += highestOldId; + await guild.updateSettings({ caseId: guild._settings.caseId }); + } + + + return { content: 'blah' }; + + } + + async settings(guild, version) { + // const { MONGODB_HOST, MONGODB_USERNAME, MONGODB_PASSWORD } = process.env; + const migrator = new SettingsMigrator(this.client, guild, { + host: version === '2' ? MONGODB_V2_HOST : MONGODB_HOST, + username: MONGODB_USERNAME, + password: MONGODB_PASSWORD, + database: dbs[version], // Default to v3 + version + }); + + await migrator.connect(); + let imported = null; + try { + imported = await migrator.import(); } catch (err) { this.client.logger.error(err.stack); return { index: 'COMMAND_IMPORT_ERROR', params: { message: err.message }, emoji: 'failure' }; @@ -68,7 +136,7 @@ class ImportCommand extends SlashCommand { if (typeof webhook === 'string') { const hooks = await guild.fetchWebhooks(); const hook = hooks.get(webhook); - if(hook) await guild.updateWebhook('messages', hook); + if (hook) await guild.updateWebhook('messages', hook); } else if (version === '3') { delete webhook.feature; await this.client.storageManager.mongodb.webhooks.updateOne({ feature: 'messages', guild: guild.id }, webhook); diff --git a/src/structure/storage/interfaces/MongodbTable.js b/src/structure/storage/interfaces/MongodbTable.js index d17b57b..03b9faf 100644 --- a/src/structure/storage/interfaces/MongodbTable.js +++ b/src/structure/storage/interfaces/MongodbTable.js @@ -1,3 +1,5 @@ +const { ObjectId } = require("mongodb"); + class MongodbTable { constructor(client, provider, opts = {}) { @@ -11,11 +13,11 @@ class MongodbTable { //Data Search - async find(query, opts = {}, { sort, skip, limit } = {}) { //opts: { projection: ... } + find(query, opts = {}, { sort, skip, limit } = {}) { //opts: { projection: ... } query = this._handleData(query); if (!this.provider._initialized) return Promise.reject(new Error('MongoDB is not connected.')); - const cursor = await this.collection.find(query, opts); + const cursor = this.collection.find(query, opts); if (sort) cursor.sort(sort); if (skip) cursor.skip(skip); if (limit) cursor.limit(limit); @@ -70,7 +72,10 @@ class MongodbTable { }); } - //NOTE: insertMany? + insertMany(documents, options = {}) { + if (!this.provider._initialized) return Promise.reject(new Error('MongoDB is not connected.')); + return this.collection.insertMany(documents, options); + } deleteOne(query) { query = this._handleData(query); @@ -164,17 +169,15 @@ class MongodbTable { }); } - //Lazy Function - // Shouldn't be necessary anymore -- cleanup later - _handleData(data) { //Convert data._id to Mongo ObjectIds (gets converted to plaintext through shard communication) - // if (data._id) { - // if (typeof data._id === 'string') data._id = ObjectId(data._id); - // else if (typeof data._id === 'object') data._id = { - // $in: Object.values(data._id)[0].map((id) => { - // return ObjectId(id); - // }) - // }; - // } + _handleData(data) { //Convert data._id to Mongo ObjectIds + if (data._id) { + if (typeof data._id === 'string') data._id = ObjectId(data._id); + else if (typeof data._id === 'object') data._id = { + $in: Object.values(data._id)[0].map((id) => { + return ObjectId(id); + }) + }; + } return data; } diff --git a/src/utilities/InfractionMigrator.js b/src/utilities/InfractionMigrator.js new file mode 100644 index 0000000..e69158f --- /dev/null +++ b/src/utilities/InfractionMigrator.js @@ -0,0 +1,137 @@ +const MongoWrapper = require('./SimpleMongoWrapper'); +const Logger = require('./Logger'); + +const modtypes = { + 'hardban': 'BAN', + 'ban': 'BAN', + 'tempban': 'BAN', + 'softban': 'SOFTBAN', + 'kick': 'KICK', + 'note': 'NOTE', + 'mute': 'MUTE', + 'unmute': 'UNMUTE', + 'warn': 'WARN', +}; + +class InfractionMigrator { + + constructor(client, guild, dbConfig) { + + this._config = dbConfig; + this.client = client; + this.mongo = new MongoWrapper(dbConfig); + this.guild = guild.id || guild; + this.logger = new Logger(this); + + } + + async connect() { + this.logger.info(`Connecting to mongo: ${this._config.database}`); + await this.mongo.init(); + this.logger.info(`Mongo connected`); + } + + async end() { + this.logger.info(`Disconnecting ${this._config.database}`); + await this.mongo.close(); + this.logger.info(`Disconnected`); + } + + async import() { + + this.logger.debug(`Attempting modlogs migration for ${this.guild}`); + + const { version } = this._config; + let idIdentifier = null, + collection = null; + if (version === '2') { + collection = 'discord_infractions'; + idIdentifier = 'guild'; + } else { + collection = 'infractions'; + idIdentifier = 'guild'; + } + + const filter = { [idIdentifier]: this.guild }; + const infractions = await this.mongo.find(collection, filter); + if (!infractions.length) return Promise.reject(new Error('No infractions found')); + + const translated = this[version](infractions); + return translated; + + } + + '2'(infractions) { + + const result = []; + for (const infraction of infractions) { + const base = this.infractionBase; + base.id = `${infraction.guild}:${infraction.id}`; + base.case = infraction.id; + base.reason = infraction.reason; + base.type = modtypes[infraction.type]; + + base.duration = infraction.modlength * 1000; + base.timestamp = infraction.timestamp * 1000; + + base.guild = infraction.guild; + base.executor = infraction.staff; + base.target = infraction.user; + + base.resolved = infraction.resolved; + base.points = infraction.modpoints; + base.expiration = infraction.expires * 1000; + + base.dmLogMessage = infraction.dm_message_id; + base.modLogMessage = infraction.message_id; + + result.push(base); + + } + + return result; + + } + + '3'(infractions) { + const result = []; + for (const infraction of infractions) { + // Ensure the infractions have all properties, shouldn't be any inconsistencies between 3 and 3.slash but just in case + result.push({ ...this.infractionBase, ...infraction }); + } + return result; + } + + get infractionBase() { + return { + id: null, + guild: null, + channel: null, + channelName: null, + message: null, + executor: null, + executorTag: null, + target: null, + targetTag: null, + targetType: null, + type: null, + case: null, + timestamp: null, + duration: null, + callback: null, + reason: null, + data: null, + flags: null, + points: null, + expiration: null, + modLogMessage: null, + dmLogMessage: null, + modLogChannel: null, + resolved: null, + changes: null, + _callbacked: true + }; + } +} + +module.exports = InfractionMigrator; \ No newline at end of file diff --git a/src/utilities/SettingsMigrator.js b/src/utilities/SettingsMigrator.js index 1d39e7a..4269a12 100644 --- a/src/utilities/SettingsMigrator.js +++ b/src/utilities/SettingsMigrator.js @@ -46,7 +46,7 @@ class SettingsMigrator { this.logger.info(`Disconnected`); } - async migrate() { + async import() { this.logger.debug(`Attempting settings migration for ${this.guild}`); @@ -65,10 +65,10 @@ class SettingsMigrator { const settings = await this.mongo.findOne(collection, filter); if (!settings) return Promise.reject(new Error('No old settings found')); - const { _version } = settings; - if (!_version) return Promise.reject(new Error('Unable to determine configuration version')); + // const { _version } = settings; + // if (!_version) return Promise.reject(new Error('Unable to determine configuration version')); - const translated = this[_version](settings); + const translated = this[version](settings); let webhook = null, permissions = null; @@ -93,7 +93,7 @@ class SettingsMigrator { permissions.guildId = this.guild; } - translated._imported = true; + // translated._imported = true; translated.guildId = this.guild; this.logger.info(`Settings migration for ${this.guild}`); @@ -220,6 +220,22 @@ class SettingsMigrator { }; } + if (selfrole) { + settings.selfrole = { + roles: selfrole.roles, message: null, channel: null, text: null + }; + } + + if (activity) settings.activity = activity; + if (killitwithfire) { + settings.dehoist = { + enabled: killitwithfire.enabled, + begin: killitwithfire.startsWith, + characters: [], + strict: killitwithfire.strict + }; + } + if (moderation || modlogs) settings.moderation = { channel: result.modlogs || null, @@ -344,6 +360,7 @@ class SettingsMigrator { const _points = entries.filter(([key]) => !key.includes('Expire') && !['enabled', 'associations', 'multiplier'].includes(key)); for (const [key, value] of _points) points[key.toUpperCase()] = value; + // eslint-disable-next-line prefer-const for (let [key, value] of _expirations) { key = key.replace('Expire', '').toUpperCase(); expirations[key] = value; diff --git a/src/utilities/index.js b/src/utilities/index.js index f5d4a7e..37d684f 100644 --- a/src/utilities/index.js +++ b/src/utilities/index.js @@ -3,5 +3,6 @@ module.exports = { BinaryTree: require('./BinaryTree.js'), FilterUtil: require('./FilterUtil.js'), Logger: require('./Logger.js'), - SettingsMigrator: require('./SettingsMigrator') + SettingsMigrator: require('./SettingsMigrator'), + InfractionMigrator: require('./InfractionMigrator') }; \ No newline at end of file