|
@@ -1,6 +1,5 @@
|
|
|
-import { ICommand } from "./command";
|
|
|
import { Dict, compareNumbers, isAuthorisedAsync } from "../util";
|
|
|
-import { ReactionCollector, Message, TextChannel, RichEmbed, MessageReaction, User, Collector, Collection, SnowflakeUtil } from "discord.js";
|
|
|
+import { Message, TextChannel, RichEmbed, MessageReaction, User, Collector, Collection, SnowflakeUtil } from "discord.js";
|
|
|
import yaml from "yaml";
|
|
|
import { getRepository, getManager } from "typeorm";
|
|
|
import { Contest } from "@db/entity/Contest";
|
|
@@ -9,139 +8,10 @@ import { client } from "../client";
|
|
|
import { scheduleJob } from "node-schedule";
|
|
|
import { ContestEntry } from "@db/entity/ContestEntry";
|
|
|
import { ContestVote } from "@db/entity/ContestVote";
|
|
|
+import { CommandSet, Command, Action, ActionType } from "src/model/command";
|
|
|
|
|
|
const CHANNEL_ID_PATTERN = /<#(\d+)>/;
|
|
|
|
|
|
-let activeContests: Dict<ActiveContest> = {};
|
|
|
-
|
|
|
-async function init() {
|
|
|
- let contestRepo = getRepository(Contest);
|
|
|
- let contests = await contestRepo.find({
|
|
|
- where: { active: true },
|
|
|
- relations: ["entries"]
|
|
|
- });
|
|
|
-
|
|
|
- for (let contest of contests)
|
|
|
- await updateContestStatus(contest);
|
|
|
-
|
|
|
- client.on("messageReactionAdd", onReact);
|
|
|
-}
|
|
|
-
|
|
|
-function diffEntryVotes(votes: ContestVote[], currentUsers: Collection<string, User>) {
|
|
|
- let votedUsersIds = new Set(votes.map(v => v.userId));
|
|
|
- let currentUsersIds = new Set(currentUsers.keys());
|
|
|
-
|
|
|
- for (let currentUserId of currentUsersIds)
|
|
|
- if (votedUsersIds.has(currentUserId))
|
|
|
- votedUsersIds.delete(currentUserId);
|
|
|
-
|
|
|
- for (let votedUserId of votedUsersIds)
|
|
|
- if (currentUsersIds.has(votedUserId))
|
|
|
- currentUsersIds.delete(votedUserId);
|
|
|
-
|
|
|
- return [currentUsersIds, votedUsersIds];
|
|
|
-}
|
|
|
-
|
|
|
-type ContestEntryMessage = {
|
|
|
- msgId?: string;
|
|
|
- timestamp?: Date;
|
|
|
-};
|
|
|
-
|
|
|
-//TODO: Convert into a transaction
|
|
|
-async function updateContestStatus(contest: Contest) {
|
|
|
- let voteRepo = getRepository(ContestVote);
|
|
|
- let entryRepo = getRepository(ContestEntry);
|
|
|
-
|
|
|
- let channel = client.channels.get(contest.channel) as TextChannel;
|
|
|
- if (!channel) {
|
|
|
- console.log(`Channel ${contest.channel} has been deleted! Removing contest ${contest.id}...`);
|
|
|
- await removeContest(contest.id);
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- let newestEntry: Date = null;
|
|
|
- let contestEntryMessageIds = new Set<string>(contest.entries.map(e => e.msgId));
|
|
|
- for (let entry of contest.entries) {
|
|
|
- try {
|
|
|
- let msg = await channel.fetchMessage(entry.msgId);
|
|
|
- let existingVotes = await voteRepo.find({ where: { contest: contest, contestEntry: entry } });
|
|
|
-
|
|
|
- let voteReaction = msg.reactions.find(r => r.emoji.toString() == contest.voteReaction);
|
|
|
- if(!voteReaction) {
|
|
|
- await voteRepo.remove(existingVotes);
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- let users = await voteReaction.fetchUsers();
|
|
|
- let [newVotes, removedVotes] = diffEntryVotes(existingVotes, users);
|
|
|
- voteRepo.remove(existingVotes.filter(v => removedVotes.has(v.userId)));
|
|
|
- let newVoteEntries = [...newVotes].map(i => voteRepo.create({
|
|
|
- userId: i,
|
|
|
- contest: contest,
|
|
|
- contestEntry: entry
|
|
|
- }));
|
|
|
- await voteRepo.save(newVoteEntries);
|
|
|
-
|
|
|
- entry.votes = [
|
|
|
- ...newVoteEntries,
|
|
|
- ...existingVotes.filter(v => !removedVotes.has(v.userId))
|
|
|
- ];
|
|
|
- await entryRepo.save(entry);
|
|
|
-
|
|
|
- if (!newestEntry || msg.createdAt > newestEntry)
|
|
|
- newestEntry = msg.createdAt;
|
|
|
- } catch (err) {
|
|
|
- console.log(`Failed to update entry ${entry.msgId} for contest ${contest.id} because ${err}!`);
|
|
|
-
|
|
|
- await voteRepo.delete({ contestEntry: entry });
|
|
|
- await entryRepo.delete({ msgId: entry.msgId });
|
|
|
- contestEntryMessageIds.delete(entry.msgId);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- let newEntries = (await channel.fetchMessages({
|
|
|
- after: SnowflakeUtil.generate(newestEntry || contest.startDate)
|
|
|
- })).filter(m => m.attachments.size != 0 && !contestEntryMessageIds.has(m.id));
|
|
|
-
|
|
|
- for (let [_, msg] of newEntries)
|
|
|
- await registerEntry(msg, contest);
|
|
|
-
|
|
|
- if (contest.endDate < new Date()) {
|
|
|
- await stopContest(contest.id);
|
|
|
- } else {
|
|
|
- scheduleJob(contest.endDate, stopContest.bind(null, contest.id));
|
|
|
- activeContests[channel.id] = {
|
|
|
- id: contest.id,
|
|
|
- voteReaction: contest.voteReaction
|
|
|
- };
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-async function registerEntry(msg: Message, contest: Contest) {
|
|
|
- let entryRepo = getRepository(ContestEntry);
|
|
|
- let voteRepo = getRepository(ContestVote);
|
|
|
-
|
|
|
- let entry = entryRepo.create({
|
|
|
- msgId: msg.id,
|
|
|
- contest: contest
|
|
|
- });
|
|
|
-
|
|
|
- await entryRepo.save(entry);
|
|
|
-
|
|
|
- let voteReaction = msg.reactions.find(r => r.emoji.toString() == contest.voteReaction);
|
|
|
-
|
|
|
- if(!voteReaction)
|
|
|
- return;
|
|
|
-
|
|
|
- let votedUsers = await voteReaction.fetchUsers();
|
|
|
-
|
|
|
- await voteRepo.save(votedUsers.map(u => voteRepo.create({
|
|
|
- userId: u.id,
|
|
|
- contest: contest,
|
|
|
- contestEntry: entry
|
|
|
- })));
|
|
|
-}
|
|
|
-
|
|
|
interface ActiveContest {
|
|
|
id: number;
|
|
|
voteReaction: string;
|
|
@@ -188,312 +58,430 @@ function parseDuration(duration: string): number | undefined {
|
|
|
return +num * DURATION_MULTIPLIERS[unit];
|
|
|
}
|
|
|
|
|
|
-async function removeContest(contestId: number) {
|
|
|
- await getManager().transaction(async em => {
|
|
|
- let contestRepo = em.getRepository(Contest);
|
|
|
- let contestEntryRepo = em.getRepository(ContestEntry);
|
|
|
- let contestVoteRepo = em.getRepository(ContestVote);
|
|
|
+type ContestEntryWithMessage = ContestEntry & { message: Message };
|
|
|
|
|
|
- let contest = contestRepo.create({
|
|
|
- id: contestId
|
|
|
- });
|
|
|
+function numberToOrdered(num: number) {
|
|
|
+ const prefixes = ["st", "nd", "rd"];
|
|
|
+ let s = num % 10;
|
|
|
+ return 0 < s && s <= prefixes.length ? `${num}${prefixes[s - 1]}` : `${num}th`;
|
|
|
+}
|
|
|
|
|
|
- await contestRepo.delete({
|
|
|
- id: contestId
|
|
|
- });
|
|
|
- await contestVoteRepo.delete({
|
|
|
+@CommandSet
|
|
|
+export class ContestCommands {
|
|
|
+
|
|
|
+ activeContests: Dict<ActiveContest> = {};
|
|
|
+
|
|
|
+ diffEntryVotes(votes: ContestVote[], currentUsers: Collection<string, User>) {
|
|
|
+ let votedUsersIds = new Set(votes.map(v => v.userId));
|
|
|
+ let currentUsersIds = new Set(currentUsers.keys());
|
|
|
+
|
|
|
+ for (let currentUserId of currentUsersIds)
|
|
|
+ if (votedUsersIds.has(currentUserId))
|
|
|
+ votedUsersIds.delete(currentUserId);
|
|
|
+
|
|
|
+ for (let votedUserId of votedUsersIds)
|
|
|
+ if (currentUsersIds.has(votedUserId))
|
|
|
+ currentUsersIds.delete(votedUserId);
|
|
|
+
|
|
|
+ return [currentUsersIds, votedUsersIds];
|
|
|
+ }
|
|
|
+
|
|
|
+ async updateContestStatus(contest: Contest) {
|
|
|
+ let voteRepo = getRepository(ContestVote);
|
|
|
+ let entryRepo = getRepository(ContestEntry);
|
|
|
+
|
|
|
+ let channel = client.channels.get(contest.channel) as TextChannel;
|
|
|
+ if (!channel) {
|
|
|
+ console.log(`Channel ${contest.channel} has been deleted! Removing contest ${contest.id}...`);
|
|
|
+ await this.removeContest(contest.id);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ let newestEntry: Date = null;
|
|
|
+ let contestEntryMessageIds = new Set<string>(contest.entries.map(e => e.msgId));
|
|
|
+ for (let entry of contest.entries) {
|
|
|
+ try {
|
|
|
+ let msg = await channel.fetchMessage(entry.msgId);
|
|
|
+ let existingVotes = await voteRepo.find({ where: { contest: contest, contestEntry: entry } });
|
|
|
+
|
|
|
+ let voteReaction = msg.reactions.find(r => r.emoji.toString() == contest.voteReaction);
|
|
|
+ if (!voteReaction) {
|
|
|
+ await voteRepo.remove(existingVotes);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ let users = await voteReaction.fetchUsers();
|
|
|
+ let [newVotes, removedVotes] = this.diffEntryVotes(existingVotes, users);
|
|
|
+ voteRepo.remove(existingVotes.filter(v => removedVotes.has(v.userId)));
|
|
|
+ let newVoteEntries = [...newVotes].map(i => voteRepo.create({
|
|
|
+ userId: i,
|
|
|
+ contest: contest,
|
|
|
+ contestEntry: entry
|
|
|
+ }));
|
|
|
+ await voteRepo.save(newVoteEntries);
|
|
|
+
|
|
|
+ entry.votes = [
|
|
|
+ ...newVoteEntries,
|
|
|
+ ...existingVotes.filter(v => !removedVotes.has(v.userId))
|
|
|
+ ];
|
|
|
+ await entryRepo.save(entry);
|
|
|
+
|
|
|
+ if (!newestEntry || msg.createdAt > newestEntry)
|
|
|
+ newestEntry = msg.createdAt;
|
|
|
+ } catch (err) {
|
|
|
+ console.log(`Failed to update entry ${entry.msgId} for contest ${contest.id} because ${err}!`);
|
|
|
+
|
|
|
+ await voteRepo.delete({ contestEntry: entry });
|
|
|
+ await entryRepo.delete({ msgId: entry.msgId });
|
|
|
+ contestEntryMessageIds.delete(entry.msgId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ let newEntries = (await channel.fetchMessages({
|
|
|
+ after: SnowflakeUtil.generate(newestEntry || contest.startDate)
|
|
|
+ })).filter(m => m.attachments.size != 0 && !contestEntryMessageIds.has(m.id));
|
|
|
+
|
|
|
+ for (let [_, msg] of newEntries)
|
|
|
+ await this.registerEntry(msg, contest);
|
|
|
+
|
|
|
+ if (contest.endDate < new Date()) {
|
|
|
+ await this.stopContest(contest.id);
|
|
|
+ } else {
|
|
|
+ scheduleJob(contest.endDate, this.stopContest.bind(null, contest.id));
|
|
|
+ this.activeContests[channel.id] = {
|
|
|
+ id: contest.id,
|
|
|
+ voteReaction: contest.voteReaction
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async registerEntry(msg: Message, contest: Contest) {
|
|
|
+ let entryRepo = getRepository(ContestEntry);
|
|
|
+ let voteRepo = getRepository(ContestVote);
|
|
|
+
|
|
|
+ let entry = entryRepo.create({
|
|
|
+ msgId: msg.id,
|
|
|
contest: contest
|
|
|
});
|
|
|
- await contestEntryRepo.delete({
|
|
|
- contest: contest
|
|
|
+
|
|
|
+ await entryRepo.save(entry);
|
|
|
+
|
|
|
+ let voteReaction = msg.reactions.find(r => r.emoji.toString() == contest.voteReaction);
|
|
|
+
|
|
|
+ if (!voteReaction)
|
|
|
+ return;
|
|
|
+
|
|
|
+ let votedUsers = await voteReaction.fetchUsers();
|
|
|
+
|
|
|
+ await voteRepo.save(votedUsers.map(u => voteRepo.create({
|
|
|
+ userId: u.id,
|
|
|
+ contest: contest,
|
|
|
+ contestEntry: entry
|
|
|
+ })));
|
|
|
+ }
|
|
|
+
|
|
|
+ async removeContest(contestId: number) {
|
|
|
+ await getManager().transaction(async em => {
|
|
|
+ let contestRepo = em.getRepository(Contest);
|
|
|
+ let contestEntryRepo = em.getRepository(ContestEntry);
|
|
|
+ let contestVoteRepo = em.getRepository(ContestVote);
|
|
|
+
|
|
|
+ let contest = contestRepo.create({
|
|
|
+ id: contestId
|
|
|
+ });
|
|
|
+
|
|
|
+ await contestRepo.delete({
|
|
|
+ id: contestId
|
|
|
+ });
|
|
|
+ await contestVoteRepo.delete({
|
|
|
+ contest: contest
|
|
|
+ });
|
|
|
+ await contestEntryRepo.delete({
|
|
|
+ contest: contest
|
|
|
+ });
|
|
|
});
|
|
|
- });
|
|
|
-}
|
|
|
+ }
|
|
|
|
|
|
-type ContestEntryWithMessage = ContestEntry & { message: Message };
|
|
|
+ async pickValidEntries(channel: TextChannel, contestEntries: ContestEntry[], max: number, unique = true) {
|
|
|
+ let addedUsers = new Set<string>();
|
|
|
+ let result: ContestEntryWithMessage[] = [];
|
|
|
+ let maxResults = Math.min(max, contestEntries.length);
|
|
|
+
|
|
|
+ for (let entry of contestEntries) {
|
|
|
+ try {
|
|
|
+ let msg = await channel.fetchMessage(entry.msgId);
|
|
|
+ if (unique && addedUsers.has(msg.author.id))
|
|
|
+ continue;
|
|
|
+
|
|
|
+ result.push({ ...entry, message: msg });
|
|
|
+ addedUsers.add(msg.author.id);
|
|
|
+ if (result.length == maxResults)
|
|
|
+ break;
|
|
|
+ } catch (err) { }
|
|
|
+ }
|
|
|
|
|
|
-async function pickValidEntries(channel: TextChannel, contestEntries: ContestEntry[], max: number, unique = true) {
|
|
|
- let addedUsers = new Set<string>();
|
|
|
- let result: ContestEntryWithMessage[] = [];
|
|
|
- let maxResults = Math.min(max, contestEntries.length);
|
|
|
-
|
|
|
- for (let entry of contestEntries) {
|
|
|
- try {
|
|
|
- let msg = await channel.fetchMessage(entry.msgId);
|
|
|
- if (unique && addedUsers.has(msg.author.id))
|
|
|
- continue;
|
|
|
-
|
|
|
- result.push({ ...entry, message: msg });
|
|
|
- addedUsers.add(msg.author.id);
|
|
|
- if (result.length == maxResults)
|
|
|
- break;
|
|
|
- } catch (err) { }
|
|
|
+ return result;
|
|
|
}
|
|
|
|
|
|
- return result;
|
|
|
-}
|
|
|
+ async printResults(contest: Contest, channel: TextChannel) {
|
|
|
+ let entryRepo = getRepository(ContestEntry);
|
|
|
|
|
|
-function numberToOrdered(num: number) {
|
|
|
- const prefixes = ["st", "nd", "rd"];
|
|
|
- let s = num % 10;
|
|
|
- return 0 < s && s <= prefixes.length ? `${num}${prefixes[s - 1]}` : `${num}th`;
|
|
|
-}
|
|
|
+ let entries = await entryRepo.find({
|
|
|
+ where: { contest: contest },
|
|
|
+ relations: ["votes"]
|
|
|
+ });
|
|
|
|
|
|
-async function printResults(contest: Contest, channel: TextChannel) {
|
|
|
- let entryRepo = getRepository(ContestEntry);
|
|
|
+ if (entries.length == 0) {
|
|
|
+ // Hmmm... maybe rich embeds?
|
|
|
+ await channel.send("No entries were sent into this contest! Therefore I declare myself a winner!");
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- let entries = await entryRepo.find({
|
|
|
- where: { contest: contest },
|
|
|
- relations: ["votes"]
|
|
|
- });
|
|
|
+ let winningEntries = await this.pickValidEntries(channel, entries.sort(compareNumbers(o => o.votes.length)), contest.maxWinners, contest.uniqueWinners);
|
|
|
+ let totalVotes = entries.reduce((p, c) => p + c.votes.length, 0);
|
|
|
+
|
|
|
+ let embed = new RichEmbed({
|
|
|
+ title: "🎆 Contest results 🎆",
|
|
|
+ color: 0x3b8dc4,
|
|
|
+ timestamp: new Date(),
|
|
|
+ description: `The contest has ended!\nCollected ${totalVotes} votes.\nHere are the results:`,
|
|
|
+ fields: winningEntries.map((e, i) => ({
|
|
|
+ name: `${numberToOrdered(i + 1)} place (${e.votes.length} votes)`,
|
|
|
+ value: `${e.message.author.toString()} ([View entry](${e.message.url}))`
|
|
|
+ }))
|
|
|
+ });
|
|
|
|
|
|
- if (entries.length == 0) {
|
|
|
- // Hmmm... maybe rich embeds?
|
|
|
- await channel.send("No entries were sent into this contest! Therefore I declare myself a winner!");
|
|
|
- return;
|
|
|
+ await channel.send(embed);
|
|
|
}
|
|
|
|
|
|
- let winningEntries = await pickValidEntries(channel, entries.sort(compareNumbers(o => o.votes.length)), contest.maxWinners, contest.uniqueWinners);
|
|
|
- let totalVotes = entries.reduce((p, c) => p + c.votes.length, 0);
|
|
|
-
|
|
|
- let embed = new RichEmbed({
|
|
|
- title: "🎆 Contest results 🎆",
|
|
|
- color: 0x3b8dc4,
|
|
|
- timestamp: new Date(),
|
|
|
- description: `The contest has ended!\nCollected ${totalVotes} votes.\nHere are the results:`,
|
|
|
- fields: winningEntries.map((e, i) => ({
|
|
|
- name: `${numberToOrdered(i + 1)} place (${e.votes.length} votes)`,
|
|
|
- value: `${e.message.author.toString()} ([View entry](${e.message.url}))`
|
|
|
- }))
|
|
|
- });
|
|
|
-
|
|
|
- await channel.send(embed);
|
|
|
-}
|
|
|
+ async stopContest(contestId: number) {
|
|
|
+ let repo = getRepository(Contest);
|
|
|
+ let contest = await repo.findOne(contestId);
|
|
|
|
|
|
-async function stopContest(contestId: number) {
|
|
|
- let repo = getRepository(Contest);
|
|
|
- let contest = await repo.findOne(contestId);
|
|
|
+ let channel = client.channels.get(contest.channel) as TextChannel;
|
|
|
+ if (!channel) {
|
|
|
+ // TODO: Don't remove; instead report in web manager
|
|
|
+ console.log(`Channel ${contest.channel} has been deleted! Removing contest ${contest.id}...`);
|
|
|
+ await this.removeContest(contestId);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ await channel.send(`Current contest has ended! Thank you for your participation!`);
|
|
|
|
|
|
- let channel = client.channels.get(contest.channel) as TextChannel;
|
|
|
- if (!channel) {
|
|
|
- // TODO: Don't remove; instead report in web manager
|
|
|
- console.log(`Channel ${contest.channel} has been deleted! Removing contest ${contest.id}...`);
|
|
|
- await removeContest(contestId);
|
|
|
- return;
|
|
|
+ if (contest.announceWinners)
|
|
|
+ await this.printResults(contest, channel);
|
|
|
+
|
|
|
+ await repo.update(contestId, { active: false });
|
|
|
}
|
|
|
|
|
|
- await channel.send(`Current contest has ended! Thank you for your participation!`);
|
|
|
+ async createContest(msg: Message, info: ContestCreationOptions) {
|
|
|
+ if (info.in) {
|
|
|
+ let matches = CHANNEL_ID_PATTERN.exec(info.in);
|
|
|
+ if (matches.length == 0) {
|
|
|
+ await msg.channel.send(`${msg.author.toString()} I can't see such a channel!`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- if (contest.announceWinners)
|
|
|
- await printResults(contest, channel);
|
|
|
+ let channelId = matches[1];
|
|
|
+ if (!msg.guild.channels.exists("id", channelId)) {
|
|
|
+ await msg.channel.send(`${msg.author.toString()} This channel is not in the current guild!`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- await repo.update(contestId, { active: false });
|
|
|
-}
|
|
|
+ info.in = channelId;
|
|
|
+ } else
|
|
|
+ info.in = msg.channel.id;
|
|
|
|
|
|
-async function createContest(msg: Message, info: ContestCreationOptions) {
|
|
|
- if (info.in) {
|
|
|
- let matches = CHANNEL_ID_PATTERN.exec(info.in);
|
|
|
- if (matches.length == 0) {
|
|
|
- await msg.channel.send(`${msg.author.toString()} I can't see such a channel!`);
|
|
|
+ if (info.max_winners < 1) {
|
|
|
+ await msg.channel.send(`${msg.author.toString()} The contest must have at least one possible winner!`);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- let channelId = matches[1];
|
|
|
- if (!msg.guild.channels.exists("id", channelId)) {
|
|
|
- await msg.channel.send(`${msg.author.toString()} This channel is not in the current guild!`);
|
|
|
+ let dur = parseDuration(info.duration);
|
|
|
+ if (!dur) {
|
|
|
+ await msg.channel.send(`${msg.author.toString()} Duration format is invalid!`);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- info.in = channelId;
|
|
|
- } else
|
|
|
- info.in = msg.channel.id;
|
|
|
+ if (!msg.guild.emojis.find(e => e.toString() == info.vote_reaction) && !emoji_regex().exec(info.vote_reaction)) {
|
|
|
+ await msg.channel.send(`${msg.author.toString()} The vote emote must be accessible by everyone on the server!`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- if (info.max_winners < 1) {
|
|
|
- await msg.channel.send(`${msg.author.toString()} The contest must have at least one possible winner!`);
|
|
|
- return;
|
|
|
- }
|
|
|
+ let repo = getRepository(Contest);
|
|
|
|
|
|
- let dur = parseDuration(info.duration);
|
|
|
- if (!dur) {
|
|
|
- await msg.channel.send(`${msg.author.toString()} Duration format is invalid!`);
|
|
|
- return;
|
|
|
- }
|
|
|
+ let contest = await repo.findOne({
|
|
|
+ where: { channel: info.in, active: true }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (contest) {
|
|
|
+ await msg.channel.send(`${msg.author.toString()} The channel already has a contest running!`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- if (!msg.guild.emojis.find(e => e.toString() == info.vote_reaction) && !emoji_regex().exec(info.vote_reaction)) {
|
|
|
- await msg.channel.send(`${msg.author.toString()} The vote emote must be accessible by everyone on the server!`);
|
|
|
- return;
|
|
|
+ contest = repo.create({
|
|
|
+ channel: info.in,
|
|
|
+ startDate: new Date(),
|
|
|
+ endDate: new Date(Date.now() + dur),
|
|
|
+ announceWinners: info.announce_winners,
|
|
|
+ voteReaction: info.vote_reaction,
|
|
|
+ maxWinners: info.max_winners,
|
|
|
+ uniqueWinners: info.unique_winners,
|
|
|
+ active: true
|
|
|
+ });
|
|
|
+
|
|
|
+ await repo.save(contest);
|
|
|
+ await msg.channel.send(`${msg.author.toString()} Started contest (ID: ${contest.id})`);
|
|
|
+
|
|
|
+ scheduleJob(contest.endDate, this.stopContest.bind(null, contest.id));
|
|
|
+ this.activeContests[contest.channel] = {
|
|
|
+ id: contest.id,
|
|
|
+ voteReaction: contest.voteReaction
|
|
|
+ };
|
|
|
}
|
|
|
|
|
|
- let repo = getRepository(Contest);
|
|
|
+ async onReact(reaction: MessageReaction, user: User) {
|
|
|
+ if (user.bot)
|
|
|
+ return;
|
|
|
+
|
|
|
+ let channel = reaction.message.channel;
|
|
|
+ let activeContest = this.activeContests[channel.id];
|
|
|
+ if (!activeContest || reaction.emoji.toString() != activeContest.voteReaction)
|
|
|
+ return;
|
|
|
+
|
|
|
+ let entryRepo = getRepository(ContestEntry);
|
|
|
+ let voteRepo = getRepository(ContestVote);
|
|
|
+
|
|
|
+ let entry = await entryRepo.findOne({
|
|
|
+ where: { msgId: reaction.message.id }
|
|
|
+ });
|
|
|
+ if (!entry)
|
|
|
+ return;
|
|
|
|
|
|
- let contest = await repo.findOne({
|
|
|
- where: { channel: info.in, active: true }
|
|
|
- });
|
|
|
+ let vote = await voteRepo.findOne({
|
|
|
+ where: { userId: user.id, contestId: activeContest.id }
|
|
|
+ });
|
|
|
+ if (!vote)
|
|
|
+ vote = voteRepo.create({ userId: user.id, contestId: activeContest.id });
|
|
|
|
|
|
- if (contest) {
|
|
|
- await msg.channel.send(`${msg.author.toString()} The channel already has a contest running!`);
|
|
|
- return;
|
|
|
+ vote.contestEntry = entry;
|
|
|
+ await voteRepo.save(vote);
|
|
|
}
|
|
|
|
|
|
- contest = repo.create({
|
|
|
- channel: info.in,
|
|
|
- startDate: new Date(),
|
|
|
- endDate: new Date(Date.now() + dur),
|
|
|
- announceWinners: info.announce_winners,
|
|
|
- voteReaction: info.vote_reaction,
|
|
|
- maxWinners: info.max_winners,
|
|
|
- uniqueWinners: info.unique_winners,
|
|
|
- active: true
|
|
|
- });
|
|
|
-
|
|
|
- await repo.save(contest);
|
|
|
- await msg.channel.send(`${msg.author.toString()} Started contest (ID: ${contest.id})`);
|
|
|
-
|
|
|
- scheduleJob(contest.endDate, stopContest.bind(null, contest.id));
|
|
|
- activeContests[contest.channel] = {
|
|
|
- id: contest.id,
|
|
|
- voteReaction: contest.voteReaction
|
|
|
- };
|
|
|
-}
|
|
|
+ @Action(ActionType.MESSAGE)
|
|
|
+ async addEntry(actionsDone: boolean, m: Message, content: string) {
|
|
|
+ if (m.attachments.size == 0)
|
|
|
+ return false;
|
|
|
|
|
|
-async function onMessage(actionsDone: boolean, m: Message, content: string) {
|
|
|
- if (m.attachments.size == 0)
|
|
|
- return false;
|
|
|
+ let channel = m.channel;
|
|
|
|
|
|
- let channel = m.channel;
|
|
|
+ let contestRepo = getRepository(Contest);
|
|
|
|
|
|
- let contestRepo = getRepository(Contest);
|
|
|
+ let contest = await contestRepo.findOne({
|
|
|
+ where: {
|
|
|
+ channel: channel.id,
|
|
|
+ active: true
|
|
|
+ },
|
|
|
+ select: ["id", "voteReaction"]
|
|
|
+ });
|
|
|
|
|
|
- let contest = await contestRepo.findOne({
|
|
|
- where: {
|
|
|
- channel: channel.id,
|
|
|
- active: true
|
|
|
- },
|
|
|
- select: ["id", "voteReaction"]
|
|
|
- });
|
|
|
+ if (!contest)
|
|
|
+ return false;
|
|
|
|
|
|
- if (!contest)
|
|
|
+ await this.registerEntry(m, contest);
|
|
|
+
|
|
|
+ // Don't prevent further actions
|
|
|
return false;
|
|
|
+ }
|
|
|
|
|
|
- await registerEntry(m, contest);
|
|
|
+ async onStart() {
|
|
|
+ let contestRepo = getRepository(Contest);
|
|
|
+ let contests = await contestRepo.find({
|
|
|
+ where: { active: true },
|
|
|
+ relations: ["entries"]
|
|
|
+ });
|
|
|
|
|
|
- // Don't prevent further actions
|
|
|
- return false;
|
|
|
-}
|
|
|
+ for (let contest of contests)
|
|
|
+ await this.updateContestStatus(contest);
|
|
|
|
|
|
-async function onReact(reaction: MessageReaction, user: User) {
|
|
|
- if(user.bot)
|
|
|
- return;
|
|
|
+ client.on("messageReactionAdd", this.onReact);
|
|
|
+ }
|
|
|
|
|
|
- let channel = reaction.message.channel;
|
|
|
- let activeContest = activeContests[channel.id];
|
|
|
- if (!activeContest || reaction.emoji.toString() != activeContest.voteReaction)
|
|
|
- return;
|
|
|
+ @Command({ pattern: "create contest", auth: true })
|
|
|
+ async startContest(m: Message) {
|
|
|
+ if (!await isAuthorisedAsync(m.member)) {
|
|
|
+ m.channel.send(`${m.author.toString()} You're not authorised to create contests!`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- let entryRepo = getRepository(ContestEntry);
|
|
|
- let voteRepo = getRepository(ContestVote);
|
|
|
+ let message = m.content.trim().substr(client.user.toString().length).trim().substr("create contest".length).trim();
|
|
|
+ let contestData: ContestCreationOptions = { ...CONTEST_DEFAULTS, ...yaml.parse(message) };
|
|
|
+ await this.createContest(m, contestData);
|
|
|
+ }
|
|
|
|
|
|
- let entry = await entryRepo.findOne({
|
|
|
- where: { msgId: reaction.message.id }
|
|
|
- });
|
|
|
- if (!entry)
|
|
|
- return;
|
|
|
+ @Command({ pattern: "contests" })
|
|
|
+ async listContests(m: Message) {
|
|
|
+ let repo = getRepository(Contest);
|
|
|
+ let contests = await repo.find({ where: { active: true } });
|
|
|
|
|
|
- let vote = await voteRepo.findOne({
|
|
|
- where: { userId: user.id, contestId: activeContest.id }
|
|
|
- });
|
|
|
- if (!vote)
|
|
|
- vote = voteRepo.create({ userId: user.id, contestId: activeContest.id });
|
|
|
+ let contestsData = contests.map(c => ({
|
|
|
+ contest: c,
|
|
|
+ channel: client.channels.get(c.channel) as TextChannel
|
|
|
+ })).filter(c => c.channel);
|
|
|
|
|
|
- vote.contestEntry = entry;
|
|
|
- await voteRepo.save(vote);
|
|
|
-}
|
|
|
+ if (contestsData.length == 0)
|
|
|
+ await m.channel.send(`${m.author.toString()} There are no currently running contests!`);
|
|
|
+ else
|
|
|
+ await m.channel.send(`${m.author.toString()} Currently there are contests active in the following channels:\n${contestsData.map((c, i) => `${i + 1}. ${c.channel.toString()}`).join("\n")}`);
|
|
|
+ }
|
|
|
|
|
|
-export default <ICommand>{
|
|
|
- commands: [
|
|
|
- {
|
|
|
- pattern: "create contest",
|
|
|
- action: async (m, contents) => {
|
|
|
- if (!await isAuthorisedAsync(m.member)) {
|
|
|
- m.channel.send(`${m.author.toString()} You're not authorised to create contests!`);
|
|
|
- return;
|
|
|
- }
|
|
|
+ @Command({ pattern: /end contest( (\d*))?/, auth: true })
|
|
|
+ async endContest(m: Message, contents: string, matches: RegExpMatchArray) {
|
|
|
+ if (!await isAuthorisedAsync(m.member)) {
|
|
|
+ m.channel.send(`${m.author.toString()} You're not authorised to create contests!`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- let message = m.content.trim().substr(client.user.toString().length).trim().substr("create contest".length).trim();
|
|
|
- let contestData: ContestCreationOptions = { ...CONTEST_DEFAULTS, ...yaml.parse(message) };
|
|
|
- await createContest(m, contestData);
|
|
|
- }
|
|
|
- },
|
|
|
- {
|
|
|
- pattern: "contests",
|
|
|
- action: async (m) => {
|
|
|
- let repo = getRepository(Contest);
|
|
|
- let contests = await repo.find({ where: { active: true } });
|
|
|
-
|
|
|
- let contestsData = contests.map(c => ({
|
|
|
- contest: c,
|
|
|
- channel: client.channels.get(c.channel) as TextChannel
|
|
|
- })).filter(c => c.channel);
|
|
|
-
|
|
|
- if(contestsData.length == 0)
|
|
|
- await m.channel.send(`${m.author.toString()} There are no currently running contests!`);
|
|
|
- else
|
|
|
- await m.channel.send(`${m.author.toString()} Currently there are contests active in the following channels:\n${contestsData.map((c, i) => `${i + 1}. ${c.channel.toString()}`).join("\n")}`);
|
|
|
- }
|
|
|
- },
|
|
|
- {
|
|
|
- pattern: /end contest( (\d*))?/,
|
|
|
- action: async (m, contents, matches) => {
|
|
|
- if (!await isAuthorisedAsync(m.member)) {
|
|
|
- m.channel.send(`${m.author.toString()} You're not authorised to create contests!`);
|
|
|
- return;
|
|
|
- }
|
|
|
+ let repo = getRepository(Contest);
|
|
|
+ let contestId = +matches[1];
|
|
|
|
|
|
- let repo = getRepository(Contest);
|
|
|
- let contestId = +matches[1];
|
|
|
+ let contest = await repo.findOne({
|
|
|
+ where: { id: contestId },
|
|
|
+ select: ["id", "active"]
|
|
|
+ });
|
|
|
|
|
|
- let contest = await repo.findOne({
|
|
|
- where: { id: contestId },
|
|
|
- select: ["id", "active"]
|
|
|
- });
|
|
|
+ if (!contest) {
|
|
|
+ await m.channel.send(`${m.author.toString()} Can't find contest with ID ${contestId}`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- if (!contest) {
|
|
|
- await m.channel.send(`${m.author.toString()} Can't find contest with ID ${contestId}`);
|
|
|
- return;
|
|
|
- }
|
|
|
+ if (!contest.active) {
|
|
|
+ await m.channel.send(`${m.author.toString()} The contest with ID ${contestId} is already inactive!`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- if (!contest.active) {
|
|
|
- await m.channel.send(`${m.author.toString()} The contest with ID ${contestId} is already inactive!`);
|
|
|
- return;
|
|
|
- }
|
|
|
+ await this.stopContest(contest.id);
|
|
|
+ }
|
|
|
|
|
|
- await stopContest(contest.id);
|
|
|
- }
|
|
|
- },
|
|
|
- {
|
|
|
- pattern: "announce winners",
|
|
|
- action: async (m, contents) => {
|
|
|
- let repo = getRepository(Contest);
|
|
|
-
|
|
|
- let contest = await repo.findOne({
|
|
|
- where: { channel: m.channel.id },
|
|
|
- order: { endDate: "DESC" }
|
|
|
- });
|
|
|
-
|
|
|
- if(!contest) {
|
|
|
- await m.channel.send(`${m.author.toString()} There have never been any contests!`);
|
|
|
- return;
|
|
|
- }
|
|
|
+ @Command({ pattern: "announce winners", auth: true })
|
|
|
+ async announceWinners(m: Message) {
|
|
|
+ let repo = getRepository(Contest);
|
|
|
|
|
|
- if(contest.active) {
|
|
|
- await stopContest(contest.id);
|
|
|
- if(contest.announceWinners)
|
|
|
- return;
|
|
|
- }
|
|
|
- await printResults(contest, m.channel as TextChannel);
|
|
|
- }
|
|
|
+ let contest = await repo.findOne({
|
|
|
+ where: { channel: m.channel.id },
|
|
|
+ order: { endDate: "DESC" }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!contest) {
|
|
|
+ await m.channel.send(`${m.author.toString()} There have never been any contests!`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (contest.active) {
|
|
|
+ await this.stopContest(contest.id);
|
|
|
+ if (contest.announceWinners)
|
|
|
+ return;
|
|
|
}
|
|
|
- ],
|
|
|
- onStart: init,
|
|
|
- onMessage: onMessage
|
|
|
+ await this.printResults(contest, m.channel as TextChannel);
|
|
|
+ }
|
|
|
};
|