|
@@ -1,22 +1,149 @@
|
|
|
import { ICommand } from "./command";
|
|
|
import { Dict, compareNumbers } from "../util";
|
|
|
-import { ReactionCollector, Message, TextChannel, RichEmbed } from "discord.js";
|
|
|
+import { ReactionCollector, 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";
|
|
|
-import { isNumber } from "util";
|
|
|
import emoji_regex from "emoji-regex";
|
|
|
import { client } from "../client";
|
|
|
import { scheduleJob } from "node-schedule";
|
|
|
-import { ContestEntry } from "../../../db/lib/src/entity/ContestEntry";
|
|
|
-import { ContestVote } from "../../../db/lib/src/entity/ContestVote";
|
|
|
+import { ContestEntry } from "@db/entity/ContestEntry";
|
|
|
+import { ContestVote } from "@db/entity/ContestVote";
|
|
|
|
|
|
const CHANNEL_ID_PATTERN = /<#(\d+)>/;
|
|
|
|
|
|
-let pendingReactors: Dict<ReactionCollector> = {};
|
|
|
+let activeContests: Dict<ActiveContest> = {};
|
|
|
|
|
|
async function init() {
|
|
|
+ let contestRepo = getRepository(Contest);
|
|
|
|
|
|
+ let contests = await contestRepo.find({
|
|
|
+ where: { active: true },
|
|
|
+ relations: ["entries"]
|
|
|
+ });
|
|
|
+
|
|
|
+ let now = new Date();
|
|
|
+
|
|
|
+ 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 entries = contest.entries.reduce((p, c) => {
|
|
|
+ p[c.msgId] = c;
|
|
|
+ return p;
|
|
|
+ }, <Dict<ContestEntry>>{});
|
|
|
+
|
|
|
+ let newestEntry: Date = null;
|
|
|
+
|
|
|
+ for (let entry of contest.entries) {
|
|
|
+ try {
|
|
|
+ let msg = await channel.fetchMessage(entry.msgId);
|
|
|
+ let voteReaction = msg.reactions.get(contest.voteReaction);
|
|
|
+ let users = await voteReaction.fetchUsers();
|
|
|
+ let existingVotes = await voteRepo.find({ where: { contest: contest } });
|
|
|
+
|
|
|
+ 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}!`);
|
|
|
+ delete entries[entry.msgId];
|
|
|
+ }
|
|
|
+
|
|
|
+ let newEntries = (await channel.fetchMessages({
|
|
|
+ after: SnowflakeUtil.generate(newestEntry || contest.startDate)
|
|
|
+ })).filter(m => m.attachments.size != 0);
|
|
|
+
|
|
|
+ for(let [_, msg] of newEntries)
|
|
|
+ await registerEntry(msg, contest);
|
|
|
+ }
|
|
|
+
|
|
|
+ if(contest.endDate < new Date()){
|
|
|
+ contest.active = false;
|
|
|
+
|
|
|
+ if(contest.announceWinners)
|
|
|
+ await printResults(contest, channel);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+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);
|
|
|
+
|
|
|
+ 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;
|
|
|
}
|
|
|
|
|
|
interface ContestCreationOptions {
|
|
@@ -86,20 +213,20 @@ type ContestEntryWithMessage = ContestEntry & { message: Message };
|
|
|
|
|
|
async function pickValidEntries(channel: TextChannel, contestEntries: ContestEntry[], max: number, unique = true) {
|
|
|
let addedUsers = new Set<string>();
|
|
|
- let result : ContestEntryWithMessage[] = [];
|
|
|
+ let result: ContestEntryWithMessage[] = [];
|
|
|
let maxResults = Math.min(max, contestEntries.length);
|
|
|
|
|
|
- for(let entry of contestEntries) {
|
|
|
- try{
|
|
|
+ for (let entry of contestEntries) {
|
|
|
+ try {
|
|
|
let msg = await channel.fetchMessage(entry.msgId);
|
|
|
- if(unique && addedUsers.has(msg.author.id))
|
|
|
+ if (unique && addedUsers.has(msg.author.id))
|
|
|
continue;
|
|
|
|
|
|
- result.push({...entry, message: msg});
|
|
|
+ result.push({ ...entry, message: msg });
|
|
|
addedUsers.add(msg.author.id);
|
|
|
- if(result.length == maxResults)
|
|
|
+ if (result.length == maxResults)
|
|
|
break;
|
|
|
- } catch(err) {}
|
|
|
+ } catch (err) { }
|
|
|
}
|
|
|
|
|
|
return result;
|
|
@@ -182,7 +309,7 @@ async function createContest(msg: Message, info: ContestCreationOptions) {
|
|
|
} else
|
|
|
info.in = msg.channel.id;
|
|
|
|
|
|
- if(info.max_winners < 1) {
|
|
|
+ if (info.max_winners < 1) {
|
|
|
await msg.channel.send(`${msg.author.toString()} The contest must have at least one possible winner!`);
|
|
|
return;
|
|
|
}
|
|
@@ -227,11 +354,72 @@ async function createContest(msg: Message, info: ContestCreationOptions) {
|
|
|
}
|
|
|
|
|
|
async function onMessage(actionsDone: boolean, m: Message, content: string) {
|
|
|
- let repo = getRepository(Contest);
|
|
|
+ if (m.attachments.size == 0)
|
|
|
+ return false;
|
|
|
+
|
|
|
+ let channel = m.channel;
|
|
|
+
|
|
|
+ let contestRepo = getRepository(Contest);
|
|
|
+ let entryRepo = getRepository(ContestEntry);
|
|
|
+
|
|
|
+ let contest = await contestRepo.findOne({
|
|
|
+ where: {
|
|
|
+ channel: channel.id,
|
|
|
+ active: true
|
|
|
+ },
|
|
|
+ select: ["id", "voteReaction"]
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!contest)
|
|
|
+ return false;
|
|
|
|
|
|
+ await registerEntry(m);
|
|
|
+
|
|
|
+ // Don't prevent further actions
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
+async function onReact(reaction: MessageReaction, user: User) {
|
|
|
+ let channel = reaction.message.channel;
|
|
|
+
|
|
|
+ let activeContest = activeContests[channel.id];
|
|
|
+ if (!activeContest)
|
|
|
+ return;
|
|
|
+
|
|
|
+ if (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 vote = await voteRepo.findOne({
|
|
|
+ where: {
|
|
|
+ userId: user.id,
|
|
|
+ contest: {
|
|
|
+ id: activeContest.id
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!vote)
|
|
|
+ vote = voteRepo.create({
|
|
|
+ userId: user.id,
|
|
|
+ contest: {
|
|
|
+ id: activeContest.id
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ vote.contestEntry = entry;
|
|
|
+ await voteRepo.save(vote);
|
|
|
+}
|
|
|
+
|
|
|
export default <ICommand>{
|
|
|
commands: [
|
|
|
{
|