<|start_filename|>src/plugins/inputs/index.js<|end_filename|>
const clientSideCommands = ["/collapse", "/expand", "/search"];

const passThroughCommands = [
	"/as",
	"/bs",
	"/cs",
	"/ho",
	"/hs",
	"/join",
	"/ms",
	"/ns",
	"/os",
	"/rs",
];

const userInputs = [
	"action",
	"away",
	"ban",
	"connect",
	"ctcp",
	"disconnect",
	"ignore",
	"invite",
	"kick",
	"kill",
	"list",
	"mode",
	"msg",
	"nick",
	"notice",
	"part",
	"quit",
	"raw",
	"rejoin",
	"topic",
	"whois",
].reduce(function (plugins, name) {
	const plugin = require(`./${name}`);
	plugin.commands.forEach((command) => plugins.set(command, plugin));
	return plugins;
}, new Map());

const pluginCommands = new Map();

const getCommands = () =>
	Array.from(userInputs.keys())
		.concat(Array.from(pluginCommands.keys()))
		.map((command) => `/${command}`)
		.concat(clientSideCommands)
		.concat(passThroughCommands)
		.sort();

const addPluginCommand = (packageInfo, command, func) => {
	func.packageInfo = packageInfo;
	pluginCommands.set(command, func);
};

module.exports = {
	addPluginCommand,
	getCommands,
	pluginCommands,
	userInputs,
};


<|start_filename|>client/js/store.js<|end_filename|>
import Vue from "vue";
import Vuex from "vuex";
import {createSettingsStore} from "./store-settings";
import storage from "./localStorage";

const appName = document.title;

Vue.use(Vuex);

function detectDesktopNotificationState() {
	if (!("Notification" in window)) {
		return "unsupported";
	} else if (Notification.permission === "granted") {
		return "granted";
	} else if (!window.isSecureContext) {
		return "nohttps";
	}

	return "blocked";
}

const store = new Vuex.Store({
	state: {
		appLoaded: false,
		activeChannel: null,
		currentUserVisibleError: null,
		desktopNotificationState: detectDesktopNotificationState(),
		isAutoCompleting: false,
		isConnected: false,
		networks: [],
		mentions: [],
		hasServiceWorker: false,
		pushNotificationState: "unsupported",
		serverConfiguration: null,
		sessions: [],
		sidebarOpen: false,
		sidebarDragging: false,
		userlistOpen: storage.get("thelounge.state.userlist") !== "false",
		versionData: null,
		versionStatus: "loading",
		versionDataExpired: false,
		serverHasSettings: false,
		messageSearchResults: null,
		messageSearchInProgress: false,
		searchEnabled: false,
	},
	mutations: {
		appLoaded(state) {
			state.appLoaded = true;
		},
		activeChannel(state, channel) {
			state.activeChannel = channel;
		},
		currentUserVisibleError(state, error) {
			state.currentUserVisibleError = error;
		},
		refreshDesktopNotificationState(state) {
			state.desktopNotificationState = detectDesktopNotificationState();
		},
		isAutoCompleting(state, isAutoCompleting) {
			state.isAutoCompleting = isAutoCompleting;
		},
		isConnected(state, payload) {
			state.isConnected = payload;
		},
		networks(state, networks) {
			state.networks = networks;
		},
		mentions(state, mentions) {
			state.mentions = mentions;
		},
		removeNetwork(state, networkId) {
			state.networks.splice(
				store.state.networks.findIndex((n) => n.uuid === networkId),
				1
			);
		},
		sortNetworks(state, sortFn) {
			state.networks.sort(sortFn);
		},
		hasServiceWorker(state) {
			state.hasServiceWorker = true;
		},
		pushNotificationState(state, pushNotificationState) {
			state.pushNotificationState = pushNotificationState;
		},
		serverConfiguration(state, serverConfiguration) {
			state.serverConfiguration = serverConfiguration;
		},
		sessions(state, payload) {
			state.sessions = payload;
		},
		sidebarOpen(state, payload) {
			state.sidebarOpen = payload;
		},
		sidebarDragging(state, payload) {
			state.sidebarDragging = payload;
		},
		toggleSidebar(state) {
			state.sidebarOpen = !state.sidebarOpen;
		},
		toggleUserlist(state) {
			state.userlistOpen = !state.userlistOpen;
		},
		userlistOpen(state, payload) {
			state.userlistOpen = payload;
		},
		versionData(state, payload) {
			state.versionData = payload;
		},
		versionStatus(state, payload) {
			state.versionStatus = payload;
		},
		versionDataExpired(state, payload) {
			state.versionDataExpired = payload;
		},
		serverHasSettings(state, value) {
			state.serverHasSettings = value;
		},
		messageSearchInProgress(state, value) {
			state.messageSearchInProgress = value;
		},
		messageSearchResults(state, value) {
			state.messageSearchResults = value;
		},
		addMessageSearchResults(state, value) {
			// Append the search results and add networks and channels to new messages
			value.results = [...state.messageSearchResults.results, ...value.results];

			state.messageSearchResults = value;
		},
	},
	getters: {
		findChannelOnCurrentNetwork: (state) => (name) => {
			name = name.toLowerCase();
			return state.activeChannel.network.channels.find((c) => c.name.toLowerCase() === name);
		},
		findChannelOnNetwork: (state) => (networkUuid, channelName) => {
			for (const network of state.networks) {
				if (network.uuid !== networkUuid) {
					continue;
				}

				for (const channel of network.channels) {
					if (channel.name === channelName) {
						return {network, channel};
					}
				}
			}

			return null;
		},
		findChannel: (state) => (id) => {
			for (const network of state.networks) {
				for (const channel of network.channels) {
					if (channel.id === id) {
						return {network, channel};
					}
				}
			}

			return null;
		},
		findNetwork: (state) => (uuid) => {
			for (const network of state.networks) {
				if (network.uuid === uuid) {
					return network;
				}
			}

			return null;
		},
		highlightCount(state) {
			let highlightCount = 0;

			for (const network of state.networks) {
				for (const channel of network.channels) {
					highlightCount += channel.highlight;
				}
			}

			return highlightCount;
		},
		title(state, getters) {
			const alertEventCount = getters.highlightCount ? `(${getters.highlightCount}) ` : "";

			const channelname = state.activeChannel ? `${state.activeChannel.channel.name} — ` : "";

			return alertEventCount + channelname + appName;
		},
		initChannel: () => (channel) => {
			// TODO: This should be a mutation
			channel.pendingMessage = "";
			channel.inputHistoryPosition = 0;

			channel.inputHistory = [""].concat(
				channel.messages
					.filter((m) => m.self && m.text && m.type === "message")
					.map((m) => m.text)
					.reverse()
					.slice(null, 99)
			);
			channel.historyLoading = false;
			channel.scrolledToBottom = true;
			channel.editTopic = false;

			channel.moreHistoryAvailable = channel.totalMessages > channel.messages.length;
			delete channel.totalMessages;

			if (channel.type === "channel") {
				channel.usersOutdated = true;
			}

			return channel;
		},
	},
});

// Settings module is registered dynamically because it benefits
// from a direct reference to the store
store.registerModule("settings", createSettingsStore(store));

export default store;


<|start_filename|>test/models/network.js<|end_filename|>
"use strict";

const expect = require("chai").expect;
const Chan = require("../../src/models/chan");
const Msg = require("../../src/models/msg");
const User = require("../../src/models/user");
const Network = require("../../src/models/network");
const Helper = require("../../src/helper");

describe("Network", function () {
	describe("#export()", function () {
		it("should produce an valid object", function () {
			const network = new Network({
				uuid: "hello world",
				awayMessage: "I am away",
				name: "networkName",
				sasl: "plain",
				saslAccount: "testaccount",
				saslPassword: "<PASSWORD>",
				channels: [
					new Chan({name: "#thelounge", key: ""}),
					new Chan({name: "&foobar", key: ""}),
					new Chan({name: "#secret", key: "foo"}),
					new Chan({name: "&secure", key: "bar"}),
					new Chan({name: "Channel List", type: Chan.Type.SPECIAL}),
					new Chan({name: "PrivateChat", type: Chan.Type.QUERY}),
				],
			});
			network.setNick("chillin`");

			expect(network.export()).to.deep.equal({
				uuid: "hello world",
				awayMessage: "I am away",
				name: "networkName",
				host: "",
				port: 6667,
				tls: false,
				userDisconnected: false,
				rejectUnauthorized: false,
				password: "",
				username: "",
				realname: "",
				leaveMessage: "",
				sasl: "plain",
				saslAccount: "testaccount",
				saslPassword: "<PASSWORD>",
				commands: [],
				nick: "chillin`",
				proxyEnabled: false,
				proxyHost: "",
				proxyPort: 1080,
				proxyUsername: "",
				channels: [
					{name: "#thelounge", key: ""},
					{name: "&foobar", key: ""},
					{name: "#secret", key: "foo"},
					{name: "&secure", key: "bar"},
					{name: "PrivateChat", type: "query"},
				],
				ignoreList: [],
			});
		});

		it("validate should set correct defaults", function () {
			Helper.config.defaults.nick = "";

			const network = new Network({
				host: "localhost",
			});

			expect(network.validate()).to.be.true;
			expect(network.nick).to.equal("thelounge");
			expect(network.username).to.equal("thelounge");
			expect(network.realname).to.equal("The Lounge User");
			expect(network.port).to.equal(6667);

			const network2 = new Network({
				host: "localhost",
				nick: "@Invalid Nick?",
			});
			expect(network2.validate()).to.be.true;
			expect(network2.username).to.equal("InvalidNick");
		});

		it("lockNetwork should be enforced when validating", function () {
			Helper.config.lockNetwork = true;

			// Make sure we lock in private mode
			Helper.config.public = false;

			const network = new Network({
				host: "",
				port: 1337,
				tls: false,
				rejectUnauthorized: false,
			});
			expect(network.validate()).to.be.true;
			expect(network.host).to.equal("irc.example.com");
			expect(network.port).to.equal(6697);
			expect(network.tls).to.be.true;
			expect(network.rejectUnauthorized).to.be.true;

			// Make sure we lock in public mode (also resets public=true for other tests)
			Helper.config.public = true;

			const network2 = new Network({
				host: "some.fake.tld",
			});
			expect(network2.validate()).to.be.true;
			expect(network2.host).to.equal("irc.example.com");

			Helper.config.lockNetwork = false;
		});

		it("editing a network should enforce correct types", function () {
			let saveCalled = false;
			let nameEmitCalled = false;

			const network = new Network();
			network.edit(
				{
					emit(name, data) {
						if (name === "network:name") {
							nameEmitCalled = true;
							expect(data.uuid).to.equal(network.uuid);
							expect(data.name).to.equal("Lounge Test Network");
						}
					},
					save() {
						saveCalled = true;
					},
				},
				{
					nick: "newNick",
					host: "new.tld",
					name: "Lounge Test Network",
					port: "1337",
					tls: undefined,
					rejectUnauthorized: undefined,
					username: 1234,
					password: <PASSWORD>,
					realname: <PASSWORD>,
					sasl: "something",
					saslAccount: 1337,
					saslPassword: <PASSWORD>,
					commands: "/command 1 2 3\r\n/ping HELLO\r\r\r\r/whois test\r\n\r\n",
					ip: "newIp",
					hostname: "newHostname",
					uuid: "newuuid",
				}
			);

			expect(saveCalled).to.be.true;
			expect(nameEmitCalled).to.be.true;
			expect(network.uuid).to.not.equal("newuuid");
			expect(network.ip).to.be.undefined;
			expect(network.hostname).to.be.undefined;

			expect(network.name).to.equal("Lounge Test Network");
			expect(network.channels[0].name).to.equal("Lounge Test Network");

			expect(network.nick).to.equal("newNick");
			expect(network.host).to.equal("new.tld");
			expect(network.port).to.equal(1337);
			expect(network.tls).to.be.false;
			expect(network.rejectUnauthorized).to.be.false;
			expect(network.username).to.equal("1234");
			expect(network.password).to.equal("4567");
			expect(network.realname).to.equal("8901");
			expect(network.sasl).to.equal("");
			expect(network.saslAccount).to.equal("1337");
			expect(network.saslPassword).to.equal("<PASSWORD>");
			expect(network.commands).to.deep.equal([
				"/command 1 2 3",
				"/ping HELLO",
				"/whois test",
			]);
		});

		it("should generate uuid (v4) for each network", function () {
			const network1 = new Network();
			const network2 = new Network();

			expect(network1.uuid).to.have.lengthOf(36);
			expect(network2.uuid).to.have.lengthOf(36);
			expect(network1.uuid).to.not.equal(network2.uuid);
		});

		it("lobby should be at the top", function () {
			const network = new Network({
				name: "Super Nice Network",
				channels: [
					new Chan({name: "AAAA!", type: Chan.Type.QUERY}),
					new Chan({name: "#thelounge"}),
					new Chan({name: "&foobar"}),
				],
			});
			network.channels.push(new Chan({name: "#swag"}));

			expect(network.channels[0].name).to.equal("Super Nice Network");
			expect(network.channels[0].type).to.equal(Chan.Type.LOBBY);
		});

		it("should maintain channel reference", function () {
			const chan = new Chan({
				name: "#506-bug-fix",
				messages: [
					new Msg({
						text: "message in constructor",
					}),
				],
			});

			const network = new Network({
				name: "networkName",
				channels: [chan],
			});

			chan.messages.push(
				new Msg({
					text: "message in original instance",
				})
			);

			network.channels[1].messages.push(
				new Msg({
					text: "message after network creation",
				})
			);

			expect(network.channels[1].messages).to.have.lengthOf(3);
			expect(network.channels[1].messages[0].text).to.equal("message in constructor");
			expect(network.channels[1].messages[1].text).to.equal("message in original instance");
			expect(network.channels[1].messages[2].text).to.equal("message after network creation");
		});
	});

	describe("#getFilteredClone(lastActiveChannel, lastMessage)", function () {
		it("should filter channels", function () {
			const chan = new Chan();
			chan.setUser(new User({nick: "test"}));

			const network = new Network({
				channels: [chan],
			});

			expect(network.channels[0].users).to.be.empty;
		});

		it("should keep necessary properties", function () {
			const network = new Network();
			const clone = network.getFilteredClone();

			expect(clone)
				.to.be.an("object")
				.that.has.all.keys("channels", "status", "nick", "name", "serverOptions", "uuid");

			expect(clone.status).to.be.an("object").that.has.all.keys("connected", "secure");
		});
	});

	describe("#addChannel(newChan)", function () {
		it("should add channel", function () {
			const chan = new Chan({name: "#thelounge"});

			const network = new Network({
				channels: [chan],
			});
			// Lobby and initial channel
			expect(network.channels.length).to.equal(2);

			const newChan = new Chan({name: "#foo"});
			network.addChannel(newChan);

			expect(network.channels.length).to.equal(3);
		});

		it("should add channel alphabetically", function () {
			const chan1 = new Chan({name: "#abc"});
			const chan2 = new Chan({name: "#thelounge"});
			const chan3 = new Chan({name: "#zero"});

			const network = new Network({
				channels: [chan1, chan2, chan3],
				name: "foo",
			});

			const newChan = new Chan({name: "#foo"});
			network.addChannel(newChan);

			expect(network.channels[0].name).to.equal("foo");
			expect(network.channels[1]).to.equal(chan1);
			expect(network.channels[2]).to.equal(newChan);
			expect(network.channels[3]).to.equal(chan2);
			expect(network.channels[4]).to.equal(chan3);
		});

		it("should sort case-insensitively", function () {
			const chan1 = new Chan({name: "#abc"});
			const chan2 = new Chan({name: "#THELOUNGE"});

			const network = new Network({
				channels: [chan1, chan2],
			});

			const newChan = new Chan({name: "#foo"});
			network.addChannel(newChan);

			expect(network.channels[1]).to.equal(chan1);
			expect(network.channels[2]).to.equal(newChan);
			expect(network.channels[3]).to.equal(chan2);
		});

		it("should sort users separately from channels", function () {
			const chan1 = new Chan({name: "#abc"});
			const chan2 = new Chan({name: "#THELOUNGE"});

			const network = new Network({
				channels: [chan1, chan2],
			});

			const newUser = new Chan({name: "mcinkay", type: Chan.Type.QUERY});
			network.addChannel(newUser);

			expect(network.channels[1]).to.equal(chan1);
			expect(network.channels[2]).to.equal(chan2);
			expect(network.channels[3]).to.equal(newUser);
		});

		it("should sort users alphabetically", function () {
			const chan1 = new Chan({name: "#abc"});
			const chan2 = new Chan({name: "#THELOUNGE"});
			const user1 = new Chan({name: "astorije", type: Chan.Type.QUERY});
			const user2 = new Chan({name: "xpaw", type: Chan.Type.QUERY});

			const network = new Network({
				channels: [chan1, chan2, user1, user2],
			});

			const newUser = new Chan({name: "mcinkay", type: Chan.Type.QUERY});
			network.addChannel(newUser);

			expect(network.channels[1]).to.equal(chan1);
			expect(network.channels[2]).to.equal(chan2);
			expect(network.channels[3]).to.equal(user1);
			expect(network.channels[4]).to.equal(newUser);
			expect(network.channels[5]).to.equal(user2);
		});

		it("should not sort special channels", function () {
			const chan1 = new Chan({name: "#abc"});
			const chan2 = new Chan({name: "#THELOUNGE"});
			const user1 = new Chan({name: "astorije", type: Chan.Type.QUERY});
			const user2 = new Chan({name: "xpaw", type: Chan.Type.QUERY});

			const network = new Network({
				channels: [chan1, chan2, user1, user2],
			});

			const newBanlist = new Chan({name: "Banlist for #THELOUNGE", type: Chan.Type.SPECIAL});
			network.addChannel(newBanlist);

			expect(network.channels[1]).to.equal(chan1);
			expect(network.channels[2]).to.equal(chan2);
			expect(network.channels[3]).to.equal(user1);
			expect(network.channels[4]).to.equal(user2);
			expect(network.channels[5]).to.equal(newBanlist);
		});

		it("should not compare against special channels", function () {
			const chan1 = new Chan({name: "#abc"});
			const chan2 = new Chan({name: "#THELOUNGE"});
			const user1 = new Chan({name: "astorije", type: Chan.Type.QUERY});

			const network = new Network({
				channels: [chan1, chan2, user1],
			});

			const newBanlist = new Chan({name: "Banlist for #THELOUNGE", type: Chan.Type.SPECIAL});
			network.addChannel(newBanlist);
			const newUser = new Chan({name: "mcinkay", type: Chan.Type.QUERY});
			network.addChannel(newUser);

			expect(network.channels[1]).to.equal(chan1);
			expect(network.channels[2]).to.equal(chan2);
			expect(network.channels[3]).to.equal(user1);
			expect(network.channels[4]).to.equal(newUser);
			expect(network.channels[5]).to.equal(newBanlist);
		});

		it("should insert before first special channel", function () {
			const banlist = new Chan({name: "Banlist for #THELOUNGE", type: Chan.Type.SPECIAL});
			const chan1 = new Chan({name: "#thelounge"});
			const user1 = new Chan({name: "astorije", type: Chan.Type.QUERY});

			const network = new Network({
				channels: [banlist, chan1, user1],
			});

			const newChan = new Chan({name: "#foo"});
			network.addChannel(newChan);

			expect(network.channels[1]).to.equal(newChan);
			expect(network.channels[2]).to.equal(banlist);
			expect(network.channels[3]).to.equal(chan1);
			expect(network.channels[4]).to.equal(user1);
		});

		it("should never add something in front of the lobby", function () {
			const network = new Network({
				name: "foo",
				channels: [],
			});

			const newUser = new Chan({name: "astorije"});
			network.addChannel(newUser);

			expect(network.channels[1]).to.equal(newUser);
		});
	});
});
