Skip to content

Add commands for upper staff to add custom avatars #5515

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions server/chat-plugins/customavatars.js
Original file line number Diff line number Diff line change
@@ -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} */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is in the wrong place, it should be above the importing of '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 => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should probably be returning a promise

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());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should probably be returning a promise

Resolve the promise on finish event of the fs write stream.

});
});
req.on('error', e => {
console.error(e);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure this is the correct error handling mechanism

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be better to throw the error to crashlogger in hindsight.

I need to review the plugin as a whole when I have time, haven't done much other than offer advice on a few issues.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error handling and logging seems to be quite badly implemented.

});
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)))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably blacklist __proto__ and constructor

.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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

URLs don't necessarily end in the extension of the actual file they point at

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:<br /><img src="${avatarUrl}" width="80" height="80">);
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.<br /><img src='" + avatarUrl + "' width='80' height='80'><br /> 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: & ~"],
};