diff --git a/server/chat-plugins/customavatars.js b/server/chat-plugins/customavatars.js
new file mode 100644
index 000000000000..6dd3c69684c8
--- /dev/null
+++ b/server/chat-plugins/customavatars.js
@@ -0,0 +1,102 @@
+// Original plugin from https://github.com/CreaturePhil/Showdown-Boilerplate/blob/master/chat-plugins/customavatar.js.
+// Credits to CreaturePhil and the other listed contributors.
+// updated for the main server by Maxalexanderpi and Hoeenhero.
+*/
+'use strict';
+
+/** @type {typeof import('../../lib/fs').FS} */
+const path = require('path');
+const fs = require('fs');
+const https = require('https');
+
+const AVATAR_PATH = 'config/avatars/';
+
+const VALID_EXTENSIONS = ['.png'];
+
+function downloadImage(image_url, name, extension) {
+ let req = https.get(image_url, res => {
+ res.setEncoding('binary');
+ res.on('response', response => {
+ if (response.statusCode !== 200) return;
+ let type = response.headers['content-type'].split('/');
+ if (type[0] !== 'image') return;
+
+ response.pipe(fs(AVATAR_PATH + name + extension).createWriteStream());
+ });
+ });
+ req.on('error', e => {
+ console.error(e);
+ });
+ req.end();
+}
+function loadCustomAvatars() {
+ fs.readdir(AVATAR_PATH, (err, files) => {
+ if (err) console.log("Error loading custom avatars: " + err);
+ if (!files) files = [];
+ files
+ .filter(file => VALID_EXTENSIONS.includes(path.extname(file)))
+ .forEach(file => {
+ let name = path.basename(file, path.extname(file));
+ Config.customavatars[name] = file;
+ });
+ });
+}
+loadCustomAvatars();
+
+exports.commands = {
+ customavatar: {
+ add(target, room, user) {
+ if (!this.can('avatar')) return false;
+ let parts = target.split(',').map(param => param.trim());
+ if (parts.length < 2) return this.parse('/help customavatar');
+
+ let name = toID(parts[0]);
+ let avatarUrl = parts[1];
+ if (!/^https?:\/\//i.test(avatarUrl)) avatarUrl = 'http://' + avatarUrl;
+ let ext = path.extname(avatarUrl);
+
+ if (!VALID_EXTENSIONS.includes(ext)) {
+ return this.errorReply("Image url must end in a .png extension.");
+ }
+
+ Config.customavatars[name] = name + ext;
+
+ downloadImage(avatarUrl, name, ext);
+ this.sendReply(`|raw|${name}${name.endsWith('s') ? "'" : "'s"} avatar was successfully set. Avatar:
);
+ Monitor.adminlog(name + "'s avatar was successfully set by " + user.name + ".");
+ if (Users(name)) Users(name).popup("|html|Upper staff have set your custom avatar.
Refresh your page if you don't see it.");
+ },
+
+ remove(target, room, user) {
+ if (!this.can('')) return false;
+
+ let userid = toID(target);
+ let image = Config.customavatars[userid];
+
+ if (!image) return this.errorReply(target + " does not have a custom avatar.");
+
+ delete Config.customavatars[userid];
+ fs.unlink(AVATAR_PATH + image, err => {
+ if (err && err.code === 'ENOENT') {
+ return this.errorReply(target + "'s avatar does not exist.");
+ } else if (err) {
+ console.error(err);
+ }
+
+ if (Users(userid)) Users(userid).popup("Upper staff have removed your custom avatar.");
+ this.sendReply(target + "'s avatar has been successfully removed.");
+ Monitor.adminlog(target + "'s avatar has been successfully removed.");
+ });
+ },
+
+ customavatarhelp: 'help',
+ help(target, room, user) {
+ this.parse('/help customavatar');
+ },
+ },
+
+ customavatarhelp: [
+ "Commands for /customavatar are:",
+ "/customavatar add [username], [image link] - Set a user's custom avatar. Requires: & ~",
+ "/customavatar remove [username] - Delete a user's custom avatar. Requires: & ~"],
+};