sync/src/channel/poll.js
Calvin Montgomery f84892dc6a Refactor polls
2021-08-02 19:23:53 -07:00

267 lines
7.6 KiB
JavaScript

var ChannelModule = require("./module");
var Poll = require("../poll").Poll;
import { ValidationError } from '../errors';
import Config from '../config';
import { ackOrErrorMsg } from '../util/ack';
const TYPE_NEW_POLL = {
title: "string",
timeout: "number,optional",
obscured: "boolean",
opts: "array"
};
const TYPE_VOTE = {
option: "number"
};
const ROOM_VIEW_HIDDEN = ":viewHidden";
const ROOM_NO_VIEW_HIDDEN = ":noViewHidden";
function PollModule(_channel) {
ChannelModule.apply(this, arguments);
this.poll = null;
this.roomViewHidden = this.channel.uniqueName + ROOM_VIEW_HIDDEN;
this.roomNoViewHidden = this.channel.uniqueName + ROOM_NO_VIEW_HIDDEN;
if (this.channel.modules.chat) {
this.channel.modules.chat.registerCommand("poll", this.handlePollCmd.bind(this, false));
this.channel.modules.chat.registerCommand("hpoll", this.handlePollCmd.bind(this, true));
}
this.supportsDirtyCheck = true;
}
PollModule.prototype = Object.create(ChannelModule.prototype);
PollModule.prototype.unload = function () {
if (this.poll && this.poll.timer) {
clearTimeout(this.poll.timer);
}
};
PollModule.prototype.load = function (data) {
if ("poll" in data) {
if (data.poll !== null) {
this.poll = Poll.fromChannelData(data.poll);
}
}
this.dirty = false;
};
PollModule.prototype.save = function (data) {
if (this.poll === null) {
data.poll = null;
return;
}
data.poll = this.poll.toChannelData();
};
PollModule.prototype.onUserPostJoin = function (user) {
this.sendPoll(user);
user.socket.typecheckedOn("newPoll", TYPE_NEW_POLL, this.handleNewPoll.bind(this, user));
user.socket.typecheckedOn("vote", TYPE_VOTE, this.handleVote.bind(this, user));
user.socket.on("closePoll", this.handleClosePoll.bind(this, user));
this.addUserToPollRoom(user);
const self = this;
user.on("effectiveRankChange", () => {
if (self.channel && !self.channel.dead) {
self.addUserToPollRoom(user);
}
});
};
PollModule.prototype.addUserToPollRoom = function (user) {
const perms = this.channel.modules.permissions;
if (perms.canViewHiddenPoll(user)) {
user.socket.leave(this.roomNoViewHidden);
user.socket.join(this.roomViewHidden);
} else {
user.socket.leave(this.roomViewHidden);
user.socket.join(this.roomNoViewHidden);
}
};
PollModule.prototype.onUserPart = function(user) {
if (this.poll && this.poll.uncountVote(user.realip)) {
this.broadcastPoll(false);
}
};
PollModule.prototype.sendPoll = function (user) {
if (!this.poll) {
return;
}
var perms = this.channel.modules.permissions;
user.socket.emit("closePoll");
if (perms.canViewHiddenPoll(user)) {
var unobscured = this.poll.toUpdateFrame(true);
user.socket.emit("newPoll", unobscured);
} else {
var obscured = this.poll.toUpdateFrame(false);
user.socket.emit("newPoll", obscured);
}
};
PollModule.prototype.broadcastPoll = function (isNewPoll) {
if (!this.poll) {
return;
}
var obscured = this.poll.toUpdateFrame(false);
var unobscured = this.poll.toUpdateFrame(true);
const event = isNewPoll ? "newPoll" : "updatePoll";
if (isNewPoll) {
this.channel.broadcastAll("closePoll");
}
this.channel.broadcastToRoom(event, unobscured, this.roomViewHidden);
this.channel.broadcastToRoom(event, obscured, this.roomNoViewHidden);
};
PollModule.prototype.validatePollInput = function validatePollInput(title, options) {
if (typeof title !== 'string') {
throw new ValidationError('Poll title must be a string.');
}
if (title.length > 255) {
throw new ValidationError('Poll title must be no more than 255 characters long.');
}
if (!Array.isArray(options)) {
throw new ValidationError('Poll options must be an array.');
}
if (options.length > Config.get('poll.max-options')) {
throw new ValidationError(`Polls are limited to a maximum of ${Config.get('poll.max-options')} options.`);
}
for (let i = 0; i < options.length; i++) {
if (typeof options[i] !== 'string') {
throw new ValidationError('Poll options must be strings.');
}
if (options[i].length === 0 || options[i].length > 255) {
throw new ValidationError('Poll options must be 1-255 characters long.');
}
}
};
PollModule.prototype.handleNewPoll = function (user, data, ack) {
if (!this.channel.modules.permissions.canControlPoll(user)) {
return;
}
ack = ackOrErrorMsg(ack, user);
if (typeof data !== 'object' || data === null) {
ack({
error: {
message: 'Invalid data received for poll creation.'
}
});
return;
}
try {
this.validatePollInput(data.title, data.opts);
} catch (error) {
ack({
error: {
message: error.message
}
});
return;
}
if (data.hasOwnProperty("timeout") &&
(isNaN(data.timeout) || data.timeout < 1 || data.timeout > 86400)) {
ack({
error: {
message: "Poll timeout must be between 1 and 86400 seconds"
}
});
return;
}
var poll = Poll.create(user.getName(), data.title, data.opts, { hideVotes: data.obscured });
var self = this;
if (data.hasOwnProperty("timeout")) {
poll.timer = setTimeout(function () {
if (self.poll === poll) {
self.handleClosePoll({
getName: function () { return "[poll timer]"; },
effectiveRank: 255
});
}
}, data.timeout * 1000);
}
this.poll = poll;
this.dirty = true;
this.broadcastPoll(true);
this.channel.logger.log("[poll] " + user.getName() + " opened poll: '" + poll.title + "'");
ack({});
};
PollModule.prototype.handleVote = function (user, data) {
if (!this.channel.modules.permissions.canVote(user)) {
return;
}
if (this.poll) {
if (this.poll.countVote(user.realip, data.option)) {
this.dirty = true;
this.broadcastPoll(false);
}
}
};
PollModule.prototype.handleClosePoll = function (user) {
if (!this.channel.modules.permissions.canControlPoll(user)) {
return;
}
if (this.poll) {
if (this.poll.hideVotes) {
this.poll.hideVotes = false;
this.channel.broadcastAll("updatePoll", this.poll.toUpdateFrame(true));
}
if (this.poll.timer) {
clearTimeout(this.poll.timer);
}
this.channel.broadcastAll("closePoll");
this.channel.logger.log("[poll] " + user.getName() + " closed the active poll");
this.poll = null;
this.dirty = true;
}
};
PollModule.prototype.handlePollCmd = function (obscured, user, msg, _meta) {
if (!this.channel.modules.permissions.canControlPoll(user)) {
return;
}
msg = msg.replace(/^\/h?poll/, "");
var args = msg.split(",");
var title = args.shift();
try {
this.validatePollInput(title, args);
} catch (error) {
user.socket.emit('errorMsg', {
msg: 'Error creating poll: ' + error.message
});
return;
}
var poll = Poll.create(user.getName(), title, args, { hideVotes: obscured });
this.poll = poll;
this.dirty = true;
this.broadcastPoll(true);
this.channel.logger.log("[poll] " + user.getName() + " opened poll: '" + poll.title + "'");
};
module.exports = PollModule;