emjack

some crap you may find useful

// ==UserScript==
// @name			emjack
// @version			4.6.6.0
// @description		some crap you may find useful
// @match			https://epicmafia.com/game/*
// @match			https://epicmafia.com/lobby
// @namespace		https://greasyfork.org/users/4723
// ==/UserScript==

// welcome back
function emjack() {

	// invalid : break
	var	page = (
		window.setup_id ? "mafia" :
		window.lobby_id ? "lobby" : ""
		);
	if(!page) return;

	// yadayada
	const self = window.user;
	var	alive = true,
		afk = false,
		list = {},
		meetd = {},
		meets = {},
		master = "",
		autobomb = "",
		highlight = "",
		roulette = 0,
		kicktimer = 0,
		keys = 0,
		auth = false,
		notes = null,
		users = {},
		round = {};
	var	ANTIDC = 0x0001,
		AUKICK = 0x0002,
		AUWILL = 0x0004,
		AUBOMB = 0x0008,
		OBEYME = 0x0010,
		UNOTES = 0x0020,
		DEVLOG = 0x0040,
		JEEVES = 0x0080,
		SYSALT = 0x0100,
		MSGMRK = 0x0200,
		DSPFMT = 0x0400,
		DSPIMG = 0x0800,
		GRAPHI = 0x4000;
	var	K_DEBUG = 0x0004,
		K_SHIFT = 0x0008;

	// public
	var	user = window.user || "",
		ranked = window.ranked === true,
		game_id = window.game_id || 0,
		setup_id = window.setup_id || localStorage.ejsid || 0,
		_emotes = window._emotes || {},
		lobby_emotes = window.lobby_emotes || (
			window.lobbyinfo ? lobbyinfo.emotes : {}
			);
	window.ej = {
		name: "emjack",
		version: 0x2e,
		vstring: "4.6.6.0",
		cmds: {},
		notes: localStorage.notes ? JSON.parse(localStorage.notes) : {},
		users: users,
		settings: + localStorage.ejs || AUKICK | UNOTES | MSGMRK | DSPFMT,
		};
	notes = ej.notes;
	afk = (ej.settings & JEEVES) === JEEVES;

	// setup role icons
	var	roleimg = document.createElement("style");
	document.head.appendChild(roleimg).type = "text/css";
	if(localStorage.roleimg) {
		setTimeout(function() {
			ej.run("icons " + localStorage.roleimg, list.chat);
			});
		}

	// update
	if(!localStorage.ejv) {
		ej.settings |= DSPFMT;
		localStorage.ejv = 0x2d;
		}
	if(localStorage.ejv < 0x2e) {
		ej.settings |= MSGMRK;
		localStorage.ejv = 0x2e;
		}

	// plug in
	var	sock = { socket: null },
		postjackl = [];
	ej.sock = sock;
	function postjack() {
		var	args = [];
		for(var i = 0; i < arguments.length-1; i++) {
			args[i] = arguments[i];
			}
		postjackl.push(args, arguments[i]);
		};
	function postjack_run() {
		while(postjackl.length) {
			postjackl.pop().apply(null, postjackl.pop());
			}
		};
	WebSocket.prototype.send = function(initial) {
		return function() {
			if(sock.socket !== this) {
				sock.build(this);
				}
			arguments[0] = sock.intercept(arguments[0]);
			initial.apply(this, arguments);
			};
		}(WebSocket.prototype.send);

	// socket
	sock.build = function(socket) {
		this.socket = socket;
		if(page === "mafia") {
			log("", "rt emote emote-" + _emotes[arand(Object.keys(_emotes))]);
			log(ej.name + ej.vstring + " connected", "rt");
			log((ej.settings | 65536).toString(2).substring(1));
			}
		socket.onmessage = function(initial) {
			return function(event) {
				sock.handle(event.data, event);
				if(alive) {
					initial.apply(this, arguments);
					setTimeout(postjack_run, 200);
					}
				};
			}(socket.onmessage);
		};
	sock.handle = function(data, event) {
		try {
			let raw = "";
			for(let i = 0, j = new DataView(data); i < j.byteLength; i++) {
				raw += String.fromCharCode(j.getUint8(i))
				}
			data = JSON.parse(raw.substr(raw.indexOf("[")));
			}
		catch(error) {
			data = null;
			}
		if(data) {
			if(page === "mafia") {
				for(var i = 0, real = null; i < data.length; i++) {
					real = sock.parseShort(data[i][0], data[i][1]);
					if(ej.settings & DEVLOG) {
						console.log(" > %s:", real[0], real[1]);
						}
					if(ej.cmds[real[0]]) {
						ej.cmds[real[0]].call(ej, real[1], event);
						}
					}
				}
			else {
				for(var i = 0; i < data.length; i += 2) {
					if(ej.ccmds[data[i]]) {
						ej.ccmds[data[i]].apply(ej, data[i + 1]);
						}
					}
				}
			}
		};
	sock.intercept = function(data) {
		try {
			let raw = "";
			for(let i = 0; i < data.byteLength; i++) {
				raw += String.fromCharCode(data[i])
				}
			if(~raw.indexOf("[")) {
				data = JSON.parse(raw.substr(raw.indexOf("[")));
				}
			else {
				return data;
				}
			}
		catch(error) {
			return data;
			}
		if(ej.settings & DEVLOG) {
			console.log(" < %s:", data[0], data[1]);
			}
		if(page === "mafia") {
			if(ej.cmdi[data[0]]) {
				data[1] = ej.cmdi[data[0]](data[1]);
				}
			}
		else {
			if(ej.ccmdi[data[0]]) {
				data = ej.ccmdi[data[0]].apply(ej, data);
				}
			}
		data = JSON.stringify(data);
		var raw = [0xd9, data.length];
		for(let i = 0; i < data.length; i++) {
			raw.push(data.charCodeAt(i));
			}
		return Uint8Array.from(raw);
		};
	sock.parseShort = function(cmd, data) {
		var	rfmt = this.short[cmd];
		if(rfmt) {
			if(data) for(var key in rfmt.data) {
				data[key] = data[rfmt.data[key]] || data[key];
				// delete data[rfmt.data[key]];
				}
			return [rfmt.cmd, data];
			}
		else {
			return [cmd, data];
			}
		};
	sock.short = function(short) {
		var	rfmt = {};
		for(var i = 0, data = null; i < short.length; i++) {
			data = short[i];
			rfmt[data.alias || data.cmd]={
				cmd: data.cmd || data.alias,
				data: data.data
				};
			}
		return rfmt;
		}(window.shorten || []);
	sock.cmd = function(cmd, data) {
		if(sock.socket) {
			data = JSON.stringify([cmd, data]);
			var raw = [0xd9, data.length];
			for(let i = 0; i < data.length; i++) {
				raw.push(data.charCodeAt(i));
				}
			sock.socket.send(
				Uint8Array.from(raw)
				);
			}
		};
	sock.chat = function(message, data) {
		if(typeof data === "object") {
			data.msg = message;
			data.meet = data.meet || meetd.meet;
			sock.cmd("<", data);
			}
		else sock.cmd("<", {
			meet: meetd.meet,
			msg: data ? "@" + data + " " + message : message
			});
		};
	sock.vote = function(vote, meeting) {
		sock.cmd("point", {
			meet: meeting || meetd.meet,
			target: vote
			});
		};
	sock.dcthen = function(callback) {
		alive = false;
		if(page === "mafia") {
			ej.redirect_back = callback;
			sock.cmd("leave");
			WebSocket.prototype.send = function() {};
			}
		else {
			callback();
			}
		};

	// packets
	ej.cmdi = {
		"$MODIFY": function(data) {
			for(var key in data) {
				data[key] = prompt(key, data[key]);
				}
			return data;
			},
		"join": function(data) {
			if(keys & K_DEBUG) {
				keys ^= K_DEBUG;
				return ej.cmdi.$MODIFY(data);
				}
			return data;
			}
		};
	ej.cmds = {
		"auth": function(data) {
			var	ofg = $("#option_fastgame"),
				ons = $("#option_nospectate");
			if(!ranked) {
				let ofg = $("#option_fastgame"),
					ons = $("#option_nospectate");
				if(!ofg.hasClass("sel")) {
					ofg.click();
					}
				if(!ons.hasClass("sel")) {
					ons.click();
					}
				}
			postjack(data, function(data) {
				auth = true;
				ku.send(0, Math.round(ej.version-42));
				});
			},
		"round": function(data) {
			round = data;
			if(auth && data.state === 1) {
				if(ej.settings & AUWILL && !ranked && !u(self).mafia) {
					postjack(data, function(data) {
						log("Wrote will.", "lastwill");
						sock.cmd("will", {
							msg: `${self}, ${u(self).role}`
							});
						});
					}
				}
			if((data.state & 1) === 0) {
				postjack(function() {
					for(let x in users) {
						let node = $(`#id_${x}`);
						if(node.length && !$(`#vc_${x}`).length) {
							node.prepend(
								$(`<span id="vc_${x}">${meetd.tally && meetd.tally[x] || 0}</span>`)
								);
							}
						}
					});
				}
			},
		"users": function(data) {
			for(var x in data.users) {
				u.make(data.users[x]);
				}
			postjack(data, function(data) {
				for(var x in data.users) {
					if(notes[x]) {
						$(`[data-uname="${x}"]`).attr("title", notes[x]);
						}
					}
				});
			},
		"left": function(data) {
			for(var i = 0; i < data.left.length; i++) {
				u(data.left[i]).dead = true;
				}
			},
		"anonymous_players": function(data) {
			for(var x in users) {
				delete users[x];
				}
			for(var i = 0; i < data.players.length; i++) {
				u.make(data.players[i]);
				}
			},
		"anonymous_reveal": function(data) {
			if(data.user === self) {
				u.make(u(this.mask));
				}
			},
		"join": function(data) {
			u.make(data.data);
			log(data.user + " has joined");
			$("#minigame").trigger("mini::sync");
			if(ej.settings & AUKICK && /autokick/i.test(notes[data.user])) {
				postjack(data.data.id, function(id) {
					sock.cmd("ban", {
						uid: id
						});
					});
				}
			else {
				postjack(data.user, function(user) {
					if(notes[user]) {
						$(`[data-uname="${user}"]`).attr("title", notes[user]);
						}
					});
				}
			},
		"leave": function(data) {
			log(data.user + " has left");
			delete users[data.user];
			},
		"kick": function(data) {
			u(data.user).dead = true;
			for(var x in meetd.votes) {
				if(meetd.votes[x] === data.user) {
					data.unpoint = true;
					ej.cmds.point(data);
					}
				}
			},
		"kill": function(data) {
			u(data.target).dead = true;
			},
		"k": function(data) {
			ku.recv(u(data.user || data.u), 1, Date.now());
			},
		"u": function(data) {
			ku.recv(u(data.user || data.u), 0, Date.now());
			},
		"<": function(data, event) {
			if(data.user === highlight) {
				postjack(function() {
					$(".talk").last().addClass("ej_highlight");
					});
				}
			if(auth && !ranked) {
				if(data.msg[0]==="$") {
					if(ej.settings & DSPFMT) {
						ej.run(data.msg.substring(1), list.format);
						}
					}
				else if(ej.settings & OBEYME) {
					if(data.msg[0]==="@") {
						var	target = data.msg.replace(/@(\w+).+/, "$1"),
							message = data.msg.replace(/@\w+ (.+)/, "$1");
						if(target === self) {
							ej.run(message, list.bot, data);
							}
						}
					}
				else if(roulette && data.msg === `@${self} roulette`) {
					ej.run("roulette", list.bot, data);
					}
				}
			},
		"speech": function(data) {
			if(data.type === "contact") {
				postjack(data, function(data) {
					log(`The roles are... ${data.data.join(", ")}`);
					});
				}
			},
		"meet": function(data) {
			const MEET_ROLES = ["mason", "templar", "mafia", "cultist", "cyborg", "zombie"];
			data.tally = {};
			data.votes = {};
			meets[data.meet] = data;
			if(data.say || !meetd.meet) {
				meetd = data;
				for(var i = 0; i < data.members.length; i++) {
					u(data.members[i]).meet = data.meet;
					}
				}
			for(var i = 0; i < data.basket.length; i++) {
				data.tally[data.basket[i]] = 0;
				}
			for(var i = 0; i < data.members.length; i++) {
				data.votes[data.members[i]]="";
				}
			if(data.non_voting) {
				for(var i = 0; i < data.non_voting.length; i++) {
					data.votes[data.non_voting[i]]="*";
					}
				}
			if(data.disguise && ej.settings & 0x800000) {
				for(var x in data.disguise) {
					postjack(x, data.disguise[x], function(fake, name) {
						log(fake + " is " + name);
						});
					}
				}
			if(~MEET_ROLES.indexOf(data.meet)) {
				if(data.meet === "mafia") {
					for(let i = 0; i < data.members.length; i++) {
						u(data.members[i]).mafia = true;
						}
					}
				postjack(data.meet, data.members, function(meet, members) {
					for(let i = 0; i < data.members.length; i++) {
						$(`[data-uname="${data.members[i]}"] .roleimg.role-unknown`)
							.removeClass("role-unknown")
							.addClass(`role-${data.meet}`);
						}
					});
				}
			},
		"end_meet": function(data) {
			if(data.meet === meetd.meet) {
				meetd = {};
				}
			delete meets[data.meet];
			for(var x in users) {
				if(users[x].meet === data.meet) {
					if(!users[x].id) {
						delete users[x];
						}
					else if(data.say) {
						users[x].meet = "";
						}
					}
				}
			},
		"point": function(data) {
			var	node = null,
				meet = meets[data.meet];
			if(meet) {
				if(data.unpoint) {
					meet.tally[data.target]--;
					meet.votes[data.user]="";
					}
				else {
					let prev = meet.votes[data.user];
					if(prev) {
						meet.tally[prev]--;
						$(`#vc_${prev}`)
							.text(meet.tally[prev]);
						}
					meet.tally[data.target]++;
					meet.votes[data.user] = data.target;
					}
				$(`#vc_${data.target}`)
					.text(meet.tally[data.target]);
				}
			},
		"reveal": function(data) {
			u(data.user).role = data.data;
			if(!u(data.user).dead) {
				postjack(data, function(data) {
					log(`${data.user === self ? "Your role" : data.user} is ${data.data}!`);
				});
				}
			},
		"countdown": function(data) {
			if(auth && !ranked && ej.settings & AUKICK) {
				clearTimeout(kicktimer);
				kicktimer = setTimeout(function() {
					jeeves.work();
					sock.cmd("kick");
					}, data.totaltime);
				}
			},
		"kickvote": function() {
			clearTimeout(kicktimer);
			if(!ranked && ej.settings & AUKICK) {
				jeeves.work();
				sock.cmd("kick");
				}
			},
		"redirect": function(data) {
			if(!alive && ej.redirect_back) {
				ej.redirect_back();
				ej.redirect_back = null;
				}
			}
		};
	ej.ccmdi = {
		"<": function(c, msg) {
			if(msg[0] === "/") {
				return ["<"];
				}
			return arguments;
			}
		};
	ej.ccmds = {
		"<": function(id, msg, t) {
			if(msg[0] === "$") {
				if(ej.settings & DSPFMT) {
					ej.run(msg.substring(1), list.format);
					}
				}
			}
		};

	// kucode
	var	ku = {};
	ku.send = function(op, code) {
		code += op << 6;
		if(ej.settings & DEVLOG) {
			log(" * " + self + ": " + (code | 1024).toString(2).substring(1));
			}
		setTimeout(function() {
			for(var i = 9; i >= 0; i--) {
				sock.cmd(code >> i & 1 ? "k" : "u");
				}
			if(code & 1) {
				sock.cmd("u");
				}
			}, 200);
		};
	ku.recv = function(u, bit, time) {
		if(time-u.kuclock > 100) {
			u.kucode = 1;
			u.kuclock = Infinity;
			}
		else {
			u.kucode <<= 1;
			u.kucode |= bit;
			if(u.kucode & 1024) {
				if(ej.settings & DEVLOG) {
					log(" * " + u.name + ": " + u.kucode.toString(2).substring(1));
					}
				if(ku.op[u.kucode >> 6 & 15]) {
					ku.op[u.kucode >> 6 & 15]
					(u, u.kucode & 63);
					}
				u.kucode = 1;
				u.kuclock = Infinity;
				}
			else {
				u.kuclock = time;
				}
			}
		};
	ku.op = [
		function(u, code) {
			if(u.emjack === null) {
				u.emjack = (42 + code) / 10 || 0;
				ku.send(0, Math.round(ej.version-42));
				}
			},
		function(u, code) {
			ku.send(0, Math.round(ej.version-42));
			},
		function(u, code) {
			if(ej.settings & 0x800000) {
				log(u.name + " sent "
					+ (code | 64).toString(2).substring(1)
					+ ":" + code.toString()
					+ ":" + String.fromCharCode(code + 96)
				   );
				}
			},
		undefined,
		undefined,
		undefined,
		function(u, code) {
			$("#minigame").trigger("mini::update", [code]);
			},
		function(u, code) {
			console.log(u.username, code);
			$("#minigame").trigger("mini::vote", [u.username, code]);
			}
		];

	// jeeves
	var	jeeves = {};
	jeeves.work = function() {
		if(afk && !ranked) {
			for(var x in meets) {
				if(!meets[x].votes[self]) {
					jeeves.think(meets[x]);
					}
				}
			}
		};
	jeeves.think = function(meet) {
		for(var x in meet.tally) {
			if(Math.random() < meet.tally[x]/meet.members.length) {
				sock.vote(x, meet.meet);
				break;
				}
			}
		if(!meet.votes[self]) {
			sock.vote(arand(meet.basket || Object.keys(users)), meet.meet);
			}
		};

	// chat base
	ej.run = function(input, list, data) {
		for(var i = 0, match = null; i < list.length; i++) {
			match = list[i].regex.exec(input);
			if(match !== null) {
				data ? match[0] = data : match.shift();
				list[i].callback.apply(list[i], match);
				break;
				}
			}
		};

	// chat commands
	list.format = [
		{
			name: "Display image",
			short: "$img [url]",
			regex: /^img (.+)/i,
			callback: function(url) {
				if(ej.settings & DSPIMG) {
					postjack(url, function(url) {
						var	img = new Image(),
							node = document.createElement("a");
						img.src = url;
						node.href = url;
						node.target = "_blank";
						node.appendChild(img);
						log(node, "ej_img");
						});
					}
				}
			},
		{
			name: "Display webm",
			short: "$webm [url]",
			regex: /^webm (.+)/i,
			callback: function(url) {
				if(ej.settings & DSPIMG) {
					postjack(url, function(url) {
						var	video = document.createElement("video");
						video.src = url;
						video.setAttribute("controls", "");
						video.setAttribute("type", "video/webm");
						log(video, "ej_img");
						});
					}
				}
			}
		];

	list.copy = {
		sc: {
			name: "Scriptcheck",
			short: "/sc",
			regex: /^sc|^scriptcheck/i,
			callback: function() {
				log(ej.name + ej.vstring);
				}
			},
		eval: {
			name: "Evaluate",
			regex: /^eval (.+)/i,
			callback: function(input) {
				log(JSON.stringify(eval(input)) || "undefined");
				}
			},
		emotes: {
			name: "Get emotes",
			short: "/emotes",
			regex: /^emotes/i,
			callback: function() {
				log("Sitewide emotes", "bold");
				log(Object.keys(_emotes).join(" ") || "none found");
				log("Lobby emotes", "bold");
				log(Object.keys(lobby_emotes).join(" ") || "none found");
				}
			},
		fmt: {
			name: "Toggle chat formatting",
			short: "/fmt [on | off | noimg]",
			regex: /^fmt ?(on|off|noimg)?/i,
			callback: function(_type) {
				if(!_type) {
					log("Type " + this.short);
					}
				else if(_type === "on") {
					ej.settings |= DSPFMT | DSPIMG;
					log("$ chat formatting on (including images)");
					}
				else if(_type === "noimg") {
					ej.settings |= DSPFMT;
					ej.settings &=~ DSPIMG;
					log("$ chat formatting on (no images)");
					}
				else {
					ej.settings &=~ (DSPFMT | DSPIMG);
					log("$ chat formatting off");
					}
				}
			},
		say: {
			name: "Send message",
			short: "/say [message]",
			regex: /^say ?(.+)?/i,
			callback: function(msg) {
				if(!msg) {
					log("Type " + this.short);
					}
				else {
					sock.chat(msg);
					}
				}
			},
		join: {
			name: "Lobby join (or host)",
			short: "/join [host]",
			regex: /^join ?(host.+)?/i,
			callback: function(host) {
				request("GET", "/game/find?page = 1", function(data) {
					if(page === "mafia") {
						log("// retrieved", "rt bold notop");
						}
					JSON.parse(JSON.parse(data)[1]).data.forEach(function(table) {
						if(!table.status_id && !table.password) {
							if(table.target === 12 && table.id !== game_id) {
								sock.dcthen(function() {
									location.href = "/game/" + table.id;
									});
								}
							}
						});
					if(alive) {
						log("No games found.");
						if(host) {
							ej.run(host, list.chat);
							}
						}
					});
				}
			},
		host: {
			name: "Lobby host",
			short: "/host [title]",
			regex: /^host(r)? ?(.+)?/i,
			callback: function(r, title) {
				log("Hosting setup#" + setup_id + "...");
				sock.dcthen(function() {
					request("GET", sformat(
						"/game/add/mafia?setupid=$1 & ranked=$2 & add_title=$3 & game_title=$4",
						[setup_id, !!r, title ? 1 : 0, title]
						), function(data) {
						location.href = "/game/" + JSON.parse(data)[1].table;
						}
						   );
					});
				}
			},
		games: {
			name: "Lobby games",
			short: "/games",
			regex: /^games/i,
			callback: function() {
				request("GET", "/game/find?page = 1", function(data) {
					var	a, div;
					if(page === "mafia") {
						log("// retrieved", "rt bold notop");
						}
					JSON.parse(JSON.parse(data)[1]).data.forEach(function(table) {
						if(table.status_id || table.password) {
							return;
							}
						a = document.createElement("a");
						a.textContent = "Table " + table.id;
						a.addEventListener("click", function(event) {
							sock.dcthen(function() {
								location.href = "/game/" + table.id;
								});
							});
						div = document.createElement("div");
						div.appendChild(a);
						div.appendChild(
							document.createTextNode(" - " + table.numplayers + " / " + table.target + " players")
							);
						if(table.id === game_id) {
							div.appendChild(
								document.createTextNode(" (you)")
								);
							}
						log(div);
						});
					});
				}
			},
		pm: {
			name: "Bugs, suggestions & spam",
			short: "/bugs",
			regex: /^bugs?/i,
			callback: function() {
				window.open("/topic/77055", "_blank");
				}
			}
		};

	// chat commands
	list.chat = [
		list.copy.sc,
		{
			regex: /^(me .+)/i,
			callback: function(msg) {
				sock.chat("/" + msg);
				}
			},
		{
			name: "About",
			short: "/help",
			regex: /^(?:info|help|about) ?(.+)?/i,
			callback: function(topic) {
				if(this.topics[topic]) {
					log(ej.name + ej.vstring + ":" + topic, "bold");
					for(var i = 0; i < this.topics[topic].length; i++) {
						log(this.topics[topic][i]);
						}
					}
				else {
					log(ej.name + ej.vstring, "bold");
					log("Type /cmdlist for a list of commands");
					log("Topics (type /help [topic]): ", "lt notop");
					log(Object.keys(this.topics).join(", "), "tinyi");
					}
				},
			topics: {
				"features": [
					"The following passive features are always active:",
					"Auto boxes \x95 Auto focus \x95 Clickable links \x95 Partner icons \x95 Agent role list \x95 Automatically write will (/autowill to toggle) \x95 etc."
					],
				"jeeves": [
					"Type /afk to toggle Jeeves or /afk [on/off] to toggle in all games",
					"Jeeves will automatically vote for you at the end of the day if you haven't " +
					"voted already. He randomly picks a player based on the popular vote (if any)"
					],
				"marking": [
					"Click on a message to (un)mark it purple (shift + click for orange)"
					],
				"ranked": [
					"The following features are disabled in ranked games:",
					"Auto will \x95 Jeeves (/afk) \x95 Fake quoting & reporting \x95 Will & death message editing \x95 misc."
					],
				"hotkeys": [
					"Ctrl + B: Toggle boxes",
					"Ctrl + Q: Quote typed message as yourself"
					]
				}
			},
		list.copy.eval,
		{
			name: "Get metadata",
			regex: /^meta(?:data)?/i,
			callback: function() {
				for(var param in ej.meta) {
					log("@" + param + ": " + ej.meta[param]);
					}
				}
			},
		{
			name: "Get whois",
			short: "/whois [name]",
			regex: /^whois ?(.+)?/i,
			callback: function(name) {
				if(!name) {
					log("Type " + this.short);
					}
				else if(users[name]) {
					log(users[name].name + " (" + users[name].id + ") " + (
						isNaN(users[name].emjack) ? "" : "ej" + users[name].emjack
						), "bold");
					log("emotes: " + (
						users[name].emotes ?
						Object.keys(users[name].emotes).join(" ") || "none found" :
						"does not own"
						));
					}
				else {
					log("Can't find '" + name + "'");
					}
				}
			},
		list.copy.emotes,
		{
			name: "Role info",
			short: "/role [name OR user]?",
			regex: /^role ?(.+)?/i,
			callback: function(id) {
				id = id ? u(id).role || id.toLowerCase() : u(self).role;
				request("GET", `/role/${id}/info/roleid`, function(data) {
					if(data) {
						var	div = document.createElement("div");
						div.innerHTML = data;
						log("// retrieved", "rt bold notop");
						log(div);
						}
					else {
						log(`Cannot find role '${id}'`);
						}
					});
				}
			},
		{
			name: "Get command list",
			short: "/cmdlist [bot | format]?",
			regex: /^cmdlist ?(bot|format)?/i,
			callback: function(type) {
				let	data = (
					type === "bot" ?
						list.bot :
					type === "format" ?
						list.format :
						list.chat
					);
				for(let i = 0; i < data.length; i++) {
					if(data[i].short) {
						log(data[i].name, "rt bold notop");
						log(" :: " + data[i].short);
						}
					}
				}
			},
		{
			name: "Toggle Jeeves",
			short: "/afk",
			regex: /^afk/i,
			callback: function(state) {
				afk = !afk;
				ej.settings ^= JEEVES;
				log(afk ?
					"Jeeves will handle your affairs." :
					"Jeeves has been dismissed."
				   );
				}
			},
		{
			name: "Toggle autowill",
			short: "/autowill",
			regex: /^aw|^autowill/i,
			callback: function() {
				ej.settings ^= AUWILL;
				log(ej.settings & AUWILL ?
					"Name & role will be written in will by default." :
					"Disabled autowill."
				   );
				}
			},
		{
			name: "Toggle autokick",
			short: "/autokick",
			regex: /^ak|^autokick/i,
			callback: function() {
				ej.settings ^= AUKICK;
				log(ej.settings & AUKICK ?
					"Autokick enabled." :
					"Disabled autokick."
				   );
				}
			},
		{
			name: "Toggle marking",
			regex: /^mark/i,
			callback: function() {
				ej.settings ^= MSGMRK;
				log(ej.settings & MSGMRK ?
					"Messages can be marked in orange or purple by clicking or shift-clicking." :
					"Messages will not be marked."
				   );
				}
			},
		list.copy.fmt,
		{
			name: "Toggle logging",
			regex: /^dev/i,
			callback: function() {
				ej.settings ^= DEVLOG;
				log(ej.settings & DEVLOG ?
					"Logging debug data." :
					"Logging disabled."
				   );
				}
			},
		{
			name: "Get Jacks",
			short: "/jax",
			regex: /^jax/i,
			callback: function() {
				var	ulist = [];
				for(var x in users) {
					if(users[x].emjack !== null) {
						ulist.push(x + " (" + users[x].emjack + ")");
						}
					}
				log(ulist.join(", ") || "no jax");
				}
			},
		list.copy.say,
		{
			name: "Cry (if Crier)",
			short: "/cry [message]",
			regex: /^cry (.+)/i,
			callback: function(message) {
				sock.cmd("<", {
					crier: true,
					meet: "village",
					msg: message
					});
				}
			},
		{
			name: "Contact (if Agent)",
			short: "/con [target] [message]",
			regex: /^con (\w+) (.+)/i,
			callback: function(target, message) {
				sock.cmd("<", {
					contact: true,
					meet: "village",
					msg: message,
					target: target
					});
				}
			},
		{
			name: "Disguise (if Ventriloquist)",
			short: "/vent [from] [to] [message]",
			regex: /^vent?(al*)? (\w+) (\S+) ?(.*)/i,
			callback: function(all, from, to, message) {
				sock.cmd("<", {
					ventrilo: true,
					meet: "village",
					msg: all ? to + " " + message : message,
					ventuser: from,
					venttarget: all ? "all" : to
					});
				}
			},
		{
			name: "Whisper",
			short: "/w [name] [message]",
			regex: /^w (\w+) (.+)/i,
			callback: function(to, message) {
				sock.chat(message, {
					whisper: true,
					target: to
					});
				}
			},
		{
			name: "Ping",
			short: "/ping",
			regex: /^ping/i,
			callback: function() {
				var	pingees = [];
				for(var x in meetd.votes) {
					if(!meetd.votes[x] && !u(x).dead && u(x).id) {
						pingees.push(x);
						}
					}
				sock.chat(pingees.join(" "));
				}
			},
		{
			name: "Kick",
			short: "/kick [name]",
			regex: /^kick (\w+)/i,
			callback: function(name) {
				sock.cmd("ban", {
					uid: u(name).id
					});
				}
			},
		{
			name: "Vote",
			short: "/vote [name] OR /nl",
			regex: /^(vote|nl) ?(\w+)?/i,
			callback: function(vote, name) {
				if(vote === "nl" || /^no ?(one)?$/.test(name)) {
					name = "*";
					}
				sock.vote(name || arand(
						meetd.basket ? meetd.basket : Object.keys(users)
						)
					);
				}
			},
		{
			name: "Shoot (with gun)",
			short: "/shoot [name]",
			regex: /^shoot ?(\w+)?/i,
			callback: function(name) {
				sock.vote(name || "*", "gun");
				}
			},
		{
			name: "Highlight user messages",
			short: "/highlight [name]",
			regex: /^(?:h\b|hl|highlight) ?(\w+)?/i,
			callback: function(name) {
				if(!name) {
					if(highlight) {
						highlight = "";
						$(".ej_highlight").removeClass("ej_highlight");
						log("Removed highlighting");
						}
					}
				else {
					highlight = name;
					$(".talk")
						.has(`.talk_username[value="${name}"]`)
						.addClass("ej_highlight");
					log("Highlighting " + name + "'s messages");
					}
				}
			},
		{
			name: "Leave game",
			short: "/leave",
			regex: /^leave|^quit/i,
			callback: function(name) {
				sock.cmd("leave");
				}
			},
		list.copy.join,
		list.copy.host,
		list.copy.games,
		list.copy.pm,
		{
			name: "[Naughty] Will",
			regex: /^will ?(.+)?/i,
			callback: function(will) {
				if(ranked) {
					log("Disabled in ranked games.");
					}
				else if(ej.settings & 0x800000) {
					log("You revised your will.", "lastwill");
					sock.cmd("will", {
						msg: will || ""
						});
					}
				}
			},
		{
			name: "[Naughty] Death Message",
			regex: /^dm (.+)?/i,
            short: "/dm [msg]",
			callback: function(msg) {
				if(ranked) {
					log("Disabled in ranked games.");
					}
				else {
					if(/\(name\)/i.test(msg)) {
						request("GET", "/user/edit_deathmessage?deathmessage="+encodeURIComponent(msg),
								function(response) {
							log("Changed death message to '"+msg.replace(/\(name\)/ig, user)+"'");
							}
							   );
						}
					else {
						log("You forgot (name) in your death message.");
						}
					}
				}
			},
		{
			name: "[Naughty] Dethulu",
			regex: /^(?:dt|thulu) (.+)/i,
            short: "/dethuku [msg]",
			callback: function(message) {
				if(ranked) {
					log("Disabled in ranked games.");
					}
				else {
					sock.cmd("<", {
						meet: meetd.meet,
						msg: message,
						quote: true,
						target: message
						});
					}
				}
			},
		{
			name: "[Naughty] Fakequote",
			regex: /^(?:fq|quote) (\w+) (.+)/i,
            short: "/quote [name] [msg]",
			callback: function(who, message) {
				if(ranked) {
					log("Disabled in ranked games.");
					}
				else {
					sock.cmd("<", {
						meet: meetd.meet,
						msg: message,
						quote: true,
						target: who
						});
					}
				}
			},
        {
			name: "[Naughty] Autobomb",
			regex: /^(?:ab|autobomb) ?(\w+)?/i,
            short: "/autobomb",
			callback: function(name) {
				if(ranked) {
					log("Disabled in ranked games.");
					}
				else {
					if(name) {
						autobomb=name;
						ej.settings|=AUBOMB;
						log("Passing the bomb to "+name);
						}
					else {
						autobomb="";
						ej.settings^=AUBOMB;
						log(ej.settings & AUBOMB ?
							"You're now an anarchist!" :
							"You're now a tree."
						   );
						}
					}
				}
			},
		{
			name: "[Naughty] Fake Sysmessage",
			regex: /^f(s)? ?(\w+)? ?(.+)?/i,
			callback: function(send, id, input) {
				if(ranked) {
					log("Disabled in ranked games.");
					}
				else /* if(ej.settings & 0x800000) */ {
					var	output = this.messages[id];
					if(!output) {
						log("System messages: " + Object.keys(this.messages).join(", "));
						}
					else {
						var	i = 0, args = output.default;
						if(input) {
							args = [];
							while(args.length < output.default.length) {
								if(input) {
									if(args.length === output.default.length-1) {
										args.push(input);
										}
									else {
										i = input.search(/ |$/);
										args.push(input.substring(0, i));
										input = input.substring(i + 1);
										}
									}
								else {
									args.push(output.default[args.length]);
									}
								}
							}
						if(send) {
							sock.chat(sformat(output.msg, args));
							}
						else {
							log(sformat(output.msg, args));
							}
						}
					}
				},
			messages: {
				angel: {
					msg: "You feel an overwhelming, unconditional love for $1. "
					+ "You feel you must protect $1 with your life.",
					default: [self]
					},
				auto: {
					msg: "There might be an autocrat among you...",
					default: []
					},
				bleed: {
					msg: "You start to bleed...",
					default: []
					},
				bomb: {
					msg: "$1 rushes at $2 and reveals a bomb!",
					default: [self, self]
					},
				carol: {
					msg: "You see a merry Caroler outside your house! "
					+ "They sing you a Carol about $1, $2, $3. At least one of which is the Mafia!",
					default: [self, self, self]
					},
				chef: {
					msg: "You find yourself in a dimly lit banquet! "
					+ "You sense the presence of a masked guest. The guest appears to be a $1.",
					default: ["ninja"]
					},
				cm: {
					msg: "You glance at your watch. The time is now $1 o'clock.",
					default: ["11"]
					},
				cmlife: {
					msg: "Your watch whispers to you. You have one extra life.",
					default: []
					},
				confess: {
					msg: "At the confessional tonight, a $1 had visited you to confess their sins.",
					default: ["survivor"]
					},
				cop: {
					msg: "After investigations, you suspect that $1 is sided with the $2.",
					default: [self, "mafia"]
					},
				cry: {
					msg: "Someone cries out | $1",
					default: [""]
					},
				det: {
					msg: "Through your detective work, you learned that $1 is a $2!",
					default: [self, "ninja"]
					},
				disc: {
					msg: "You discover that $1 is the $2!",
					default: [self, "interceptor"]
					},
				dream: {
					msg: "You had a dream... where at least one of $1, $2, $3 is a mafia...",
					default: [self, self, self]
					},
				fire: {
					msg: "Somebody threw a match into the crowd! " +
					"$1 suddenly lights on fire and burns to a crisp!",
					default: [self]
					},
				firefail: {
					msg: "Somebody threw a match into the crowd!",
					default: []
					},
				guise: {
					msg: "You are now disguised as $1.",
					default: [self]
					},
				guised: {
					msg: "$1 has stolen your identity!",
					default: [self]
					},
				gun: {
					msg: "You hear a gunshot!",
					default: []
					},
				gunfail: {
					msg: "$1 reveals a gun! The gun backfires!",
					default: [self]
					},
				gunhit: {
					msg: "$1 reveals a gun and shoots $2!",
					default: [self, self]
					},
				hit: {
					msg: "A bullet hits your vest! You cannot survive another hit!",
					default: []
					},
				invis: {
					msg: "Someone whispers $1",
					default: [""]
					},
				item: {
					msg: "You received a $1!",
					default: ["key"]
					},
				jail: {
					msg: "You have been blindfolded and sent to jail!",
					default: []
					},
				jan: {
					msg: "While cleaning up the mess, you learned that $1 was a $2.",
					default: [self, "cop"]
					},
				janday: {
					msg: "$1 is missing!",
					default: [self]
					},
				journ: {
					msg: "You received all reports that $1 received: ($2).",
					default: [self, ""]
					},
				learn: {
					msg: "You learn that $1 is a $2",
					default: [self, "cop"]
					},
				lm: {
					msg: "A loud voice was heard during the night: \"Curses! $1 woke me from my slumber!\"",
					default: [self]
					},
				lonely: {
					msg: "You spent a silent and lonely night at church. No one came to visit you.",
					default: []
					},
				love: {
					msg: "During the night, you fall in love with $1 after a romantic conversation!",
					default: [self]
					},
				lynch: {
					msg: "You feel very irritated by $1.",
					default: [self]
					},
				matin: {
					msg: "Penguins be matin'",
					default: []
					},
				message: {
					msg: "You received a message: $1",
					default: [""]
					},
				mfail: {
					msg: "No matter how much you worked your magic, $1 and $2 refuses to fall in love!",
					default: [self, self]
					},
				mlove: {
					msg: "You cast a Christmas spell on $1 and $2... they are now in love!",
					default: [self, self]
					},
				mm: {
					msg: "There might be a mastermind among you...",
					default: []
					},
				mort: {
					msg: "You learned that $1 is a $2!",
					default: [self, "villager"]
					},
				party: {
					msg: "You find yourself at a vibrant party!",
					default: []
					},
				pengi: {
					msg: "During the night a fluffy penguin visits you and tells you that " +
					"$1 is carrying a $2.",
					default: [self, self]
					},
				pengno: {
					msg: "During the night a fluffy penguin visits you and tells you that " +
					"$1 has taken no action over the course of the night.",
					default: [self]
					},
				poison: {
					msg: "You feel sick, as though you had been poisoned!",
					default: []
					},
				pop: {
					msg: "$1 feels immensely frustrated!",
					default: [self]
					},
				psy: {
					msg: "You read $1's mind... they are thinking $2 thoughts.",
					default: [self, "evil"]
					},
				psyfail: {
					msg: "You tried to read $1's mind, but something distracted you.",
					default: [self]
					},
				pvis: {
					msg: "During the night a fluffy penguin visits you and tells you that " +
					"$1 visited $2.",
					default: [self, "no one"]
					},
				pvisd: {
					msg: "During the night a fluffy penguin visits you and tells you that " +
					"$1 was visited by $2.",
					default: [self, "no one"]
					},
				santa: {
					msg: "After going out on your sleigh, you find that $1 is $2!",
					default: [self, "neither naughty nor nice"]
					},
				snoop: {
					msg: "After some snooping, you find out $1 is carrying $3 $2.",
					default: [self, "gun", "1"]
					},
				noitems: {
					msg: "After some snooping, you find out $1 is not carrying any items..",
					default: [self]
					},
				stalk: {
					msg: "Through stalking, you learned that $1 is a $2!",
					default: [self, "journalist"]
					},
				thulu: {
					msg: "You were witness to an unimaginable evil... you cannot forget... "
					+ "your mind descends into eternal hell.",
					default: []
					},
				track: {
					msg: "You followed $1 throughout the night. $1 visited $2.",
					default: [self, "no one"]
					},
				tree: {
					msg: "You became a tree!",
					default: []
					},
				trust: {
					msg: "You had a dream... you learned you can trust $1...",
					default: [self]
					},
				virgin: {
					msg: "The virgin has been sacrified!",
					default: []
					},
				voodoo: {
					msg: "$1 suddenly feels a chill and falls to the ground!",
					default: [self]
					},
				watch: {
					msg: "You watched $1 throughout the night. $2 has visited $1.",
					default: [self, "No one"]
					},
				will: {
					msg: "You read the will of $1, it reads: $2",
					default: [self, ""]
					},
				ww: {
					msg: "You devoured a human and feel very powerful... "
					+ "as though you are immortal for the day!",
					default: []
					}
				}
			}
		];

	// lobby commands
	list.lobby = [
		list.copy.sc,
		{
			name: "About",
			short: "/help",
			regex: /^(?:info|help|about) ?(.+)?/i,
			callback: function(topic) {
				if(this.topics[topic]) {
					log(this.topics[topic]);
					}
				else {
					log("You can /join games and toggle /fmt on or off (/help fmt for more info)");
					}
				},
			topics: {
				"fmt": "/fmt on enables chat formatting like displaying images for messages beginning "
				+ "with $img ($img [url])"
				}
			},
		list.copy.eval,
		list.copy.emotes,
		list.copy.fmt,
		list.copy.say,
		list.copy.join,
		list.copy.host,
		list.copy.games,
		list.copy.pm
		];

	// this is a sin
	list.bot = [
		{
			name: "Scriptcheck",
			short: "@bot sc",
			regex: /^sc|^scriptcheck/,
			callback: function(data) {
				sock.chat(ej.name + ej.vstring, data.user);
				}
			}
		];

	// utility
	function u(name) {
		return users[name || self] || u.make({
			id: 0,
			username: name || self
			});
		};
	u.make = function(data) {
		Object.assign(data, {
			name: data.username || data.user,
			emjack: null,
			role: null,
			meet: meetd.meet,
			mafia: false,
			dead: false,
			muted: false,
			kucode: 1,
			kuclock: Infinity
			});
		users[data.name] = data;
		if(data.emotes) {
			data.emotes = JSON.parse(data.emotes);
			}
		return data;
		};
	function log(message, classes) {
		var	node = document.createElement("div");
		node.className = classes ? "log emjack " + classes : "log emjack";
		typeof message === "string" ?
			node.textContent = message :
		node.appendChild(message);
		if(chat.scrollTop >= chat.scrollHeight-chat.clientHeight) {
			requestAnimationFrame(function() {
				chat.scrollTop = chat.scrollHeight;
				});
			}
		if(page === "mafia") {
			chat.appendChild(node);
			}
		else {
			chat.insertBefore(node, chat.lastChild);
			}
		};
	function request(method, url, callback) {
		var	req = new XMLHttpRequest();
		req.open(method, url, true);
		req.onreadystatechange = function(event) {
			if(this.readyState === 4) {
				callback.call(this, this.responseText);
				}
			};
		req.send();
		};
	function arand(array) {
		return array[Math.floor(Math.random()*array.length)];
		};
	function rchar(x, y) {
		return String.fromCharCode(
			x + Math.floor(Math.random()*(y-x))
			);
		};
	function sformat(string, data) {
		return string.replace(/\$(\d+)/g, function(match, i) {
			return data[i-1];
			});
		};

	// keep chat
	if(page === "mafia") {
		$("#speak_container").css("display", "initial !important");
		}

	// townie input
	var	chat = document.getElementById(page === "mafia" ? "window" : "window_i"),
		typebox = document.getElementById(page === "mafia" ? "typebox" : "chatbar");
	$(typebox).on("keydown", function(event) {
		if(event.which === 13 && this.value[0]==="/") {
			ej.run(this.value.substring(1), page === "mafia" ? list.chat : list.lobby);
			this.value = "";
			}
		});
	if(page === "mafia") {
		$("textarea.notes")
			.on("focus", function(event) {
				if(ej.settings & UNOTES && !ranked) {
					this.value = notes[$(".user_header > h2").text()];
					}
				})
			.on("keyup", function(event) {
				if(ej.settings & UNOTES && !ranked) {
					notes[$(".user_header > h2").text()] = this.value;
					}
				});
		}

	// clickables
	if(window.vocabs) {
		vocabs.push("https?://\\S + ");
		}
	window.addEventListener("click", function(event) {
		var	classList = event.target.classList;
		if(classList.contains("msg")) {
			if(ej.settings & MSGMRK) {
				var	mark = keys & K_SHIFT ? "ej_mark_alt" : "ej_mark";
				if(classList.contains(mark)) {
					classList.remove(mark);
					}
				else {
					classList.add(mark);
					classList.remove(keys & K_SHIFT ? "ej_mark" : "ej_mark_alt");
					}
				}
			}
		else if(classList.contains("acronym")) {
			if(/https?:\/\//i.test(event.target.textContent)) {
				window.open(event.target.textContent, "_blank");
				event.stopPropagation();
				}
			}
		}, true);

	// clean up
	var	last_error = null;
	$(document).on("keydown", function(event) {
		if(event.target === document.body && !event.altKey && !event.ctrlKey && !event.metaKey) {
			typebox.focus();
			}
		});
	$(document).on("click", ".roleimg.tt", function(event) {
		$(this).removeClass("tt");
		});
	$(document).on("mouseenter", ".roleimg:not(.tt, .role-unknown)", function(event) {
		if(!$(this).parents(".tip").length) {
			$(this)
				.data("type", "roleinfo")
				.data("align", "right")
				.data("roleid", this.className.match(/role-(\w+)/)[1])
				.addClass("tt")
				.trigger("mouseover");
		    }
		});
	window.addEventListener("error", function(event) {
		var	message = event.error.message;
		if(message !== last_error) {
			log("You've got error!", "bold");
			log(last_error = message);
			console.log(event);
			}
		});
	window.addEventListener("beforeunload", function(event) {
		localStorage.ejs = ej.settings;
		if(ej.settings & UNOTES && !ranked) {
			localStorage.notes = JSON.stringify(notes);
			}
		if(window.setup_id) {
			localStorage.ejsid = setup_id;
			}
		});
	window.addEventListener("keyup", function(event) {
		if(event.which === 16) {
			keys &=~ K_SHIFT;
			}
		else if(event.which === 192) {
			keys &=~ K_DEBUG;
			}
		});
	window.addEventListener("keydown", function(event) {
		if(event.ctrlKey) {
			if(event.which === 66) {
				sock.cmd("option", {
					field: "fastgame"
					});
				sock.cmd("option", {
					field: "nospectate"
					});
				}
			else if(event.which === 81) {
				ej.run("fq " + self + " " + typebox.value, list.chat);
				typebox.value = "";
				}
			}
		else if(event.target.value === undefined) {
			if(event.which === 16) {
				keys |= K_SHIFT;
				}
			else if(event.which === 192) {
				keys |= K_DEBUG;
				}
			}
		});

	}

// add node
function inject(parent, tag, type, content) {
	var	node = document.createElement(tag);
	node.type = type;
	node.appendChild(
		document.createTextNode(content)
		);
	return parent.appendChild(node);
	};

// jack in
inject(document.head, "style", "text/css", `
	.log {
	color: #bb4444;
		}
	.notop {
	margin-top: 0 !important
		}
	.ej_mark {
	background-color: rgba(250, 50, 250, 0.5);
		}
	.ej_mark_alt {
	background-color: rgba(250, 150, 0, 0.5);
		}
	.ej_highlight {
	background-color: rgba(255, 255, 0, 0.5);
		}
	.ej_img * {
	max-width: 100%;
		}
	.meet_username {
	cursor: pointer;
		}
	#lobbychat #window {
	width: 100% !important;
		}
	#lobbychat #window_i {
	width: auto !important;
		}
	`);
setTimeout(function() {
	inject(document.body, "script", "text/javascript", "(" + emjack.toString() + ")()");
	document.body.addEventListener("contextmenu", function(event) {
		event.stopPropagation();
		}, true);
	});