소스 검색

init

db

persistent score

Fix typos

set the score after user score update

Remove logs

Change question text

Handle errors

voice users with score

revoice on login

[add] Display answer on successful response
[bugfix] batch set voice
[WIP] score saving

manage user renames and nick case sensitivity

save scores to mysql db

[bugfix] fail fast

[bugfix] undefined previous mysql save

[bugfix] undefined previous mysql save

[bugfix] setTimeout int overflow

delays

wrong pseudo, wrong chan

add !rename
bugfix double response on between question delay

bugfix !rename case sensitivity

delay

Le mer morte

Boolean response, string response managment

oops

number hint

oops

bugfix rename

quizz db clean

questions with multiple responses

oops static context

!report cmd
isundil 6 년 전
부모
커밋
606d24ebee
5개의 변경된 파일726개의 추가작업 그리고 0개의 파일을 삭제
  1. 1 0
      .gitignore
  2. 25 0
      config.js
  3. 648 0
      index.js
  4. 16 0
      package.json
  5. 36 0
      strpad.js

+ 1 - 0
.gitignore

@@ -28,3 +28,4 @@ build/Release
 # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
 node_modules
 
+/persistentDb.json

+ 25 - 0
config.js

@@ -0,0 +1,25 @@
+
+module.exports = {
+// Gameplay params
+    AUTO_HINT_DELAY: 1 * 60 * 1000, // 1 minute
+    MIN_HINT_DELAY: 10 * 1000, // 10 seconds between 2 hint requests
+    NEXT_QUESTION_DELAY: 10 * 1000, // 10 seconds between questions
+    GAME_DURATION: 30 * 24 * 60 * 60 * 1000, // 1 month game duration
+    QUESTIONS_PATH: "./quizz.json",
+
+    START_TIME: Date.now(),
+
+// IRC params
+    NS_PASSWORD: "XXX",
+    USE_NS: true,
+
+// MySQL params
+    MySQL_DB: "XXX",
+    MySQL_PERIOD_TABLE: "knackizz_period",
+    MySQL_SCORES_TABLE: "knackizz_scores",
+    MySQL_HOST: "mysql.knacki.info",
+    MySQL_USER: "XXX",
+    MySQL_PASS: "XXX",
+    USE_MYSQL: true
+};
+

+ 648 - 0
index.js

@@ -0,0 +1,648 @@
+
+const irc = require("irc"),
+    fs = require("fs"),
+    readline = require("readline"),
+    cache = new (require("data-store"))({ path: "persistentDb.json" }),
+    arrayPad = require('./strpad.js').arrayPad;
+
+Object.assign(global, require("./config.js"));
+
+const MySQL = USE_MYSQL ? require("mysql2").createConnection({host: MySQL_HOST, user: MySQL_USER, database: MySQL_DB, password: MySQL_PASS}) : null;
+
+const HOSTNAME = require('os').hostname(); // For Mysql bot identification
+
+function Cache() {}
+Cache.SetScores = function(scores) {
+    var dataToSet = {};
+    for (var i in scores)
+        if (scores[i].score)
+            dataToSet[scores[i].name] = scores[i].score;
+    cache.set("scores", dataToSet);
+    cache.set("saveTs", Date.now());
+}
+
+Cache.getReportedQuestions = function(questionId, username) {
+    return cache.get("report") || {};
+}
+
+Cache.reportQuestion = function(questionId, username) {
+    var reported = cache.get("report") || {};
+    var questionReported = reported[questionId] || {};
+    reported[questionId] = questionReported;
+    if (questionReported[username])
+        return;
+    questionReported[username] = Date.now();
+    cache.set("report", reported);
+}
+
+Cache.unreportQuestion = function(questionId) {
+    var reported = cache.get("report") || {};
+    if (!reported[questionId])
+        return;
+    delete reported[questionId];
+    cache.set("report", reported);
+}
+
+Cache.setExportTs = function() {
+    cache.set("mysqlTs", Date.now());
+}
+
+Cache.getLastMysqlSave = function() {
+    return cache.get("mysqlTs") || 0;
+}
+
+Cache.GetData = function() {
+    return {
+        scores: cache.get("scores"),
+        lastSave: cache.get("saveTs") || 0,
+        lastMysqlExport: cache.get("mysqlTs") || 0
+    };
+}
+
+function Question(id, obj) {
+    this.id = id;
+    this.question = obj.question;
+    this.response = obj.response;
+    this.normalizedResponse = "";
+    if (Array.isArray(obj.response))
+        this.normalizedResponse = this.response.map(i => Question.normalize(i));
+    else
+        this.normalizedResponse = Question.normalize(this.response);
+}
+
+Question.normalize = function(str) {
+    return str.normalize('NFD').replace(/[\u0300-\u036f]/g, "").toLowerCase().trim();
+}
+
+const QuestionType = { bool: {}, number: {}, string: {} };
+
+Question.prototype.booleanValue = function(str) {
+    str = str || (Array.isArray(this.normalizedResponse) ? this.normalizedResponse[0] : this.normalizedResponse);
+    var index = ["non", "oui", "faux", "vrai"].indexOf(str);
+    if (index >= 0)
+        return index % 2 === 1;
+    return undefined;
+}
+
+Question.prototype.isBoolean = function(str) {
+    return this.booleanValue(str) !== undefined;
+}
+
+String.prototype.isWord = function() {
+    return (/\W/).exec(this) === null;
+}
+
+Question.prototype.getQuestionType = function() {
+    if (this.isBoolean())
+        return QuestionType.bool;
+    if (!isNaN(Number(this.response)))
+        return QuestionType.number;
+    return QuestionType.string;
+}
+
+Question.toHint = function(response, normalizedResponse, boundaries, responseIndex, questionType, hintLevel) {
+    if (questionType === QuestionType.string) {
+        if (hintLevel == 0)
+            return response.replace(/[\w]/g, '*');
+        else if (hintLevel == 1)
+            return response.replace(/[\w]/g, (a, b) => b ? '*' : response.charAt(b));
+        else if (normalizedResponse.isWord() && !responseIndex) {
+            var displayed = [];
+            const revealPercent = 0.1;
+            displayed[normalizedResponse.length -2] = 1;
+            displayed.fill(1, 0, normalizedResponse.length-1).fill(0, Math.ceil(normalizedResponse.length * revealPercent), normalizedResponse.length);
+            displayed.sort(()=>Math.random() > 0.5?-1:1)
+            return normalizedResponse.replace(/./g, (a, b) => b && !displayed[b -1] ? '*':response.charAt(b));
+        }
+        else
+            return normalizedResponse.replace(/[\w]+/g, (a, wordIndex) => a.replace(/./g, (a, b) => b ? '*':response.charAt(wordIndex)));
+    }
+    else if (questionType === QuestionType.number) {
+        const responseInt = Number(normalizedResponse),
+            randomMin = ([ 30, 10, 3 ])[hintLevel],
+            randomSpread = 5 * (5 -hintLevel);
+        boundaries[0] = Math.max(
+                Math.floor(responseInt -(Math.random() *randomSpread) -randomMin),
+                boundaries[0]);
+        boundaries[1] = Math.min(
+                Math.ceil(responseInt +(Math.random() *randomSpread) +randomMin),
+                boundaries[1]);
+        return "Un nombre entre " +boundaries[0] +" et " +boundaries[1];
+    }
+    else
+        console.error("Unknown response type !");
+};
+
+Question.prototype.getHint = function(hintLevel) {
+    var type = this.getQuestionType();
+    if (type === QuestionType.bool)
+        return "Vrai / Faux ?";
+    else if (type === QuestionType.number)
+        this.boundaries = this.boundaries || [ -Infinity, Infinity];
+    if (!Array.isArray(this.response))
+        return Question.toHint(this.response, this.normalizedResponse, this.boundaries, 0, type, hintLevel);
+    var hints = [];
+    for (var i =0, len = this.response.length; i < len; ++i)
+        hints.push(Question.toHint(this.response[i], this.normalizedResponse[i], this.boundaries, i, type, hintLevel));
+    return hints.join (" ou ");
+}
+
+Question.prototype.check = function(response) {
+    response = Question.normalize(response);
+    var boolValue = this.booleanValue();
+    if (boolValue !== undefined)
+        return boolValue === this.booleanValue(response);
+    if (Array.isArray(this.normalizedResponse))
+        return this.normalizedResponse.indexOf(response) >= 0;
+    return response === this.normalizedResponse;
+}
+
+Question.prototype.end = function() {
+    if (this.boundaries)
+        delete this.boundaries;
+}
+
+function initQuestionList(filename) {
+    return new Promise((ok, ko) => {
+        var stream = fs.createReadStream(filename),
+            reader = readline.createInterface({input: stream}),
+            questions = [],
+            lineNo = 0,
+            borken = false;
+        reader.on("line", line => {
+            if (borken) return;
+            try {
+                var question = new Question(++lineNo, JSON.parse(line));
+                if (question.question && question.response)
+                    questions.push(question);
+            } catch (e) {
+                console.error("Failed to load Database: ", e, "on line", lineNo);
+                borken = true;
+                reader.close();
+                stream.destroy();
+                ko("Failed to load database: syntax error on question #" +lineNo);
+            }
+        });
+        reader.on("close", () => {
+            if (!borken)
+                ok(questions);
+        });
+    });
+}
+
+function User(nick) {
+    this.admin = false;
+    this.name = nick;
+    this.score = 0;
+}
+User.prototype.setModeChar = function(mode) {
+    this.admin = !!(mode && (mode.indexOf('~') >= 0 || mode.indexOf('@') >= 0 || mode.indexOf('%') >= 0));
+}
+User.prototype.setMode = function(mode) {
+    if (mode == 'h' || mode == 'o' || mode == 'q' || mode == 'a')
+        this.admin = true;
+};
+User.prototype.unsetMode = function(mode) {
+    if (mode == 'h' || mode == 'o' || mode == 'q' || mode == 'a')
+        this.admin = false;
+};
+
+function KnackiBot(previousData) {
+    var _this = this;
+    this.name = "Knackizz";
+    this.room = "#quizz";
+    this.init = false;
+    this.password = NS_PASSWORD;
+    this.users = {};
+    this.activeUsers = {};
+
+    if (previousData) {
+        for (var i in previousData.scores) {
+            this.users[i.toLowerCase()] = new User(i);
+            this.users[i.toLowerCase()].score = previousData.scores[i];
+        }
+    }
+
+    this.mySQLExportWrapper();
+
+    this.bot = new irc.Client("irc.knacki.info", this.name, {
+        channels: [ this.room ],
+        userName: this.name,
+        realName: this.name,
+        stripColors: true
+    });
+    this.bot.addListener("error", console.error);
+    this.bot.addListener("join" +this.room, (nick) => {
+        if (nick == _this.name) {
+            if (USE_NS) {
+                _this.bot.say("NickServ", "recover " +_this.name +" " +_this.password);
+                _this.bot.say("NickServ", "identify " +_this.password);
+            }
+            _this.init = true;
+            _this.reloadDb();
+        } else {
+            _this.users[nick.toLowerCase()] = _this.users[nick.toLowerCase()] || new User(nick);
+            _this.activeUsers[nick.toLowerCase()] = true;
+        }
+    });
+    this.bot.addListener("names"+this.room, (nicks) => {
+        var oldusers = _this.users;
+        _this.activeUsers = {};
+        for (var i in nicks) {
+            var u = _this.users[i.toLowerCase()] = (_this.users[i.toLowerCase()] || new User(i));
+            u.setModeChar(nicks[i]);
+            _this.activeUsers[i.toLowerCase()] = true;
+        }
+    });
+    this.bot.addListener("part"+this.room, (user) => delete _this.activeUsers[user.toLowerCase()]);
+    this.bot.addListener("kick"+this.room, (user) => delete _this.activeUsers[user.toLowerCase()]);
+    this.bot.addListener("+mode", (channel, by, mode, user) => {
+        if (user === _this.name) {
+            usersToVoice = [];
+            for (var i in _this.users) {
+                if (_this.users[i].score && _this.activeUsers[i])
+                    usersToVoice.push(i);
+            }
+            _this.voice(usersToVoice);
+        } else {
+            user && _this.users[user.toLowerCase()] && _this.users[user.toLowerCase()].setMode(mode);
+        }
+    });
+    this.bot.addListener("nick", (oldNick, newNick) => {
+        _this.users[newNick.toLowerCase()] = _this.users[newNick.toLowerCase()] || new User(newNick);
+        _this.users[newNick.toLowerCase()].isAdmin = _this.users[oldNick.toLowerCase()] && _this.users[oldNick.toLowerCase()].isAdmin;
+        _this.activeUsers[newNick.toLowerCase()] = true;
+        delete _this.activeUsers[oldNick.toLowerCase()];
+    });
+    this.bot.addListener("-mode", (channel, by, mode, user) => {
+        user && _this.users[user.toLowerCase()] && _this.users[user.toLowerCase()].unsetMode(mode);
+    });
+    this.bot.addListener("message"+this.room, (user, text) => {
+        _this.users[user.toLowerCase()] && _this.onMessage(user, _this.users[user.toLowerCase()], text.trimEnd());
+    });
+}
+
+KnackiBot.prototype.mySQLExportWrapper = function() {
+    if (!USE_MYSQL)
+        return;
+    var msRemaining = 0,
+        lastMySQLExport = Cache.getLastMysqlSave();
+    if (lastMySQLExport)
+        msRemaining = GAME_DURATION -(Date.now() -lastMySQLExport);
+    if (msRemaining <= 0)
+        this.mySQLExport();
+    else
+        this.exportScoresTimer = setTimeout(this.mySQLExportWrapper.bind(this), Math.min(2147483000, msRemaining));
+}
+
+KnackiBot.prototype.mySQLExport = function() {
+    this.exportScores().then(() => {
+        Cache.setExportTs();
+        this.resetScores();
+        console.log("Successfully exported scores to MySQL");
+        this.mySQLExportWrapper();
+    }).catch((errString) => {
+        console.error("mySQL Export error saving to database: ", errString);
+    });
+}
+
+KnackiBot.prototype.exportScores = function() {
+    return new Promise((ok, ko) => {
+        var ts = Cache.getLastMysqlSave() || START_TIME;
+        ts = Math.floor(ts / 1000) *1000;
+        ts = MySQL.escape(new Date(ts));
+        ts = ts.substr(1, ts.length -2);
+        var sep = ts.lastIndexOf('.');
+        if (sep > 12) ts = ts.substr(0, sep);
+
+        var toSave = [];
+        for (var i in this.users)
+            if (this.users[i].score) {
+                toSave.push(this.users[i].name);
+                toSave.push(this.users[i].score);
+            }
+        if (toSave.length == 0)
+            return ok();
+        MySQL.execute("INSERT INTO " +MySQL_PERIOD_TABLE +" (start, host) VALUES(?, ?)", [ts, HOSTNAME], (err, result) => {
+            if (err || !result.insertId)
+                return ko(err || "Cannot get last inserted id");
+            MySQL.execute("INSERT INTO " +MySQL_SCORES_TABLE +"(period_id, pseudo, score) VALUES " +(",("+result.insertId+",?,?)").repeat(toSave.length /2).substr(1), toSave, (err) => {
+                if (err)
+                    ko(err);
+                else
+                    ok();
+            });
+        });
+    });
+}
+
+KnackiBot.prototype.resetScores = function() {
+    if (this.sumScores(false) > 0) {
+        this.sendMsg("Fin de la manche ! Voici les scores finaux:");
+        this.sendScore(false);
+    }
+    for (var i in this.users)
+        this.users[i].score = 0;
+    Cache.SetScores({});
+}
+
+KnackiBot.prototype.voice = function(username) {
+    var usernames = Array.isArray(username) ? username : [ username ];
+    if (usernames.length > 10)
+    {
+        this.voice(usernames.slice(0, 10));
+        this.voice(usernames.slice(10));
+        return;
+    }
+    usernames.splice(0, 0, "MODE", this.room, "+" +"v".repeat(usernames.length));
+    this.bot.send.apply(this.bot, usernames);
+}
+
+KnackiBot.prototype.sendNotice = function(username, msg) {
+    this.bot.notice(username, msg);
+}
+
+KnackiBot.prototype.sendMsg = function(msg) {
+    this.bot.say(this.room, msg);
+}
+
+KnackiBot.prototype.stop = function() {
+    this.timer && clearTimeout(this.timer);
+    this.timer = null;
+    this.currentQuestion = null;
+    this.currentHint = 0;
+}
+
+KnackiBot.prototype.onTick = function() {
+    if (!this.currentQuestion)
+        return;
+    if (this.currentHint < 3)
+        this.sendNextHint(false);
+    else {
+        var response = Array.isArray(this.currentQuestion.response) ?
+            this.currentQuestion.response.map(i => "\""+i+"\"").join(" ou ") :
+            this.currentQuestion.response;
+        this.sendMsg("Perdu, la réponse était: " +response);
+        this.nextQuestionWrapper();
+    }
+}
+
+KnackiBot.prototype.resetTimer = function() {
+    this.timer && clearTimeout(this.timer);
+    this.timer = setInterval(this.onTick.bind(this), AUTO_HINT_DELAY);
+}
+
+KnackiBot.prototype.sendNextHint = function(resetTimer) {
+    this.sendMsg(this.currentQuestion.getHint(this.currentHint));
+    ++this.currentHint;
+    this.lastHint = Date.now();
+    resetTimer !== false && this.resetTimer();
+}
+
+KnackiBot.prototype.nextQuestionWrapper = function() {
+    this.currentQuestion.end();
+    this.currentQuestion = null;
+    setTimeout(this.nextQuestion.bind(this), NEXT_QUESTION_DELAY);
+}
+
+KnackiBot.prototype.nextQuestion = function() {
+    this.currentQuestion = this.questions[Math.floor(Math.random() * this.questions.length)];
+    console.log(this.currentQuestion);
+    this.currentHint = 0;
+    this.questionDate = Date.now();
+    this.sendMsg("#" +this.currentQuestion.id +" " +this.currentQuestion.question);
+    this.sendNextHint();
+}
+
+KnackiBot.prototype.start = function() {
+    if (this.reloading) {
+        this.sendMsg("Error: database still reloading");
+        return;
+    }
+    if (!this.currentQuestion) {
+        this.nextQuestion();
+    }
+}
+
+KnackiBot.prototype.reloadDb = function() {
+    var _this = this;
+    this.stop();
+    this.reloading = true;
+    initQuestionList(QUESTIONS_PATH).then(questions => {
+        _this.reloading = false;
+        _this.questions = questions;
+        _this.sendMsg(questions.length +" questions loaded from database");
+        _this.start();
+    }).catch(err => {
+        console.error(err);
+        _this.sendMsg(err);
+    });
+}
+
+KnackiBot.prototype.delScore = function(user) {
+    var data = this.users[user.toLowerCase()];
+    if (!data) {
+        this.sendMsg("User not found...");
+        return;
+    }
+    if (!data.score) {
+        this.sendMsg("Score for user already null");
+        return;
+    }
+    data.score = 0;
+    this.sendMsg("Removed score");
+    Cache.SetScores(this.users);
+}
+
+KnackiBot.prototype.sumScores = function(onlyPresent) {
+    var score = 0;
+    for (var i in this.users)
+        score += (this.users[i].score && (this.activeUsers[i] || !onlyPresent)) ? this.users[i].score : 0;
+    return score;
+}
+
+KnackiBot.prototype.sendScore = function(onlyPresent) {
+    var scores = [];
+    for (var i in this.users)
+        this.users[i].score && (this.activeUsers[i] || !onlyPresent) && scores.push({name: this.users[i].name, score: this.users[i].score});
+    if (scores.length == 0) {
+        this.sendMsg("Pas de points pour le moment");
+        return;
+    }
+    scores = scores.sort((a, b) => b.score - a.score).slice(0, 10);
+    var index = 0;
+    var scoreLines = arrayPad(scores.map(i => [ ((++index) +"."), i.name, (i.score +" points") ]));
+    if (scoreLines[0].length < 30) {
+        // merge score lines 2 by 2
+        var tmp = [];
+        for (var i =0, len = scoreLines.length; i < len; i += 2)
+            tmp.push((scoreLines[i] || "") +"   -   " +(scoreLines[i +1] || ""));
+        scoreLines = tmp;
+    }
+    scoreLines.forEach(i => this.sendMsg(i));
+}
+
+KnackiBot.prototype.computeScore = function(username) {
+    var rep = Array.isArray(this.currentQuestion.response) ? this.currentQuestion.response.map(i => '"'+i+'"').join(" ou ") : this.currentQuestion.response,
+        responseMsg = "Réponse `" +rep +"` trouvée en " +Math.floor((Date.now() -this.questionDate) / 1000) +" secondes ";
+    if (this.currentHint <= 1)
+        responseMsg += "sans indice";
+    else if (this.currentHint === 2)
+        responseMsg += "avec 1 seul indice";
+    else
+        responseMsg += "avec " +this.currentHint +" indices";
+    var score = 4 - this.currentHint;
+    this.sendMsg(responseMsg);
+    this.sendMsg(score +" points pour " +username);
+    return score;
+}
+
+KnackiBot.prototype.findQuestionById = function(qId) {
+    for (var i =0, len = this.questions.length; i < len; ++i) {
+        if (this.questions[i].id === qId)
+            return this.questions[i];
+        if (this.questions[i].id > qId)
+            break;
+    }
+}
+
+KnackiBot.prototype.onMessage = function(username, user, msg) {
+    if (msg.startsWith("!reload")) {
+        if (user.admin)
+            this.reloadDb();
+        else
+            this.sendMsg("Must be channel operator");
+    }
+    else if (msg.startsWith("!aide")) {
+        this.sendMsg("Usage: !aide | !indice | !reload | !next | !rename | !score [del pseudo] || !top");
+    }
+    else if (msg === "!indice" || msg === "!conseil") {
+        if (this.currentQuestion) {
+            if (this.currentHint < 3) {
+                if (Date.now() -this.lastHint > MIN_HINT_DELAY)
+                    this.sendNextHint();
+            }
+            else
+                this.sendMsg("Pas plus d'indice...");
+        }
+    }
+    else if (msg === "!next") {
+        if (user.admin) {
+            if (!this.currentQuestion)
+                return;
+            var response = Array.isArray(this.currentQuestion.response) ?
+                this.currentQuestion.response.map(i => "\""+i+"\"").join(" ou ") :
+                this.currentQuestion.response;
+            this.sendMsg("La réponse était: " +response);
+            this.nextQuestionWrapper();
+        } else {
+            this.sendMsg("Must be channel operator");
+        }
+    }
+    else if (msg.startsWith("!report list")) {
+        if (user.admin) {
+            var questions = Cache.getReportedQuestions();
+            for (var i in questions)
+                for (var j in questions[i])
+                    questions[i][j] = (new Date(questions[i][j])).toLocaleString();
+            this.sendNotice(username, JSON.stringify(questions));
+        }
+        else
+            this.sendMsg("Must be channel operator");
+    }
+    else if (msg.startsWith("!report del ")) {
+        if (user.admin) {
+            var questionId = msg.substr("!report del ".length).trim();
+            if (questionId.startsWith('#'))
+                questionId = questionId.substr(1);
+            questionId = Number(questionId);
+            if (isNaN(questionId)) {
+                this.sendMsg("Erreur: Usage: !report del #1234");
+                return;
+            }
+            Cache.unreportQuestion(questionId);
+            this.sendMsg("Question #" +questionId +" marquée comme restaurée");
+        }
+        else
+            this.sendMsg("Must be channel operator");
+    }
+    else if (msg.startsWith("!report ")) {
+        var questionId = msg.substr("!report ".length).trim();
+        if (questionId.startsWith('#'))
+            questionId = questionId.substr(1);
+        questionId = Number(questionId);
+        if (isNaN(questionId)) {
+            this.sendMsg("Erreur: Usage: !report #1234");
+            return;
+        }
+        if (!this.findQuestionById(questionId))
+            this.sendMsg("Erreur: question non trouvée");
+        else {
+            Cache.reportQuestion(questionId, username);
+            this.sendMsg("Question #" +questionId +" marquée comme déffectueuse par " +username);
+        }
+    }
+    else if (msg.startsWith("!score del ")) {
+        if (user.admin)
+            this.delScore(msg.substr("!score del ".length).trim());
+        else
+            this.sendMsg("Must be channel operator");
+    }
+    else if (msg.startsWith("!rename")) {
+        if (!user.admin) {
+            this.sendMsg("Must be channel operator");
+            return;
+        }
+        var args = (msg.split(/\s+/)).splice(1);
+        if (args.length < 2) {
+            this.sendNotice(username, "Usage: !rename nouveau_pseudo ancien_pseudo [ancien_pseudo...]");
+            return;
+        }
+        var sum = 0,
+            users = [],
+            target = args[0];
+
+        args = args.map(username => username.toLowerCase());
+        var userToMod = null;
+        for (var i in this.users) {
+            var userIndex = args.indexOf(i.toLowerCase());
+            if (userIndex > 0) {
+                sum += this.users[i].score;
+                this.users[i].score = 0;
+                if (!this.activeUsers[i])
+                    delete this.users[i];
+            }
+            else if (userIndex == 0)
+                userToMod = this.users[i];
+        }
+        if (!userToMod) {
+            userToMod = this.users[target] = new User(target);
+            this.sendMsg("Created user " +target);
+        }
+        userToMod.score += sum;
+        Cache.SetScores(this.users);
+    }
+    else if (msg.startsWith("!score")) {
+        this.sendScore(true);
+    }
+    else if (msg.startsWith("!top")) {
+        this.sendScore(false);
+    }
+    else if (this.currentQuestion) {
+        var dieOnFailure = this.currentQuestion.isBoolean() && this.currentQuestion.isBoolean(Question.normalize(msg));
+        if (this.currentQuestion.check(msg)) {
+            user.score += this.computeScore(username);
+            Cache.SetScores(this.users);
+            this.voice(username);
+            this.nextQuestionWrapper();
+        }
+        else if (dieOnFailure) {
+            var rep = Array.isArray(this.currentQuestion.response) ? this.currentQuestion.response.map(i => '"'+i+'"').join(" ou ") : this.currentQuestion.response;
+            this.sendMsg("Perdu, la réponse était: " +rep);
+            this.nextQuestionWrapper();
+        }
+    }
+};
+
+new KnackiBot(Cache.GetData());
+

+ 16 - 0
package.json

@@ -0,0 +1,16 @@
+{
+  "name": "ircquizz",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "data-store": "^3.1.0",
+    "irc": "^0.5.2",
+    "mysql2": "^1.6.5"
+  }
+}

+ 36 - 0
strpad.js

@@ -0,0 +1,36 @@
+
+function strpad(data, opts) {
+    var max = 0;
+    opts = opts || {};
+    opts.marginLeft = opts.marginLeft || 0;
+    opts.marginRight = opts.marginRight || 0;
+    opts.padFnc = opts.padFnc || String.prototype.padEnd;
+
+    data.forEach(i => {
+        max = Math.max(i.length, max);
+    });
+    return data.map(i => {
+        i = opts.padFnc.call(i, max);
+        if (opts.marginLeft)
+            i = (" ".repeat(opts.marginLeft)) +i;
+        if (opts.marginRight)
+            i = i +(" ".repeat(opts.marginRight));
+        return i;
+    });
+}
+
+function arrayPad(arr, opts) {
+    var padded = [];
+    for (var i =0, len = arr[0].length; i < len; ++i)
+        padded.push(strpad(arr.map(arrItem => arrItem[i]), opts ? opts[i] : {}));
+    var result = [];
+    for (var i =0, len = padded[0].length; i < len; ++i)
+        result.push(padded.map(arrItem => arrItem[i]).join(" "));
+    return result;
+}
+
+module.exports = {
+    arrayPad: arrayPad,
+    strpad: strpad
+};
+