473 lines
10 KiB
JavaScript
473 lines
10 KiB
JavaScript
"use strict";
|
|
|
|
var _ = require("lodash");
|
|
var pkg = require("../package.json");
|
|
var Chan = require("./models/chan");
|
|
var crypto = require("crypto");
|
|
var userLog = require("./userLog");
|
|
var Msg = require("./models/msg");
|
|
var Network = require("./models/network");
|
|
var ircFramework = require("irc-framework");
|
|
var Helper = require("./helper");
|
|
|
|
module.exports = Client;
|
|
|
|
var id = 0;
|
|
var events = [
|
|
"connection",
|
|
"unhandled",
|
|
"ctcp",
|
|
"error",
|
|
"invite",
|
|
"join",
|
|
"kick",
|
|
"mode",
|
|
"motd",
|
|
"message",
|
|
"link",
|
|
"names",
|
|
"nick",
|
|
"part",
|
|
"quit",
|
|
"topic",
|
|
"welcome",
|
|
"list",
|
|
"whois"
|
|
];
|
|
var inputs = [
|
|
"ctcp",
|
|
"msg",
|
|
"part",
|
|
"action",
|
|
"connect",
|
|
"disconnect",
|
|
"invite",
|
|
"kick",
|
|
"mode",
|
|
"nick",
|
|
"notice",
|
|
"query",
|
|
"quit",
|
|
"raw",
|
|
"topic",
|
|
"list",
|
|
].reduce(function(plugins, name) {
|
|
var path = "./plugins/inputs/" + name;
|
|
var plugin = require(path);
|
|
plugin.commands.forEach(command => plugins[command] = plugin);
|
|
return plugins;
|
|
}, {});
|
|
|
|
function Client(manager, name, config) {
|
|
if (typeof config !== "object") {
|
|
config = {};
|
|
}
|
|
_.merge(this, {
|
|
lastActiveChannel: -1,
|
|
attachedClients: {},
|
|
config: config,
|
|
id: id++,
|
|
name: name,
|
|
networks: [],
|
|
sockets: manager.sockets,
|
|
manager: manager
|
|
});
|
|
|
|
var client = this;
|
|
|
|
if (client.name && !client.config.token) {
|
|
client.updateToken(function(token) {
|
|
client.manager.updateUser(client.name, {token: token});
|
|
});
|
|
}
|
|
|
|
var delay = 0;
|
|
(client.config.networks || []).forEach(n => {
|
|
setTimeout(function() {
|
|
client.connect(n);
|
|
}, delay);
|
|
delay += 1000;
|
|
});
|
|
|
|
if (client.name) {
|
|
log.info("User '" + client.name + "' loaded");
|
|
}
|
|
}
|
|
|
|
Client.prototype.emit = function(event, data) {
|
|
if (this.sockets !== null) {
|
|
this.sockets.in(this.id).emit(event, data);
|
|
}
|
|
if (this.config.log === true) {
|
|
if (event === "msg") {
|
|
var target = this.find(data.chan);
|
|
if (target) {
|
|
var chan = target.chan.name;
|
|
if (target.chan.type === Chan.Type.LOBBY) {
|
|
chan = target.network.host;
|
|
}
|
|
userLog.write(
|
|
this.name,
|
|
target.network.host,
|
|
chan,
|
|
data.msg
|
|
);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
Client.prototype.find = function(channelId) {
|
|
var network = null;
|
|
var chan = null;
|
|
for (var i in this.networks) {
|
|
var n = this.networks[i];
|
|
chan = _.find(n.channels, {id: channelId});
|
|
if (chan) {
|
|
network = n;
|
|
break;
|
|
}
|
|
}
|
|
if (network && chan) {
|
|
return {
|
|
network: network,
|
|
chan: chan
|
|
};
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
Client.prototype.connect = function(args) {
|
|
var config = Helper.config;
|
|
var client = this;
|
|
|
|
var nick = args.nick || "lounge-user";
|
|
var webirc = null;
|
|
var channels = [];
|
|
|
|
if (args.channels) {
|
|
var badName = false;
|
|
|
|
args.channels.forEach(chan => {
|
|
if (!chan.name) {
|
|
badName = true;
|
|
return;
|
|
}
|
|
|
|
channels.push(new Chan({
|
|
name: chan.name
|
|
}));
|
|
});
|
|
|
|
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
|
|
.replace(/,/g, " ")
|
|
.split(/\s+/g)
|
|
.map(function(chan) {
|
|
return new Chan({
|
|
name: chan
|
|
});
|
|
});
|
|
}
|
|
|
|
var network = new Network({
|
|
name: args.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",
|
|
commands: args.commands,
|
|
ip: args.ip,
|
|
hostname: args.hostname,
|
|
channels: channels,
|
|
});
|
|
network.setNick(nick);
|
|
|
|
client.networks.push(network);
|
|
client.emit("network", {
|
|
networks: [network]
|
|
});
|
|
|
|
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);
|
|
return;
|
|
}
|
|
|
|
network.host = config.defaults.host;
|
|
network.port = config.defaults.port;
|
|
network.tls = config.defaults.tls;
|
|
}
|
|
|
|
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);
|
|
return;
|
|
}
|
|
|
|
if (config.webirc && network.host in config.webirc) {
|
|
args.ip = args.ip || (client.config && client.config.ip) || client.ip;
|
|
args.hostname = args.hostname || (client.config && client.config.hostname) || client.hostname || args.ip;
|
|
|
|
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,
|
|
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();
|
|
|
|
network.irc.requestCap([
|
|
"echo-message",
|
|
"znc.in/self-message",
|
|
]);
|
|
|
|
events.forEach(plugin => {
|
|
var path = "./plugins/irc-events/" + plugin;
|
|
require(path).apply(client, [
|
|
network.irc,
|
|
network
|
|
]);
|
|
});
|
|
|
|
network.irc.connect({
|
|
version: pkg.name + " " + Helper.getVersion() + " -- " + pkg.homepage,
|
|
host: network.host,
|
|
port: network.port,
|
|
nick: nick,
|
|
username: network.username,
|
|
gecos: network.realname,
|
|
password: network.password,
|
|
tls: network.tls,
|
|
localAddress: config.bind,
|
|
rejectUnauthorized: false,
|
|
auto_reconnect: true,
|
|
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
|
|
webirc: webirc,
|
|
});
|
|
};
|
|
|
|
Client.prototype.updateToken = function(callback) {
|
|
var client = this;
|
|
|
|
crypto.randomBytes(48, function(err, buf) {
|
|
if (err) {
|
|
throw err;
|
|
}
|
|
|
|
callback(client.config.token = buf.toString("hex"));
|
|
});
|
|
};
|
|
|
|
Client.prototype.setPassword = function(hash, callback) {
|
|
var client = this;
|
|
|
|
client.updateToken(function(token) {
|
|
client.manager.updateUser(client.name, {
|
|
token: token,
|
|
password: hash
|
|
}, function(err) {
|
|
if (err) {
|
|
log.error("Failed to update password of", client.name, err);
|
|
return callback(false);
|
|
}
|
|
|
|
client.config.password = hash;
|
|
return callback(true);
|
|
});
|
|
});
|
|
};
|
|
|
|
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;
|
|
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;
|
|
|
|
// This is either a normal message or a command escaped with a leading '/'
|
|
if (text.charAt(0) !== "/" || text.charAt(1) === "/") {
|
|
text = "say " + text.replace(/^\//, "");
|
|
} else {
|
|
text = text.substr(1);
|
|
}
|
|
|
|
var args = text.split(" ");
|
|
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."
|
|
}));
|
|
}
|
|
};
|
|
|
|
Client.prototype.more = function(data) {
|
|
var client = this;
|
|
var target = client.find(data.target);
|
|
if (!target) {
|
|
return;
|
|
}
|
|
var chan = target.chan;
|
|
var count = chan.messages.length - (data.count || 0);
|
|
var messages = chan.messages.slice(Math.max(0, count - 100), count);
|
|
client.emit("more", {
|
|
chan: chan.id,
|
|
messages: messages
|
|
});
|
|
};
|
|
|
|
Client.prototype.open = function(socketId, data) {
|
|
var target = this.find(data);
|
|
if (!target) {
|
|
return;
|
|
}
|
|
|
|
target.chan.firstUnread = 0;
|
|
target.chan.unread = 0;
|
|
target.chan.highlight = false;
|
|
|
|
this.attachedClients[socketId] = target.chan.id;
|
|
this.lastActiveChannel = target.chan.id;
|
|
|
|
this.emit("open", target.chan.id);
|
|
};
|
|
|
|
Client.prototype.sort = function(data) {
|
|
var self = this;
|
|
|
|
var type = data.type;
|
|
var order = data.order || [];
|
|
var sorted = [];
|
|
|
|
switch (type) {
|
|
case "networks":
|
|
order.forEach(i => {
|
|
var find = _.find(self.networks, {id: i});
|
|
if (find) {
|
|
sorted.push(find);
|
|
}
|
|
});
|
|
self.networks = sorted;
|
|
break;
|
|
|
|
case "channels":
|
|
var target = data.target;
|
|
var network = _.find(self.networks, {id: target});
|
|
if (!network) {
|
|
return;
|
|
}
|
|
order.forEach(i => {
|
|
var find = _.find(network.channels, {id: i});
|
|
if (find) {
|
|
sorted.push(find);
|
|
}
|
|
});
|
|
network.channels = sorted;
|
|
break;
|
|
}
|
|
|
|
self.save();
|
|
};
|
|
|
|
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
|
|
});
|
|
};
|
|
|
|
Client.prototype.quit = function() {
|
|
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");
|
|
}
|
|
});
|
|
};
|
|
|
|
Client.prototype.clientAttach = function(socketId) {
|
|
this.attachedClients[socketId] = this.lastActiveChannel;
|
|
};
|
|
|
|
Client.prototype.clientDetach = function(socketId) {
|
|
delete this.attachedClients[socketId];
|
|
};
|
|
|
|
Client.prototype.save = _.debounce(function SaveClient() {
|
|
if (Helper.config.public) {
|
|
return;
|
|
}
|
|
|
|
const client = this;
|
|
let json = {};
|
|
json.networks = this.networks.map(n => n.export());
|
|
client.manager.updateUser(client.name, json);
|
|
}, 1000, {maxWait: 10000});
|