Repository: thelounge/thelounge
Pull request #4287 (2021-07-17 22:56:31)

Revert #4186 "Support animated webp images"
Reverts #4186. See https://github.com/thelounge/thelounge/pull/4257 for reasoning



MaxLeiter: @Nachtalb can you make sure I didn't miss anything? 

Patch:
diff --git a/client/components/Windows/Settings.vue b/client/components/Windows/Settings.vue
index 1c6e264f..6847f705 100644
--- a/client/components/Windows/Settings.vue
+++ b/client/components/Windows/Settings.vue
@@ -248,11 +248,18 @@
 				<div>
 					<label class="opt">
 						<input
-							:checked="$store.state.settings.removeImageMetadata"
+							:checked="$store.state.settings.uploadCanvas"
 							type="checkbox"
-							name="removeImageMetadata"
+							name="uploadCanvas"
 						/>
-						Remove metadata from uploaded images
+						Attempt to remove metadata from images before uploading
+						<span
+							class="tooltipped tooltipped-n tooltipped-no-delay"
+							aria-label="This option renders the image into a canvas element to remove metadata from the image.
+This may break orientation if your browser does not support that."
+						>
+							<button class="extra-help" />
+						</span>
 					</label>
 				</div>
 			</div>
@@ -367,7 +374,7 @@
 						Custom highlights
 						<span
 							class="tooltipped tooltipped-n tooltipped-no-delay"
-							aria-label="If a message contains any of these comma-separated
+							aria-label="If a message contains any of these comma-separated 
 expressions, it will trigger a highlight."
 						>
 							<button class="extra-help" />
@@ -390,8 +397,8 @@ expressions, it will trigger a highlight."
 						Highlight exceptions
 						<span
 							class="tooltipped tooltipped-n tooltipped-no-delay"
-							aria-label="If a message contains any of these comma-separated
-expressions, it will not trigger a highlight even if it contains
+							aria-label="If a message contains any of these comma-separated 
+expressions, it will not trigger a highlight even if it contains 
 your nickname or expressions defined in custom highlights."
 						>
 							<button class="extra-help" />
diff --git a/client/js/helpers/contextMenu.js b/client/js/helpers/contextMenu.js
index feadd15c..b17c3794 100644
--- a/client/js/helpers/contextMenu.js
+++ b/client/js/helpers/contextMenu.js
@@ -252,22 +252,30 @@ export function generateUserContextMenu($root, channel, network, user) {
 		return items;
 	}
 
-	// Names of the modes we are able to change
-	const modes = {
-		"~": ["owner", "q"],
-		"&": ["admin", "a"],
-		"@": ["operator", "o"],
-		"%": ["half-op", "h"],
-		"+": ["voice", "v"],
+	// Names of the standard modes we are able to change
+	const modeCharToName = {
+		"~": "owner",
+		"&": "admin",
+		"@": "operator",
+		"%": "half-op",
+		"+": "voice",
 	};
 
-	// Labels for the mode changes.  For example .rev(['admin', 'a']) => 'Revoke admin (-a)'
+	// Labels for the mode changes.  For example .rev({mode: "a", symbol: "&"}) => 'Revoke admin (-a)'
 	const modeTextTemplate = {
-		revoke: (m) => `Revoke ${m[0]} (-${m[1]})`,
-		give: (m) => `Give ${m[0]} (+${m[1]})`,
+		revoke(m) {
+			const name = modeCharToName[m.symbol];
+			const res = name ? `Revoke ${name} (-${m.mode})` : `Mode -${m.mode}`;
+			return res;
+		},
+		give(m) {
+			const name = modeCharToName[m.symbol];
+			const res = name ? `Give ${name} (+${m.mode})` : `Mode +${m.mode}`;
+			return res;
+		},
 	};
 
-	const networkModes = network.serverOptions.PREFIX;
+	const networkModeSymbols = network.serverOptions.PREFIX.symbols;
 
 	/**
 	 * Determine whether the prefix of mode p1 has access to perform actions on p2.
@@ -284,38 +292,38 @@ export function generateUserContextMenu($root, channel, network, user) {
 	function compare(p1, p2) {
 		// The modes ~ and @ can perform actions on their own mode.  The others on modes below.
 		return "~@".indexOf(p1) > -1
-			? networkModes.indexOf(p1) <= networkModes.indexOf(p2)
-			: networkModes.indexOf(p1) < networkModes.indexOf(p2);
+			? networkModeSymbols.indexOf(p1) <= networkModeSymbols.indexOf(p2)
+			: networkModeSymbols.indexOf(p1) < networkModeSymbols.indexOf(p2);
 	}
 
-	networkModes.forEach((prefix) => {
-		if (!compare(currentChannelUser.modes[0], prefix)) {
+	network.serverOptions.PREFIX.prefix.forEach((mode) => {
+		if (!compare(currentChannelUser.modes[0], mode.symbol)) {
 			// Our highest mode is below the current mode.  Bail.
 			return;
 		}
 
-		if (!user.modes.includes(prefix)) {
+		if (!user.modes.includes(mode.symbol)) {
 			// The target doesn't already have this mode, therefore we can set it.
 			items.push({
-				label: modeTextTemplate.give(modes[prefix]),
+				label: modeTextTemplate.give(mode),
 				type: "item",
 				class: "action-set-mode",
 				action() {
 					socket.emit("input", {
 						target: channel.id,
-						text: "/mode +" + modes[prefix][1] + " " + user.nick,
+						text: "/mode +" + mode.mode + " " + user.nick,
 					});
 				},
 			});
 		} else {
 			items.push({
-				label: modeTextTemplate.revoke(modes[prefix]),
+				label: modeTextTemplate.revoke(mode),
 				type: "item",
 				class: "action-revoke-mode",
 				action() {
 					socket.emit("input", {
 						target: channel.id,
-						text: "/mode -" + modes[prefix][1] + " " + user.nick,
+						text: "/mode -" + mode.mode + " " + user.nick,
 					});
 				},
 			});
@@ -323,9 +331,9 @@ export function generateUserContextMenu($root, channel, network, user) {
 	});
 
 	// Determine if we are half-op or op depending on the network modes so we can kick.
-	if (!compare(networkModes.indexOf("%") > -1 ? "%" : "@", currentChannelUser.modes[0])) {
+	if (!compare(networkModeSymbols.indexOf("%") > -1 ? "%" : "@", currentChannelUser.modes[0])) {
+		// Check if the target user has no mode or a mode lower than ours.
 		if (user.modes.length === 0 || compare(currentChannelUser.modes[0], user.modes[0])) {
-			// Check if the target user has no mode or a mode lower than ours.
 			items.push({
 				label: "Kick",
 				type: "item",
diff --git a/client/js/helpers/parse.js b/client/js/helpers/parse.js
index 9097d96f..675dd0b0 100644
--- a/client/js/helpers/parse.js
+++ b/client/js/helpers/parse.js
@@ -79,7 +79,7 @@ function parse(createElement, text, message = undefined, network = undefined) {
 	// arrays of objects containing start and end markers, as well as metadata
 	// depending on what was found (channel or link).
 	const channelPrefixes = network ? network.serverOptions.CHANTYPES : ["#", "&"];
-	const userModes = network ? network.serverOptions.PREFIX : ["!", "@", "%", "+"];
+	const userModes = network?.serverOptions?.PREFIX.symbols || ["!", "@", "%", "+"];
 	const channelParts = findChannels(cleanText, channelPrefixes, userModes);
 	const linkParts = findLinks(cleanText);
 	const emojiParts = findEmoji(cleanText);
diff --git a/client/js/settings.js b/client/js/settings.js
index 403db792..3e3e5581 100644
--- a/client/js/settings.js
+++ b/client/js/settings.js
@@ -98,7 +98,7 @@ export const config = normalizeConfig({
 	media: {
 		default: true,
 	},
-	removeImageMetadata: {
+	uploadCanvas: {
 		default: true,
 	},
 	userStyles: {
diff --git a/client/js/store-settings.js b/client/js/store-settings.js
index abcc3e4f..bed36968 100644
--- a/client/js/store-settings.js
+++ b/client/js/store-settings.js
@@ -86,15 +86,6 @@ function loadFromLocalStorage() {
 		storedSettings.highlights = storedSettings.highlights.join(", ");
 	}
 
-	// Convert deprecated uploadCanvas to removeImageMetadata
-	if (
-		storedSettings.uploadCanvas !== undefined &&
-		storedSettings.removeImageMetadata === undefined
-	) {
-		storedSettings.removeImageMetadata = storedSettings.uploadCanvas;
-		delete storedSettings.uploadCanvas;
-	}
-
 	return storedSettings;
 }
 
diff --git a/client/js/upload.js b/client/js/upload.js
index e5e3e6f9..fb68746d 100644
--- a/client/js/upload.js
+++ b/client/js/upload.js
@@ -142,7 +142,46 @@ class Uploader {
 		// This issue only happens if The Lounge is proxied through other software
 		// as it may buffer the upload before the upload request will be processed by The Lounge.
 		this.tokenKeepAlive = setInterval(() => socket.emit("upload:ping", token), 40 * 1000);
-		this.performUpload(token, file);
+
+		if (
+			store.state.settings.uploadCanvas &&
+			file.type.startsWith("image/") &&
+			!file.type.includes("svg") &&
+			file.type !== "image/gif"
+		) {
+			this.renderImage(file, (newFile) => this.performUpload(token, newFile));
+		} else {
+			this.performUpload(token, file);
+		}
+	}
+
+	renderImage(file, callback) {
+		const fileReader = new FileReader();
+
+		fileReader.onabort = () => callback(file);
+		fileReader.onerror = () => fileReader.abort();
+
+		fileReader.onload = () => {
+			const img = new Image();
+
+			img.onerror = () => callback(file);
+
+			img.onload = () => {
+				const canvas = document.createElement("canvas");
+				canvas.width = img.width;
+				canvas.height = img.height;
+				const ctx = canvas.getContext("2d");
+				ctx.drawImage(img, 0, 0);
+
+				canvas.toBlob((blob) => {
+					callback(new File([blob], file.name));
+				}, file.type);
+			};
+
+			img.src = fileReader.result;
+		};
+
+		fileReader.readAsDataURL(file);
 	}
 
 	performUpload(token, file) {
@@ -185,7 +224,6 @@ class Uploader {
 		};
 
 		const formData = new FormData();
-		formData.append("removeMetadata", store.state.settings.removeImageMetadata);
 		formData.append("file", file);
 		this.xhr.open("POST", `uploads/new/${token}`);
 		this.xhr.send(formData);
diff --git a/package.json b/package.json
index 27a5a8f7..f64607ad 100644
--- a/package.json
+++ b/package.json
@@ -61,7 +61,6 @@
     "read": "1.0.7",
     "read-chunk": "3.2.0",
     "semver": "7.3.4",
-    "sharp": "0.28.0",
     "socket.io": "3.1.2",
     "tlds": "1.216.0",
     "ua-parser-js": "0.7.24",
diff --git a/src/client.js b/src/client.js
index fe905c81..4a8aa080 100644
--- a/src/client.js
+++ b/src/client.js
@@ -104,11 +104,6 @@ function Client(manager, name, config = {}) {
 		delete client.config.awayMessage;
 	}
 
-	if (client.config.uploadCanvas) {
-		client.config.clientSettings.removeImageMetadata = client.config.uploadCanvas;
-		delete client.config.uploadCanvas;
-	}
-
 	if (client.config.clientSettings.awayMessage) {
 		client.awayMessage = client.config.clientSettings.awayMessage;
 	}
diff --git a/src/models/network.js b/src/models/network.js
index 13879fb2..20e58752 100644
--- a/src/models/network.js
+++ b/src/models/network.js
@@ -5,6 +5,7 @@ const {v4: uuidv4} = require("uuid");
 const IrcFramework = require("irc-framework");
 const Chan = require("./chan");
 const Msg = require("./msg");
+const Prefix = require("./prefix");
 const Helper = require("../helper");
 const STSPolicies = require("../plugins/sts");
 const ClientCertificate = require("../plugins/clientCertificate");
@@ -43,7 +44,12 @@ function Network(attr) {
 		irc: null,
 		serverOptions: {
 			CHANTYPES: ["#", "&"],
-			PREFIX: ["!", "@", "%", "+"],
+			PREFIX: new Prefix([
+				{symbol: "!", mode: "Y"},
+				{symbol: "@", mode: "o"},
+				{symbol: "%", mode: "h"},
+				{symbol: "+", mode: "v"},
+			]),
 			NETWORK: "",
 		},
 
diff --git a/src/models/prefix.js b/src/models/prefix.js
new file mode 100644
index 00000000..331efdff
--- /dev/null
+++ b/src/models/prefix.js
@@ -0,0 +1,33 @@
+"use strict";
+
+class Prefix {
+	constructor(prefix) {
+		this.prefix = prefix || []; // [{symbol: "@", mode: "o"}, ... ]
+		this.modeToSymbol = {};
+		this.symbols = [];
+		this._update_internals();
+	}
+
+	_update_internals() {
+		// clean out the old cruft
+		this.modeToSymbol = {};
+		this.symbols = [];
+
+		const that = this;
+		this.prefix.forEach(function (p) {
+			that.modeToSymbol[p.mode] = p.symbol;
+			that.symbols.push(p.symbol);
+		});
+	}
+
+	update(prefix) {
+		this.prefix = prefix || [];
+		this._update_internals();
+	}
+
+	forEach(f) {
+		return this.prefix.forEach(f);
+	}
+}
+
+module.exports = Prefix;
diff --git a/src/models/user.js b/src/models/user.js
index 591ebddd..dee1e9d0 100644
--- a/src/models/user.js
+++ b/src/models/user.js
@@ -4,7 +4,7 @@ const _ = require("lodash");
 
 module.exports = User;
 
-function User(attr, prefixLookup) {
+function User(attr, prefix) {
 	_.defaults(this, attr, {
 		modes: [],
 		away: "",
@@ -18,12 +18,12 @@ function User(attr, prefixLookup) {
 		},
 	});
 
-	this.setModes(this.modes, prefixLookup);
+	this.setModes(this.modes, prefix);
 }
 
-User.prototype.setModes = function (modes, prefixLookup) {
+User.prototype.setModes = function (modes, prefix) {
 	// irc-framework sets character mode, but The Lounge works with symbols
-	this.modes = modes.map((mode) => prefixLookup[mode]);
+	this.modes = modes.map((mode) => prefix.modeToSymbol[mode]);
 };
 
 User.prototype.toJSON = function () {
diff --git a/src/plugins/irc-events/connection.js b/src/plugins/irc-events/connection.js
index 029eefc2..3ef3000a 100644
--- a/src/plugins/irc-events/connection.js
+++ b/src/plugins/irc-events/connection.js
@@ -63,10 +63,9 @@ module.exports = function (irc, network) {
 	});
 
 	irc.on("socket connected", function () {
-		network.prefixLookup = {};
-		irc.network.options.PREFIX.forEach(function (mode) {
-			network.prefixLookup[mode.mode] = mode.symbol;
-		});
+		if (irc.network.options.PREFIX) {
+			network.serverOptions.PREFIX.update(irc.network.options.PREFIX);
+		}
 
 		network.channels[0].pushMessage(
 			client,
@@ -197,20 +196,12 @@ module.exports = function (irc, network) {
 	});
 
 	irc.on("server options", function (data) {
-		network.prefixLookup = {};
-
-		data.options.PREFIX.forEach((mode) => {
-			network.prefixLookup[mode.mode] = mode.symbol;
-		});
+		network.serverOptions.PREFIX.update(data.options.PREFIX);
 
 		if (data.options.CHANTYPES) {
 			network.serverOptions.CHANTYPES = data.options.CHANTYPES;
 		}
 
-		if (network.serverOptions.PREFIX) {
-			network.serverOptions.PREFIX = data.options.PREFIX.map((p) => p.symbol);
-		}
-
 		network.serverOptions.NETWORK = data.options.NETWORK;
 
 		client.emit("network:options", {
diff --git a/src/plugins/irc-events/mode.js b/src/plugins/irc-events/mode.js
index b07f0954..9c9bf6b2 100644
--- a/src/plugins/irc-events/mode.js
+++ b/src/plugins/irc-events/mode.js
@@ -107,7 +107,7 @@ module.exports = function (irc, network) {
 				return;
 			}
 
-			const changedMode = network.prefixLookup[char];
+			const changedMode = network.serverOptions.PREFIX.modeToSymbol[char];
 
 			if (!add) {
 				_.pull(user.modes, changedMode);
diff --git a/src/plugins/irc-events/names.js b/src/plugins/irc-events/names.js
index 2e0a7db8..8368b281 100644
--- a/src/plugins/irc-events/names.js
+++ b/src/plugins/irc-events/names.js
@@ -14,7 +14,7 @@ module.exports = function (irc, network) {
 
 		data.users.forEach((user) => {
 			const newUser = chan.getUser(user.nick);
-			newUser.setModes(user.modes, network.prefixLookup);
+			newUser.setModes(user.modes, network.serverOptions.PREFIX);
 
 			newUsers.set(user.nick.toLowerCase(), newUser);
 		});
diff --git a/src/plugins/uploader.js b/src/plugins/uploader.js
index e803690f..eec8b6d6 100644
--- a/src/plugins/uploader.js
+++ b/src/plugins/uploader.js
@@ -11,7 +11,6 @@ const crypto = require("crypto");
 const isUtf8 = require("is-utf8");
 const log = require("../log");
 const contentDisposition = require("content-disposition");
-const sharp = require("sharp");
 
 // Map of allowed mime types to their respecive default filenames
 // that will be rendered in browser without forcing them to be downloaded
@@ -134,7 +133,6 @@ class Uploader {
 		let destDir;
 		let destPath;
 		let streamWriter;
-		let removeMetadata;
 
 		const doneCallback = () => {
 			// detach the stream and drain any remaining data
@@ -153,19 +151,6 @@ class Uploader {
 			}
 		};
 
-		const successfullCompletion = () => {
-			doneCallback();
-
-			if (!uploadUrl) {
-				return res.status(400).json({error: "Missing file"});
-			}
-
-			// upload was done, send the generated file url to the client
-			res.status(200).json({
-				url: uploadUrl,
-			});
-		};
-
 		const abortWithError = (err) => {
 			doneCallback();
 
@@ -212,11 +197,6 @@ class Uploader {
 		busboyInstance.on("partsLimit", () => abortWithError(Error("Parts limit reached")));
 		busboyInstance.on("filesLimit", () => abortWithError(Error("Files limit reached")));
 		busboyInstance.on("fieldsLimit", () => abortWithError(Error("Fields limit reached")));
-		busboyInstance.on("field", (fieldname, val) => {
-			if (fieldname === "removeMetadata") {
-				removeMetadata = val === "true";
-			}
-		});
 
 		// generate a random output filename for the file
 		// we use do/while loop to prevent the rare case of generating a file name
@@ -237,7 +217,11 @@ class Uploader {
 			return abortWithError(err);
 		}
 
-		busboyInstance.on("file", (fieldname, fileStream, filename, encoding, contentType) => {
+		// Open a file stream for writing
+		streamWriter = fs.createWriteStream(destPath);
+		streamWriter.on("error", abortWithError);
+
+		busboyInstance.on("file", (fieldname, fileStream, filename) => {
 			uploadUrl = `${randomName}/${encodeURIComponent(filename)}`;
 
 			if (Helper.config.fileUpload.baseUrl) {
@@ -246,55 +230,31 @@ class Uploader {
 				uploadUrl = `uploads/${uploadUrl}`;
 			}
 
-			// Sharps prebuilt libvips does not include gif support, but that is not a problem,
-			// as GIFs don't support EXIF metadata or anything alike
-			const isImage = contentType.startsWith("image/") && !contentType.endsWith("gif");
-
 			// if the busboy data stream errors out or goes over the file size limit
 			// abort the processing with an error
 			fileStream.on("error", abortWithError);
 			fileStream.on("limit", () => {
-				if (!isImage) {
-					fileStream.unpipe(streamWriter);
-				}
-
+				fileStream.unpipe(streamWriter);
 				fileStream.on("readable", fileStream.read.bind(fileStream));
 
 				abortWithError(Error("File size limit reached"));
 			});
 
-			if (isImage) {
-				let sharpInstance = sharp({
-					animated: true,
-					pages: -1,
-					sequentialRead: true,
-				});
-
-				if (!removeMetadata) {
-					sharpInstance = sharpInstance.withMetadata();
-				}
-
-				sharpInstance
-					.rotate() // auto-orient based on the EXIF Orientation tag
-					.toFile(destPath, (err) => {
-						// Removes metadata by default https://sharp.pixelplumbing.com/api-output#tofile if no `withMetadata` is present
-						if (err) {
-							abortWithError(err);
-						} else {
-							successfullCompletion();
-						}
-					});
-
-				fileStream.pipe(sharpInstance);
-			} else {
-				// Open a file stream for writing
-				streamWriter = fs.createWriteStream(destPath);
-				streamWriter.on("error", abortWithError);
-				streamWriter.on("finish", successfullCompletion);
+			// Attempt to write the stream to file
+			fileStream.pipe(streamWriter);
+		});
+
+		busboyInstance.on("finish", () => {
+			doneCallback();
 
-				// Attempt to write the stream to file
-				fileStream.pipe(streamWriter);
+			if (!uploadUrl) {
+				return res.status(400).json({error: "Missing file"});
 			}
+
+			// upload was done, send the generated file url to the client
+			res.status(200).json({
+				url: uploadUrl,
+			});
 		});
 
 		// pipe request body to busboy for processing
diff --git a/webpack.config.js b/webpack.config.js
index e9ed55e1..4475e52d 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -85,7 +85,6 @@ const config = {
 	},
 	externals: {
 		json3: "JSON", // socket.io uses json3.js, but we do not target any browsers that need it
-		sharp: "commonjs sharp",
 	},
 	plugins: [
 		new VueLoaderPlugin(),
diff --git a/yarn.lock b/yarn.lock
index 0c39b605..96616874 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1705,15 +1705,6 @@ binary-extensions@^2.0.0:
   resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
   integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
 
-bl@^4.0.3:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
-  integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==
-  dependencies:
-    buffer "^5.5.0"
-    inherits "^2.0.4"
-    readable-stream "^3.4.0"
-
 block-stream@*:
   version "0.0.9"
   resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a"
@@ -1793,14 +1784,6 @@ buffer-from@^1.0.0:
   resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
   integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
 
-buffer@^5.5.0:
-  version "5.7.1"
-  resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
-  integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
-  dependencies:
-    base64-js "^1.3.1"
-    ieee754 "^1.1.13"
-
 buffer@^6.0.3:
   version "6.0.3"
   resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
@@ -2147,7 +2130,7 @@ color-string@^1.5.4:
     color-name "^1.0.0"
     simple-swizzle "^0.2.2"
 
-color@^3.0.0, color@^3.1.3:
+color@^3.0.0:
   version "3.1.3"
   resolved "https://registry.yarnpkg.com/color/-/color-3.1.3.tgz#ca67fb4e7b97d611dcde39eceed422067d91596e"
   integrity sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ==
@@ -2606,13 +2589,6 @@ decompress-response@^3.3.0:
   dependencies:
     mimic-response "^1.0.0"
 
-decompress-response@^4.2.0:
-  version "4.2.1"
-  resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986"
-  integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==
-  dependencies:
-    mimic-response "^2.0.0"
-
 decompress-response@^6.0.0:
   version "6.0.0"
   resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc"
@@ -2681,7 +2657,7 @@ destroy@~1.0.4:
   resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
   integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
 
-detect-libc@^1.0.2, detect-libc@^1.0.3:
+detect-libc@^1.0.2:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
   integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
@@ -2847,7 +2823,7 @@ encodeurl@~1.0.2:
   resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
   integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
 
-end-of-stream@^1.1.0, end-of-stream@^1.4.1:
+end-of-stream@^1.1.0:
   version "1.4.4"
   resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
   integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
@@ -3182,11 +3158,6 @@ execall@^2.0.0:
   dependencies:
     clone-regexp "^2.1.0"
 
-expand-template@^2.0.3:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
-  integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==
-
 express@4.17.1:
   version "4.17.1"
   resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
@@ -3443,11 +3414,6 @@ fromentries@^1.2.0:
   resolved "https://registry.yarnpkg.com/fromentries/-/fromentries-1.3.2.tgz#e4bca6808816bf8f93b52750f1127f5a6fd86e3a"
   integrity sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==
 
-fs-constants@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
-  integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
-
 fs-minipass@^1.2.5:
   version "1.2.7"
   resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7"
@@ -3574,12 +3540,14 @@ getpass@^0.1.1:
   dependencies:
     assert-plus "^1.0.0"
 
-github-from-package@0.0.0:
-  version "0.0.0"
-  resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce"
-  integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=
+glob-parent@^5.0.0, glob-parent@^5.1.1, glob-parent@~5.1.0:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229"
+  integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==
+  dependencies:
+    is-glob "^4.0.1"
 
-glob-parent@^5.0.0, glob-parent@^5.1.1, glob-parent@^5.1.2, glob-parent@~5.1.0:
+glob-parent@^5.1.2:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
   integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
@@ -3964,7 +3932,7 @@ icss-utils@^5.0.0, icss-utils@^5.1.0:
   resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae"
   integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==
 
-ieee754@^1.1.13, ieee754@^1.2.1:
+ieee754@^1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
   integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
@@ -4038,7 +4006,7 @@ inflight@^1.0.4:
     once "^1.3.0"
     wrappy "1"
 
-inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.3, inherits@~2.0.4:
+inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.3, inherits@~2.0.4:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
   integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@@ -5007,11 +4975,6 @@ mimic-response@^1.0.0, mimic-response@^1.0.1:
   resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
   integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==
 
-mimic-response@^2.0.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43"
-  integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==
-
 mimic-response@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9"
@@ -5052,7 +5015,7 @@ minimist-options@4.1.0:
     is-plain-obj "^1.1.0"
     kind-of "^6.0.3"
 
-minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5:
+minimist@^1.2.0, minimist@^1.2.5:
   version "1.2.5"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
   integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
@@ -5072,11 +5035,6 @@ minizlib@^1.2.1:
   dependencies:
     minipass "^2.9.0"
 
-mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
-  version "0.5.3"
-  resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
-  integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
-
 "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1:
   version "0.5.5"
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
@@ -5171,11 +5129,6 @@ nanoid@^3.1.22, nanoid@^3.1.23:
   resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81"
   integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==
 
-napi-build-utils@^1.0.1:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806"
-  integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==
-
 natural-compare@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@@ -5216,17 +5169,10 @@ nise@^4.0.4:
     just-extend "^4.0.2"
     path-to-regexp "^1.7.0"
 
-node-abi@^2.21.0:
-  version "2.30.0"
-  resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.30.0.tgz#8be53bf3e7945a34eea10e0fc9a5982776cf550b"
-  integrity sha512-g6bZh3YCKQRdwuO/tSZZYJAw622SjsRfJ2X0Iy4sSOHZ34/sPPdVBn8fev2tj7njzLwuqPw9uMtGsGkO5kIQvg==
-  dependencies:
-    semver "^5.4.1"
-
-node-addon-api@^3.0.0, node-addon-api@^3.1.0:
-  version "3.2.1"
-  resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161"
-  integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==
+node-addon-api@^3.0.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.1.0.tgz#98b21931557466c6729e51cb77cd39c965f42239"
+  integrity sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw==
 
 node-forge@0.10.0:
   version "0.10.0"
@@ -5399,7 +5345,7 @@ npm-run-path@^4.0.0, npm-run-path@^4.0.1:
   dependencies:
     path-key "^3.0.0"
 
-"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.1, npmlog@^4.0.2:
+"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.2:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
   integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
@@ -6517,25 +6463,6 @@ postcss@^8.2.6:
     nanoid "^3.1.23"
     source-map-js "^0.6.2"
 
-prebuild-install@^6.0.1:
-  version "6.1.3"
-  resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-6.1.3.tgz#8ea1f9d7386a0b30f7ef20247e36f8b2b82825a2"
-  integrity sha512-iqqSR84tNYQUQHRXalSKdIaM8Ov1QxOVuBNWI7+BzZWv6Ih9k75wOnH1rGQ9WWTaaLkTpxWKIciOF0KyfM74+Q==
-  dependencies:
-    detect-libc "^1.0.3"
-    expand-template "^2.0.3"
-    github-from-package "0.0.0"
-    minimist "^1.2.3"
-    mkdirp-classic "^0.5.3"
-    napi-build-utils "^1.0.1"
-    node-abi "^2.21.0"
-    npmlog "^4.0.1"
-    pump "^3.0.0"
-    rc "^1.2.7"
-    simple-get "^3.0.3"
-    tar-fs "^2.0.0"
-    tunnel-agent "^0.6.0"
-
 precond@0.2:
   version "0.2.3"
   resolved "https://registry.yarnpkg.com/precond/-/precond-0.2.3.tgz#aa9591bcaa24923f1e0f4849d240f47efc1075ac"
@@ -6777,7 +6704,7 @@ readable-stream@^2.0.6:
     string_decoder "~1.1.1"
     util-deprecate "~1.0.1"
 
-readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0:
+readable-stream@^3.1.1, readable-stream@^3.5.0, readable-stream@^3.6.0:
   version "3.6.0"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
   integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
@@ -7092,7 +7019,7 @@ semver-regex@^3.1.2:
   resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-3.1.2.tgz#34b4c0d361eef262e07199dbef316d0f2ab11807"
   integrity sha512-bXWyL6EAKOJa81XG1OZ/Yyuq+oT0b2YLlxx7c+mrdYPaPbnj6WgVULXhinMIeZGufuUBu/eVRqXEhiv4imfwxA==
 
-"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0:
+"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.5.0, semver@^5.6.0:
   version "5.7.1"
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
   integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
@@ -7114,7 +7041,7 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0:
   resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
   integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
 
-semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5:
+semver@^7.2.1, semver@^7.3.2, semver@^7.3.4:
   version "7.3.5"
   resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
   integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
@@ -7193,20 +7120,6 @@ shallow-clone@^3.0.0:
   dependencies:
     kind-of "^6.0.2"
 
-sharp@0.28.0:
-  version "0.28.0"
-  resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.28.0.tgz#93297cec530b3709e11677cf41565d9a654075a0"
-  integrity sha512-kGTaWLNMCkLYxkH2Pv7s+5LQBnWQ4mRKXs1XD19AWOxShWvU8b78qaWqTR/4ryNcPORO+qBoBnFF/Lzda5HgkQ==
-  dependencies:
-    color "^3.1.3"
-    detect-libc "^1.0.3"
-    node-addon-api "^3.1.0"
-    prebuild-install "^6.0.1"
-    semver "^7.3.5"
-    simple-get "^3.1.0"
-    tar-fs "^2.1.1"
-    tunnel-agent "^0.6.0"
-
 shebang-command@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
@@ -7246,20 +7159,6 @@ signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3:
   resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
   integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
 
-simple-concat@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f"
-  integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==
-
-simple-get@^3.0.3, simple-get@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.0.tgz#b45be062435e50d159540b576202ceec40b9c6b3"
-  integrity sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==
-  dependencies:
-    decompress-response "^4.2.0"
-    once "^1.3.1"
-    simple-concat "^1.0.0"
-
 simple-swizzle@^0.2.2:
   version "0.2.2"
   resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
@@ -7798,27 +7697,6 @@ tapable@^2.1.1, tapable@^2.2.0:
   resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.0.tgz#5c373d281d9c672848213d0e037d1c4165ab426b"
   integrity sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==
 
-tar-fs@^2.0.0, tar-fs@^2.1.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784"
-  integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==
-  dependencies:
-    chownr "^1.1.1"
-    mkdirp-classic "^0.5.2"
-    pump "^3.0.0"
-    tar-stream "^2.1.4"
-
-tar-stream@^2.1.4:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
-  integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==
-  dependencies:
-    bl "^4.0.3"
-    end-of-stream "^1.4.1"
-    fs-constants "^1.0.0"
-    inherits "^2.0.3"
-    readable-stream "^3.1.1"
-
 tar@^2.0.0:
   version "2.2.2"
   resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.2.tgz#0ca8848562c7299b8b446ff6a4d60cdbb23edc40"


Test patch:
diff --git a/test/models/chan.js b/test/models/chan.js
index 33ee41e5..613bece3 100644
--- a/test/models/chan.js
+++ b/test/models/chan.js
@@ -20,10 +20,10 @@ describe("Chan", function () {
 		},
 	};
 
-	const prefixLookup = {};
+	const prefixLookup = {modeToSymbol: {}};
 
 	network.network.options.PREFIX.forEach((mode) => {
-		prefixLookup[mode.mode] = mode.symbol;
+		prefixLookup.modeToSymbol[mode.mode] = mode.symbol;
 	});
 
 	describe("#findMessage(id)", function () {
diff --git a/test/models/msg.js b/test/models/msg.js
index 3c7d59b7..7690754a 100644
--- a/test/models/msg.js
+++ b/test/models/msg.js
@@ -8,7 +8,7 @@ const User = require("../../src/models/user");
 describe("Msg", function () {
 	["from", "target"].forEach((prop) => {
 		it(`should keep a copy of the original user in the \`${prop}\` property`, function () {
-			const prefixLookup = {a: "&", o: "@"};
+			const prefixLookup = {modeToSymbol: {a: "&", o: "@"}};
 			const user = new User(
 				{
 					modes: ["o"],