"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var poll_exports = {};
__export(poll_exports, {
Poll: () => Poll,
commands: () => commands,
pages: () => pages
});
module.exports = __toCommonJS(poll_exports);
var import_lib = require("../../lib");
const MINUTES = 6e4;
const MAX_QUESTIONS = 10;
class Poll extends Rooms.MinorActivity {
constructor(room, options) {
super(room);
this.activityid = "poll";
this.name = "Poll";
this.activityNumber = options.activityNumber || room.nextGameNumber();
this.question = options.question;
this.supportHTML = options.supportHTML;
this.multiPoll = options.multiPoll;
this.pendingVotes = options.pendingVotes || {};
this.voters = options.voters || {};
this.voterIps = options.voterIps || {};
this.totalVotes = options.totalVotes || 0;
this.maxVotes = options.maxVotes || 0;
if (!options.answers)
options.answers = options.questions;
this.answers = Poll.getAnswers(options.answers);
this.isQuiz = options.isQuiz ?? [...this.answers.values()].some((answer) => answer.correct);
this.setTimer(options);
}
select(user, option) {
const userid = user.id;
if (!this.multiPoll) {
this.pendingVotes[userid] = [option];
this.submit(user);
return;
}
if (!this.pendingVotes[userid]) {
this.pendingVotes[userid] = [];
}
if (this.pendingVotes[userid].includes(option)) {
throw new Chat.ErrorMessage(this.room.tr`That option is already selected.`);
}
this.pendingVotes[userid].push(option);
this.updateFor(user);
this.save();
}
deselect(user, option) {
const userid = user.id;
const pendingVote = this.pendingVotes[userid];
if (!pendingVote?.includes(option)) {
throw new Chat.ErrorMessage(this.room.tr`That option is not selected.`);
}
pendingVote.splice(pendingVote.indexOf(option), 1);
this.updateFor(user);
this.save();
}
submit(user) {
const ip = user.latestIp;
const userid = user.id;
if (userid in this.voters || !Config.noipchecks && ip in this.voterIps) {
delete this.pendingVotes[userid];
throw new Chat.ErrorMessage(this.room.tr`You have already voted for this poll.`);
}
const selected = this.pendingVotes[userid];
if (!selected)
throw new Chat.ErrorMessage(this.room.tr`No options selected.`);
this.voters[userid] = selected;
this.voterIps[ip] = selected;
for (const option of selected) {
this.answers.get(option).votes++;
}
delete this.pendingVotes[userid];
this.totalVotes++;
if (this.maxVotes && this.totalVotes >= this.maxVotes) {
this.end(this.room);
return this.room.add(`|c|~|/log The poll hit the max vote cap of ${this.maxVotes}, and has ended.`).update();
}
this.update();
this.save();
}
blankvote(user) {
const ip = user.latestIp;
const userid = user.id;
if (!(userid in this.voters) || !(ip in this.voterIps)) {
this.voters[userid] = [];
this.voterIps[ip] = [];
}
this.updateTo(user);
this.save();
}
generateVotes(user) {
const iconText = this.isQuiz ? ` ${this.room.tr`Quiz`}` : ` ${this.room.tr`Poll`}`;
let output = `
${iconText}`;
output += ` ${Poll.getQuestionMarkup(this.question, this.supportHTML)}
`;
if (this.multiPoll) {
const empty = `
`;
const chosen = `
`;
const pendingVotes = user && this.pendingVotes[user.id] || [];
for (const [num, answer] of this.answers) {
const selected = pendingVotes.includes(num);
output += `
`;
}
const submitButton = pendingVotes.length ? `
` : `
`;
output += `
${submitButton}
`;
output += `
`;
} else {
for (const [num, answer] of this.answers) {
output += ``;
}
output += ``;
output += ``;
}
return output;
}
static generateResults(options, room, ended = false, choice = null) {
const iconText = options.isQuiz ? ` ${room.tr`Quiz`}` : ` ${room.tr`Poll`}`;
const icon = `${iconText}${ended ? " " + room.tr`ended` : ""} ${options.totalVotes || 0} ${room.tr`votes`}`;
let output = `${icon} ${this.getQuestionMarkup(options.question, options.supportHTML)}
`;
const answers = Poll.getAnswers(options.answers);
const colors = ["#88B", "#79A", "#8A8"];
for (const [num, answer] of answers) {
const chosen = choice?.includes(num);
const percentage = Math.round(answer.votes * 100 / (options.totalVotes || 1));
const answerMarkup = options.isQuiz ? `
${answer.correct ? "" : ""}${this.getAnswerMarkup(answer, options.supportHTML)}${answer.correct ? "" : ""}` : this.getAnswerMarkup(answer, options.supportHTML);
output += `
${num}. ${chosen ? "" : ""}${answerMarkup}${chosen ? "" : ""} (${answer.votes} vote${answer.votes === 1 ? "" : "s"})
${percentage}%
`;
}
if (!choice && !ended) {
output += `
(${room.tr`You can't vote after viewing results`})
`;
}
output += "
";
return output;
}
static getQuestionMarkup(question, supportHTML = false) {
if (supportHTML)
return question;
return Chat.formatText(question);
}
static getAnswerMarkup(answer, supportHTML = false) {
if (supportHTML)
return answer.name;
return Chat.formatText(answer.name);
}
update() {
const state = this.toJSON();
const blankvote = Poll.generateResults(state, this.room, false);
for (const id in this.room.users) {
const user = this.room.users[id];
const selection = this.voters[user.id] || this.voterIps[user.latestIp];
if (selection) {
if (selection.length) {
user.sendTo(
this.room,
`|uhtmlchange|poll${this.activityNumber}|${Poll.generateResults(state, this.room, false, selection)}`
);
} else {
user.sendTo(this.room, `|uhtmlchange|poll${this.activityNumber}|${blankvote}`);
}
}
}
}
updateTo(user, connection = null) {
const state = this.toJSON();
const recipient = connection || user;
const selection = this.voters[user.id] || this.voterIps[user.latestIp];
if (selection) {
recipient.sendTo(
this.room,
`|uhtmlchange|poll${this.activityNumber}|${Poll.generateResults(state, this.room, false, selection)}`
);
} else {
recipient.sendTo(this.room, `|uhtmlchange|poll${this.activityNumber}|${this.generateVotes(user)}`);
}
}
updateFor(user) {
const state = this.toJSON();
if (user.id in this.voters) {
user.sendTo(
this.room,
`|uhtmlchange|poll${this.activityNumber}|${Poll.generateResults(state, this.room, false, this.voters[user.id])}`
);
} else {
user.sendTo(this.room, `|uhtmlchange|poll${this.activityNumber}|${this.generateVotes(user)}`);
}
}
display() {
const state = this.toJSON();
const blankvote = Poll.generateResults(state, this.room, false);
const blankquestions = this.generateVotes(null);
for (const id in this.room.users) {
const thisUser = this.room.users[id];
const selection = this.voters[thisUser.id] || this.voterIps[thisUser.latestIp];
if (selection) {
if (selection.length) {
thisUser.sendTo(
this.room,
`|uhtml|poll${this.activityNumber}|${Poll.generateResults(state, this.room, false, selection)}`
);
} else {
thisUser.sendTo(this.room, `|uhtml|poll${this.activityNumber}|${blankvote}`);
}
} else {
if (this.multiPoll && thisUser.id in this.pendingVotes) {
thisUser.sendTo(this.room, `|uhtml|poll${this.activityNumber}|${this.generateVotes(thisUser)}`);
} else {
thisUser.sendTo(this.room, `|uhtml|poll${this.activityNumber}|${blankquestions}`);
}
}
}
}
displayTo(user, connection = null) {
const state = this.toJSON();
const recipient = connection || user;
if (user.id in this.voters) {
recipient.sendTo(
this.room,
`|uhtml|poll${this.activityNumber}|${Poll.generateResults(state, this.room, false, this.voters[user.id])}`
);
} else if (user.latestIp in this.voterIps && !Config.noipchecks) {
recipient.sendTo(this.room, `|uhtml|poll${this.activityNumber}|${Poll.generateResults(
state,
this.room,
false,
this.voterIps[user.latestIp]
)}`);
} else {
recipient.sendTo(this.room, `|uhtml|poll${this.activityNumber}|${this.generateVotes(user)}`);
}
}
onConnect(user, connection = null) {
this.displayTo(user, connection);
}
onRename(user, oldid, joining) {
if (user.id in this.voters) {
this.updateFor(user);
}
}
destroy() {
const results = Poll.generateResults(this.toJSON(), this.room, true);
this.room.send(`|uhtmlchange|poll${this.activityNumber}|(${this.room.tr`The poll has ended – scroll down to see the results`})
`);
this.room.add(`|html|${results}`).update();
this.room.setMinorActivity(null);
}
toJSON() {
return {
activityid: "poll",
activityNumber: this.activityNumber,
question: this.question,
supportHTML: this.supportHTML,
multiPoll: this.multiPoll,
pendingVotes: this.pendingVotes,
voters: this.voters,
voterIps: this.voterIps,
totalVotes: this.totalVotes,
timeoutMins: this.timeoutMins,
timerEnd: this.timerEnd,
isQuiz: this.isQuiz,
answers: [...this.answers.values()]
};
}
save() {
this.room.settings.minorActivity = this.toJSON();
this.room.saveSettings();
}
static getAnswers(answers) {
const out = /* @__PURE__ */ new Map();
if (answers.length && typeof answers[0] === "string") {
for (const [i, answer] of answers.entries()) {
out.set(i + 1, {
name: answer.startsWith("+") ? answer.slice(1) : answer,
votes: 0,
correct: answer.startsWith("+")
});
}
} else {
for (const [i, answer] of answers.entries()) {
out.set(i + 1, answer);
}
}
return out;
}
}
const commands = {
poll: {
htmlcreate: "new",
create: "new",
createmulti: "new",
htmlcreatemulti: "new",
queue: "new",
queuehtml: "new",
htmlqueue: "new",
queuemulti: "new",
htmlqueuemulti: "new",
new(target, room, user, connection, cmd, message) {
room = this.requireRoom();
if (!target)
return this.parse("/help poll new");
target = target.trim();
if (target.length > 1024)
return this.errorReply(this.tr`Poll too long.`);
if (room.battle)
return this.errorReply(this.tr`Battles do not support polls.`);
const text = this.filter(target);
if (target !== text)
return this.errorReply(this.tr`You are not allowed to use filtered words in polls.`);
const supportHTML = cmd.includes("html");
const multiPoll = cmd.includes("multi");
const queue = cmd.includes("queue");
let params = [];
let separator = "";
if (text.includes("\n")) {
separator = "\n";
} else if (text.includes("|")) {
separator = "|";
} else if (text.includes(",")) {
separator = ",";
} else {
return this.errorReply(this.tr`Not enough arguments for /poll new.`);
}
let currentParam = "";
for (let i = 0; i < text.length; ++i) {
const currentCharacter = text[i];
const nextCharacter = text[i + 1];
const isEscapeCharacter = currentCharacter === "\\";
if (isEscapeCharacter) {
if (nextCharacter) {
currentParam += nextCharacter;
i += 1;
} else {
return this.errorReply(this.tr`Extra escape character. To end a poll with '\\', enter it as '\\\\'`);
}
continue;
}
const isSeparator = currentCharacter === separator;
if (isSeparator) {
params.push(currentParam);
currentParam = "";
continue;
}
currentParam += currentCharacter;
}
params.push(currentParam);
params = params.map((param) => param.trim());
this.checkCan("minigame", null, room);
if (supportHTML)
this.checkCan("declare", null, room);
this.checkChat();
if (room.minorActivity && !queue) {
return this.errorReply(this.tr`There is already a poll or announcement in progress in this room.`);
}
if (params.length < 3)
return this.errorReply(this.tr`Not enough arguments for /poll new.`);
if (supportHTML)
params = params.map((parameter) => this.checkHTML(parameter));
const questions = params.splice(1);
if (questions.length > MAX_QUESTIONS) {
return this.errorReply(this.tr`Too many options for poll (maximum is ${MAX_QUESTIONS}).`);
}
if (new Set(questions).size !== questions.length) {
return this.errorReply(this.tr`There are duplicate options in the poll.`);
}
if (room.minorActivity) {
room.queueMinorActivity({
question: params[0],
answers: questions,
multiPoll,
supportHTML,
activityid: "poll"
});
this.modlog("QUEUEPOLL");
return this.privateModAction(room.tr`${user.name} queued a poll.`);
}
room.setMinorActivity(new Poll(room, {
question: params[0],
supportHTML,
answers: questions,
multiPoll
}));
this.roomlog(`${user.name} used ${message}`);
this.modlog("POLL");
this.addModAction(room.tr`A poll was started by ${user.name}.`);
},
newhelp: [
`/poll create [question], [option1], [option2], [...] - Creates a poll. Requires: % @ # ~`,
`/poll createmulti [question], [option1], [option2], [...] - Creates a poll, allowing for multiple answers to be selected. Requires: % @ # ~`,
`To queue a poll, use [queue], [queuemulti], [queuehtml], or [htmlqueuemulti].`,
`Polls can be used as quiz questions. To do this, prepend all correct answers with a +.`
],
viewqueue(target, room, user) {
room = this.requireRoom();
this.checkCan("mute", null, room);
this.parse(`/join view-pollqueue-${room.roomid}`);
},
viewqueuehelp: [`/viewqueue - view the queue of polls in the room. Requires: % @ # ~`],
deletequeue(target, room, user) {
room = this.requireRoom();
if (!target)
return this.parse("/help deletequeue");
this.checkCan("mute", null, room);
const queue = room.getMinorActivityQueue();
if (!queue) {
return this.errorReply(this.tr`The queue is already empty.`);
}
const slot = parseInt(target);
if (isNaN(slot)) {
return this.errorReply(this.tr`Can't delete poll at slot ${target} - "${target}" is not a number.`);
}
if (!queue[slot - 1])
return this.errorReply(this.tr`There is no poll in queue at slot ${slot}.`);
room.clearMinorActivityQueue(slot - 1);
room.modlog({
action: "DELETEQUEUE",
loggedBy: user.id,
note: slot.toString()
});
room.sendMods(this.tr`(${user.name} deleted the queued poll in slot ${slot}.)`);
room.update();
this.refreshPage(`pollqueue-${room.roomid}`);
},
deletequeuehelp: [
`/poll deletequeue [number] - deletes poll at the corresponding queue slot (1 = next, 2 = the one after that, etc). Requires: % @ # ~`
],
clearqueue(target, room, user, connection, cmd) {
room = this.requireRoom();
this.checkCan("mute", null, room);
const queue = room.getMinorActivityQueue();
if (!queue) {
return this.errorReply(this.tr`The queue is already empty.`);
}
room.clearMinorActivityQueue();
this.modlog("CLEARQUEUE");
this.sendReply(this.tr`Cleared poll queue.`);
},
clearqueuehelp: [
`/poll clearqueue - deletes the queue of polls. Requires: % @ # ~`
],
deselect: "select",
vote: "select",
select(target, room, user, connection, cmd) {
room = this.requireRoom();
const poll = this.requireMinorActivity(Poll);
if (!target)
return this.parse("/help poll vote");
const parsed = parseInt(target);
if (isNaN(parsed))
return this.errorReply(this.tr`To vote, specify the number of the option.`);
if (!poll.answers.has(parsed))
return this.sendReply(this.tr`Option not in poll.`);
if (cmd === "deselect") {
poll.deselect(user, parsed);
} else {
poll.select(user, parsed);
}
},
selecthelp: [
`/poll select [number] - Select option [number].`,
`/poll deselect [number] - Deselects option [number].`
],
submit(target, room, user) {
room = this.requireRoom();
const poll = this.requireMinorActivity(Poll);
poll.submit(user);
},
submithelp: [`/poll submit - Submits your vote.`],
timer(target, room, user) {
room = this.requireRoom();
const poll = this.requireMinorActivity(Poll);
if (target) {
this.checkCan("minigame", null, room);
if (target === "clear") {
if (!poll.endTimer())
return this.errorReply(this.tr("There is no timer to clear."));
return this.add(this.tr`The poll timer was turned off.`);
}
const timeoutMins = parseFloat(target);
if (isNaN(timeoutMins) || timeoutMins <= 0 || timeoutMins > 7 * 24 * 60) {
return this.errorReply(this.tr`Time should be a number of minutes less than one week.`);
}
poll.setTimer({ timeoutMins });
room.add(this.tr`The poll timer was turned on: the poll will end in ${Chat.toDurationString(timeoutMins * MINUTES)}.`);
this.modlog("POLL TIMER", null, `${timeoutMins} minutes`);
return this.privateModAction(room.tr`The poll timer was set to ${timeoutMins} minute(s) by ${user.name}.`);
} else {
if (!this.runBroadcast())
return;
if (poll.timeout) {
return this.sendReply(this.tr`The poll timer is on and will end in ${Chat.toDurationString(poll.timeoutMins * MINUTES)}.`);
} else {
return this.sendReply(this.tr`The poll timer is off.`);
}
}
},
timerhelp: [
`/poll timer [minutes] - Sets the poll to automatically end after [minutes] minutes. Requires: % @ # ~`,
`/poll timer clear - Clears the poll's timer. Requires: % @ # ~`
],
results(target, room, user) {
room = this.requireRoom();
const poll = this.requireMinorActivity(Poll);
poll.blankvote(user);
},
resultshelp: [
`/poll results - Shows the results of the poll without voting. NOTE: you can't go back and vote after using this.`
],
close: "end",
stop: "end",
end(target, room, user) {
room = this.requireRoom();
this.checkCan("minigame", null, room);
this.checkChat();
const poll = this.requireMinorActivity(Poll);
this.modlog("POLL END");
this.privateModAction(room.tr`The poll was ended by ${user.name}.`);
poll.end(room, Poll);
},
endhelp: [`/poll end - Ends a poll and displays the results. Requires: % @ # ~`],
show: "",
display: "",
""(target, room, user, connection) {
room = this.requireRoom();
const poll = this.requireMinorActivity(Poll);
if (!this.runBroadcast())
return;
room.update();
if (this.broadcasting) {
poll.display();
} else {
poll.displayTo(user, connection);
}
},
displayhelp: [`/poll display - Displays the poll`],
mv: "maxvotes",
maxvotes(target, room, user) {
room = this.requireRoom();
this.checkCan("mute", null, room);
const poll = this.requireMinorActivity(Poll);
let num = parseInt(target);
if (this.meansNo(target)) {
num = 0;
}
if (isNaN(num)) {
return this.errorReply(`Invalid max vote cap: '${target}'`);
}
if (poll.maxVotes === num) {
return this.errorReply(`The poll's vote cap is already set to ${num}.`);
}
poll.maxVotes = num;
this.addModAction(`${user.name} set the poll's vote cap to ${num}.`);
let ended = false;
if (poll.totalVotes > poll.maxVotes) {
poll.end(room);
this.addModAction(`The poll has more votes than the maximum vote cap, and has ended.`);
ended = true;
}
if (!ended)
poll.save();
this.modlog("POLL MAXVOTES", null, `${poll.maxVotes}${ended ? ` (ended poll)` : ""}`);
}
},
pollhelp() {
this.sendReply(
`|html|/poll allows rooms to run their own polls (limit 1 at a time).
Polls can be used as quiz questions, by putting +
before correct answers.
/poll create [question], [option1], [option2], [...]
- Creates a poll. Requires: % @ # ~
/poll createmulti [question], [option1], [option2], [...]
- Creates a poll, allowing for multiple answers to be selected. Requires: % @ # ~
/poll htmlcreate(multi) [question], [option1], [option2], [...]
- Creates a poll, with HTML allowed in the question and options. Requires: # ~
/poll vote [number]
- Votes for option [number].
/poll timer [minutes]
- Sets the poll to automatically end after [minutes]. Requires: % @ # ~.
/poll results
- Shows the results of the poll without voting. NOTE: you can't go back and vote after using this.
/poll display
- Displays the poll.
/poll end
- Ends a poll and displays the results. Requires: % @ # ~.
/poll queue [question], [option1], [option2], [...]
- Add a poll in queue. Requires: % @ # ~
/poll deletequeue [number]
- Deletes poll at the corresponding queue slot (1 = next, 2 = the one after that, etc).
/poll clearqueue
- Deletes the queue of polls. Requires: % @ # ~.
/poll viewqueue
- View the queue of polls in the room. Requires: % @ # ~
/poll maxvotes [number]
- Set the max poll votes to the given [number]. Requires: % @ # ~
`
);
}
};
const pages = {
pollqueue(args, user) {
const room = this.requireRoom();
let buf = `${this.tr`Queued polls:`}`;
buf += `
`;
const queue = room.getMinorActivityQueue()?.filter((activity) => activity.activityid === "poll");
if (!queue) {
buf += `
${this.tr`No polls queued.`}`;
return buf;
}
for (const [i, poll] of queue.entries()) {
const number = i + 1;
const button = `${this.tr`#${number} in queue`} `;
buf += `
`;
buf += `${button}
${Poll.generateResults(poll, room, false)}`;
}
buf += `
`;
return buf;
}
};
process.nextTick(() => {
Chat.multiLinePattern.register("/poll (new|create|createmulti|htmlcreate|htmlcreatemulti|queue|queuemulti|htmlqueuemulti) ");
});
for (const room of Rooms.rooms.values()) {
if (room.getMinorActivityQueue(true)) {
for (const poll of room.getMinorActivityQueue(true)) {
if (!poll.activityid) {
poll.activityid = poll.activityId;
delete poll.activityId;
}
if (!poll.activityNumber) {
poll.activityNumber = poll.pollNumber;
delete poll.pollNumber;
}
room.saveSettings();
}
}
if (room.settings.minorActivity) {
if (!room.settings.minorActivity.activityid) {
room.settings.minorActivity.activityid = room.settings.minorActivity.activityId;
delete room.settings.minorActivity.activityId;
}
if (typeof room.settings.minorActivity.activityNumber !== "number") {
room.settings.minorActivity.activityNumber = room.settings.minorActivity.pollNumber || // @ts-expect-error old format
room.settings.minorActivity.announcementNumber;
}
room.saveSettings();
}
if (room.settings.minorActivity?.activityid === "poll") {
room.setMinorActivity(new Poll(room, room.settings.minorActivity), true);
}
}
//# sourceMappingURL=poll.js.map