Merge branch '3.0' of https://github.com/calzoneman/sync into 3.0
[KisSync.git] / src / user.js
blobe4c1d8ebd2e75c3a6968a65fabcaf84cf40e1d11
1 var Server = require("./server");
2 var util = require("./utilities");
3 var db = require("./database");
4 var Config = require("./config");
5 var ACP = require("./acp");
6 var Account = require("./account");
7 var Flags = require("./flags");
8 import { EventEmitter } from 'events';
9 import Logger from './logger';
10 import net from 'net';
12 const LOGGER = require('@calzoneman/jsli')('user');
14 function User(socket, ip, loginInfo) {
15     this.flags = 0;
16     this.socket = socket;
17     // Expanding IPv6 addresses shouldn't really be necessary
18     // At some point, the IPv6 related stuff should be revisited
19     this.realip = net.isIPv6(ip) ? util.expandIPv6(ip) : ip;
20     this.displayip = util.cloakIP(this.realip);
21     this.channel = null;
22     this.queueLimiter = util.newRateLimiter();
23     this.chatLimiter = util.newRateLimiter();
24     this.reqPlaylistLimiter = util.newRateLimiter();
25     this.awaytimer = false;
27     if (loginInfo) {
28         this.account = new Account.Account(this.realip, loginInfo, socket.context.aliases);
29         this.registrationTime = new Date(this.account.user.time);
30         this.setFlag(Flags.U_REGISTERED | Flags.U_LOGGED_IN | Flags.U_READY);
31         socket.emit("login", {
32             success: true,
33             name: this.getName(),
34             guest: false
35         });
36         socket.emit("rank", this.account.effectiveRank);
37         if (this.account.globalRank >= 255) {
38             this.initAdminCallbacks();
39         }
40         this.emit("login", this.account);
41         LOGGER.info(ip + " logged in as " + this.getName());
42     } else {
43         this.account = new Account.Account(this.realip, null, socket.context.aliases);
44         socket.emit("rank", -1);
45         this.setFlag(Flags.U_READY);
46         this.once("login", account => {
47             if (account.globalRank >= 255) {
48                 this.initAdminCallbacks();
49             }
50         });
51     }
53     socket.once("joinChannel", data => this.handleJoinChannel(data));
54     socket.once("initACP", () => this.handleInitACP());
55     socket.on("login", data => this.handleLogin(data));
58 User.prototype = Object.create(EventEmitter.prototype);
60 User.prototype.handleJoinChannel = function handleJoinChannel(data) {
61     if (typeof data !== "object" || typeof data.name !== "string") {
62         return;
63     }
65     if (this.inChannel()) {
66         return;
67     }
69     if (!util.isValidChannelName(data.name)) {
70         this.socket.emit("errorMsg", {
71             msg: "Invalid channel name.  Channel names may consist of 1-30 " +
72                  "characters in the set a-z, A-Z, 0-9, -, and _"
73         });
74         this.kick("Invalid channel name");
75         return;
76     }
78     data.name = data.name.toLowerCase();
79     if (data.name in Config.get("channel-blacklist")) {
80         this.kick("This channel is blacklisted.");
81         return;
82     }
84     this.waitFlag(Flags.U_READY, () => {
85         var chan;
86         try {
87             chan = Server.getServer().getChannel(data.name);
88         } catch (error) {
89             if (error.code === 'EWRONGPART') {
90                 this.socket.emit("errorMsg", {
91                     msg: "Channel '" + data.name + "' is hosted on another server.  " +
92                          "Try refreshing the page to update the connection URL."
93                 });
94             } else {
95                 LOGGER.error("Unexpected error from getChannel(): %s", error.stack);
96                 this.socket.emit("errorMsg", {
97                     msg: "Unable to join channel due to an internal error"
98                 });
99             }
100             return;
101         }
103         if (!chan.is(Flags.C_READY)) {
104             chan.once("loadFail", reason => {
105                 this.socket.emit("errorMsg", {
106                     msg: reason,
107                     alert: true
108                 });
109                 this.kick(`Channel could not be loaded: ${reason}`);
110             });
111         }
112         chan.joinUser(this, data);
113     });
116 User.prototype.handleInitACP = function handleInitACP() {
117     this.waitFlag(Flags.U_LOGGED_IN, () => {
118         if (this.account.globalRank >= 255) {
119             ACP.init(this);
120         } else {
121             this.kick("Attempted initACP from non privileged user.  This incident " +
122                       "will be reported.");
123             Logger.eventlog.log("[acp] Attempted initACP from socket client " +
124                                 this.getName() + "@" + this.realip);
125         }
126     });
129 User.prototype.handleLogin = function handleLogin(data) {
130     if (typeof data !== "object") {
131         this.socket.emit("errorMsg", {
132             msg: "Invalid login frame"
133         });
134         return;
135     }
137     var name = data.name;
138     if (typeof name !== "string") {
139         return;
140     }
142     var pw = data.pw || "";
143     if (typeof pw !== "string") {
144         pw = "";
145     }
147     if (this.is(Flags.U_LOGGING_IN) || this.is(Flags.U_LOGGED_IN)) {
148         return;
149     }
151     if (!pw) {
152         this.guestLogin(name);
153     } else {
154         this.login(name, pw);
155     }
158 User.prototype.die = function () {
159     for (const key in this.socket._events) {
160         delete this.socket._events[key];
161     }
163     delete this.socket.typecheckedOn;
164     delete this.socket.typecheckedOnce;
166     for (const key in this.__evHandlers) {
167         delete this.__evHandlers[key];
168     }
170     if (this.awaytimer) {
171         clearTimeout(this.awaytimer);
172     }
174     this.dead = true;
177 User.prototype.is = function (flag) {
178     return Boolean(this.flags & flag);
181 User.prototype.setFlag = function (flag) {
182     this.flags |= flag;
183     this.emit("setFlag", flag);
186 User.prototype.clearFlag = function (flag) {
187     this.flags &= ~flag;
188     this.emit("clearFlag", flag);
191 User.prototype.waitFlag = function (flag, cb) {
192     var self = this;
193     if (self.is(flag)) {
194         cb();
195     } else {
196         var wait = function (f) {
197             if (f === flag) {
198                 self.removeListener("setFlag", wait);
199                 cb();
200             }
201         };
202         self.on("setFlag", wait);
203     }
206 User.prototype.getName = function () {
207     return this.account.name;
210 User.prototype.getLowerName = function () {
211     return this.account.lowername;
214 User.prototype.inChannel = function () {
215     return this.channel != null && !this.channel.dead;
218 User.prototype.inRegisteredChannel = function () {
219     return this.inChannel() && this.channel.is(Flags.C_REGISTERED);
222 /* Called when a user's AFK status changes */
223 User.prototype.setAFK = function (afk) {
224     if (!this.inChannel()) {
225         return;
226     }
228     /* No change in AFK status, don't need to change anything */
229     if (this.is(Flags.U_AFK) === afk) {
230         this.autoAFK();
231         return;
232     }
234     if (afk) {
235         this.setFlag(Flags.U_AFK);
236         if (this.channel.modules.voteskip) {
237             this.channel.modules.voteskip.unvote(this.realip);
238             this.socket.emit("clearVoteskipVote");
239         }
240     } else {
241         this.clearFlag(Flags.U_AFK);
242         this.autoAFK();
243     }
245     if (!this.inChannel()) {
246         /*
247          * In unusual circumstances, the above emit("clearVoteskipVote")
248          * can cause the "disconnect" event to be fired synchronously,
249          * which results in this user no longer being in the channel.
250          */
251         return;
252     }
254     /* Number of AFK users changed, voteskip state changes */
255     if (this.channel.modules.voteskip) {
256         this.channel.modules.voteskip.update();
257     }
259     this.emit('afk', afk);
262 /* Automatically tag a user as AFK after a period of inactivity */
263 User.prototype.autoAFK = function () {
264     var self = this;
265     if (self.awaytimer) {
266         clearTimeout(self.awaytimer);
267     }
269     if (!self.inChannel() || !self.channel.modules.options) {
270         return;
271     }
273     /* Don't set a timer if the duration is invalid */
274     var timeout = parseFloat(self.channel.modules.options.get("afk_timeout"));
275     if (isNaN(timeout) || timeout <= 0) {
276         return;
277     }
279     self.awaytimer = setTimeout(function () {
280         self.setAFK(true);
281     }, timeout * 1000);
284 User.prototype.kick = function (reason) {
285     LOGGER.info(
286         '%s (%s) was kicked: "%s"',
287         this.realip,
288         this.getName(),
289         reason
290     );
291     this.socket.emit("kick", { reason: reason });
292     this.socket.disconnect();
295 User.prototype.isAnonymous = function(){
296     var self = this;
297     return !self.is(Flags.U_LOGGED_IN);
300 User.prototype.initAdminCallbacks = function () {
301     var self = this;
302     self.socket.on("borrow-rank", function (rank) {
303         if (self.inChannel()) {
304             if (typeof rank !== "number") {
305                 return;
306             }
308             if (rank > self.account.globalRank) {
309                 return;
310             }
312             if (rank === 255 && self.account.globalRank > 255) {
313                 rank = self.account.globalRank;
314             }
316             self.account.channelRank = rank;
317             self.account.effectiveRank = rank;
318             self.socket.emit("rank", rank);
319             self.channel.broadcastAll("setUserRank", {
320                 name: self.getName(),
321                 rank: rank
322             });
323         }
324     });
327 User.prototype.login = function (name, pw) {
328     var self = this;
329     self.setFlag(Flags.U_LOGGING_IN);
331     db.users.verifyLogin(name, pw, function (err, user) {
332         if (err) {
333             if (err === "Invalid username/password combination") {
334                 Logger.eventlog.log("[loginfail] Login failed (bad password): " + name
335                                   + "@" + self.realip);
336             }
338             self.socket.emit("login", {
339                 success: false,
340                 error: err
341             });
342             self.clearFlag(Flags.U_LOGGING_IN);
343             return;
344         }
346         const oldRank = self.account.effectiveRank;
347         self.account.user = user;
348         self.account.update();
349         self.socket.emit("rank", self.account.effectiveRank);
350         self.emit("effectiveRankChange", self.account.effectiveRank, oldRank);
351         self.registrationTime = new Date(user.time);
352         self.setFlag(Flags.U_REGISTERED);
353         self.socket.emit("login", {
354             success: true,
355             name: user.name
356         });
357         db.recordVisit(self.realip, self.getName());
358         LOGGER.info(self.realip + " logged in as " + user.name);
359         self.setFlag(Flags.U_LOGGED_IN);
360         self.clearFlag(Flags.U_LOGGING_IN);
361         self.emit("login", self.account);
362     });
365 var lastguestlogin = {};
366 User.prototype.guestLogin = function (name) {
367     var self = this;
369     if (!self.channel.modules.options.get("allow_anon_chat") && self.realip in lastguestlogin) {
370         var diff = (Date.now() - lastguestlogin[self.realip]) / 1000;
371         if (diff < Config.get("guest-login-delay")) {
372             self.socket.emit("login", {
373                 success: false,
374                 error: "Guest logins are restricted to one per IP address per " +
375                        Config.get("guest-login-delay") + " seconds."
376             });
377             return;
378         }
379     }
381     if (!util.isValidUserName(name)) {
382         self.socket.emit("login", {
383             success: false,
384             error: "Invalid username.  Usernames must be 1-20 characters long and " +
385                    "consist only of characters a-z, A-Z, 0-9, -, or _."
386         });
387         return;
388     }
390     if (!self.channel.modules.options.get("allow_anon_chat") && name.match(Config.get("reserved-names.usernames"))) {
391         LOGGER.warn(
392             'Rejecting attempt by %s to use reserved username "%s"',
393             self.realip,
394             name
395         );
396         self.socket.emit("login", {
397             success: false,
398             error: "That username is reserved."
399         });
400         return;
401     }
403     // Prevent duplicate logins
404     self.setFlag(Flags.U_LOGGING_IN);
405     db.users.isUsernameTaken(name, function (err, taken) {
406         self.clearFlag(Flags.U_LOGGING_IN);
407         if (err) {
408             self.socket.emit("login", {
409                 success: false,
410                 error: err
411             });
412             return;
413         }
415         if (taken) {
416             self.socket.emit("login", {
417                 success: false,
418                 error: "That username is registered."
419             });
420             return;
421         }
423         if (!self.channel.modules.options.get("allow_anon_chat") && self.inChannel()) {
424             var nameLower = name.toLowerCase();
425             for (var i = 0; i < self.channel.users.length; i++) {
426                 if (self.channel.users[i].getLowerName() === nameLower) {
427                     self.socket.emit("login", {
428                         success: false,
429                         error: "That name is already in use on this channel."
430                     });
431                     return;
432                 }
433             }
434         }
435         // Login succeeded
436         lastguestlogin[self.realip] = Date.now();
438         const oldRank = self.account.effectiveRank;
439         self.account.guestName = name;
440         self.account.update();
441         self.socket.emit("rank", self.account.effectiveRank);
442         self.emit("effectiveRankChange", self.account.effectiveRank, oldRank);
443         self.socket.emit("login", {
444             success: true,
445             name: name,
446             guest: true
447         });
448         db.recordVisit(self.realip, self.getName());
449         LOGGER.info(self.realip + " signed in as " + name);
450         self.setFlag(Flags.U_LOGGED_IN);
451         self.emit("login", self.account);
452     });
455 /* Clean out old login throttlers to save memory */
456 setInterval(function () {
457     var delay = Config.get("guest-login-delay");
458     for (var ip in lastguestlogin) {
459         var diff = (Date.now() - lastguestlogin[ip]) / 1000;
460         if (diff > delay) {
461             delete lastguestlogin[ip];
462         }
463     }
465     if (Config.get("aggressive-gc") && global && global.gc) {
466         global.gc();
467     }
468 }, 5 * 60 * 1000);
470 User.prototype.getFirstSeenTime = function getFirstSeenTime() {
471     if (this.registrationTime && this.socket.context.ipSessionFirstSeen) {
472         return Math.min(
473             this.registrationTime.getTime(),
474             this.socket.context.ipSessionFirstSeen.getTime()
475         );
476     } else if (this.registrationTime) {
477         return this.registrationTime.getTime();
478     } else if (this.socket.context.ipSessionFirstSeen) {
479         return this.socket.context.ipSessionFirstSeen.getTime();
480     } else {
481         LOGGER.error(`User "${this.getName()}" (IP: ${this.realip}) has neither ` +
482                 "an IP session first seen time nor a registered account.");
483         return Date.now();
484     }
487 User.prototype.setChannelRank = function setRank(rank) {
488     const oldRank = this.account.effectiveRank;
489     const changed = oldRank !== rank;
490     this.account.channelRank = rank;
491     this.account.update();
492     this.socket.emit("rank", this.account.effectiveRank);
493     if (changed) {
494         this.emit("effectiveRankChange", this.account.effectiveRank, oldRank);
495     }
498 module.exports = User;