diff --git a/cli/_lib/users.js b/cli/_lib/users.js index ea65d35..68bd7a1 100644 --- a/cli/_lib/users.js +++ b/cli/_lib/users.js @@ -3,10 +3,11 @@ 'use strict'; -const Promise = require('bluebird'); -const mongoose = require('mongoose'); -const html_unescape = require('nodeca.vbconvert/lib/html_unescape_entities'); -const progress = require('./utils').progress; +const Promise = require('bluebird'); +const mongoose = require('mongoose'); +const html_unescape = require('nodeca.vbconvert/lib/html_unescape_entities'); +const nick_transform = require('nodeca.vbconvert/lib/nick_transform'); +const progress = require('./utils').progress; const UNCONFIRMED = 3; const MEMBERS = 11; @@ -14,6 +15,10 @@ const VIOLATORS = 12; module.exports = async function (N) { + let bot = await N.models.users.User.findOne() + .where('hid').equals(N.config.bots.default_bot_hid) + .lean(true); + let usergroups = await N.models.vbconvert.UserGroupMapping.find().lean(true); let mongoid = {}; @@ -24,6 +29,24 @@ module.exports = async function (N) { let conn = await N.vbconvert.getConnection(); + let rows = (await conn.query('SELECT userid, username FROM user ORDER BY userid ASC'))[0]; + let username_mapping = {}; + let nicks_seen = new Set(rows.map(row => nick_transform.normalize(row.username))); + + for (let { userid, username } of rows) { + let nick = html_unescape(username); + + if (!nick_transform.valid(nick)) { + username_mapping[userid] = nick_transform(nick, nicks_seen, nick_transform.rules); + } + } + + N.logger.info('Username mapping created ' + + `(changing ${Object.keys(username_mapping).length} out of ${rows.length} nicknames)`); + + // remove old records of name change in case import is restarted + await N.models.users.UserNickChange.deleteMany({}); + let gi_rows = (await conn.query('SELECT value FROM setting WHERE varname = "globalignore" LIMIT 1'))[0]; let hellbanned_ids = []; @@ -32,7 +55,7 @@ module.exports = async function (N) { hellbanned_ids = gi_rows[0].value.split(' ').map(Number); } - let rows = (await conn.query(` + rows = (await conn.query(` SELECT userid,usergroupid,membergroupids,username,email,password,salt, passworddate,ipaddress,joindate,lastactivity,icq,skype, CAST(birthday_search as char) as birthday, @@ -58,7 +81,7 @@ module.exports = async function (N) { } user.hid = row.userid; - user.nick = html_unescape(row.username); + user.nick = username_mapping[row.userid] || html_unescape(row.username); user.email = row.email; user.joined_ts = new Date(row.joindate * 1000); user.joined_ip = row.ipaddress; @@ -124,6 +147,15 @@ module.exports = async function (N) { await user.save(); + if (username_mapping[row.userid]) { + await new N.models.users.UserNickChange({ + from: bot._id, + user: user._id, + old_nick: html_unescape(row.username), + new_nick: username_mapping[row.userid] + }).save(); + } + let authProvider = new N.models.users.AuthProvider(); authProvider.user = user._id; diff --git a/lib/nick_transform.js b/lib/nick_transform.js new file mode 100644 index 0000000..dc0f085 --- /dev/null +++ b/lib/nick_transform.js @@ -0,0 +1,91 @@ +'use strict'; + +const regexp = /(?=.{2,})^=?[\p{L}\d]+(?:[-_=][\p{L}\d]+)*[#=]*$/u; + +let rules = []; + +rules.push([ n => n.replace(/\$+/, m => 'S'.repeat(m.length)), 10 ]); +rules.push([ n => n.replace(/^Ⓔⓤⓖⓔⓔ$/, 'Eugee'), 10 ]); +rules.push([ n => n.replace(/\][I|]\[|\}[I|]\{|\)[I|]\(/ug, 'Ж'), 10 ]); +rules.push([ n => n.replace(/\]\[|\}\{/ug, 'X'), 10 ]); +rules.push([ n => n.replace(/\(\)/ug, 'O'), 10 ]); +rules.push([ n => n.replace(/\|\{/ug, 'K'), 11 ]); +rules.push([ n => n.replace(/@/g, () => (n.indexOf('.') === -1 ? 'A' : '@')), 10 ]); +rules.push([ n => n.replace(/@[a-z0-9]+\.[a-z]*$/i, ''), 10 ]); +rules.push([ n => n.replace(/^&&&$/, 'AAA'), 10 ]); +rules.push([ n => n.replace(/^\^\^\^$/, 'aaa'), 10 ]); + +rules.push([ n => n.replace(/^[^-_=\p{L}\d]+/ug, ''), 100 ]); +rules.push([ n => n.replace(/[^-_=\p{L}\d]+$/ug, ''), 1000 ]); + +rules.push([ n => n.replace(/[^\$@\-_=\p{L}\d]/ug, '_'), 10000 ]); +rules.push([ n => n.replace(/[^\$@\-_=\p{L}\d]/ug, '-'), 11000 ]); +rules.push([ n => n.replace(/(\p{L}\d)[^\$@\-_=\p{L}\d](\p{L}\d)/ug, '$1=$2'), 12000 ]); + +rules.push([ n => n.replace(/^[-_]+|[-_]+$/ug, ''), 100 ]); +rules.push([ n => n.replace(/[-_=]{2,}/ug, '_'), 1000 ]); +rules.push([ n => n.replace(/[-_=]{2,}/ug, '-'), 1100 ]); +rules.push([ n => n.replace(/[-_=]{2,}/ug, '='), 20000 ]); + +rules.push([ n => n.replace(/$/, '='), 100000 ]); +rules.push([ n => n.replace(/$/, '#'), 110000 ]); +rules.push([ n => n.replace(/^(.*)$/, '=$1#'), 150000 ]); +rules.push([ n => n.replace(/^(.*)$/, '=$1='), 160000 ]); + +rules = rules.sort((a, b) => a[1] - b[1]); + +function valid(nick) { return nick.match(regexp); } +function normalize(nick) { return nick.toUpperCase().toLowerCase(); } + +function transform_nick(nick, nicks_seen, rules/*, debug = null*/) { + let visited = new Set(); + + let weights = new Map(); + weights.set(nick, 0); + + /*let paths; + + if (debug) { + paths = new Map(); + paths.set(nick, []); + }*/ + + for (;;) { + let min_entry = [ '', Infinity ]; + + for (let entry of weights.entries()) { + if (entry[1] >= min_entry[1]) continue; + if (visited.has(entry[0])) continue; + min_entry = entry; + } + + let [ current_nick, current_weight ] = min_entry; + let nick_norm = normalize(current_nick); + + if (valid(current_nick) && !nicks_seen.has(nick_norm)) { + nicks_seen.add(nick_norm); + //if (debug) return [ current_nick, current_weight, paths.get(current_nick) ]; + return current_nick; + } + + for (let i = 0; i < rules.length; i++) { + let [ fn, weight ] = rules[i]; + let new_nick = fn(current_nick); + let new_weight = current_weight + weight; + let existing_weight = weights.get(new_nick); + + if (typeof existing_weight === 'undefined' || new_weight < existing_weight) { + weights.set(new_nick, new_weight); + //if (debug) paths.set(new_nick, paths.get(current_nick).concat([ i ])); + } + } + + visited.add(current_nick); + } +} + +module.exports = transform_nick; + +module.exports.valid = valid; +module.exports.normalize = normalize; +module.exports.rules = rules;