hardlounge/src/client.js

607 lines
13 KiB
JavaScript
Raw Normal View History

"use strict";
2014-09-13 21:29:45 +00:00
var _ = require("lodash");
var colors = require("colors/safe");
var pkg = require("../package.json");
2014-09-16 20:06:13 +00:00
var Chan = require("./models/chan");
2014-09-15 21:13:03 +00:00
var crypto = require("crypto");
2016-04-16 10:35:04 +00:00
var userLog = require("./userLog");
2014-09-13 21:29:45 +00:00
var Msg = require("./models/msg");
var Network = require("./models/network");
var ircFramework = require("irc-framework");
var Helper = require("./helper");
const UAParser = require("ua-parser-js");
2014-09-13 21:29:45 +00:00
module.exports = Client;
var id = 0;
var events = [
"connection",
"unhandled",
2017-04-22 12:51:21 +00:00
"banlist",
2014-09-13 21:29:45 +00:00
"ctcp",
"error",
2016-02-12 11:24:13 +00:00
"invite",
2014-09-13 21:29:45 +00:00
"join",
"kick",
"mode",
"motd",
"message",
"names",
"nick",
"part",
"quit",
"topic",
"welcome",
"list",
2014-09-13 21:29:45 +00:00
"whois"
];
var inputs = [
2017-04-24 10:40:53 +00:00
"ban",
"ctcp",
2016-03-06 09:24:56 +00:00
"msg",
"part",
2014-09-13 21:29:45 +00:00
"action",
2016-11-19 08:24:39 +00:00
"away",
2014-09-13 21:29:45 +00:00
"connect",
2016-04-14 08:56:02 +00:00
"disconnect",
2014-09-13 21:29:45 +00:00
"invite",
"kick",
"mode",
"nick",
2014-09-13 21:29:45 +00:00
"notice",
"query",
2014-09-13 21:29:45 +00:00
"quit",
"raw",
"topic",
2016-05-29 02:07:34 +00:00
"list",
"whois"
].reduce(function(plugins, name) {
var path = "./plugins/inputs/" + name;
var plugin = require(path);
plugin.commands.forEach((command) => plugins[command] = plugin);
return plugins;
}, {});
2014-09-13 21:29:45 +00:00
function Client(manager, name, config) {
if (typeof config !== "object") {
config = {};
}
2014-09-13 21:29:45 +00:00
_.merge(this, {
awayMessage: config.awayMessage || "",
lastActiveChannel: -1,
attachedClients: {},
2014-09-13 21:29:45 +00:00
config: config,
id: id++,
2014-09-16 19:47:01 +00:00
name: name,
2014-09-13 21:29:45 +00:00
networks: [],
sockets: manager.sockets,
manager: manager
2014-09-13 21:29:45 +00:00
});
2016-05-31 21:28:31 +00:00
2014-09-15 21:13:03 +00:00
var client = this;
2016-05-31 21:28:31 +00:00
var delay = 0;
(client.config.networks || []).forEach((n) => {
setTimeout(function() {
client.connect(n);
}, delay);
delay += 1000;
});
2016-04-16 11:32:38 +00:00
if (typeof client.config.sessions !== "object") {
client.config.sessions = {};
}
2017-07-10 19:47:03 +00:00
_.forOwn(client.config.sessions, (session) => {
if (session.pushSubscription) {
this.registerPushSubscription(session, session.pushSubscription, true);
}
});
if (client.name) {
log.info(`User ${colors.bold(client.name)} loaded`);
}
2014-09-13 21:29:45 +00:00
}
2017-07-10 19:47:03 +00:00
Client.prototype.isRegistered = function() {
return this.name.length > 0;
};
2014-09-13 21:29:45 +00:00
Client.prototype.emit = function(event, data) {
if (this.sockets !== null) {
this.sockets.in(this.id).emit(event, data);
}
if (this.config.log === true) {
2015-09-30 22:39:57 +00:00
if (event === "msg") {
2014-09-16 19:47:01 +00:00
var target = this.find(data.chan);
if (target) {
2014-09-16 20:06:13 +00:00
var chan = target.chan.name;
2015-09-30 22:39:57 +00:00
if (target.chan.type === Chan.Type.LOBBY) {
2014-09-16 20:06:13 +00:00
chan = target.network.host;
}
2016-04-16 10:35:04 +00:00
userLog.write(
2014-09-16 20:06:13 +00:00
this.name,
target.network.host,
chan,
data.msg
);
2014-09-16 19:47:01 +00:00
}
}
}
2014-09-13 17:18:42 +00:00
};
2014-09-13 21:29:45 +00:00
2016-10-09 08:54:44 +00:00
Client.prototype.find = function(channelId) {
2014-09-13 21:29:45 +00:00
var network = null;
var chan = null;
for (var i in this.networks) {
var n = this.networks[i];
2016-10-09 08:54:44 +00:00
chan = _.find(n.channels, {id: channelId});
2014-09-13 21:29:45 +00:00
if (chan) {
network = n;
break;
}
}
if (network && chan) {
return {
network: network,
chan: chan
};
}
2016-10-09 08:54:44 +00:00
return false;
2014-09-13 21:29:45 +00:00
};
Client.prototype.connect = function(args) {
var config = Helper.config;
2014-09-13 21:29:45 +00:00
var client = this;
2016-02-21 12:02:35 +00:00
var nick = args.nick || "lounge-user";
2016-04-03 05:12:49 +00:00
var webirc = null;
var channels = [];
2016-06-19 17:12:42 +00:00
if (args.channels) {
var badName = false;
args.channels.forEach((chan) => {
2016-06-19 17:12:42 +00:00
if (!chan.name) {
badName = true;
return;
}
channels.push(new Chan({
2017-04-01 08:33:17 +00:00
name: chan.name,
key: chan.key || "",
}));
});
2016-06-19 17:12:42 +00:00
if (badName && client.name) {
log.warn("User '" + client.name + "' on network '" + args.name + "' has an invalid channel which has been ignored");
}
// `join` is kept for backwards compatibility when updating from versions <2.0
// also used by the "connect" window
} else if (args.join) {
channels = args.join
2016-10-09 08:54:44 +00:00
.replace(/,/g, " ")
2016-06-19 17:12:42 +00:00
.split(/\s+/g)
.map(function(chan) {
return new Chan({
name: chan
});
});
}
args.ip = args.ip || (client.config && client.config.ip) || client.ip;
args.hostname = args.hostname || (client.config && client.config.hostname) || client.hostname;
var network = new Network({
name: args.name || (config.displayNetwork ? "" : config.defaults.name) || "",
host: args.host || "",
port: parseInt(args.port, 10) || (args.tls ? 6697 : 6667),
tls: !!args.tls,
password: args.password,
username: args.username || nick.replace(/[^a-zA-Z0-9]/g, ""),
realname: args.realname || "The Lounge User",
2016-04-03 05:12:49 +00:00
commands: args.commands,
ip: args.ip,
hostname: args.hostname,
channels: channels,
});
2016-05-12 11:15:38 +00:00
network.setNick(nick);
client.networks.push(network);
client.emit("network", {
networks: [network]
});
2016-02-21 12:02:35 +00:00
if (config.lockNetwork) {
// This check is needed to prevent invalid user configurations
if (args.host && args.host.length > 0 && args.host !== config.defaults.host) {
network.channels[0].pushMessage(client, new Msg({
type: Msg.Type.ERROR,
text: "Hostname you specified is not allowed."
}), true);
2016-02-21 12:02:35 +00:00
return;
}
network.host = config.defaults.host;
network.port = config.defaults.port;
network.tls = config.defaults.tls;
2016-02-21 12:02:35 +00:00
}
if (network.host.length === 0) {
network.channels[0].pushMessage(client, new Msg({
type: Msg.Type.ERROR,
text: "You must specify a hostname to connect."
}), true);
2016-02-21 12:02:35 +00:00
return;
}
if (config.webirc && network.host in config.webirc) {
if (!args.hostname) {
args.hostname = args.ip;
}
2016-04-03 05:12:49 +00:00
if (args.ip) {
if (config.webirc[network.host] instanceof Function) {
webirc = config.webirc[network.host](client, args);
} else {
webirc = {
password: config.webirc[network.host],
username: pkg.name,
2016-04-03 05:12:49 +00:00
address: args.ip,
hostname: args.hostname
};
}
} else {
log.warn("Cannot find a valid WEBIRC configuration for " + nick
+ "!" + network.username + "@" + network.host);
}
}
network.irc = new ircFramework.Client({
version: pkg.name + " " + Helper.getVersion() + " -- " + pkg.homepage,
host: network.host,
port: network.port,
nick: nick,
username: config.useHexIp ? Helper.ip2hex(args.ip) : network.username,
gecos: network.realname,
password: network.password,
tls: network.tls,
localAddress: config.bind,
rejectUnauthorized: false,
enable_echomessage: true,
2016-04-13 07:10:44 +00:00
auto_reconnect: true,
2016-07-02 18:45:41 +00:00
auto_reconnect_wait: 10000 + Math.floor(Math.random() * 1000), // If multiple users are connected to the same network, randomize their reconnections a little
auto_reconnect_max_retries: 360, // At least one hour (plus timeouts) worth of reconnections
2016-04-03 05:12:49 +00:00
webirc: webirc,
2014-09-13 21:29:45 +00:00
});
network.irc.requestCap([
"znc.in/self-message", // Legacy echo-message for ZNc
]);
events.forEach((plugin) => {
var path = "./plugins/irc-events/" + plugin;
require(path).apply(client, [
network.irc,
network
]);
});
network.irc.connect();
client.save();
2014-09-13 21:29:45 +00:00
};
Client.prototype.generateToken = function(callback) {
crypto.randomBytes(48, (err, buf) => {
2016-10-09 08:54:44 +00:00
if (err) {
throw err;
}
callback(buf.toString("hex"));
2016-05-31 21:28:31 +00:00
});
};
Client.prototype.updateSession = function(token, ip, request) {
const client = this;
const agent = UAParser(request.headers["user-agent"] || "");
let friendlyAgent = "";
2017-08-13 18:37:12 +00:00
if (agent.browser.name) {
friendlyAgent = `${agent.browser.name} ${agent.browser.major}`;
} else {
friendlyAgent = "Unknown browser";
}
2017-08-13 18:37:12 +00:00
if (agent.os.name) {
2017-08-08 19:46:55 +00:00
friendlyAgent += ` on ${agent.os.name} ${agent.os.version}`;
}
2017-07-10 19:47:03 +00:00
client.config.sessions[token] = _.assign({
lastUse: Date.now(),
ip: ip,
agent: friendlyAgent,
2017-07-10 19:47:03 +00:00
}, client.config.sessions[token]);
};
2016-05-31 21:28:31 +00:00
Client.prototype.setPassword = function(hash, callback) {
var client = this;
client.manager.updateUser(client.name, {
password: hash
}, function(err) {
if (err) {
log.error("Failed to update password of", client.name, err);
return callback(false);
}
2016-05-31 21:28:31 +00:00
client.config.password = hash;
return callback(true);
2016-05-31 21:28:31 +00:00
});
};
2014-09-13 21:29:45 +00:00
Client.prototype.input = function(data) {
var client = this;
data.text.split("\n").forEach((line) => {
data.text = line;
client.inputLine(data);
});
};
Client.prototype.inputLine = function(data) {
var client = this;
var text = data.text;
2014-09-13 21:29:45 +00:00
var target = client.find(data.target);
if (!target) {
return;
}
// Sending a message to a channel is higher priority than merely opening one
// so that reloading the page will open this channel
this.lastActiveChannel = target.chan.id;
2016-03-06 09:24:56 +00:00
// This is either a normal message or a command escaped with a leading '/'
if (text.charAt(0) !== "/" || text.charAt(1) === "/") {
if (target.chan.type === Chan.Type.LOBBY) {
target.chan.pushMessage(this, new Msg({
type: Msg.Type.ERROR,
text: "Messages can not be sent to lobbies."
}));
return;
}
2016-03-06 09:24:56 +00:00
text = "say " + text.replace(/^\//, "");
} else {
text = text.substr(1);
2014-09-13 21:29:45 +00:00
}
2016-03-06 09:24:56 +00:00
2014-09-13 21:29:45 +00:00
var args = text.split(" ");
2016-03-06 09:24:56 +00:00
var cmd = args.shift().toLowerCase();
var irc = target.network.irc;
var connected = irc && irc.connection && irc.connection.connected;
if (cmd in inputs) {
var plugin = inputs[cmd];
if (connected || plugin.allowDisconnected) {
connected = true;
plugin.input.apply(client, [target.network, target.chan, cmd, args]);
}
} else if (connected) {
irc.raw(text);
}
if (!connected) {
target.chan.pushMessage(this, new Msg({
type: Msg.Type.ERROR,
text: "You are not connected to the IRC network, unable to send your command."
}));
2016-03-06 09:24:56 +00:00
}
2014-09-13 21:29:45 +00:00
};
Client.prototype.more = function(data) {
var client = this;
var target = client.find(data.target);
if (!target) {
return;
}
var chan = target.chan;
var index = chan.messages.findIndex((val) => val.id === data.lastId);
var messages = chan.messages.slice(Math.max(0, index - 100), index);
2014-09-13 21:29:45 +00:00
client.emit("more", {
chan: chan.id,
messages: messages
});
};
Client.prototype.open = function(socketId, target) {
// Opening a window like settings
if (target === null) {
2017-07-10 19:47:03 +00:00
this.attachedClients[socketId].openChannel = -1;
return;
}
target = this.find(target);
if (!target) {
return;
}
target.chan.firstUnread = 0;
target.chan.unread = 0;
target.chan.highlight = false;
2017-07-10 19:47:03 +00:00
this.attachedClients[socketId].openChannel = target.chan.id;
this.lastActiveChannel = target.chan.id;
this.emit("open", target.chan.id);
};
2014-09-24 19:42:36 +00:00
Client.prototype.sort = function(data) {
const order = data.order;
2014-09-24 19:42:36 +00:00
if (!_.isArray(order)) {
return;
}
2014-09-24 19:42:36 +00:00
switch (data.type) {
2014-09-24 19:42:36 +00:00
case "networks":
this.networks.sort((a, b) => order.indexOf(a.id) - order.indexOf(b.id));
// Sync order to connected clients
this.emit("sync_sort", {order: this.networks.map((obj) => obj.id), type: data.type, target: data.target});
2014-09-24 19:42:36 +00:00
break;
case "channels":
var network = _.find(this.networks, {id: data.target});
2014-09-24 19:42:36 +00:00
if (!network) {
return;
}
network.channels.sort((a, b) => order.indexOf(a.id) - order.indexOf(b.id));
// Sync order to connected clients
this.emit("sync_sort", {order: network.channels.map((obj) => obj.id), type: data.type, target: data.target});
2014-09-24 19:42:36 +00:00
break;
}
this.save();
2014-09-24 19:42:36 +00:00
};
Client.prototype.names = function(data) {
var client = this;
var target = client.find(data.target);
if (!target) {
return;
}
client.emit("names", {
id: target.chan.id,
users: target.chan.users
});
};
2014-09-13 21:29:45 +00:00
Client.prototype.quit = function() {
2014-09-29 15:49:38 +00:00
var sockets = this.sockets.sockets;
var room = sockets.adapter.rooms[this.id] || [];
for (var user in room) {
var socket = sockets.adapter.nsp.connected[user];
if (socket) {
socket.disconnect();
}
}
this.networks.forEach((network) => {
if (network.irc) {
network.irc.quit("Page closed");
}
network.destroy();
2014-09-13 21:29:45 +00:00
});
2014-09-13 17:18:42 +00:00
};
2014-10-11 20:44:56 +00:00
2017-07-10 19:47:03 +00:00
Client.prototype.clientAttach = function(socketId, token) {
var client = this;
var save = false;
if (client.awayMessage && _.size(client.attachedClients) === 0) {
client.networks.forEach(function(network) {
// Only remove away on client attachment if
// there is no away message on this network
if (!network.awayMessage) {
network.irc.raw("AWAY");
}
});
}
2017-07-10 19:47:03 +00:00
client.attachedClients[socketId] = {
token: token,
openChannel: client.lastActiveChannel
};
// Update old networks to store ip and hostmask
client.networks.forEach((network) => {
if (!network.ip) {
save = true;
network.ip = (client.config && client.config.ip) || client.ip;
}
if (!network.hostname) {
var hostmask = (client.config && client.config.hostname) || client.hostname;
if (hostmask) {
save = true;
network.hostmask = hostmask;
}
}
});
if (save) {
client.save();
}
};
Client.prototype.clientDetach = function(socketId) {
const client = this;
delete this.attachedClients[socketId];
if (client.awayMessage && _.size(client.attachedClients) === 0) {
client.networks.forEach(function(network) {
// Only set away on client deattachment if
// there is no away message on this network
if (!network.awayMessage) {
network.irc.raw("AWAY", client.awayMessage);
}
});
}
};
2017-07-10 19:47:03 +00:00
Client.prototype.registerPushSubscription = function(session, subscription, noSave) {
if (!_.isPlainObject(subscription) || !_.isPlainObject(subscription.keys)
|| typeof subscription.endpoint !== "string" || !/^https?:\/\//.test(subscription.endpoint)
|| typeof subscription.keys.p256dh !== "string" || typeof subscription.keys.auth !== "string") {
session.pushSubscription = null;
return;
}
const data = {
endpoint: subscription.endpoint,
keys: {
p256dh: subscription.keys.p256dh,
auth: subscription.keys.auth
}
};
session.pushSubscription = data;
if (!noSave) {
this.manager.updateUser(this.name, {
sessions: this.config.sessions
});
}
return data;
};
Client.prototype.unregisterPushSubscription = function(token) {
this.config.sessions[token].pushSubscription = null;
this.manager.updateUser(this.name, {
sessions: this.config.sessions
});
};
2016-11-19 21:00:54 +00:00
Client.prototype.save = _.debounce(function SaveClient() {
if (Helper.config.public) {
2014-10-12 05:15:03 +00:00
return;
}
2014-11-09 16:01:22 +00:00
2016-11-19 21:00:54 +00:00
const client = this;
const json = {};
json.networks = this.networks.map((n) => n.export());
client.manager.updateUser(client.name, json);
2016-11-19 21:00:54 +00:00
}, 1000, {maxWait: 10000});