From 5a6c66cb3ee7726e96ef9d9dc6c470e22e5ab8b1 Mon Sep 17 00:00:00 2001 From: kenobi Date: Sun, 25 Jun 2023 01:57:14 +0200 Subject: [PATCH 01/72] export newRequestId from logger --- server/logger.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/logger.ts b/server/logger.ts index 6b53f3b..335439d 100644 --- a/server/logger.ts +++ b/server/logger.ts @@ -1,5 +1,8 @@ import { createLogger, format, transports } from "winston" import { config } from "./configuration" +import { v4 } from "uuid" +export const newRequestId = v4() +export const noGuildId = 'NoGuildId' const printFn = format.printf(({ guildId, level, message, errorCode, requestId, timestamp: logTimestamp }: { [k: string]: string }) => { -- 2.40.1 From e3e755011d4a4374a9df16ca5ddd76679ec7365b Mon Sep 17 00:00:00 2001 From: kenobi Date: Sun, 25 Jun 2023 01:57:30 +0200 Subject: [PATCH 02/72] add messageIdentifier helper --- server/helper/messageIdentifiers.ts | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 server/helper/messageIdentifiers.ts diff --git a/server/helper/messageIdentifiers.ts b/server/helper/messageIdentifiers.ts new file mode 100644 index 0000000..1a570ba --- /dev/null +++ b/server/helper/messageIdentifiers.ts @@ -0,0 +1,6 @@ +import { Message } from "discord.js"; + +export function messageIsVoteMessage(msg: Message): boolean { + return msg.content.includes('[Abstimmung]') + +} -- 2.40.1 From b8a32aab409f1f9e0a8a940823997f24a54842e0 Mon Sep 17 00:00:00 2001 From: kenobi Date: Sun, 25 Jun 2023 01:57:40 +0200 Subject: [PATCH 03/72] stub for reactionhandling --- server/events/handleReactionAdd.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 server/events/handleReactionAdd.ts diff --git a/server/events/handleReactionAdd.ts b/server/events/handleReactionAdd.ts new file mode 100644 index 0000000..f2fd445 --- /dev/null +++ b/server/events/handleReactionAdd.ts @@ -0,0 +1,20 @@ + +import { Message, MessageReaction, User } from "discord.js"; +import { messageIsVoteMessage } from "../helper/messageIdentifiers"; +import { logger, newRequestId, noGuildId } from "../logger"; + + +export const name = 'messageReactionAdd' + +export async function execute(messageReaction: MessageReaction, user: User) { + const requestId = newRequestId + const guildId = messageReaction.message.inGuild() ? messageReaction.message.guildId : noGuildId + + const reactedUponMessage: Message = messageReaction.message.partial ? await messageReaction.message.fetch() : messageReaction.message + + if (messageIsVoteMessage(reactedUponMessage)) { + logger.info(`Got reaction on message`, { requestId, guildId }) + } + + return +} -- 2.40.1 From ca0a9e3cb8d49cc440c2585fa0a79372250fade1 Mon Sep 17 00:00:00 2001 From: kenobi Date: Sun, 25 Jun 2023 02:20:34 +0200 Subject: [PATCH 04/72] more message identifiers --- server/helper/messageIdentifiers.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/server/helper/messageIdentifiers.ts b/server/helper/messageIdentifiers.ts index 1a570ba..1b33bde 100644 --- a/server/helper/messageIdentifiers.ts +++ b/server/helper/messageIdentifiers.ts @@ -1,6 +1,11 @@ import { Message } from "discord.js"; export function messageIsVoteMessage(msg: Message): boolean { - return msg.content.includes('[Abstimmung]') - + return msg.cleanContent.includes('[Abstimmung]') +} +export function messageIsInitialAnnouncement(msg: Message): boolean { + return msg.cleanContent.includes("[initial]") +} +export function messageIsVoteEndedMessage(msg: Message): boolean { + return msg.cleanContent.includes("[Abstimmung beendet]") } -- 2.40.1 From b6034d4fb7ebfb4d867e4c24f3221ea5466c3079 Mon Sep 17 00:00:00 2001 From: kenobi Date: Sun, 25 Jun 2023 02:20:45 +0200 Subject: [PATCH 05/72] use message identifiers --- server/commands/announce.ts | 3 ++- server/commands/closepoll.ts | 3 ++- server/events/deleteAnnouncementsWhenWPEnds.ts | 3 ++- server/structures/client.ts | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/server/commands/announce.ts b/server/commands/announce.ts index 4a6eff3..e803177 100644 --- a/server/commands/announce.ts +++ b/server/commands/announce.ts @@ -6,6 +6,7 @@ import { Maybe } from '../interfaces' import { logger } from '../logger' import { Command } from '../structures/command' import { RunOptions } from '../types/commandTypes' +import { messageIsInitialAnnouncement } from '../helper/messageIdentifiers' export default new Command({ name: 'announce', @@ -61,7 +62,7 @@ async function sendInitialAnnouncement(guildId: string, requestId: string): Prom return } - const currentPinnedAnnouncementMessages = (await announcementChannel.messages.fetchPinned()).filter(message => message.cleanContent.includes("[initial]")) + const currentPinnedAnnouncementMessages = (await announcementChannel.messages.fetchPinned()).filter(message => messageIsInitialAnnouncement(message)) currentPinnedAnnouncementMessages.forEach(async (message) => await message.unpin()) currentPinnedAnnouncementMessages.forEach(message => message.delete()) diff --git a/server/commands/closepoll.ts b/server/commands/closepoll.ts index 7cf05c2..fea96fe 100644 --- a/server/commands/closepoll.ts +++ b/server/commands/closepoll.ts @@ -8,6 +8,7 @@ import { Maybe } from '../interfaces' import { logger } from '../logger' import { Command } from '../structures/command' import { RunOptions } from '../types/commandTypes' +import { messageIsVoteEndedMessage, messageIsVoteMessage } from '../helper/messageIdentifiers' export default new Command({ name: 'closepoll', @@ -41,7 +42,7 @@ export async function closePoll(guild: Guild, requestId: string) { const messages: Message[] = (await announcementChannel.messages.fetch()) //todo: fetch only pinned messages .map((value) => value) - .filter(message => !message.cleanContent.includes("[Abstimmung beendet]") && message.cleanContent.includes("[Abstimmung]")) + .filter(message => !messageIsVoteEndedMessage(message) && messageIsVoteMessage(message)) .sort((a, b) => b.createdTimestamp - a.createdTimestamp) if (!messages || messages.length <= 0) { diff --git a/server/events/deleteAnnouncementsWhenWPEnds.ts b/server/events/deleteAnnouncementsWhenWPEnds.ts index 8d7acfe..9b1a0b0 100644 --- a/server/events/deleteAnnouncementsWhenWPEnds.ts +++ b/server/events/deleteAnnouncementsWhenWPEnds.ts @@ -2,6 +2,7 @@ import { Collection, GuildScheduledEvent, GuildScheduledEventStatus, Message } f import { v4 as uuid } from "uuid"; import { client } from "../.."; import { logger } from "../logger"; +import { messageIsInitialAnnouncement } from "../helper/messageIdentifiers"; export const name = 'guildScheduledEventUpdate' @@ -25,7 +26,7 @@ export async function execute(oldEvent: GuildScheduledEvent, newEvent: GuildSche const events = await newEvent.guild.scheduledEvents.fetch() - const wpAnnouncements = (await announcementChannel.messages.fetch()).filter(message => !message.cleanContent.includes("[initial]")) + const wpAnnouncements = (await announcementChannel.messages.fetch()).filter(message => !messageIsInitialAnnouncement(message)) const announcementsWithoutEvent = filterAnnouncementsByPendingWPs(wpAnnouncements, events) logger.info(`Deleting ${announcementsWithoutEvent.length} announcements.`, { guildId, requestId }) announcementsWithoutEvent.forEach(message => message.delete()) diff --git a/server/structures/client.ts b/server/structures/client.ts index 10be4af..4f4047f 100644 --- a/server/structures/client.ts +++ b/server/structures/client.ts @@ -9,6 +9,7 @@ import { JellyfinHandler } from "../jellyfin/handler"; import { logger } from "../logger"; import { CommandType } from "../types/commandTypes"; import { checkForPollsToClose } from "../commands/closepoll"; +import { messageIsInitialAnnouncement } from "../helper/messageIdentifiers"; @@ -136,7 +137,7 @@ export class ExtendedClient extends Client { } this.announcementRoleHandlerTask.set(guild.id, schedule("*/10 * * * * *", async () => { const requestId = uuid() - const messages = (await textChannel.messages.fetchPinned()).filter(message => message.cleanContent.includes("[initial]")) + const messages = (await textChannel.messages.fetchPinned()).filter(message => messageIsInitialAnnouncement(message)) if (messages.size > 1) { logger.error("More than one pinned announcement Messages found. Unable to know which one people react to. Please fix!", { guildId: guild.id, requestId }) -- 2.40.1 From 6220268b1469c85c259cfd17a1843caa58d959e3 Mon Sep 17 00:00:00 2001 From: kenobi Date: Sun, 25 Jun 2023 22:46:46 +0200 Subject: [PATCH 06/72] move emotes and reaction constants --- server/commands/closepoll.ts | 2 +- server/constants.ts | 3 +++ server/events/autoCreateVoteByWPEvent.ts | 4 +--- 3 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 server/constants.ts diff --git a/server/commands/closepoll.ts b/server/commands/closepoll.ts index fea96fe..756d1bc 100644 --- a/server/commands/closepoll.ts +++ b/server/commands/closepoll.ts @@ -3,12 +3,12 @@ import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildSchedu import { v4 as uuid } from 'uuid' import { client } from '../..' import { config } from '../configuration' -import { Emotes } from '../events/autoCreateVoteByWPEvent' import { Maybe } from '../interfaces' import { logger } from '../logger' import { Command } from '../structures/command' import { RunOptions } from '../types/commandTypes' import { messageIsVoteEndedMessage, messageIsVoteMessage } from '../helper/messageIdentifiers' +import { Emotes } from '../constants' export default new Command({ name: 'closepoll', diff --git a/server/constants.ts b/server/constants.ts new file mode 100644 index 0000000..d60b4a9 --- /dev/null +++ b/server/constants.ts @@ -0,0 +1,3 @@ + +export enum Emotes { "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟" } +export const NONE_OF_THAT = "❌" diff --git a/server/events/autoCreateVoteByWPEvent.ts b/server/events/autoCreateVoteByWPEvent.ts index 8ccdddb..323556d 100644 --- a/server/events/autoCreateVoteByWPEvent.ts +++ b/server/events/autoCreateVoteByWPEvent.ts @@ -6,14 +6,12 @@ import { config } from "../configuration"; import { createDateStringFromEvent } from "../helper/dateHelper"; import { Maybe } from "../interfaces"; import { logger } from "../logger"; +import { Emotes, NONE_OF_THAT } from "../constants"; export const name = 'guildScheduledEventCreate' -export enum Emotes { "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟" } -export const NONE_OF_THAT = "❌" -export let task: ScheduledTask | undefined export async function execute(event: GuildScheduledEvent) { const requestId = uuid() -- 2.40.1 From f6476c609b394be5482cce38f6f7f701253c97c6 Mon Sep 17 00:00:00 2001 From: kenobi Date: Sun, 25 Jun 2023 22:47:06 +0200 Subject: [PATCH 07/72] fetch members of roleId from guild --- server/helper/roleFilter.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/server/helper/roleFilter.ts b/server/helper/roleFilter.ts index 23ea237..02df4da 100644 --- a/server/helper/roleFilter.ts +++ b/server/helper/roleFilter.ts @@ -1,5 +1,5 @@ -import { Collection, GuildMember } from "discord.js" -import { ChangedRoles, PermissionLevel } from "../interfaces" +import { Collection, Guild, GuildMember, Role, User } from "discord.js" +import { ChangedRoles, Maybe, PermissionLevel } from "../interfaces" import { logger } from "../logger" import { config } from "../configuration" @@ -16,6 +16,13 @@ export function filterRolesFromMemberUpdate(oldMember: GuildMember, newMember: G return { addedRoles, removedRoles } } +export async function getMembersWithRoleFromGuild(roleId: string, guild: Guild): Promise> { + const emptyResponse = new Collection + const guildRole: Maybe = guild.roles.resolve(roleId) + if (!guildRole) return emptyResponse + return guildRole.members +} + export function getGuildSpecificTriggerRoleId(): Collection { const outVal = new Collection() outVal.set(config.bot.watcher_role, "VIEWER") -- 2.40.1 From 331ff89060018fe7b529efba69179c983135e6ea Mon Sep 17 00:00:00 2001 From: kenobi Date: Sun, 25 Jun 2023 22:48:55 +0200 Subject: [PATCH 08/72] fetch all message from announcement channel on start This is necessary because message sent before the bot has started up are not cached and reactions will not be registered. If the messages are cached manually the reactions will be received and can be processed using the regular event handling --- server/structures/client.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/server/structures/client.ts b/server/structures/client.ts index 4f4047f..fe08582 100644 --- a/server/structures/client.ts +++ b/server/structures/client.ts @@ -23,7 +23,7 @@ export class ExtendedClient extends Client { private pollCloseBackgroundTasks: Collection = new Collection() public constructor(jf: JellyfinHandler) { const intents: IntentsBitField = new IntentsBitField() - intents.add(IntentsBitField.Flags.GuildMembers, IntentsBitField.Flags.MessageContent, IntentsBitField.Flags.Guilds, IntentsBitField.Flags.DirectMessages, IntentsBitField.Flags.GuildScheduledEvents, IntentsBitField.Flags.GuildVoiceStates) + intents.add(IntentsBitField.Flags.GuildMembers, IntentsBitField.Flags.MessageContent, IntentsBitField.Flags.Guilds, IntentsBitField.Flags.DirectMessages, IntentsBitField.Flags.GuildScheduledEvents, IntentsBitField.Flags.GuildMessageReactions, IntentsBitField.Flags.GuildVoiceStates) const options: ClientOptions = { intents } super(options) this.jellyfin = jf @@ -75,6 +75,7 @@ export class ExtendedClient extends Client { this.registerCommands(slashCommands, guilds) this.cacheUsers(guilds) await this.cacheAnnouncementServer(guilds) + this.fetchAnnouncementChannelMessage(this.announcementChannels) this.startAnnouncementRoleBackgroundTask(guilds) this.startPollCloseBackgroundTasks() }) @@ -82,6 +83,11 @@ export class ExtendedClient extends Client { logger.info(`Error refreshing slash commands: ${error}`) } } + private async fetchAnnouncementChannelMessage(channels: Collection): Promise { + channels.each(async ch => { + ch.messages.fetch() + }) + } private async cacheAnnouncementServer(guilds: Collection) { for (const guild of guilds.values()) { const channels: TextChannel[] = (await guild.channels.fetch()) -- 2.40.1 From d9d1d74ef909532f383ea5292aadd29221887081 Mon Sep 17 00:00:00 2001 From: kenobi Date: Sun, 25 Jun 2023 22:49:21 +0200 Subject: [PATCH 09/72] WIP: basic handling of adding a reaction to a message and deciding whether to reroll or not --- server/events/handleReactionAdd.ts | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/server/events/handleReactionAdd.ts b/server/events/handleReactionAdd.ts index f2fd445..6ad27e6 100644 --- a/server/events/handleReactionAdd.ts +++ b/server/events/handleReactionAdd.ts @@ -2,18 +2,43 @@ import { Message, MessageReaction, User } from "discord.js"; import { messageIsVoteMessage } from "../helper/messageIdentifiers"; import { logger, newRequestId, noGuildId } from "../logger"; +import { NONE_OF_THAT } from "../constants"; +import { client } from "../.."; +import { getMembersWithRoleFromGuild } from "../helper/roleFilter"; +import { config } from "../configuration"; export const name = 'messageReactionAdd' export async function execute(messageReaction: MessageReaction, user: User) { + if (user.id == client.user?.id) + logger.info('Skipping bot reaction') + const requestId = newRequestId const guildId = messageReaction.message.inGuild() ? messageReaction.message.guildId : noGuildId - const reactedUponMessage: Message = messageReaction.message.partial ? await messageReaction.message.fetch() : messageReaction.message + if (!messageReaction.message.guild) return 'No guild' + + logger.info(`Got reaction on message`, { requestId, guildId }) + logger.debug(`reactedUponMessage payload: ${JSON.stringify(reactedUponMessage)}`) if (messageIsVoteMessage(reactedUponMessage)) { - logger.info(`Got reaction on message`, { requestId, guildId }) + logger.debug(`${reactedUponMessage.id} is vote message`, { requestId, guildId }) + if (messageReaction.message.reactions.cache.find(reaction => reaction.emoji.toString() == NONE_OF_THAT)) { + const watcherRoleMember = await getMembersWithRoleFromGuild(config.bot.announcement_role, messageReaction.message.guild) + logger.info("ROLE MEMBERS " + JSON.stringify(watcherRoleMember), { requestId, guildId }) + const watcherRoleMemberCount = watcherRoleMember.size + logger.info(`MEMBER COUNT: ${watcherRoleMemberCount}`, { requestId, guildId }) + let noneOfThatReactions = messageReaction.message.reactions.cache.get(NONE_OF_THAT)?.users.cache.filter(x => x.id !== client.user?.id).size ?? 0 + + const memberThreshold = (watcherRoleMemberCount / 2) + logger.info(`Reroll ${noneOfThatReactions} > ${memberThreshold} ?`, { requestId, guildId }) + if (noneOfThatReactions > memberThreshold) { + logger.info('Starting poll reroll', { requestId, guildId }) + messageReaction.message.edit((messageReaction.message.content ?? "").concat('\nDiese Abstimmung muss wiederholt werden.')) + } + logger.info(`No reroll`, { requestId, guildId }) + } } return -- 2.40.1 From e8dcfd834060eb6bd11c194b36ec78f64040556e Mon Sep 17 00:00:00 2001 From: kenobi Date: Mon, 26 Jun 2023 23:47:43 +0200 Subject: [PATCH 10/72] add votecontroller to consolidate handling of votes --- server/helper/vote.controller.ts | 33 ++++++++++++++++++++++++++++++++ server/structures/client.ts | 2 ++ 2 files changed, 35 insertions(+) create mode 100644 server/helper/vote.controller.ts diff --git a/server/helper/vote.controller.ts b/server/helper/vote.controller.ts new file mode 100644 index 0000000..8d342e6 --- /dev/null +++ b/server/helper/vote.controller.ts @@ -0,0 +1,33 @@ +import { Message, MessageReaction, User } from "discord.js" +import { client } from "../.." +import { NONE_OF_THAT } from "../constants" +import { logger } from "../logger" +import { messageIsVoteMessage } from "./messageIdentifiers" +import { getMembersWithRoleFromGuild } from "./roleFilter" +import { config } from "../configuration" + +export default class VoteController { + + public async handleNoneOfThatVote(messageReaction: MessageReaction, user: User, reactedUponMessage: Message, requestId: string, guildId: string) { + if (!messageReaction.message.guild) return 'No guild' + if (messageIsVoteMessage(reactedUponMessage)) { + logger.debug(`${reactedUponMessage.id} is vote message`, { requestId, guildId }) + if (messageReaction.message.reactions.cache.find(reaction => reaction.emoji.toString() == NONE_OF_THAT)) { + const watcherRoleMember = await getMembersWithRoleFromGuild(config.bot.announcement_role, messageReaction.message.guild) + logger.info("ROLE MEMBERS " + JSON.stringify(watcherRoleMember), { requestId, guildId }) + const watcherRoleMemberCount = watcherRoleMember.size + logger.info(`MEMBER COUNT: ${watcherRoleMemberCount}`, { requestId, guildId }) + let noneOfThatReactions = messageReaction.message.reactions.cache.get(NONE_OF_THAT)?.users.cache.filter(x => x.id !== client.user?.id).size ?? 0 + + const memberThreshold = (watcherRoleMemberCount / 2) + logger.info(`Reroll ${noneOfThatReactions} > ${memberThreshold} ?`, { requestId, guildId }) + if (noneOfThatReactions > memberThreshold) { + logger.info('Starting poll reroll', { requestId, guildId }) + messageReaction.message.edit((messageReaction.message.content ?? "").concat('\nDiese Abstimmung muss wiederholt werden.')) + } + logger.info(`No reroll`, { requestId, guildId }) + } + } + + } +} diff --git a/server/structures/client.ts b/server/structures/client.ts index fe08582..9e0c2d8 100644 --- a/server/structures/client.ts +++ b/server/structures/client.ts @@ -10,6 +10,7 @@ import { logger } from "../logger"; import { CommandType } from "../types/commandTypes"; import { checkForPollsToClose } from "../commands/closepoll"; import { messageIsInitialAnnouncement } from "../helper/messageIdentifiers"; +import VoteController from "../helper/vote.controller"; @@ -17,6 +18,7 @@ export class ExtendedClient extends Client { private eventFilePath = `${__dirname}/../events` private commandFilePath = `${__dirname}/../commands` private jellyfin: JellyfinHandler + public VoteController: VoteController = new VoteController() public commands: Collection = new Collection() private announcementChannels: Collection = new Collection() //guildId to TextChannel private announcementRoleHandlerTask: Collection = new Collection() //one task per guild -- 2.40.1 From a4a834ad27af82fd5c729a31d56b5d62c580c5d4 Mon Sep 17 00:00:00 2001 From: kenobi Date: Mon, 26 Jun 2023 23:48:52 +0200 Subject: [PATCH 11/72] refactor reaction handling - rename - externalise handling of none_of_that to vote controller - base for extensions for more reaction handling --- server/events/handleMessageReactionAdd.ts | 31 ++++++++++++++++ server/events/handleReactionAdd.ts | 45 ----------------------- 2 files changed, 31 insertions(+), 45 deletions(-) create mode 100644 server/events/handleMessageReactionAdd.ts delete mode 100644 server/events/handleReactionAdd.ts diff --git a/server/events/handleMessageReactionAdd.ts b/server/events/handleMessageReactionAdd.ts new file mode 100644 index 0000000..e137c17 --- /dev/null +++ b/server/events/handleMessageReactionAdd.ts @@ -0,0 +1,31 @@ + +import { Message, MessageReaction, User } from "discord.js"; +import { logger, newRequestId, noGuildId } from "../logger"; +import { NONE_OF_THAT } from "../constants"; +import { client } from "../.."; + + +export const name = 'messageReactionAdd' + +export async function execute(messageReaction: MessageReaction, user: User) { + if (user.id == client.user?.id) + logger.info('Skipping bot reaction') + const requestId = newRequestId + const guildId = messageReaction.message.inGuild() ? messageReaction.message.guildId : noGuildId + const reactedUponMessage: Message = messageReaction.message.partial ? await messageReaction.message.fetch() : messageReaction.message + if (!messageReaction.message.guild) { + logger.warn(`Received messageReactionAdd on non-guild message.`, { requestId }) + return + } + + logger.info(`Got reaction on message`, { requestId, guildId }) + //logger.debug(`reactedUponMessage payload: ${JSON.stringify(reactedUponMessage)}`) + + logger.info(`emoji: ${messageReaction.emoji.toString()}`) + if (messageReaction.emoji.toString() === NONE_OF_THAT) { + logger.info(`Reaction is NONE_OF_THAT. Handling`, { requestId, guildId }) + return client.VoteController.handleNoneOfThatVote(messageReaction, user, reactedUponMessage, requestId, guildId) + } + + return +} diff --git a/server/events/handleReactionAdd.ts b/server/events/handleReactionAdd.ts deleted file mode 100644 index 6ad27e6..0000000 --- a/server/events/handleReactionAdd.ts +++ /dev/null @@ -1,45 +0,0 @@ - -import { Message, MessageReaction, User } from "discord.js"; -import { messageIsVoteMessage } from "../helper/messageIdentifiers"; -import { logger, newRequestId, noGuildId } from "../logger"; -import { NONE_OF_THAT } from "../constants"; -import { client } from "../.."; -import { getMembersWithRoleFromGuild } from "../helper/roleFilter"; -import { config } from "../configuration"; - - -export const name = 'messageReactionAdd' - -export async function execute(messageReaction: MessageReaction, user: User) { - if (user.id == client.user?.id) - logger.info('Skipping bot reaction') - - const requestId = newRequestId - const guildId = messageReaction.message.inGuild() ? messageReaction.message.guildId : noGuildId - const reactedUponMessage: Message = messageReaction.message.partial ? await messageReaction.message.fetch() : messageReaction.message - if (!messageReaction.message.guild) return 'No guild' - - logger.info(`Got reaction on message`, { requestId, guildId }) - logger.debug(`reactedUponMessage payload: ${JSON.stringify(reactedUponMessage)}`) - - if (messageIsVoteMessage(reactedUponMessage)) { - logger.debug(`${reactedUponMessage.id} is vote message`, { requestId, guildId }) - if (messageReaction.message.reactions.cache.find(reaction => reaction.emoji.toString() == NONE_OF_THAT)) { - const watcherRoleMember = await getMembersWithRoleFromGuild(config.bot.announcement_role, messageReaction.message.guild) - logger.info("ROLE MEMBERS " + JSON.stringify(watcherRoleMember), { requestId, guildId }) - const watcherRoleMemberCount = watcherRoleMember.size - logger.info(`MEMBER COUNT: ${watcherRoleMemberCount}`, { requestId, guildId }) - let noneOfThatReactions = messageReaction.message.reactions.cache.get(NONE_OF_THAT)?.users.cache.filter(x => x.id !== client.user?.id).size ?? 0 - - const memberThreshold = (watcherRoleMemberCount / 2) - logger.info(`Reroll ${noneOfThatReactions} > ${memberThreshold} ?`, { requestId, guildId }) - if (noneOfThatReactions > memberThreshold) { - logger.info('Starting poll reroll', { requestId, guildId }) - messageReaction.message.edit((messageReaction.message.content ?? "").concat('\nDiese Abstimmung muss wiederholt werden.')) - } - logger.info(`No reroll`, { requestId, guildId }) - } - } - - return -} -- 2.40.1 From 8ad651c753eacc69e50e866fc7f3960742c7188f Mon Sep 17 00:00:00 2001 From: kenobi Date: Mon, 26 Jun 2023 23:51:14 +0200 Subject: [PATCH 12/72] prepare unicode representation of emoji for cleaner handling as pure ASCII emoji handling in editors and browsers is iffy, as such a pure ascii code base is easier to handle (imho) --- server/constants.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/server/constants.ts b/server/constants.ts index d60b4a9..4efe5ba 100644 --- a/server/constants.ts +++ b/server/constants.ts @@ -1,3 +1,15 @@ export enum Emotes { "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟" } export const NONE_OF_THAT = "❌" +export const Emoji = { + "one": "\u0031\uFE0F\u20E3", + "two": "\u0032\uFE0F\u20E3", + "three": "\u0033\uFE0F\u20E3", + "four": "\u0034\uFE0F\u20E3", + "five": "\u0035\uFE0F\u20E3", + "six": "\u0036\uFE0F\u20E3", + "seven": "\u0037\uFE0F\u20E3", + "eight": "\u0038\uFE0F\u20E3", + "nine": "\u0039\uFE0F\u20E3", + "ten": "\uD83D\uDD1F" +} -- 2.40.1 From ee742018e99ea8285e8c5b23cb55c2e36e619f94 Mon Sep 17 00:00:00 2001 From: kenobi Date: Tue, 27 Jun 2023 20:08:39 +0200 Subject: [PATCH 13/72] adds comment to fetchAnnouncementChannelMessage --- server/structures/client.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/server/structures/client.ts b/server/structures/client.ts index 9e0c2d8..f445585 100644 --- a/server/structures/client.ts +++ b/server/structures/client.ts @@ -85,6 +85,16 @@ export class ExtendedClient extends Client { logger.info(`Error refreshing slash commands: ${error}`) } } + /** + * Fetches all messages from the provided channel collection. + * This is necessary for announcementChannels, because 'old' messages don't receive + * messageReactionAdd Events, only messages that were sent while the bot is online are tracked + * automatically. + * To prevent the need for a dedicated 'Collector' implementation which would listen on specific + * it's easiest to just fetch all messages from the backlog, which automatically makes the bot track them + * again. + * @param {Collection} channels - All channels which should be fecthed for reactionTracking + */ private async fetchAnnouncementChannelMessage(channels: Collection): Promise { channels.each(async ch => { ch.messages.fetch() -- 2.40.1 From 98d1ca73b5ae0eb5f8682922dad481749cdae467 Mon Sep 17 00:00:00 2001 From: kenobi Date: Tue, 27 Jun 2023 20:19:42 +0200 Subject: [PATCH 14/72] fix newRequestId function --- server/events/handleMessageReactionAdd.ts | 2 +- server/logger.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/events/handleMessageReactionAdd.ts b/server/events/handleMessageReactionAdd.ts index e137c17..1820b93 100644 --- a/server/events/handleMessageReactionAdd.ts +++ b/server/events/handleMessageReactionAdd.ts @@ -10,7 +10,7 @@ export const name = 'messageReactionAdd' export async function execute(messageReaction: MessageReaction, user: User) { if (user.id == client.user?.id) logger.info('Skipping bot reaction') - const requestId = newRequestId + const requestId = newRequestId() const guildId = messageReaction.message.inGuild() ? messageReaction.message.guildId : noGuildId const reactedUponMessage: Message = messageReaction.message.partial ? await messageReaction.message.fetch() : messageReaction.message if (!messageReaction.message.guild) { diff --git a/server/logger.ts b/server/logger.ts index 335439d..060c72a 100644 --- a/server/logger.ts +++ b/server/logger.ts @@ -1,7 +1,7 @@ import { createLogger, format, transports } from "winston" import { config } from "./configuration" import { v4 } from "uuid" -export const newRequestId = v4() +export function newRequestId() { return v4() } export const noGuildId = 'NoGuildId' -- 2.40.1 From 3f071c8a4e761253ec9f5f8915926acc4ef08463 Mon Sep 17 00:00:00 2001 From: kenobi Date: Tue, 27 Jun 2023 20:22:44 +0200 Subject: [PATCH 15/72] remove duplicate check for none_of_that vote --- server/helper/vote.controller.ts | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/server/helper/vote.controller.ts b/server/helper/vote.controller.ts index 8d342e6..9ddfd5b 100644 --- a/server/helper/vote.controller.ts +++ b/server/helper/vote.controller.ts @@ -10,24 +10,19 @@ export default class VoteController { public async handleNoneOfThatVote(messageReaction: MessageReaction, user: User, reactedUponMessage: Message, requestId: string, guildId: string) { if (!messageReaction.message.guild) return 'No guild' - if (messageIsVoteMessage(reactedUponMessage)) { - logger.debug(`${reactedUponMessage.id} is vote message`, { requestId, guildId }) - if (messageReaction.message.reactions.cache.find(reaction => reaction.emoji.toString() == NONE_OF_THAT)) { - const watcherRoleMember = await getMembersWithRoleFromGuild(config.bot.announcement_role, messageReaction.message.guild) - logger.info("ROLE MEMBERS " + JSON.stringify(watcherRoleMember), { requestId, guildId }) - const watcherRoleMemberCount = watcherRoleMember.size - logger.info(`MEMBER COUNT: ${watcherRoleMemberCount}`, { requestId, guildId }) - let noneOfThatReactions = messageReaction.message.reactions.cache.get(NONE_OF_THAT)?.users.cache.filter(x => x.id !== client.user?.id).size ?? 0 + logger.debug(`${reactedUponMessage.id} is vote message`, { requestId, guildId }) + const watcherRoleMember = await getMembersWithRoleFromGuild(config.bot.announcement_role, messageReaction.message.guild) + logger.info("ROLE MEMBERS " + JSON.stringify(watcherRoleMember), { requestId, guildId }) + const watcherRoleMemberCount = watcherRoleMember.size + logger.info(`MEMBER COUNT: ${watcherRoleMemberCount}`, { requestId, guildId }) + let noneOfThatReactions = messageReaction.message.reactions.cache.get(NONE_OF_THAT)?.users.cache.filter(x => x.id !== client.user?.id).size ?? 0 - const memberThreshold = (watcherRoleMemberCount / 2) - logger.info(`Reroll ${noneOfThatReactions} > ${memberThreshold} ?`, { requestId, guildId }) - if (noneOfThatReactions > memberThreshold) { - logger.info('Starting poll reroll', { requestId, guildId }) - messageReaction.message.edit((messageReaction.message.content ?? "").concat('\nDiese Abstimmung muss wiederholt werden.')) - } - logger.info(`No reroll`, { requestId, guildId }) - } + const memberThreshold = (watcherRoleMemberCount / 2) + logger.info(`Reroll ${noneOfThatReactions} > ${memberThreshold} ?`, { requestId, guildId }) + if (noneOfThatReactions > memberThreshold) { + logger.info('Starting poll reroll', { requestId, guildId }) + messageReaction.message.edit((messageReaction.message.content ?? "").concat('\nDiese Abstimmung muss wiederholt werden.')) } - + logger.info(`No reroll`, { requestId, guildId }) } } -- 2.40.1 From 6d3bea169eee318d1e688d5ffe788cdaf1434700 Mon Sep 17 00:00:00 2001 From: kenobi Date: Tue, 27 Jun 2023 20:23:22 +0200 Subject: [PATCH 16/72] return on bot reaction --- server/events/handleMessageReactionAdd.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/events/handleMessageReactionAdd.ts b/server/events/handleMessageReactionAdd.ts index 1820b93..48a8883 100644 --- a/server/events/handleMessageReactionAdd.ts +++ b/server/events/handleMessageReactionAdd.ts @@ -8,8 +8,10 @@ import { client } from "../.."; export const name = 'messageReactionAdd' export async function execute(messageReaction: MessageReaction, user: User) { - if (user.id == client.user?.id) + if (user.id == client.user?.id) { logger.info('Skipping bot reaction') + return + } const requestId = newRequestId() const guildId = messageReaction.message.inGuild() ? messageReaction.message.guildId : noGuildId const reactedUponMessage: Message = messageReaction.message.partial ? await messageReaction.message.fetch() : messageReaction.message -- 2.40.1 From c351e27fdd423420cc59fb4e10abaf0f617b2736 Mon Sep 17 00:00:00 2001 From: kenobi Date: Tue, 27 Jun 2023 20:23:36 +0200 Subject: [PATCH 17/72] perform vote message check in reaction handler --- server/events/handleMessageReactionAdd.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/server/events/handleMessageReactionAdd.ts b/server/events/handleMessageReactionAdd.ts index 48a8883..f624e40 100644 --- a/server/events/handleMessageReactionAdd.ts +++ b/server/events/handleMessageReactionAdd.ts @@ -3,6 +3,7 @@ import { Message, MessageReaction, User } from "discord.js"; import { logger, newRequestId, noGuildId } from "../logger"; import { NONE_OF_THAT } from "../constants"; import { client } from "../.."; +import { messageIsVoteMessage } from "../helper/messageIdentifiers"; export const name = 'messageReactionAdd' @@ -25,8 +26,11 @@ export async function execute(messageReaction: MessageReaction, user: User) { logger.info(`emoji: ${messageReaction.emoji.toString()}`) if (messageReaction.emoji.toString() === NONE_OF_THAT) { - logger.info(`Reaction is NONE_OF_THAT. Handling`, { requestId, guildId }) - return client.VoteController.handleNoneOfThatVote(messageReaction, user, reactedUponMessage, requestId, guildId) + if (messageIsVoteMessage(reactedUponMessage)) { + logger.info(`Reaction is NONE_OF_THAT on a vote message. Handling`, { requestId, guildId }) + return client.VoteController.handleNoneOfThatVote(messageReaction, user, reactedUponMessage, requestId, guildId) + + } } return -- 2.40.1 From 1a13638ed93c4baf8f2f31e01f4e6be71b5d72b9 Mon Sep 17 00:00:00 2001 From: kenobi Date: Tue, 27 Jun 2023 20:34:20 +0200 Subject: [PATCH 18/72] linting --- server/events/autoCreateVoteByWPEvent.ts | 3 --- server/helper/dateHelper.ts | 2 +- server/helper/roleFilter.ts | 2 +- server/helper/vote.controller.ts | 3 +-- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/server/events/autoCreateVoteByWPEvent.ts b/server/events/autoCreateVoteByWPEvent.ts index 323556d..fab0844 100644 --- a/server/events/autoCreateVoteByWPEvent.ts +++ b/server/events/autoCreateVoteByWPEvent.ts @@ -1,5 +1,4 @@ import { GuildScheduledEvent, Message, MessageCreateOptions, TextChannel } from "discord.js"; -import { ScheduledTask } from "node-cron"; import { v4 as uuid } from "uuid"; import { client, yavinJellyfinHandler } from "../.."; import { config } from "../configuration"; @@ -11,8 +10,6 @@ import { Emotes, NONE_OF_THAT } from "../constants"; export const name = 'guildScheduledEventCreate' - - export async function execute(event: GuildScheduledEvent) { const requestId = uuid() diff --git a/server/helper/dateHelper.ts b/server/helper/dateHelper.ts index dd9c5b2..8fdab45 100644 --- a/server/helper/dateHelper.ts +++ b/server/helper/dateHelper.ts @@ -1,4 +1,4 @@ -import { format, isToday, toDate } from "date-fns"; +import { format, isToday } from "date-fns"; import { utcToZonedTime } from "date-fns-tz" import { GuildScheduledEvent } from "discord.js"; import { logger } from "../logger"; diff --git a/server/helper/roleFilter.ts b/server/helper/roleFilter.ts index 02df4da..af65678 100644 --- a/server/helper/roleFilter.ts +++ b/server/helper/roleFilter.ts @@ -1,4 +1,4 @@ -import { Collection, Guild, GuildMember, Role, User } from "discord.js" +import { Collection, Guild, GuildMember, Role } from "discord.js" import { ChangedRoles, Maybe, PermissionLevel } from "../interfaces" import { logger } from "../logger" import { config } from "../configuration" diff --git a/server/helper/vote.controller.ts b/server/helper/vote.controller.ts index 9ddfd5b..d56d009 100644 --- a/server/helper/vote.controller.ts +++ b/server/helper/vote.controller.ts @@ -2,7 +2,6 @@ import { Message, MessageReaction, User } from "discord.js" import { client } from "../.." import { NONE_OF_THAT } from "../constants" import { logger } from "../logger" -import { messageIsVoteMessage } from "./messageIdentifiers" import { getMembersWithRoleFromGuild } from "./roleFilter" import { config } from "../configuration" @@ -15,7 +14,7 @@ export default class VoteController { logger.info("ROLE MEMBERS " + JSON.stringify(watcherRoleMember), { requestId, guildId }) const watcherRoleMemberCount = watcherRoleMember.size logger.info(`MEMBER COUNT: ${watcherRoleMemberCount}`, { requestId, guildId }) - let noneOfThatReactions = messageReaction.message.reactions.cache.get(NONE_OF_THAT)?.users.cache.filter(x => x.id !== client.user?.id).size ?? 0 + const noneOfThatReactions = messageReaction.message.reactions.cache.get(NONE_OF_THAT)?.users.cache.filter(x => x.id !== client.user?.id).size ?? 0 const memberThreshold = (watcherRoleMemberCount / 2) logger.info(`Reroll ${noneOfThatReactions} > ${memberThreshold} ?`, { requestId, guildId }) -- 2.40.1 From 8c3cf7829bc50ba554c00fb3e4d876c7c95b2359 Mon Sep 17 00:00:00 2001 From: kenobi Date: Wed, 5 Jul 2023 22:54:43 +0200 Subject: [PATCH 19/72] use branded types for messageType determination --- server/commands/announce.ts | 4 ++-- server/events/deleteAnnouncementsWhenWPEnds.ts | 4 ++-- server/helper/messageIdentifiers.ts | 15 ++++++++++++--- server/structures/client.ts | 4 ++-- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/server/commands/announce.ts b/server/commands/announce.ts index e803177..80472d5 100644 --- a/server/commands/announce.ts +++ b/server/commands/announce.ts @@ -6,7 +6,7 @@ import { Maybe } from '../interfaces' import { logger } from '../logger' import { Command } from '../structures/command' import { RunOptions } from '../types/commandTypes' -import { messageIsInitialAnnouncement } from '../helper/messageIdentifiers' +import { isInitialAnnouncement } from '../helper/messageIdentifiers' export default new Command({ name: 'announce', @@ -62,7 +62,7 @@ async function sendInitialAnnouncement(guildId: string, requestId: string): Prom return } - const currentPinnedAnnouncementMessages = (await announcementChannel.messages.fetchPinned()).filter(message => messageIsInitialAnnouncement(message)) + const currentPinnedAnnouncementMessages = (await announcementChannel.messages.fetchPinned()).filter(message => isInitialAnnouncement(message)) currentPinnedAnnouncementMessages.forEach(async (message) => await message.unpin()) currentPinnedAnnouncementMessages.forEach(message => message.delete()) diff --git a/server/events/deleteAnnouncementsWhenWPEnds.ts b/server/events/deleteAnnouncementsWhenWPEnds.ts index 9b1a0b0..18c572d 100644 --- a/server/events/deleteAnnouncementsWhenWPEnds.ts +++ b/server/events/deleteAnnouncementsWhenWPEnds.ts @@ -2,7 +2,7 @@ import { Collection, GuildScheduledEvent, GuildScheduledEventStatus, Message } f import { v4 as uuid } from "uuid"; import { client } from "../.."; import { logger } from "../logger"; -import { messageIsInitialAnnouncement } from "../helper/messageIdentifiers"; +import { isInitialAnnouncement } from "../helper/messageIdentifiers"; export const name = 'guildScheduledEventUpdate' @@ -26,7 +26,7 @@ export async function execute(oldEvent: GuildScheduledEvent, newEvent: GuildSche const events = await newEvent.guild.scheduledEvents.fetch() - const wpAnnouncements = (await announcementChannel.messages.fetch()).filter(message => !messageIsInitialAnnouncement(message)) + const wpAnnouncements = (await announcementChannel.messages.fetch()).filter(message => !isInitialAnnouncement(message)) const announcementsWithoutEvent = filterAnnouncementsByPendingWPs(wpAnnouncements, events) logger.info(`Deleting ${announcementsWithoutEvent.length} announcements.`, { guildId, requestId }) announcementsWithoutEvent.forEach(message => message.delete()) diff --git a/server/helper/messageIdentifiers.ts b/server/helper/messageIdentifiers.ts index 1b33bde..76832af 100644 --- a/server/helper/messageIdentifiers.ts +++ b/server/helper/messageIdentifiers.ts @@ -1,11 +1,20 @@ import { Message } from "discord.js"; -export function messageIsVoteMessage(msg: Message): boolean { + +// branded types to differentiate objects of identical Type but different contents +export type VoteEndMessage = Message & { readonly __brand: 'vote' } +export type AnnouncementMessage = Message & { readonly __brand: 'announcement' } +export type VoteMessage = Message & { readonly __brand: 'voteend' } + +export type DiscordMessage = VoteMessage | VoteEndMessage | AnnouncementMessage + +export function isVoteMessage(msg: Message): msg is VoteMessage { return msg.cleanContent.includes('[Abstimmung]') } -export function messageIsInitialAnnouncement(msg: Message): boolean { +export function isInitialAnnouncement(msg: Message): msg is AnnouncementMessage { return msg.cleanContent.includes("[initial]") } -export function messageIsVoteEndedMessage(msg: Message): boolean { +export function isVoteEndedMessage(msg: Message): msg is VoteEndMessage { return msg.cleanContent.includes("[Abstimmung beendet]") } + diff --git a/server/structures/client.ts b/server/structures/client.ts index f445585..84fe333 100644 --- a/server/structures/client.ts +++ b/server/structures/client.ts @@ -9,7 +9,7 @@ import { JellyfinHandler } from "../jellyfin/handler"; import { logger } from "../logger"; import { CommandType } from "../types/commandTypes"; import { checkForPollsToClose } from "../commands/closepoll"; -import { messageIsInitialAnnouncement } from "../helper/messageIdentifiers"; +import { isInitialAnnouncement } from "../helper/messageIdentifiers"; import VoteController from "../helper/vote.controller"; @@ -155,7 +155,7 @@ export class ExtendedClient extends Client { } this.announcementRoleHandlerTask.set(guild.id, schedule("*/10 * * * * *", async () => { const requestId = uuid() - const messages = (await textChannel.messages.fetchPinned()).filter(message => messageIsInitialAnnouncement(message)) + const messages = (await textChannel.messages.fetchPinned()).filter(message => isInitialAnnouncement(message)) if (messages.size > 1) { logger.error("More than one pinned announcement Messages found. Unable to know which one people react to. Please fix!", { guildId: guild.id, requestId }) -- 2.40.1 From 4cd9c771f0441badad83e78cfcab6b60fd3c544d Mon Sep 17 00:00:00 2001 From: kenobi Date: Wed, 5 Jul 2023 22:55:24 +0200 Subject: [PATCH 20/72] transfer many poll functions to VoteController --- server/commands/closepoll.ts | 162 +----------------- server/events/autoCreateVoteByWPEvent.ts | 26 +-- server/events/handleMessageReactionAdd.ts | 28 +++- server/helper/vote.controller.ts | 193 +++++++++++++++++++++- server/structures/client.ts | 5 +- 5 files changed, 215 insertions(+), 199 deletions(-) diff --git a/server/commands/closepoll.ts b/server/commands/closepoll.ts index 756d1bc..38ff732 100644 --- a/server/commands/closepoll.ts +++ b/server/commands/closepoll.ts @@ -1,14 +1,8 @@ -import { addDays, differenceInDays, format, isAfter, toDate } from 'date-fns' -import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildScheduledEventSetStatusArg, GuildScheduledEventStatus, Message, MessageCreateOptions, TextChannel } from 'discord.js' import { v4 as uuid } from 'uuid' import { client } from '../..' -import { config } from '../configuration' -import { Maybe } from '../interfaces' import { logger } from '../logger' import { Command } from '../structures/command' import { RunOptions } from '../types/commandTypes' -import { messageIsVoteEndedMessage, messageIsVoteMessage } from '../helper/messageIdentifiers' -import { Emotes } from '../constants' export default new Command({ name: 'closepoll', @@ -26,160 +20,6 @@ export default new Command({ logger.info("Got command for closing poll!", { guildId, requestId }) command.followUp("Alles klar, beende die Umfrage :)") - closePoll(command.guild, requestId) + client.voteController.closePoll(command.guild, requestId) } }) - -export async function closePoll(guild: Guild, requestId: string) { - const guildId = guild.id - logger.info("stopping poll", { guildId, requestId }) - - const announcementChannel: Maybe = client.getAnnouncementChannelForGuild(guildId) - if (!announcementChannel) { - logger.error("Could not find the textchannel. Unable to close poll.", { guildId, requestId }) - return - } - - const messages: Message[] = (await announcementChannel.messages.fetch()) //todo: fetch only pinned messages - .map((value) => value) - .filter(message => !messageIsVoteEndedMessage(message) && messageIsVoteMessage(message)) - .sort((a, b) => b.createdTimestamp - a.createdTimestamp) - - if (!messages || messages.length <= 0) { - logger.info("Could not find any vote messages. Cancelling pollClose", { guildId, requestId }) - return - } - - const lastMessage: Message = messages[0] - - logger.debug(`Found messages: ${JSON.stringify(messages, null, 2)}`, { guildId, requestId }) - - logger.debug(`Last message: ${JSON.stringify(lastMessage, null, 2)}`, { guildId, requestId }) - - - const votes = await (await getVotesByEmote(lastMessage, guildId, requestId)) - .sort((a, b) => b.count - a.count) - - logger.debug(`votes: ${JSON.stringify(votes, null, 2)}`, { guildId, requestId }) - - logger.info("Deleting vote message") - await lastMessage.delete() - const event = await getEvent(guild, guild.id, requestId) - if (event) { - updateEvent(event, votes, guild, guildId, requestId) - sendVoteClosedMessage(event, votes[0].movie, guildId, requestId) - } - - //lastMessage.unpin() //todo: uncomment when bot has permission to pin/unpin -} - -async function sendVoteClosedMessage(event: GuildScheduledEvent, movie: string, guildId: string, requestId: string) { - const date = event.scheduledStartAt ? format(event.scheduledStartAt, "dd.MM") : "Fehler, event hatte kein Datum" - const time = event.scheduledStartAt ? format(event.scheduledStartAt, "HH:mm") : "Fehler, event hatte kein Datum" - const body = `[Abstimmung beendet] für https://discord.com/events/${event.guildId}/${event.id}\n<@&${config.bot.announcement_role}> Wir gucken ${movie} am ${date} um ${time}` - const options: MessageCreateOptions = { - content: body, - allowedMentions: { parse: ["roles"] } - } - const announcementChannel = client.getAnnouncementChannelForGuild(guildId) - logger.info("Sending vote closed message.", { guildId, requestId }) - if (!announcementChannel) { - logger.error("Could not find announcement channel. Please fix!", { guildId, requestId }) - return - } - announcementChannel.send(options) -} - -async function updateEvent(voteEvent: GuildScheduledEvent, votes: Vote[], guild: Guild, guildId: string, requestId: string) { - logger.info(`Updating event with movie ${votes[0].movie}.`, { guildId, requestId }) - const options: GuildScheduledEventEditOptions> = { - name: votes[0].movie, - description: `!wp\nNummer 2: ${votes[1].movie} mit ${votes[1].count - 1} Stimmen\nNummer 3: ${votes[2].movie} mit ${votes[2].count - 1} Stimmen` - } - logger.debug(`Updating event: ${JSON.stringify(voteEvent, null, 2)}`, { guildId, requestId }) - logger.info("Updating event.", { guildId, requestId }) - voteEvent.edit(options) -} - -async function getEvent(guild: Guild, guildId: string, requestId: string): Promise { - const voteEvents = (await guild.scheduledEvents.fetch()) - .map((value) => value) - .filter(event => event.name.toLowerCase().includes("voting offen")) - logger.debug(`Found events: ${JSON.stringify(voteEvents, null, 2)}`, { guildId, requestId }) - - if (!voteEvents || voteEvents.length <= 0) { - logger.error("Could not find vote event. Cancelling update!", { guildId, requestId }) - return null - } - return voteEvents[0] -} - -type Vote = { - emote: string, //todo habs nicht hinbekommen hier Emotes zu nutzen - count: number, - movie: string -} - -async function getVotesByEmote(message: Message, guildId: string, requestId: string): Promise { - const votes: Vote[] = [] - logger.debug(`Number of items in emotes: ${Object.values(Emotes).length}`, { guildId, requestId }) - for (let i = 0; i < Object.keys(Emotes).length / 2; i++) { - const emote = Emotes[i] - logger.debug(`Getting reaction for emote ${emote}`, { guildId, requestId }) - const reaction = await message.reactions.resolve(emote) - logger.debug(`Reaction for emote ${emote}: ${JSON.stringify(reaction, null, 2)}`, { guildId, requestId }) - if (reaction) { - const vote: Vote = { emote: emote, count: reaction.count, movie: extractMovieFromMessageByEmote(message, emote) } - votes.push(vote) - } - } - return votes -} - -function extractMovieFromMessageByEmote(message: Message, emote: string): string { - const lines = message.cleanContent.split("\n") - const emoteLines = lines.filter(line => line.includes(emote)) - - if (!emoteLines) { - return "" - } - const movie = emoteLines[0].substring(emoteLines[0].indexOf(emote) + emote.length + 2) // plus colon and space - - return movie -} - -export async function checkForPollsToClose(guild: Guild): Promise { - const requestId = uuid() - logger.info(`Automatic check for poll closing.`, { guildId: guild.id, requestId }) - const events = (await guild.scheduledEvents.fetch()).filter(event => event.name.toLocaleLowerCase().includes("voting offen")).map(event => event) - if (events.length > 1) { - logger.error("Handling more than one Event is not implemented yet. Found more than one poll to close") - return - } else if (events.length == 0) { - logger.info("Could not find any events. Cancelling", { guildId: guild.id, requestId }) - } - - const updatedEvent = events[0] //add two hours because of different timezones in discord api and Date.now() - if (!updatedEvent.scheduledStartTimestamp) { - logger.error("Event does not have a scheduled start time. Cancelling", { guildId: guild.id, requestId }) - return - } - - const createDate: Date = toDate(updatedEvent.createdTimestamp) - const eventDate: Date = toDate(updatedEvent.scheduledStartTimestamp) - const difference: number = differenceInDays(createDate, eventDate) - - if (difference <= 2) { - logger.info("Less than two days between event create and event start. Not closing poll.", { guildId: guild.id, requestId }) - return - } - - const closePollDate: Date = addDays(eventDate, -2) - - if (isAfter(Date.now(), closePollDate)) { - logger.info("Less than two days until event. Closing poll", { guildId: guild.id, requestId }) - closePoll(guild, requestId) - } else { - logger.info(`ScheduledStart: ${closePollDate}. Now: ${toDate(Date.now())}`, { guildId: guild.id, requestId }) - } -} diff --git a/server/events/autoCreateVoteByWPEvent.ts b/server/events/autoCreateVoteByWPEvent.ts index fab0844..d5cfba6 100644 --- a/server/events/autoCreateVoteByWPEvent.ts +++ b/server/events/autoCreateVoteByWPEvent.ts @@ -1,11 +1,8 @@ -import { GuildScheduledEvent, Message, MessageCreateOptions, TextChannel } from "discord.js"; +import { GuildScheduledEvent, TextChannel } from "discord.js"; import { v4 as uuid } from "uuid"; import { client, yavinJellyfinHandler } from "../.."; -import { config } from "../configuration"; -import { createDateStringFromEvent } from "../helper/dateHelper"; import { Maybe } from "../interfaces"; import { logger } from "../logger"; -import { Emotes, NONE_OF_THAT } from "../constants"; export const name = 'guildScheduledEventCreate' @@ -33,26 +30,9 @@ export async function execute(event: GuildScheduledEvent) { logger.info("EVENT DOES NOT HAVE STARTDATE; CANCELLING", { guildId: event.guildId, requestId }) return } - let message = `[Abstimmung] für https://discord.com/events/${event.guildId}/${event.id}\n<@&${config.bot.announcement_role}> Es gibt eine neue Abstimmung für die nächste Watchparty ${createDateStringFromEvent(event, event.guildId, requestId)}! Stimme hierunter für den nächsten Film ab!\n` + const sentMessage = await client.voteController.createVoteMessage(event, announcementChannel, movies, event.guild?.id ?? "", requestId) - for (let i = 0; i < movies.length; i++) { - message = message.concat(Emotes[i]).concat(": ").concat(movies[i]).concat("\n") - } - message = message.concat(NONE_OF_THAT).concat(": Wenn dir nichts davon gefällt.") - - const options: MessageCreateOptions = { - allowedMentions: { parse: ["roles"] }, - content: message, - } - - const sentMessage: Message = await (await announcementChannel.fetch()).send(options) - - for (let i = 0; i < movies.length; i++) { - sentMessage.react(Emotes[i]) - } - sentMessage.react(NONE_OF_THAT) - - // sentMessage.pin() //todo: uncomment when bot has permission to pin messages. Also update closepoll.ts to only fetch pinned messages + sentMessage.pin() //todo: uncomment when bot has permission to pin messages. Also update closepoll.ts to only fetch pinned messages } } diff --git a/server/events/handleMessageReactionAdd.ts b/server/events/handleMessageReactionAdd.ts index f624e40..c1a0a56 100644 --- a/server/events/handleMessageReactionAdd.ts +++ b/server/events/handleMessageReactionAdd.ts @@ -1,9 +1,9 @@ import { Message, MessageReaction, User } from "discord.js"; import { logger, newRequestId, noGuildId } from "../logger"; -import { NONE_OF_THAT } from "../constants"; +import { Emoji, Emotes, NONE_OF_THAT } from "../constants"; import { client } from "../.."; -import { messageIsVoteMessage } from "../helper/messageIdentifiers"; +import { isInitialAnnouncement, isVoteMessage } from "../helper/messageIdentifiers"; export const name = 'messageReactionAdd' @@ -25,13 +25,25 @@ export async function execute(messageReaction: MessageReaction, user: User) { //logger.debug(`reactedUponMessage payload: ${JSON.stringify(reactedUponMessage)}`) logger.info(`emoji: ${messageReaction.emoji.toString()}`) - if (messageReaction.emoji.toString() === NONE_OF_THAT) { - if (messageIsVoteMessage(reactedUponMessage)) { - logger.info(`Reaction is NONE_OF_THAT on a vote message. Handling`, { requestId, guildId }) - return client.VoteController.handleNoneOfThatVote(messageReaction, user, reactedUponMessage, requestId, guildId) + if (!Object.values(Emotes).includes(messageReaction.emoji.toString())) { + logger.info(`${messageReaction.emoji.toString()} currently not handled`) + return + } + logger.info(`Found a match for ${messageReaction.emoji.toString()}`) + if (isVoteMessage(reactedUponMessage)) { + if (messageReaction.emoji.toString() === NONE_OF_THAT) { + logger.info(`Reaction is NONE_OF_THAT on a vote message. Handling`, { requestId, guildId }) + return client.voteController.handleNoneOfThatVote(messageReaction, user, reactedUponMessage, requestId, guildId) + } + if (messageReaction.emoji.toString() === Emoji.one) { + // do something } } - - return + else if (isInitialAnnouncement(reactedUponMessage)) { + if (messageReaction.emoji.toString() === Emoji.ticket) { + logger.error(`Got a role emoji. Not implemented yet. ${reactedUponMessage.id}`) + } + return + } } diff --git a/server/helper/vote.controller.ts b/server/helper/vote.controller.ts index d56d009..de46219 100644 --- a/server/helper/vote.controller.ts +++ b/server/helper/vote.controller.ts @@ -1,19 +1,35 @@ -import { Message, MessageReaction, User } from "discord.js" +import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildScheduledEventSetStatusArg, GuildScheduledEventStatus, Message, MessageCreateOptions, MessageReaction, TextChannel, User } from "discord.js" import { client } from "../.." -import { NONE_OF_THAT } from "../constants" -import { logger } from "../logger" +import { Emotes, NONE_OF_THAT } from "../constants" +import { logger, newRequestId } from "../logger" import { getMembersWithRoleFromGuild } from "./roleFilter" import { config } from "../configuration" +import { VoteMessage, isVoteEndedMessage, isVoteMessage } from "./messageIdentifiers" +import { createDateStringFromEvent } from "./dateHelper" +import { Maybe } from "../interfaces" +import format from "date-fns/format" +import toDate from "date-fns/toDate" +import differenceInDays from "date-fns/differenceInDays" +import addDays from "date-fns/addDays" +import isAfter from "date-fns/isAfter" +export type Vote = { + emote: string, //todo habs nicht hinbekommen hier Emotes zu nutzen + count: number, + movie: string +} export default class VoteController { - public async handleNoneOfThatVote(messageReaction: MessageReaction, user: User, reactedUponMessage: Message, requestId: string, guildId: string) { + public async handleNoneOfThatVote(messageReaction: MessageReaction, user: User, reactedUponMessage: VoteMessage, requestId: string, guildId: string) { if (!messageReaction.message.guild) return 'No guild' logger.debug(`${reactedUponMessage.id} is vote message`, { requestId, guildId }) + const watcherRoleMember = await getMembersWithRoleFromGuild(config.bot.announcement_role, messageReaction.message.guild) logger.info("ROLE MEMBERS " + JSON.stringify(watcherRoleMember), { requestId, guildId }) + const watcherRoleMemberCount = watcherRoleMember.size logger.info(`MEMBER COUNT: ${watcherRoleMemberCount}`, { requestId, guildId }) + const noneOfThatReactions = messageReaction.message.reactions.cache.get(NONE_OF_THAT)?.users.cache.filter(x => x.id !== client.user?.id).size ?? 0 const memberThreshold = (watcherRoleMemberCount / 2) @@ -21,7 +37,176 @@ export default class VoteController { if (noneOfThatReactions > memberThreshold) { logger.info('Starting poll reroll', { requestId, guildId }) messageReaction.message.edit((messageReaction.message.content ?? "").concat('\nDiese Abstimmung muss wiederholt werden.')) + } logger.info(`No reroll`, { requestId, guildId }) } + + + public async createVoteMessage(event: GuildScheduledEvent, announcementChannel: TextChannel, movies: string[], guildId: string, requestId: string): Promise> { + + let message = `[Abstimmung] für https://discord.com/events/${event.guildId}/${event.id}\n<@&${config.bot.announcement_role}> Es gibt eine neue Abstimmung für die nächste Watchparty ${createDateStringFromEvent(event, event.guildId, requestId)}! Stimme hierunter für den nächsten Film ab!\n` + + for (let i = 0; i < movies.length; i++) { + message = message.concat(Emotes[i]).concat(": ").concat(movies[i]).concat("\n") + } + message = message.concat(NONE_OF_THAT).concat(": Wenn dir nichts davon gefällt.") + + const options: MessageCreateOptions = { + allowedMentions: { parse: ["roles"] }, + content: message, + } + + const sentMessage: Message = await (await announcementChannel.fetch()).send(options) + + for (let i = 0; i < movies.length; i++) { + sentMessage.react(Emotes[i]) + } + sentMessage.react(NONE_OF_THAT) + + return sentMessage + } + + public async closePoll(guild: Guild, requestId: string) { + const guildId = guild.id + logger.info("stopping poll", { guildId, requestId }) + + const announcementChannel: Maybe = client.getAnnouncementChannelForGuild(guildId) + if (!announcementChannel) { + logger.error("Could not find the textchannel. Unable to close poll.", { guildId, requestId }) + return + } + + const messages: Message[] = (await announcementChannel.messages.fetch()) //todo: fetch only pinned messages + .map((value) => value) + .filter(message => !isVoteEndedMessage(message) && isVoteMessage(message)) + .sort((a, b) => b.createdTimestamp - a.createdTimestamp) + + if (!messages || messages.length <= 0) { + logger.info("Could not find any vote messages. Cancelling pollClose", { guildId, requestId }) + return + } + + const lastMessage: Message = messages[0] + + logger.debug(`Found messages: ${JSON.stringify(messages, null, 2)}`, { guildId, requestId }) + + logger.debug(`Last message: ${JSON.stringify(lastMessage, null, 2)}`, { guildId, requestId }) + + + const votes = (await this.getVotesByEmote(lastMessage, guildId, requestId)) + .sort((a, b) => b.count - a.count) + + logger.debug(`votes: ${JSON.stringify(votes, null, 2)}`, { guildId, requestId }) + + logger.info("Deleting vote message") + await lastMessage.delete() + const event = await this.getEvent(guild, guild.id, requestId) + if (event) { + this.updateEvent(event, votes, guild, guildId, requestId) + this.sendVoteClosedMessage(event, votes[0].movie, guildId, requestId) + } + + lastMessage.unpin() //todo: uncomment when bot has permission to pin/unpin + + } + public async getVotesByEmote(message: Message, guildId: string, requestId: string): Promise { + const votes: Vote[] = [] + logger.debug(`Number of items in emotes: ${Object.values(Emotes).length}`, { guildId, requestId }) + for (let i = 0; i < Object.keys(Emotes).length / 2; i++) { + const emote = Emotes[i] + logger.debug(`Getting reaction for emote ${emote}`, { guildId, requestId }) + const reaction = message.reactions.resolve(emote) + logger.debug(`Reaction for emote ${emote}: ${JSON.stringify(reaction, null, 2)}`, { guildId, requestId }) + if (reaction) { + const vote: Vote = { emote: emote, count: reaction.count, movie: this.extractMovieFromMessageByEmote(message, emote) } + votes.push(vote) + } + } + return votes + } + public async getEvent(guild: Guild, guildId: string, requestId: string): Promise { + const voteEvents = (await guild.scheduledEvents.fetch()) + .map((value) => value) + .filter(event => event.name.toLowerCase().includes("voting offen")) + logger.debug(`Found events: ${JSON.stringify(voteEvents, null, 2)}`, { guildId, requestId }) + + if (!voteEvents || voteEvents.length <= 0) { + logger.error("Could not find vote event. Cancelling update!", { guildId, requestId }) + return null + } + return voteEvents[0] + } + public async updateEvent(voteEvent: GuildScheduledEvent, votes: Vote[], guild: Guild, guildId: string, requestId: string) { + logger.info(`Updating event with movie ${votes[0].movie}.`, { guildId, requestId }) + const options: GuildScheduledEventEditOptions> = { + name: votes[0].movie, + description: `!wp\nNummer 2: ${votes[1].movie} mit ${votes[1].count - 1} Stimmen\nNummer 3: ${votes[2].movie} mit ${votes[2].count - 1} Stimmen` + } + logger.debug(`Updating event: ${JSON.stringify(voteEvent, null, 2)}`, { guildId, requestId }) + logger.info("Updating event.", { guildId, requestId }) + voteEvent.edit(options) + } + public async sendVoteClosedMessage(event: GuildScheduledEvent, movie: string, guildId: string, requestId: string) { + const date = event.scheduledStartAt ? format(event.scheduledStartAt, "dd.MM") : "Fehler, event hatte kein Datum" + const time = event.scheduledStartAt ? format(event.scheduledStartAt, "HH:mm") : "Fehler, event hatte kein Datum" + const body = `[Abstimmung beendet] für https://discord.com/events/${event.guildId}/${event.id}\n<@&${config.bot.announcement_role}> Wir gucken ${movie} am ${date} um ${time}` + const options: MessageCreateOptions = { + content: body, + allowedMentions: { parse: ["roles"] } + } + const announcementChannel = client.getAnnouncementChannelForGuild(guildId) + logger.info("Sending vote closed message.", { guildId, requestId }) + if (!announcementChannel) { + logger.error("Could not find announcement channel. Please fix!", { guildId, requestId }) + return + } + announcementChannel.send(options) + } + private extractMovieFromMessageByEmote(message: Message, emote: string): string { + const lines = message.cleanContent.split("\n") + const emoteLines = lines.filter(line => line.includes(emote)) + + if (!emoteLines) { + return "" + } + const movie = emoteLines[0].substring(emoteLines[0].indexOf(emote) + emote.length + 2) // plus colon and space + + return movie + } + public async checkForPollsToClose(guild: Guild): Promise { + const requestId = newRequestId() + logger.info(`Automatic check for poll closing.`, { guildId: guild.id, requestId }) + const events = (await guild.scheduledEvents.fetch()).filter(event => event.name.toLocaleLowerCase().includes("voting offen")).map(event => event) + if (events.length > 1) { + logger.error("Handling more than one Event is not implemented yet. Found more than one poll to close") + return + } else if (events.length == 0) { + logger.info("Could not find any events. Cancelling", { guildId: guild.id, requestId }) + } + + const updatedEvent = events[0] //add two hours because of different timezones in discord api and Date.now() + if (!updatedEvent.scheduledStartTimestamp) { + logger.error("Event does not have a scheduled start time. Cancelling", { guildId: guild.id, requestId }) + return + } + + const createDate: Date = toDate(updatedEvent.createdTimestamp) + const eventDate: Date = toDate(updatedEvent.scheduledStartTimestamp) + const difference: number = differenceInDays(createDate, eventDate) + + if (difference <= 2) { + logger.info("Less than two days between event create and event start. Not closing poll.", { guildId: guild.id, requestId }) + return + } + + const closePollDate: Date = addDays(eventDate, -2) + + if (isAfter(Date.now(), closePollDate)) { + logger.info("Less than two days until event. Closing poll", { guildId: guild.id, requestId }) + this.closePoll(guild, requestId) + } else { + logger.info(`ScheduledStart: ${closePollDate}. Now: ${toDate(Date.now())}`, { guildId: guild.id, requestId }) + } + } } diff --git a/server/structures/client.ts b/server/structures/client.ts index 84fe333..0767793 100644 --- a/server/structures/client.ts +++ b/server/structures/client.ts @@ -8,7 +8,6 @@ import { Maybe } from "../interfaces"; import { JellyfinHandler } from "../jellyfin/handler"; import { logger } from "../logger"; import { CommandType } from "../types/commandTypes"; -import { checkForPollsToClose } from "../commands/closepoll"; import { isInitialAnnouncement } from "../helper/messageIdentifiers"; import VoteController from "../helper/vote.controller"; @@ -18,7 +17,7 @@ export class ExtendedClient extends Client { private eventFilePath = `${__dirname}/../events` private commandFilePath = `${__dirname}/../commands` private jellyfin: JellyfinHandler - public VoteController: VoteController = new VoteController() + public voteController: VoteController = new VoteController() public commands: Collection = new Collection() private announcementChannels: Collection = new Collection() //guildId to TextChannel private announcementRoleHandlerTask: Collection = new Collection() //one task per guild @@ -194,7 +193,7 @@ export class ExtendedClient extends Client { private async startPollCloseBackgroundTasks() { for (const guild of this.guilds.cache) { - this.pollCloseBackgroundTasks.set(guild[1].id, schedule("0 * * * * *", () => checkForPollsToClose(guild[1]))) + this.pollCloseBackgroundTasks.set(guild[1].id, schedule("0 * * * * *", () => this.voteController.checkForPollsToClose(guild[1]))) } } } -- 2.40.1 From ffba737e5acbf7a43ee7154b089830895031243c Mon Sep 17 00:00:00 2001 From: kenobi Date: Wed, 5 Jul 2023 22:56:01 +0200 Subject: [PATCH 21/72] update tsconfig --- tsconfig.json | 105 +++++++++++++++++++++----------------------------- 1 file changed, 44 insertions(+), 61 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 30cff04..539aeba 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,63 +1,46 @@ { - "extends":"@tsconfig/recommended/tsconfig.json", - "exclude":["node_modules"], - "compilerOptions": { - /* Basic Options */ - "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, - "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, - "resolveJsonModule": true, - // "lib": [], /* Specify library files to be included in the compilation. */ - // "allowJs": true, /* Allow javascript files to be compiled. */ - // "checkJs": true, /* Report errors in .js files. */ - // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ - // "declaration": true, /* Generates corresponding '.d.ts' file. */ - // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ - // "sourceMap": true, /* Generates corresponding '.map' file. */ - // "outFile": "./", /* Concatenate and emit output to single file. */ - "outDir": "./build" /* Redirect output structure to the directory. */, - // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ - // "composite": true, /* Enable project compilation */ - // "removeComments": true, /* Do not emit comments to output. */ - // "noEmit": true, /* Do not emit outputs. */ - // "importHelpers": true, /* Import emit helpers from 'tslib'. */ - // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ - // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ - - /* Strict Type-Checking Options */ - "strict": true /* Enable all strict type-checking options. */, - // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* Enable strict null checks. */ - // "strictFunctionTypes": true, /* Enable strict checking of function types. */ - // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ - // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ - // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ - // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ - - /* Additional Checks */ - // "noUnusedLocals": true, /* Report errors on unused locals. */ - // "noUnusedParameters": true, /* Report errors on unused parameters. */ - // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ - // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ - - /* Module Resolution Options */ - "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ - // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ - // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ - // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ - // "typeRoots": [], /* List of folders to include type definitions from. */ - // "types": [], /* Type declaration files to be included in compilation. */ - "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ - "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, - // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ - - /* Source Map Options */ - // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - "inlineSourceMap": true /* Emit a single file with source maps instead of having a separate file. */ - // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ - - /* Experimental Options */ - // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ - // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ - } + "extends": "@tsconfig/recommended/tsconfig.json", + "exclude": [ + "node_modules" + ], + "compilerOptions": { + /* Basic Options */ + "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, + "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, + "resolveJsonModule": true, + "outDir": "./build" /* Redirect output structure to the directory. */, + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + "removeComments": true, /* Do not emit comments to output. */ + /* Strict Type-Checking Options */ + "strict": true /* Enable all strict type-checking options. */, + "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + "strictNullChecks": true, /* Enable strict null checks. */ + "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + //"noUncheckedIndexedAccess": true, + /* Additional Checks */ + //"noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + /* Module Resolution Options */ + "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + "inlineSourceMap": true /* Emit a single file with source maps instead of having a separate file. */ + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + } } -- 2.40.1 From 0748097a1f916c6dbeeacd35d3412cf364a7347b Mon Sep 17 00:00:00 2001 From: kenobi Date: Wed, 5 Jul 2023 23:21:44 +0200 Subject: [PATCH 22/72] refactor datestring function --- server/helper/dateHelper.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/helper/dateHelper.ts b/server/helper/dateHelper.ts index 8fdab45..38c7ca0 100644 --- a/server/helper/dateHelper.ts +++ b/server/helper/dateHelper.ts @@ -4,14 +4,14 @@ import { GuildScheduledEvent } from "discord.js"; import { logger } from "../logger"; import de from "date-fns/locale/de"; -export function createDateStringFromEvent(event: GuildScheduledEvent, requestId: string, guildId?: string): string { - if (!event.scheduledStartAt) { +export function createDateStringFromEvent(eventStartDate:Date, requestId: string, guildId?: string): string { + if (!eventStartDate) { logger.error("Event has no start. Cannot create dateString.", { guildId, requestId }) return `"habe keinen Startzeitpunkt ermitteln können"` } const timeZone = 'Europe/Berlin' - const zonedDateTime = utcToZonedTime(event.scheduledStartAt, timeZone) + const zonedDateTime = utcToZonedTime(eventStartDate, timeZone) const time = format(zonedDateTime, "HH:mm", { locale: de }) if (isToday(zonedDateTime)) { -- 2.40.1 From 9383cee4a07c5b67dd12e4db56559657cd21f152 Mon Sep 17 00:00:00 2001 From: kenobi Date: Wed, 5 Jul 2023 23:22:01 +0200 Subject: [PATCH 23/72] scaffolding for poll reroll function --- server/helper/vote.controller.ts | 43 ++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/server/helper/vote.controller.ts b/server/helper/vote.controller.ts index de46219..68eb8c0 100644 --- a/server/helper/vote.controller.ts +++ b/server/helper/vote.controller.ts @@ -1,5 +1,5 @@ import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildScheduledEventSetStatusArg, GuildScheduledEventStatus, Message, MessageCreateOptions, MessageReaction, TextChannel, User } from "discord.js" -import { client } from "../.." +import { client, yavinJellyfinHandler } from "../.." import { Emotes, NONE_OF_THAT } from "../constants" import { logger, newRequestId } from "../logger" import { getMembersWithRoleFromGuild } from "./roleFilter" @@ -37,21 +37,54 @@ export default class VoteController { if (noneOfThatReactions > memberThreshold) { logger.info('Starting poll reroll', { requestId, guildId }) messageReaction.message.edit((messageReaction.message.content ?? "").concat('\nDiese Abstimmung muss wiederholt werden.')) + // get movies that _had_ votes + const oldMovieNames: string[] = this.parseMoviesFromVoteMessage(messageReaction.message,requestId) + const eventId = this.parseEventIdFromMessage(messageReaction.message, requestId) + const eventStartDate: Date = this.fetchEventStartDateByEventId(eventId,requestId) //TODO + // + // get movies from jellyfin to fill the remaining slots + const newMovieCount = config.bot.random_movie_count - oldMovieNames.length + const newMovies = await yavinJellyfinHandler.getRandomMovieNames(newMovieCount, guildId, requestId) + // merge + const movies = oldMovieNames.concat(newMovies) + + // create new message + await this.closePoll(messageReaction.message.guild, requestId) + const message = this.createVoteMessageText(eventId, eventStartDate, movies, guildId, requestId) + const announcementChannel = client.getAnnouncementChannelForGuild(guildId) + if (!announcementChannel) { + logger.error(`No announcementChannel found for ${guildId}, can't post poll`) + return + } + const sentMessage = await this.sendVoteMessage(message, movies.length, announcementChannel) + sentMessage.pin() } logger.info(`No reroll`, { requestId, guildId }) } + private fetchEventStartDateByEventId(eventId: string, requestId: string): Date { + throw new Error("Method not implemented.") + } + private parseMoviesFromVoteMessage(message: Message | import("discord.js").PartialMessage, requestId: string): string[] { + throw new Error("Method not implemented.") + } + private parseEventIdFromMessage(message: Message | import("discord.js").PartialMessage, requestId: string): string { + throw new Error("Method not implemented.") + } + public createVoteMessageText(eventId: string, eventStartDate: Date, movies: string[], guildId: string, requestId: string): string { - public async createVoteMessage(event: GuildScheduledEvent, announcementChannel: TextChannel, movies: string[], guildId: string, requestId: string): Promise> { - - let message = `[Abstimmung] für https://discord.com/events/${event.guildId}/${event.id}\n<@&${config.bot.announcement_role}> Es gibt eine neue Abstimmung für die nächste Watchparty ${createDateStringFromEvent(event, event.guildId, requestId)}! Stimme hierunter für den nächsten Film ab!\n` + let message = `[Abstimmung] für https://discord.com/events/${guildId}/${eventId}\n<@&${config.bot.announcement_role}> Es gibt eine neue Abstimmung für die nächste Watchparty ${createDateStringFromEvent(eventStartDate, guildId, requestId)}! Stimme hierunter für den nächsten Film ab!\n` for (let i = 0; i < movies.length; i++) { message = message.concat(Emotes[i]).concat(": ").concat(movies[i]).concat("\n") } message = message.concat(NONE_OF_THAT).concat(": Wenn dir nichts davon gefällt.") + return message + } + public async sendVoteMessage(message: string, movieCount: number, announcementChannel: TextChannel) { + const options: MessageCreateOptions = { allowedMentions: { parse: ["roles"] }, content: message, @@ -59,7 +92,7 @@ export default class VoteController { const sentMessage: Message = await (await announcementChannel.fetch()).send(options) - for (let i = 0; i < movies.length; i++) { + for (let i = 0; i < movieCount; i++) { sentMessage.react(Emotes[i]) } sentMessage.react(NONE_OF_THAT) -- 2.40.1 From e61b3a7b1677b9d07cd060fbab8d559717c2c969 Mon Sep 17 00:00:00 2001 From: kenobi Date: Wed, 5 Jul 2023 23:22:13 +0200 Subject: [PATCH 24/72] split vote message handling --- server/events/autoCreateVoteByWPEvent.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/events/autoCreateVoteByWPEvent.ts b/server/events/autoCreateVoteByWPEvent.ts index d5cfba6..00ee9d9 100644 --- a/server/events/autoCreateVoteByWPEvent.ts +++ b/server/events/autoCreateVoteByWPEvent.ts @@ -30,9 +30,10 @@ export async function execute(event: GuildScheduledEvent) { logger.info("EVENT DOES NOT HAVE STARTDATE; CANCELLING", { guildId: event.guildId, requestId }) return } - const sentMessage = await client.voteController.createVoteMessage(event, announcementChannel, movies, event.guild?.id ?? "", requestId) + const sentMessageText = client.voteController.createVoteMessageText(event.id, event.scheduledStartAt, movies, event.guild?.id ?? "", requestId) + const sentMessage = await client.voteController.sendVoteMessage(sentMessageText, movies.length, announcementChannel) - sentMessage.pin() //todo: uncomment when bot has permission to pin messages. Also update closepoll.ts to only fetch pinned messages + sentMessage.pin() } } -- 2.40.1 From e8893646f0e354c52e5e599838c037696f1e4867 Mon Sep 17 00:00:00 2001 From: kenobi Date: Wed, 5 Jul 2023 23:22:25 +0200 Subject: [PATCH 25/72] add config values --- server/configuration.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/configuration.ts b/server/configuration.ts index 1e5f848..7fddfc0 100644 --- a/server/configuration.ts +++ b/server/configuration.ts @@ -30,6 +30,7 @@ export interface Config { yavin_jellyfin_url: string yavin_jellyfin_token: string yavin_jellyfin_collection_user: string + random_movie_count: number } } export const config: Config = { @@ -69,6 +70,7 @@ export const config: Config = { yavin_jellyfin_url: process.env.YAVIN_JELLYFIN_URL ?? "", yavin_jellyfin_token: process.env.YAVIN_TOKEN ?? "", yavin_jellyfin_collection_user: process.env.YAVIN_COLLECTION_USER ?? "", - jf_user: process.env.JELLYFIN_USER ?? "" + jf_user: process.env.JELLYFIN_USER ?? "", + random_movie_count: parseInt(process.env.RANDOM_MOVIE_COUNT ?? "") ?? 5 } } -- 2.40.1 From ca19168cf49ee4cce27b57547c3e2d8a0a12a780 Mon Sep 17 00:00:00 2001 From: kenobi Date: Thu, 13 Jul 2023 22:45:28 +0200 Subject: [PATCH 26/72] add early abort message to announce watch party --- server/events/announceManualWatchparty.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/events/announceManualWatchparty.ts b/server/events/announceManualWatchparty.ts index acaccf6..b4f38ab 100644 --- a/server/events/announceManualWatchparty.ts +++ b/server/events/announceManualWatchparty.ts @@ -32,7 +32,11 @@ export async function execute(event: GuildScheduledEvent) { return } - const message = `[Watchparty] https://discord.com/events/${event.guildId}/${event.id} \nHey <@&${config.bot.announcement_role}>, wir gucken ${event.name} ${createDateStringFromEvent(event, guildId, requestId)}` + if (!event.scheduledStartAt) { + logger.error('Event has no start date, bailing out') + return + } + const message = `[Watchparty] https://discord.com/events/${event.guildId}/${event.id} \nHey <@&${config.bot.announcement_role}>, wir gucken ${event.name} ${createDateStringFromEvent(event.scheduledStartAt, guildId, requestId)}` channel.send(message) } else { -- 2.40.1 From 878c81bfa7ea29fda63ef57ad7be55d6a90ce452 Mon Sep 17 00:00:00 2001 From: kenobi Date: Thu, 13 Jul 2023 22:46:03 +0200 Subject: [PATCH 27/72] linting --- tests/helpers/date.test.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/helpers/date.test.ts b/tests/helpers/date.test.ts index 52f7ff9..b1b5e57 100644 --- a/tests/helpers/date.test.ts +++ b/tests/helpers/date.test.ts @@ -1,16 +1,15 @@ -import { GuildScheduledEvent } from "discord.js" import { createDateStringFromEvent } from "../../server/helper/dateHelper" import MockDate from 'mockdate' beforeAll(() => { - MockDate.set('01-01-2023') + MockDate.set('01-01-2023') }) -function getTestDate(date: string): GuildScheduledEvent { - return { scheduledStartAt: new Date(date) } +function getTestDate(date: string): Date { + return new Date(date) } test('createDateStringFromEvent - correct formatting', () => { - expect(createDateStringFromEvent(getTestDate('01-01-2023 12:30'), "")).toEqual('heute um 12:30') - expect(createDateStringFromEvent(getTestDate('01-02-2023 12:30'), "")).toEqual('am Montag 02.01 um 12:30') - expect(createDateStringFromEvent(getTestDate('01-03-2023 12:30'), "")).toEqual('am Dienstag 03.01 um 12:30') + expect(createDateStringFromEvent(getTestDate('01-01-2023 12:30'), "")).toEqual('heute um 12:30') + expect(createDateStringFromEvent(getTestDate('01-02-2023 12:30'), "")).toEqual('am Montag 02.01 um 12:30') + expect(createDateStringFromEvent(getTestDate('01-03-2023 12:30'), "")).toEqual('am Dienstag 03.01 um 12:30') }) -- 2.40.1 From 8f02e11dbadb44d395328af88c7ee32317e3a5e3 Mon Sep 17 00:00:00 2001 From: kenobi Date: Thu, 13 Jul 2023 22:46:14 +0200 Subject: [PATCH 28/72] add ticket to emoji list --- server/constants.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/constants.ts b/server/constants.ts index 4efe5ba..d11ca88 100644 --- a/server/constants.ts +++ b/server/constants.ts @@ -11,5 +11,6 @@ export const Emoji = { "seven": "\u0037\uFE0F\u20E3", "eight": "\u0038\uFE0F\u20E3", "nine": "\u0039\uFE0F\u20E3", - "ten": "\uD83D\uDD1F" + "ten": "\uD83D\uDD1F", + "ticket": "🎫" } -- 2.40.1 From fe45445811dc5f5f3847f0ed28d5bc93436578e3 Mon Sep 17 00:00:00 2001 From: kenobi Date: Thu, 13 Jul 2023 22:46:28 +0200 Subject: [PATCH 29/72] add a test case to check for proper message parsing --- tests/discord/votes.test.ts | 54 +++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 tests/discord/votes.test.ts diff --git a/tests/discord/votes.test.ts b/tests/discord/votes.test.ts new file mode 100644 index 0000000..ff3ef4f --- /dev/null +++ b/tests/discord/votes.test.ts @@ -0,0 +1,54 @@ +import { Emoji } from "../../server/constants" +import VoteController, { Vote, VoteMessageInfo } from "../../server/helper/vote.controller" +import { JellyfinHandler } from "../../server/jellyfin/handler" +import { ExtendedClient } from "../../server/structures/client" +import { VoteMessage } from "../../server/helper/messageIdentifiers" +test('parse votes from vote message', () => { + const testMovies = [ + 'Movie1', + 'Movie2', + 'Movie3', + 'Movie4', + 'Movie5' + ] + const testEventId = '1234321' + const testEventDate = new Date('2023-01-01') + const voteController: VoteController = new VoteController({}, {}) + const testMessage = voteController.createVoteMessageText(testEventId, testEventDate, testMovies, "guildid", "requestId") + + + const expectedResult: VoteMessageInfo = { + eventId: testEventId, + eventDate: testEventDate, + votes: [ + { emote: Emoji.one, count: 1, movie: testMovies[0] }, + { emote: Emoji.two, count: 2, movie: testMovies[1] }, + { emote: Emoji.three, count: 3, movie: testMovies[2] }, + { emote: Emoji.four, count: 1, movie: testMovies[3] }, + { emote: Emoji.five, count: 1, movie: testMovies[4] }, + ] + } + + const msg: VoteMessage = { + cleanContent: testMessage, + reactions: { + cache: { + get: jest.fn().mockImplementation((input: any) => { + // Abusing duck typing + // Message Reaction has a method `count` and the expected votes + // have a field `count` + // this will evaluate to the same 'result' + return expectedResult.votes.find(e => e.emote === input) + }) + } + } + } + + const result = voteController.parseVotesFromVoteMessage(msg, 'requestId') + console.log(JSON.stringify(result)) + expect(Array.isArray(result)).toBe(false) + expect(result.eventId).toEqual(testEventId) + expect(result.eventDate).toEqual(testEventDate) + expect(result.votes.length).toEqual(testMovies.length) + expect(result).toEqual(expectedResult) +}) -- 2.40.1 From e54f03292e467b4ab59da9c6b5465a2e5aaf14be Mon Sep 17 00:00:00 2001 From: kenobi Date: Thu, 13 Jul 2023 22:47:28 +0200 Subject: [PATCH 30/72] add a message parser to vote controller parses a vote message line by line to extract - eventdate - eventid - movies - votes This depends on the structure of the message to not change substantially. as such it's quite brittle --- server/helper/vote.controller.ts | 75 +++++++++++++++++++++++++------- server/structures/client.ts | 3 +- 2 files changed, 61 insertions(+), 17 deletions(-) diff --git a/server/helper/vote.controller.ts b/server/helper/vote.controller.ts index 68eb8c0..a3864a6 100644 --- a/server/helper/vote.controller.ts +++ b/server/helper/vote.controller.ts @@ -1,6 +1,5 @@ -import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildScheduledEventSetStatusArg, GuildScheduledEventStatus, Message, MessageCreateOptions, MessageReaction, TextChannel, User } from "discord.js" -import { client, yavinJellyfinHandler } from "../.." -import { Emotes, NONE_OF_THAT } from "../constants" +import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildScheduledEventSetStatusArg, GuildScheduledEventStatus, Message, MessageCreateOptions, MessageReaction, PartialMessage, TextChannel, User } from "discord.js" +import { Emoji, Emotes, NONE_OF_THAT } from "../constants" import { logger, newRequestId } from "../logger" import { getMembersWithRoleFromGuild } from "./roleFilter" import { config } from "../configuration" @@ -12,13 +11,28 @@ import toDate from "date-fns/toDate" import differenceInDays from "date-fns/differenceInDays" import addDays from "date-fns/addDays" import isAfter from "date-fns/isAfter" +import { ExtendedClient } from "../structures/client" +import { JellyfinHandler } from "../jellyfin/handler" +import { getYear } from "date-fns" export type Vote = { emote: string, //todo habs nicht hinbekommen hier Emotes zu nutzen count: number, movie: string } +export type VoteMessageInfo = { + votes: Vote[], + eventId: string, + eventDate: Date +} export default class VoteController { + private client: ExtendedClient + private yavinJellyfinHandler: JellyfinHandler + + public constructor(_client: ExtendedClient, _yavin: JellyfinHandler) { + this.client = _client + this.yavinJellyfinHandler = _yavin + } public async handleNoneOfThatVote(messageReaction: MessageReaction, user: User, reactedUponMessage: VoteMessage, requestId: string, guildId: string) { if (!messageReaction.message.guild) return 'No guild' @@ -30,7 +44,7 @@ export default class VoteController { const watcherRoleMemberCount = watcherRoleMember.size logger.info(`MEMBER COUNT: ${watcherRoleMemberCount}`, { requestId, guildId }) - const noneOfThatReactions = messageReaction.message.reactions.cache.get(NONE_OF_THAT)?.users.cache.filter(x => x.id !== client.user?.id).size ?? 0 + const noneOfThatReactions = messageReaction.message.reactions.cache.get(NONE_OF_THAT)?.users.cache.filter(x => x.id !== this.client.user?.id).size ?? 0 const memberThreshold = (watcherRoleMemberCount / 2) logger.info(`Reroll ${noneOfThatReactions} > ${memberThreshold} ?`, { requestId, guildId }) @@ -38,21 +52,21 @@ export default class VoteController { logger.info('Starting poll reroll', { requestId, guildId }) messageReaction.message.edit((messageReaction.message.content ?? "").concat('\nDiese Abstimmung muss wiederholt werden.')) // get movies that _had_ votes - const oldMovieNames: string[] = this.parseMoviesFromVoteMessage(messageReaction.message,requestId) + //const oldMovieNames: Vote[] = this.parseVotesFromVoteMessage(messageReaction.message, requestId) const eventId = this.parseEventIdFromMessage(messageReaction.message, requestId) - const eventStartDate: Date = this.fetchEventStartDateByEventId(eventId,requestId) //TODO + const eventStartDate: Date = this.fetchEventStartDateByEventId(eventId, requestId) //TODO // // get movies from jellyfin to fill the remaining slots - const newMovieCount = config.bot.random_movie_count - oldMovieNames.length - const newMovies = await yavinJellyfinHandler.getRandomMovieNames(newMovieCount, guildId, requestId) + const newMovieCount = config.bot.random_movie_count //- oldMovieNames.length + const newMovies = await this.yavinJellyfinHandler.getRandomMovieNames(newMovieCount, guildId, requestId) // merge - const movies = oldMovieNames.concat(newMovies) + const movies = newMovies // create new message await this.closePoll(messageReaction.message.guild, requestId) const message = this.createVoteMessageText(eventId, eventStartDate, movies, guildId, requestId) - const announcementChannel = client.getAnnouncementChannelForGuild(guildId) + const announcementChannel = this.client.getAnnouncementChannelForGuild(guildId) if (!announcementChannel) { logger.error(`No announcementChannel found for ${guildId}, can't post poll`) return @@ -65,16 +79,45 @@ export default class VoteController { private fetchEventStartDateByEventId(eventId: string, requestId: string): Date { throw new Error("Method not implemented.") } - private parseMoviesFromVoteMessage(message: Message | import("discord.js").PartialMessage, requestId: string): string[] { - throw new Error("Method not implemented.") + public parseVotesFromVoteMessage(message: VoteMessage, requestId: string): VoteMessageInfo { + const lines = message.cleanContent.split('\n') + let eventId = "" + let eventDate: Date = new Date() + let votes: Vote[] = [] + for (const line of lines) { + if (line.includes('https://discord.com/events')) { + const urlMatcher = RegExp(/(http|https|ftp):\/\/(\S*)/ig) + const result = line.match(urlMatcher) + if (!result) throw Error('No event url found in Message') + eventId = result?.[0].split('/').at(-1) ?? "" + } else if (!line.slice(0, 5).includes(':')) { + const datematcher = RegExp(/((0[1-9]|[12][0-9]|3[01])\.(0[1-9]|1[012]))(\ um\ )(([012][0-9]:[0-5][0-9]))/i) + const result: RegExpMatchArray | null = line.match(datematcher) + const timeFromResult = result?.at(-1) + const dateFromResult = result?.at(1)?.concat(format(new Date(), '.yyyy')).concat(" " + timeFromResult) ?? "" + eventDate = new Date(dateFromResult) + } else if (line.slice(0, 5).includes(':')) { + const splitLine = line.split(":") + const [emoji, movie] = splitLine + if (emoji === NONE_OF_THAT) continue + const fetchedVoteFromMessage = message.reactions.cache.get(emoji) + if (fetchedVoteFromMessage) { + votes.push({ movie:movie.trim(), emote: emoji, count: fetchedVoteFromMessage.count }) + } else { + logger.error(`No vote reaction found for movie, assuming 0`, requestId) + votes.push({ movie, emote: emoji, count: 0 }) + } + } + } + return { eventId, eventDate, votes } } - private parseEventIdFromMessage(message: Message | import("discord.js").PartialMessage, requestId: string): string { + private parseEventIdFromMessage(message: Message | PartialMessage, requestId: string): string { throw new Error("Method not implemented.") } public createVoteMessageText(eventId: string, eventStartDate: Date, movies: string[], guildId: string, requestId: string): string { - let message = `[Abstimmung] für https://discord.com/events/${guildId}/${eventId}\n<@&${config.bot.announcement_role}> Es gibt eine neue Abstimmung für die nächste Watchparty ${createDateStringFromEvent(eventStartDate, guildId, requestId)}! Stimme hierunter für den nächsten Film ab!\n` + let message = `[Abstimmung] für https://discord.com/events/${guildId}/${eventId} \n<@&${config.bot.announcement_role}> Es gibt eine neue Abstimmung für die nächste Watchparty ${createDateStringFromEvent(eventStartDate, guildId, requestId)}! Stimme hierunter für den nächsten Film ab!\n` for (let i = 0; i < movies.length; i++) { message = message.concat(Emotes[i]).concat(": ").concat(movies[i]).concat("\n") @@ -104,7 +147,7 @@ export default class VoteController { const guildId = guild.id logger.info("stopping poll", { guildId, requestId }) - const announcementChannel: Maybe = client.getAnnouncementChannelForGuild(guildId) + const announcementChannel: Maybe = this.client.getAnnouncementChannelForGuild(guildId) if (!announcementChannel) { logger.error("Could not find the textchannel. Unable to close poll.", { guildId, requestId }) return @@ -188,7 +231,7 @@ export default class VoteController { content: body, allowedMentions: { parse: ["roles"] } } - const announcementChannel = client.getAnnouncementChannelForGuild(guildId) + const announcementChannel = this.client.getAnnouncementChannelForGuild(guildId) logger.info("Sending vote closed message.", { guildId, requestId }) if (!announcementChannel) { logger.error("Could not find announcement channel. Please fix!", { guildId, requestId }) diff --git a/server/structures/client.ts b/server/structures/client.ts index 0767793..caaad67 100644 --- a/server/structures/client.ts +++ b/server/structures/client.ts @@ -10,6 +10,7 @@ import { logger } from "../logger"; import { CommandType } from "../types/commandTypes"; import { isInitialAnnouncement } from "../helper/messageIdentifiers"; import VoteController from "../helper/vote.controller"; +import { yavinJellyfinHandler } from "../.."; @@ -17,7 +18,7 @@ export class ExtendedClient extends Client { private eventFilePath = `${__dirname}/../events` private commandFilePath = `${__dirname}/../commands` private jellyfin: JellyfinHandler - public voteController: VoteController = new VoteController() + public voteController: VoteController = new VoteController(this, yavinJellyfinHandler) public commands: Collection = new Collection() private announcementChannels: Collection = new Collection() //guildId to TextChannel private announcementRoleHandlerTask: Collection = new Collection() //one task per guild -- 2.40.1 From 146848b759661a9ee57d5b7b7b0f7bd27e14599f Mon Sep 17 00:00:00 2001 From: kenobi Date: Mon, 17 Jul 2023 21:29:47 +0200 Subject: [PATCH 31/72] add none of that as expected value to test --- tests/discord/votes.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/discord/votes.test.ts b/tests/discord/votes.test.ts index ff3ef4f..e6afb38 100644 --- a/tests/discord/votes.test.ts +++ b/tests/discord/votes.test.ts @@ -1,4 +1,4 @@ -import { Emoji } from "../../server/constants" +import { Emoji, NONE_OF_THAT } from "../../server/constants" import VoteController, { Vote, VoteMessageInfo } from "../../server/helper/vote.controller" import { JellyfinHandler } from "../../server/jellyfin/handler" import { ExtendedClient } from "../../server/structures/client" @@ -9,7 +9,7 @@ test('parse votes from vote message', () => { 'Movie2', 'Movie3', 'Movie4', - 'Movie5' + 'Movie5', ] const testEventId = '1234321' const testEventDate = new Date('2023-01-01') @@ -26,6 +26,7 @@ test('parse votes from vote message', () => { { emote: Emoji.three, count: 3, movie: testMovies[2] }, { emote: Emoji.four, count: 1, movie: testMovies[3] }, { emote: Emoji.five, count: 1, movie: testMovies[4] }, + { emote: NONE_OF_THAT, count: 1, movie: NONE_OF_THAT }, ] } @@ -49,6 +50,6 @@ test('parse votes from vote message', () => { expect(Array.isArray(result)).toBe(false) expect(result.eventId).toEqual(testEventId) expect(result.eventDate).toEqual(testEventDate) - expect(result.votes.length).toEqual(testMovies.length) + expect(result.votes.length).toEqual(expectedResult.votes.length) expect(result).toEqual(expectedResult) }) -- 2.40.1 From fdfe7ce404ee085f093f42284fd5f71b5ca8e2df Mon Sep 17 00:00:00 2001 From: kenobi Date: Mon, 17 Jul 2023 21:30:02 +0200 Subject: [PATCH 32/72] move date parsing to separate function --- server/helper/vote.controller.ts | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/server/helper/vote.controller.ts b/server/helper/vote.controller.ts index a3864a6..fa13798 100644 --- a/server/helper/vote.controller.ts +++ b/server/helper/vote.controller.ts @@ -13,7 +13,6 @@ import addDays from "date-fns/addDays" import isAfter from "date-fns/isAfter" import { ExtendedClient } from "../structures/client" import { JellyfinHandler } from "../jellyfin/handler" -import { getYear } from "date-fns" export type Vote = { emote: string, //todo habs nicht hinbekommen hier Emotes zu nutzen @@ -76,6 +75,9 @@ export default class VoteController { } logger.info(`No reroll`, { requestId, guildId }) } + parseEventIdFromMessage(message: Message | PartialMessage, requestId: string): string { + throw new Error("Method not implemented.") + } private fetchEventStartDateByEventId(eventId: string, requestId: string): Date { throw new Error("Method not implemented.") } @@ -91,18 +93,16 @@ export default class VoteController { if (!result) throw Error('No event url found in Message') eventId = result?.[0].split('/').at(-1) ?? "" } else if (!line.slice(0, 5).includes(':')) { - const datematcher = RegExp(/((0[1-9]|[12][0-9]|3[01])\.(0[1-9]|1[012]))(\ um\ )(([012][0-9]:[0-5][0-9]))/i) - const result: RegExpMatchArray | null = line.match(datematcher) - const timeFromResult = result?.at(-1) - const dateFromResult = result?.at(1)?.concat(format(new Date(), '.yyyy')).concat(" " + timeFromResult) ?? "" - eventDate = new Date(dateFromResult) + eventDate = this.parseEventDateFromLine(line) } else if (line.slice(0, 5).includes(':')) { const splitLine = line.split(":") const [emoji, movie] = splitLine - if (emoji === NONE_OF_THAT) continue const fetchedVoteFromMessage = message.reactions.cache.get(emoji) if (fetchedVoteFromMessage) { - votes.push({ movie:movie.trim(), emote: emoji, count: fetchedVoteFromMessage.count }) + if (emoji === NONE_OF_THAT) { + votes.push({ movie: NONE_OF_THAT, emote: NONE_OF_THAT, count: fetchedVoteFromMessage.count }) + } else + votes.push({ movie: movie.trim(), emote: emoji, count: fetchedVoteFromMessage.count }) } else { logger.error(`No vote reaction found for movie, assuming 0`, requestId) votes.push({ movie, emote: emoji, count: 0 }) @@ -111,8 +111,12 @@ export default class VoteController { } return { eventId, eventDate, votes } } - private parseEventIdFromMessage(message: Message | PartialMessage, requestId: string): string { - throw new Error("Method not implemented.") + public parseEventDateFromLine(line: string): Date { + const datematcher = RegExp(/((0[1-9]|[12][0-9]|3[01])\.(0[1-9]|1[012]))(\ um\ )(([012][0-9]:[0-5][0-9]))/i) + const result: RegExpMatchArray | null = line.match(datematcher) + const timeFromResult = result?.at(-1) + const dateFromResult = result?.at(1)?.concat(format(new Date(), '.yyyy')).concat(" " + timeFromResult) ?? "" + return new Date(dateFromResult) } public createVoteMessageText(eventId: string, eventStartDate: Date, movies: string[], guildId: string, requestId: string): string { -- 2.40.1 From 137d15698182edbedc1f204ee6d5981181a94e70 Mon Sep 17 00:00:00 2001 From: kenobi Date: Mon, 17 Jul 2023 22:48:57 +0200 Subject: [PATCH 33/72] fix date string in vote message --- server/helper/dateHelper.ts | 2 +- tests/helpers/date.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/helper/dateHelper.ts b/server/helper/dateHelper.ts index 38c7ca0..9da4c53 100644 --- a/server/helper/dateHelper.ts +++ b/server/helper/dateHelper.ts @@ -18,6 +18,6 @@ export function createDateStringFromEvent(eventStartDate:Date, requestId: string return `heute um ${time}` } - const date = format(zonedDateTime, "eeee dd.MM", { locale: de }) + const date = format(zonedDateTime, "eeee dd.MM.", { locale: de }) return `am ${date} um ${time}` } diff --git a/tests/helpers/date.test.ts b/tests/helpers/date.test.ts index b1b5e57..106c3b0 100644 --- a/tests/helpers/date.test.ts +++ b/tests/helpers/date.test.ts @@ -10,6 +10,6 @@ function getTestDate(date: string): Date { } test('createDateStringFromEvent - correct formatting', () => { expect(createDateStringFromEvent(getTestDate('01-01-2023 12:30'), "")).toEqual('heute um 12:30') - expect(createDateStringFromEvent(getTestDate('01-02-2023 12:30'), "")).toEqual('am Montag 02.01 um 12:30') - expect(createDateStringFromEvent(getTestDate('01-03-2023 12:30'), "")).toEqual('am Dienstag 03.01 um 12:30') + expect(createDateStringFromEvent(getTestDate('01-02-2023 12:30'), "")).toEqual('am Montag 02.01. um 12:30') + expect(createDateStringFromEvent(getTestDate('01-03-2023 12:30'), "")).toEqual('am Dienstag 03.01. um 12:30') }) -- 2.40.1 From e763e764138228f2f1194f6aded6e3b0dcb21752 Mon Sep 17 00:00:00 2001 From: kenobi Date: Mon, 17 Jul 2023 22:49:12 +0200 Subject: [PATCH 34/72] add new test for eventId parsing --- tests/discord/votes.test.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/discord/votes.test.ts b/tests/discord/votes.test.ts index e6afb38..9072c60 100644 --- a/tests/discord/votes.test.ts +++ b/tests/discord/votes.test.ts @@ -13,8 +13,9 @@ test('parse votes from vote message', () => { ] const testEventId = '1234321' const testEventDate = new Date('2023-01-01') + const testGuildId = "888999888" const voteController: VoteController = new VoteController({}, {}) - const testMessage = voteController.createVoteMessageText(testEventId, testEventDate, testMovies, "guildid", "requestId") + const testMessage = voteController.createVoteMessageText(testEventId, testEventDate, testMovies, testGuildId, "requestId") const expectedResult: VoteMessageInfo = { @@ -53,3 +54,21 @@ test('parse votes from vote message', () => { expect(result.votes.length).toEqual(expectedResult.votes.length) expect(result).toEqual(expectedResult) }) + +test('parse votes from vote message', () => { + const testMovies = [ + 'Movie1', + 'Movie2', + 'Movie3', + 'Movie4', + 'Movie5', + ] + const testEventId = '1234321' + const testEventDate = new Date('2023-01-01') + const testGuildId = "888999888" + const voteController: VoteController = new VoteController({}, {}) + const testMessage = voteController.createVoteMessageText(testEventId, testEventDate, testMovies, testGuildId, "requestId") + + const result = voteController.parseGuildIdAndEventIdFromWholeMessage(testMessage) + expect(result).toEqual({ guildId: testGuildId, eventId: testEventId }) +}) -- 2.40.1 From c022cc32d5de3f2bb695f7dad3a19d6347fbc9b8 Mon Sep 17 00:00:00 2001 From: kenobi Date: Mon, 17 Jul 2023 22:50:24 +0200 Subject: [PATCH 35/72] refactor eventId parsing to separate function prepare for querying discord api for event info instead of parsing via regex --- server/helper/vote.controller.ts | 40 +++++++++++++++----------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/server/helper/vote.controller.ts b/server/helper/vote.controller.ts index fa13798..9910fed 100644 --- a/server/helper/vote.controller.ts +++ b/server/helper/vote.controller.ts @@ -13,6 +13,7 @@ import addDays from "date-fns/addDays" import isAfter from "date-fns/isAfter" import { ExtendedClient } from "../structures/client" import { JellyfinHandler } from "../jellyfin/handler" +import { ObjectGroupUpdateToJSON } from "../jellyfin" export type Vote = { emote: string, //todo habs nicht hinbekommen hier Emotes zu nutzen @@ -52,8 +53,8 @@ export default class VoteController { messageReaction.message.edit((messageReaction.message.content ?? "").concat('\nDiese Abstimmung muss wiederholt werden.')) // get movies that _had_ votes //const oldMovieNames: Vote[] = this.parseVotesFromVoteMessage(messageReaction.message, requestId) - const eventId = this.parseEventIdFromMessage(messageReaction.message, requestId) - const eventStartDate: Date = this.fetchEventStartDateByEventId(eventId, requestId) //TODO + const parsedIds = this.parseGuildIdAndEventIdFromWholeMessage(messageReaction.message.cleanContent ?? '') + const eventStartDate: Date = this.fetchEventStartDateByEventId(parsedIds.eventId, requestId) //TODO // // get movies from jellyfin to fill the remaining slots const newMovieCount = config.bot.random_movie_count //- oldMovieNames.length @@ -64,7 +65,7 @@ export default class VoteController { // create new message await this.closePoll(messageReaction.message.guild, requestId) - const message = this.createVoteMessageText(eventId, eventStartDate, movies, guildId, requestId) + const message = this.createVoteMessageText(parsedIds.guildId, eventStartDate, movies, guildId, requestId) const announcementChannel = this.client.getAnnouncementChannelForGuild(guildId) if (!announcementChannel) { logger.error(`No announcementChannel found for ${guildId}, can't post poll`) @@ -75,26 +76,23 @@ export default class VoteController { } logger.info(`No reroll`, { requestId, guildId }) } - parseEventIdFromMessage(message: Message | PartialMessage, requestId: string): string { - throw new Error("Method not implemented.") - } private fetchEventStartDateByEventId(eventId: string, requestId: string): Date { throw new Error("Method not implemented.") } + public parseGuildIdAndEventIdFromWholeMessage(message: string) { + const idmatch = RegExp(/(?:http|https):\/\/discord\.com\/events\/(\d*)\/(\d*)/) + const matches = message.match(idmatch) + if (matches && matches.length == 3) + return { guildId: matches[1], eventId: matches[2] } + throw Error(`Could not find eventId in Vote Message`) + } public parseVotesFromVoteMessage(message: VoteMessage, requestId: string): VoteMessageInfo { const lines = message.cleanContent.split('\n') - let eventId = "" - let eventDate: Date = new Date() + let parsedIds = this.parseGuildIdAndEventIdFromWholeMessage(message.cleanContent) + let eventDate: Date = this.parseEventDateFromMessage(message.cleanContent) let votes: Vote[] = [] for (const line of lines) { - if (line.includes('https://discord.com/events')) { - const urlMatcher = RegExp(/(http|https|ftp):\/\/(\S*)/ig) - const result = line.match(urlMatcher) - if (!result) throw Error('No event url found in Message') - eventId = result?.[0].split('/').at(-1) ?? "" - } else if (!line.slice(0, 5).includes(':')) { - eventDate = this.parseEventDateFromLine(line) - } else if (line.slice(0, 5).includes(':')) { + if (line.slice(0, 5).includes(':')) { const splitLine = line.split(":") const [emoji, movie] = splitLine const fetchedVoteFromMessage = message.reactions.cache.get(emoji) @@ -109,13 +107,13 @@ export default class VoteController { } } } - return { eventId, eventDate, votes } + return { eventId: parsedIds.eventId, eventDate, votes } } - public parseEventDateFromLine(line: string): Date { - const datematcher = RegExp(/((0[1-9]|[12][0-9]|3[01])\.(0[1-9]|1[012]))(\ um\ )(([012][0-9]:[0-5][0-9]))/i) - const result: RegExpMatchArray | null = line.match(datematcher) + public parseEventDateFromMessage(message: string): Date { + const datematcher = RegExp(/((?:0[1-9]|[12][0-9]|3[01])\.(?:0[1-9]|1[012])\.)(?:\ um\ )((?:(?:[01][0-9]|[2][0-3])\:[0-5][0-9])|(?:[2][4]\:00))!/i) + const result: RegExpMatchArray | null = message.match(datematcher) const timeFromResult = result?.at(-1) - const dateFromResult = result?.at(1)?.concat(format(new Date(), '.yyyy')).concat(" " + timeFromResult) ?? "" + const dateFromResult = result?.at(1)?.concat(format(new Date(), 'yyyy')).concat(" " + timeFromResult) ?? "" return new Date(dateFromResult) } -- 2.40.1 From dc66c277b20e0b45270dddc648e16158a4b4d765 Mon Sep 17 00:00:00 2001 From: kenobi Date: Mon, 17 Jul 2023 23:30:48 +0200 Subject: [PATCH 36/72] big refactoring of none_of_that handler extracting, better typing, reduction of complexity --- server/events/handleMessageReactionAdd.ts | 2 +- server/helper/vote.controller.ts | 92 +++++++++++++---------- 2 files changed, 54 insertions(+), 40 deletions(-) diff --git a/server/events/handleMessageReactionAdd.ts b/server/events/handleMessageReactionAdd.ts index c1a0a56..24d1cd2 100644 --- a/server/events/handleMessageReactionAdd.ts +++ b/server/events/handleMessageReactionAdd.ts @@ -34,7 +34,7 @@ export async function execute(messageReaction: MessageReaction, user: User) { if (isVoteMessage(reactedUponMessage)) { if (messageReaction.emoji.toString() === NONE_OF_THAT) { logger.info(`Reaction is NONE_OF_THAT on a vote message. Handling`, { requestId, guildId }) - return client.voteController.handleNoneOfThatVote(messageReaction, user, reactedUponMessage, requestId, guildId) + return client.voteController.handleNoneOfThatVote(messageReaction, reactedUponMessage, requestId, guildId) } if (messageReaction.emoji.toString() === Emoji.one) { // do something diff --git a/server/helper/vote.controller.ts b/server/helper/vote.controller.ts index 9910fed..601204d 100644 --- a/server/helper/vote.controller.ts +++ b/server/helper/vote.controller.ts @@ -34,8 +34,9 @@ export default class VoteController { this.yavinJellyfinHandler = _yavin } - public async handleNoneOfThatVote(messageReaction: MessageReaction, user: User, reactedUponMessage: VoteMessage, requestId: string, guildId: string) { + public async handleNoneOfThatVote(messageReaction: MessageReaction, reactedUponMessage: VoteMessage, requestId: string, guildId: string) { if (!messageReaction.message.guild) return 'No guild' + const guild = messageReaction.message.guild logger.debug(`${reactedUponMessage.id} is vote message`, { requestId, guildId }) const watcherRoleMember = await getMembersWithRoleFromGuild(config.bot.announcement_role, messageReaction.message.guild) @@ -44,52 +45,58 @@ export default class VoteController { const watcherRoleMemberCount = watcherRoleMember.size logger.info(`MEMBER COUNT: ${watcherRoleMemberCount}`, { requestId, guildId }) - const noneOfThatReactions = messageReaction.message.reactions.cache.get(NONE_OF_THAT)?.users.cache.filter(x => x.id !== this.client.user?.id).size ?? 0 + const noneOfThatReactions = reactedUponMessage.reactions.cache.get(NONE_OF_THAT)?.users.cache.filter(x => x.id !== this.client.user?.id).size ?? 0 const memberThreshold = (watcherRoleMemberCount / 2) logger.info(`Reroll ${noneOfThatReactions} > ${memberThreshold} ?`, { requestId, guildId }) - if (noneOfThatReactions > memberThreshold) { + if (noneOfThatReactions > memberThreshold) + logger.info(`No reroll`, { requestId, guildId }) + else logger.info('Starting poll reroll', { requestId, guildId }) - messageReaction.message.edit((messageReaction.message.content ?? "").concat('\nDiese Abstimmung muss wiederholt werden.')) - // get movies that _had_ votes - //const oldMovieNames: Vote[] = this.parseVotesFromVoteMessage(messageReaction.message, requestId) - const parsedIds = this.parseGuildIdAndEventIdFromWholeMessage(messageReaction.message.cleanContent ?? '') - const eventStartDate: Date = this.fetchEventStartDateByEventId(parsedIds.eventId, requestId) //TODO - // - // get movies from jellyfin to fill the remaining slots - const newMovieCount = config.bot.random_movie_count //- oldMovieNames.length - const newMovies = await this.yavinJellyfinHandler.getRandomMovieNames(newMovieCount, guildId, requestId) + await this.handleReroll(reactedUponMessage, guild, guild.id, requestId) + } + public async handleReroll(voteMessage: VoteMessage, guild: Guild, guildId: string, requestId: string) { - // merge - const movies = newMovies + //get movies that already had votes to give them a second chance + const voteInfo = await this.parseVoteInfoFromVoteMessage(voteMessage, requestId) - // create new message - await this.closePoll(messageReaction.message.guild, requestId) - const message = this.createVoteMessageText(parsedIds.guildId, eventStartDate, movies, guildId, requestId) - const announcementChannel = this.client.getAnnouncementChannelForGuild(guildId) - if (!announcementChannel) { - logger.error(`No announcementChannel found for ${guildId}, can't post poll`) - return - } - const sentMessage = await this.sendVoteMessage(message, movies.length, announcementChannel) - sentMessage.pin() + // get movies from jellyfin to fill the remaining slots + const newMovieCount = config.bot.random_movie_count - voteInfo.votes.filter(x => x.count > 2).length + const newMovies = await this.yavinJellyfinHandler.getRandomMovieNames(newMovieCount, guildId, requestId) + + // merge + const movies = newMovies.concat(voteInfo.votes.map(x => x.movie)) + + // create new message + await this.closePoll(guild, requestId) + const message = this.createVoteMessageText(guild.id, voteInfo.eventDate, movies, guildId, requestId) + const announcementChannel = this.client.getAnnouncementChannelForGuild(guildId) + if (!announcementChannel) { + logger.error(`No announcementChannel found for ${guildId}, can't post poll`) + return } - logger.info(`No reroll`, { requestId, guildId }) + const sentMessage = await this.sendVoteMessage(message, movies.length, announcementChannel) + sentMessage.pin() } - private fetchEventStartDateByEventId(eventId: string, requestId: string): Date { - throw new Error("Method not implemented.") + + + private async fetchEventStartDateByEventId(guild: Guild, eventId: string, requestId: string): Promise> { + const guildEvent: GuildScheduledEvent = await guild.scheduledEvents.fetch(eventId) + if (!guildEvent) logger.error(`GuildScheduledEvent with id${eventId} could not be found`, { requestId, guildId: guild.id }) + if (guildEvent.scheduledStartAt) + return guildEvent.scheduledStartAt } - public parseGuildIdAndEventIdFromWholeMessage(message: string) { - const idmatch = RegExp(/(?:http|https):\/\/discord\.com\/events\/(\d*)\/(\d*)/) - const matches = message.match(idmatch) - if (matches && matches.length == 3) - return { guildId: matches[1], eventId: matches[2] } - throw Error(`Could not find eventId in Vote Message`) - } - public parseVotesFromVoteMessage(message: VoteMessage, requestId: string): VoteMessageInfo { + + public async parseVoteInfoFromVoteMessage(message: VoteMessage, requestId: string): Promise { const lines = message.cleanContent.split('\n') let parsedIds = this.parseGuildIdAndEventIdFromWholeMessage(message.cleanContent) - let eventDate: Date = this.parseEventDateFromMessage(message.cleanContent) + + if (!message.guild) + throw new Error(`Message ${message.id} not a guild message`) + + let eventStartDate: Maybe = await this.fetchEventStartDateByEventId(message.guild, parsedIds.eventId, requestId) + if (!eventStartDate) eventStartDate = this.parseEventDateFromMessage(message.cleanContent, message.guild.id, requestId) + let votes: Vote[] = [] for (const line of lines) { if (line.slice(0, 5).includes(':')) { @@ -107,18 +114,25 @@ export default class VoteController { } } } - return { eventId: parsedIds.eventId, eventDate, votes } + return { eventId: parsedIds.eventId, eventDate: eventStartDate, votes } } - public parseEventDateFromMessage(message: string): Date { + public parseEventDateFromMessage(message: string, guildId: string, requestId: string): Date { + logger.warn(`Falling back to RegEx parsing to get Event Date`, { guildId, requestId }) const datematcher = RegExp(/((?:0[1-9]|[12][0-9]|3[01])\.(?:0[1-9]|1[012])\.)(?:\ um\ )((?:(?:[01][0-9]|[2][0-3])\:[0-5][0-9])|(?:[2][4]\:00))!/i) const result: RegExpMatchArray | null = message.match(datematcher) const timeFromResult = result?.at(-1) const dateFromResult = result?.at(1)?.concat(format(new Date(), 'yyyy')).concat(" " + timeFromResult) ?? "" return new Date(dateFromResult) } + public parseGuildIdAndEventIdFromWholeMessage(message: string) { + const idmatch = RegExp(/(?:http|https):\/\/discord\.com\/events\/(\d*)\/(\d*)/) + const matches = message.match(idmatch) + if (matches && matches.length == 3) + return { guildId: matches[1], eventId: matches[2] } + throw Error(`Could not find eventId in Vote Message`) + } public createVoteMessageText(eventId: string, eventStartDate: Date, movies: string[], guildId: string, requestId: string): string { - let message = `[Abstimmung] für https://discord.com/events/${guildId}/${eventId} \n<@&${config.bot.announcement_role}> Es gibt eine neue Abstimmung für die nächste Watchparty ${createDateStringFromEvent(eventStartDate, guildId, requestId)}! Stimme hierunter für den nächsten Film ab!\n` for (let i = 0; i < movies.length; i++) { -- 2.40.1 From a2adef808f26382c8980d777bcba30bb5334317a Mon Sep 17 00:00:00 2001 From: kenobi Date: Mon, 17 Jul 2023 23:31:00 +0200 Subject: [PATCH 37/72] add guildscheduledevents to unit test mock --- tests/discord/votes.test.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/discord/votes.test.ts b/tests/discord/votes.test.ts index 9072c60..070096a 100644 --- a/tests/discord/votes.test.ts +++ b/tests/discord/votes.test.ts @@ -3,7 +3,7 @@ import VoteController, { Vote, VoteMessageInfo } from "../../server/helper/vote. import { JellyfinHandler } from "../../server/jellyfin/handler" import { ExtendedClient } from "../../server/structures/client" import { VoteMessage } from "../../server/helper/messageIdentifiers" -test('parse votes from vote message', () => { +test('parse votes from vote message', async () => { const testMovies = [ 'Movie1', 'Movie2', @@ -33,6 +33,17 @@ test('parse votes from vote message', () => { const msg: VoteMessage = { cleanContent: testMessage, + guild:{ + id:testGuildId, + scheduledEvents:{ + fetch: jest.fn().mockImplementation((input:any)=>{ + if(input === testEventId) + return { + scheduledStartAt: testEventDate + } + }) + } + }, reactions: { cache: { get: jest.fn().mockImplementation((input: any) => { @@ -46,7 +57,7 @@ test('parse votes from vote message', () => { } } - const result = voteController.parseVotesFromVoteMessage(msg, 'requestId') + const result = await voteController.parseVoteInfoFromVoteMessage(msg, 'requestId') console.log(JSON.stringify(result)) expect(Array.isArray(result)).toBe(false) expect(result.eventId).toEqual(testEventId) -- 2.40.1 From 5e58765cf4ec90f298eeecf000c86a4f988a077b Mon Sep 17 00:00:00 2001 From: kenobi Date: Sun, 6 Aug 2023 02:32:44 +0200 Subject: [PATCH 38/72] also enabled NONE_OF_THAT to be handled --- server/events/handleMessageReactionAdd.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/events/handleMessageReactionAdd.ts b/server/events/handleMessageReactionAdd.ts index 24d1cd2..8772fad 100644 --- a/server/events/handleMessageReactionAdd.ts +++ b/server/events/handleMessageReactionAdd.ts @@ -26,7 +26,7 @@ export async function execute(messageReaction: MessageReaction, user: User) { logger.info(`emoji: ${messageReaction.emoji.toString()}`) - if (!Object.values(Emotes).includes(messageReaction.emoji.toString())) { + if (!Object.values(Emotes).includes(messageReaction.emoji.toString()) && messageReaction.emoji.toString() !== NONE_OF_THAT) { logger.info(`${messageReaction.emoji.toString()} currently not handled`) return } -- 2.40.1 From 91ec2ece7e1dd917d43b67ce64bf0802b19a1d9b Mon Sep 17 00:00:00 2001 From: kenobi Date: Sun, 6 Aug 2023 02:33:17 +0200 Subject: [PATCH 39/72] explicit typing --- server/helper/vote.controller.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/helper/vote.controller.ts b/server/helper/vote.controller.ts index 601204d..de08034 100644 --- a/server/helper/vote.controller.ts +++ b/server/helper/vote.controller.ts @@ -58,14 +58,14 @@ export default class VoteController { public async handleReroll(voteMessage: VoteMessage, guild: Guild, guildId: string, requestId: string) { //get movies that already had votes to give them a second chance - const voteInfo = await this.parseVoteInfoFromVoteMessage(voteMessage, requestId) + const voteInfo: VoteMessageInfo = await this.parseVoteInfoFromVoteMessage(voteMessage, requestId) // get movies from jellyfin to fill the remaining slots - const newMovieCount = config.bot.random_movie_count - voteInfo.votes.filter(x => x.count > 2).length - const newMovies = await this.yavinJellyfinHandler.getRandomMovieNames(newMovieCount, guildId, requestId) + const newMovieCount: number = config.bot.random_movie_count - voteInfo.votes.filter(x => x.count > 2).length + const newMovies: string[] = await this.yavinJellyfinHandler.getRandomMovieNames(newMovieCount, guildId, requestId) // merge - const movies = newMovies.concat(voteInfo.votes.map(x => x.movie)) + const movies: string[] = newMovies.concat(voteInfo.votes.map(x => x.movie)) // create new message await this.closePoll(guild, requestId) -- 2.40.1 From 1101a84501f012edb09bd504364184c9b8d8c5a3 Mon Sep 17 00:00:00 2001 From: kenobi Date: Sun, 6 Aug 2023 02:33:23 +0200 Subject: [PATCH 40/72] imports --- server/helper/vote.controller.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/server/helper/vote.controller.ts b/server/helper/vote.controller.ts index de08034..801a5bb 100644 --- a/server/helper/vote.controller.ts +++ b/server/helper/vote.controller.ts @@ -1,5 +1,5 @@ -import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildScheduledEventSetStatusArg, GuildScheduledEventStatus, Message, MessageCreateOptions, MessageReaction, PartialMessage, TextChannel, User } from "discord.js" -import { Emoji, Emotes, NONE_OF_THAT } from "../constants" +import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildScheduledEventSetStatusArg, GuildScheduledEventStatus, Message, MessageCreateOptions, MessageReaction, TextChannel } from "discord.js" +import { Emotes, NONE_OF_THAT } from "../constants" import { logger, newRequestId } from "../logger" import { getMembersWithRoleFromGuild } from "./roleFilter" import { config } from "../configuration" @@ -13,7 +13,6 @@ import addDays from "date-fns/addDays" import isAfter from "date-fns/isAfter" import { ExtendedClient } from "../structures/client" import { JellyfinHandler } from "../jellyfin/handler" -import { ObjectGroupUpdateToJSON } from "../jellyfin" export type Vote = { emote: string, //todo habs nicht hinbekommen hier Emotes zu nutzen -- 2.40.1 From 8ff5aeff03c00661470c68f9b9a1f5d2034249cf Mon Sep 17 00:00:00 2001 From: kenobi Date: Sun, 6 Aug 2023 02:33:28 +0200 Subject: [PATCH 41/72] logging --- server/helper/vote.controller.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/helper/vote.controller.ts b/server/helper/vote.controller.ts index 801a5bb..c417186 100644 --- a/server/helper/vote.controller.ts +++ b/server/helper/vote.controller.ts @@ -53,7 +53,9 @@ export default class VoteController { else logger.info('Starting poll reroll', { requestId, guildId }) await this.handleReroll(reactedUponMessage, guild, guild.id, requestId) + logger.info(`Finished handling NONE_OF_THAT vote`, { requestId, guildId }) } + public async handleReroll(voteMessage: VoteMessage, guild: Guild, guildId: string, requestId: string) { //get movies that already had votes to give them a second chance -- 2.40.1 From 2ebc7fbdbee55b29bc967392eadc0634c1a9437b Mon Sep 17 00:00:00 2001 From: kenobi Date: Sun, 6 Aug 2023 02:37:49 +0200 Subject: [PATCH 42/72] restructure docker build a bit --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 086648b..439b899 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,13 +2,13 @@ FROM node:alpine as files ENV TZ="Europe/Berlin" WORKDIR /app COPY [ "package-lock.json", "package.json", "index.ts", "tsconfig.json", "./" ] -COPY server ./server FROM files as proddependencies ENV NODE_ENV=production RUN npm ci --omit=dev FROM proddependencies as compile +COPY server ./server RUN npm run build CMD ["npm","run","start"] @@ -16,6 +16,7 @@ FROM files as dependencies RUN npm ci FROM dependencies as test +COPY server ./server COPY jest.config.js . COPY tests ./tests RUN npm run test -- 2.40.1 From b6a1e06b03364fe8f5fac71e0fb314e32cce5ec3 Mon Sep 17 00:00:00 2001 From: kenobi Date: Sun, 13 Aug 2023 18:14:16 +0200 Subject: [PATCH 43/72] update default movie env var --- server/configuration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/configuration.ts b/server/configuration.ts index 7fddfc0..832e48f 100644 --- a/server/configuration.ts +++ b/server/configuration.ts @@ -71,6 +71,6 @@ export const config: Config = { yavin_jellyfin_token: process.env.YAVIN_TOKEN ?? "", yavin_jellyfin_collection_user: process.env.YAVIN_COLLECTION_USER ?? "", jf_user: process.env.JELLYFIN_USER ?? "", - random_movie_count: parseInt(process.env.RANDOM_MOVIE_COUNT ?? "") ?? 5 + random_movie_count: parseInt(process.env.RANDOM_MOVIE_COUNT ?? "5") ?? 5 } } -- 2.40.1 From 4e563d57fd3aaf189a8c7357bfcf43665fc999fa Mon Sep 17 00:00:00 2001 From: kenobi Date: Sun, 13 Aug 2023 18:31:15 +0200 Subject: [PATCH 44/72] fix else branch of memberthreshold --- server/helper/vote.controller.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server/helper/vote.controller.ts b/server/helper/vote.controller.ts index c417186..55a5068 100644 --- a/server/helper/vote.controller.ts +++ b/server/helper/vote.controller.ts @@ -50,10 +50,11 @@ export default class VoteController { logger.info(`Reroll ${noneOfThatReactions} > ${memberThreshold} ?`, { requestId, guildId }) if (noneOfThatReactions > memberThreshold) logger.info(`No reroll`, { requestId, guildId }) - else + else { logger.info('Starting poll reroll', { requestId, guildId }) - await this.handleReroll(reactedUponMessage, guild, guild.id, requestId) - logger.info(`Finished handling NONE_OF_THAT vote`, { requestId, guildId }) + await this.handleReroll(reactedUponMessage, guild, guild.id, requestId) + logger.info(`Finished handling NONE_OF_THAT vote`, { requestId, guildId }) + } } public async handleReroll(voteMessage: VoteMessage, guild: Guild, guildId: string, requestId: string) { -- 2.40.1 From b76df79d2a5f20d02cfad0cd025f8b66c2e06538 Mon Sep 17 00:00:00 2001 From: kenobi Date: Sun, 13 Aug 2023 18:32:43 +0200 Subject: [PATCH 45/72] testcases --- tests/discord/noneofthat.test.ts | 96 +++++++++++++++++++++++++++++ tests/discord/votes.test.ts | 101 +++++++++++++++++++++++++++++-- 2 files changed, 192 insertions(+), 5 deletions(-) create mode 100644 tests/discord/noneofthat.test.ts diff --git a/tests/discord/noneofthat.test.ts b/tests/discord/noneofthat.test.ts new file mode 100644 index 0000000..5a83fef --- /dev/null +++ b/tests/discord/noneofthat.test.ts @@ -0,0 +1,96 @@ +import { Guild, GuildScheduledEvent, Message } from "discord.js" +import VoteController from "../../server/helper/vote.controller" +import { JellyfinHandler } from "../../server/jellyfin/handler" +import { ExtendedClient } from "../../server/structures/client" +import { Emoji, NONE_OF_THAT } from "../../server/constants" + +describe('vote controller - none_of_that functions', () => { + const testEventId = '1234321' + const testEventDate = new Date('2023-01-01') + const testGuildId = "888999888" + const testMovies = [ + 'Movie1', + 'Movie2', + 'Movie3', + 'Movie4', + 'Movie5', + ] + const votesList = [ + { emote: Emoji.one, count: 1, movie: testMovies[0] }, + { emote: Emoji.two, count: 2, movie: testMovies[1] }, + { emote: Emoji.three, count: 3, movie: testMovies[2] }, + { emote: Emoji.four, count: 1, movie: testMovies[3] }, + { emote: Emoji.five, count: 1, movie: testMovies[4] }, + { emote: NONE_OF_THAT, count: 2, movie: NONE_OF_THAT }, + ] + const mockClient: ExtendedClient = { + user: { + id: 'mockId' + } + } + const mockJellyfinHandler: JellyfinHandler = { + getRandomMovieNames: jest.fn().mockReturnValue(["movie1"]) + } + const votes = new VoteController(mockClient, mockJellyfinHandler) + const mockMessageContent = votes.createVoteMessageText(testEventId, testEventDate, testMovies, testGuildId, "requestId") + + test('sendVoteClosedMessage', async () => { + mockClient.getAnnouncementChannelForGuild = jest.fn().mockReturnValue({ + send: jest.fn().mockImplementation((options: any) => { + return new Promise((resolve) => { + resolve(options) + }) + }) + }) + const scheduledEvent: GuildScheduledEvent = { + scheduledStartAt: testEventDate, + guildId: testGuildId, + id: testEventId + } + + const res = await votes.sendVoteClosedMessage(scheduledEvent, 'MovieNew', 'guild', 'request') + expect(res).toEqual({ + allowedMentions: { + parse: ["roles"] + }, + content: `[Abstimmung beendet] für https://discord.com/events/${testGuildId}/${testEventId}\n<@&> Wir gucken MovieNew am 01.01. um 01:00` + }) + }) + // test('checkForPollsToClose', async () => { + // + // const testGuild: Guild = { + // scheduledEvents: { + // fetch: jest.fn().mockImplementation(() => { + // return new Promise(resolve => { + // resolve([ + // { name: "Event Name" }, + // { name: "Event: VOTING OFFEN", scheduledStartTimestamp: "" }, + // { name: "another voting" }, + // ] + // ) + // }) + // }) + // } + // } + // + // const result = await votes.checkForPollsToClose(testGuild) + // + // + // + // + // }) + + test('getVotesByEmote', async () => { + const mockMessage: Message = { + cleanContent: mockMessageContent, + reactions: { + resolve: jest.fn().mockImplementation((input: any) => { + return votesList.find(e => e.emote === input) + }) + } + } + const result = await votes.getVotesByEmote(mockMessage, 'guildId', 'requestId') + expect(result.length).toEqual(5) + expect(result).toEqual(votesList.filter(x => x.movie != NONE_OF_THAT)) + }) +}) diff --git a/tests/discord/votes.test.ts b/tests/discord/votes.test.ts index 070096a..754be96 100644 --- a/tests/discord/votes.test.ts +++ b/tests/discord/votes.test.ts @@ -3,6 +3,7 @@ import VoteController, { Vote, VoteMessageInfo } from "../../server/helper/vote. import { JellyfinHandler } from "../../server/jellyfin/handler" import { ExtendedClient } from "../../server/structures/client" import { VoteMessage } from "../../server/helper/messageIdentifiers" +import { Message, MessageReaction } from "discord.js" test('parse votes from vote message', async () => { const testMovies = [ 'Movie1', @@ -33,11 +34,11 @@ test('parse votes from vote message', async () => { const msg: VoteMessage = { cleanContent: testMessage, - guild:{ - id:testGuildId, - scheduledEvents:{ - fetch: jest.fn().mockImplementation((input:any)=>{ - if(input === testEventId) + guild: { + id: testGuildId, + scheduledEvents: { + fetch: jest.fn().mockImplementation((input: any) => { + if (input === testEventId) return { scheduledStartAt: testEventDate } @@ -83,3 +84,93 @@ test('parse votes from vote message', () => { const result = voteController.parseGuildIdAndEventIdFromWholeMessage(testMessage) expect(result).toEqual({ guildId: testGuildId, eventId: testEventId }) }) + + +test.skip('handles complete none_of_that vote', () => { + + const mockJellyfinHandler: JellyfinHandler = { + getRandomMovieNames: jest.fn().mockReturnValue(["movie1"]) + } + + const testMovies = [ + 'Movie1', + 'Movie2', + 'Movie3', + 'Movie4', + 'Movie5', + ] + const testEventId = '1234321' + const testEventDate = new Date('2023-01-01') + const testGuildId = "888999888" + const mockClient: ExtendedClient = { + user: { + id: 'mockId' + } + } + const voteController = new VoteController(mockClient, mockJellyfinHandler) + const mockMessageContent = voteController.createVoteMessageText(testEventId, testEventDate, testMovies, testGuildId, "requestId") + const reactedUponMessage: VoteMessage = { + cleanContent: mockMessageContent, + guild: { + id: 'id', + roles: { + resolve: jest.fn().mockReturnValue({ + members: [{}, {}, {}, {}, {}]//content does not matter + }) + }, + scheduledEvents: { + fetch: jest.fn().mockReturnValue([ + { + name: 'voting offen' + } + ]) + } + }, + unpin: jest.fn().mockImplementation(() => { + + }), + delete: jest.fn().mockImplementation(() => { + + }), + reactions: { + resolve: jest.fn().mockImplementation((input: any) => { + console.log(JSON.stringify(input)) + }), + cache: { + get: jest.fn().mockReturnValue({ + users: { + cache: [ + { + id: "mockId"//to filter out + }, + { + id: "userId1" + }, + { + id: "userId2" + }, + { + id: "userId3" + } + ] + } + }) + } + } + } + const msgReaction: MessageReaction = { + message: reactedUponMessage + } + + mockClient.getAnnouncementChannelForGuild = jest.fn().mockReturnValue({ + messages: { + fetch: jest.fn().mockReturnValue([ + reactedUponMessage + ]) + } + }) + + const res = voteController.handleNoneOfThatVote(msgReaction, reactedUponMessage, 'requestId', 'guildId') + + +}) -- 2.40.1 From ce4dc81f7d01c2a666b51dd8f3555ef825fb6e82 Mon Sep 17 00:00:00 2001 From: kenobi Date: Sun, 13 Aug 2023 18:35:22 +0200 Subject: [PATCH 46/72] fix incorrect reroll behaviour now correctly fetches old movies, filters already voted on movies, gets new movies, creates new poll message, deletes old message --- server/helper/vote.controller.ts | 58 ++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/server/helper/vote.controller.ts b/server/helper/vote.controller.ts index 55a5068..30529d2 100644 --- a/server/helper/vote.controller.ts +++ b/server/helper/vote.controller.ts @@ -57,31 +57,53 @@ export default class VoteController { } } + private async removeMessage(msg: Message): Promise> { + if (msg.pinned) { + await msg.unpin() + } + return await msg.delete() + } + public isAboveThreshold(vote: Vote): boolean { + const aboveThreshold = (vote.count - 1) >= 1 + logger.debug(`${vote.movie} : ${vote.count} -> above: ${aboveThreshold}`) + return aboveThreshold + } public async handleReroll(voteMessage: VoteMessage, guild: Guild, guildId: string, requestId: string) { - //get movies that already had votes to give them a second chance const voteInfo: VoteMessageInfo = await this.parseVoteInfoFromVoteMessage(voteMessage, requestId) + const votedOnMovies = voteInfo.votes.filter(this.isAboveThreshold).filter(x => x.emote !== NONE_OF_THAT) + logger.info(`Found ${votedOnMovies.length} with votes`, { requestId, guildId }) // get movies from jellyfin to fill the remaining slots - const newMovieCount: number = config.bot.random_movie_count - voteInfo.votes.filter(x => x.count > 2).length + const newMovieCount: number = config.bot.random_movie_count - votedOnMovies.length + logger.info(`Fetching ${newMovieCount} from jellyfin`) const newMovies: string[] = await this.yavinJellyfinHandler.getRandomMovieNames(newMovieCount, guildId, requestId) // merge - const movies: string[] = newMovies.concat(voteInfo.votes.map(x => x.movie)) + const movies: string[] = newMovies.concat(votedOnMovies.map(x => x.movie)) // create new message - await this.closePoll(guild, requestId) - const message = this.createVoteMessageText(guild.id, voteInfo.eventDate, movies, guildId, requestId) + + logger.info(`Creating new poll message with new movies: ${movies}`, { requestId, guildId }) + const message = this.createVoteMessageText(voteInfo.eventId, voteInfo.eventDate, movies, guildId, requestId) const announcementChannel = this.client.getAnnouncementChannelForGuild(guildId) if (!announcementChannel) { logger.error(`No announcementChannel found for ${guildId}, can't post poll`) return } + + try { + logger.info(`Trying to remove old vote Message`, { requestId, guildId }) + this.removeMessage(voteMessage) + } catch (err) { + logger.error(`Error during removeMessage: ${err}`) + } + const sentMessage = await this.sendVoteMessage(message, movies.length, announcementChannel) sentMessage.pin() + logger.info(`Sent and pinned new poll message`, { requestId, guildId }) } - private async fetchEventStartDateByEventId(guild: Guild, eventId: string, requestId: string): Promise> { const guildEvent: GuildScheduledEvent = await guild.scheduledEvents.fetch(eventId) if (!guildEvent) logger.error(`GuildScheduledEvent with id${eventId} could not be found`, { requestId, guildId: guild.id }) @@ -196,14 +218,15 @@ export default class VoteController { logger.info("Deleting vote message") await lastMessage.delete() const event = await this.getEvent(guild, guild.id, requestId) - if (event) { + if (event && votes?.length > 0) { this.updateEvent(event, votes, guild, guildId, requestId) this.sendVoteClosedMessage(event, votes[0].movie, guildId, requestId) } - lastMessage.unpin() //todo: uncomment when bot has permission to pin/unpin - } + /** + * gets votes for the movies without the NONE_OF_THAT votes + */ public async getVotesByEmote(message: Message, guildId: string, requestId: string): Promise { const votes: Vote[] = [] logger.debug(`Number of items in emotes: ${Object.values(Emotes).length}`, { guildId, requestId }) @@ -241,10 +264,10 @@ export default class VoteController { logger.info("Updating event.", { guildId, requestId }) voteEvent.edit(options) } - public async sendVoteClosedMessage(event: GuildScheduledEvent, movie: string, guildId: string, requestId: string) { - const date = event.scheduledStartAt ? format(event.scheduledStartAt, "dd.MM") : "Fehler, event hatte kein Datum" + public async sendVoteClosedMessage(event: GuildScheduledEvent, movie: string, guildId: string, requestId: string): Promise> { + const date = event.scheduledStartAt ? format(event.scheduledStartAt, "dd.MM.") : "Fehler, event hatte kein Datum" const time = event.scheduledStartAt ? format(event.scheduledStartAt, "HH:mm") : "Fehler, event hatte kein Datum" - const body = `[Abstimmung beendet] für https://discord.com/events/${event.guildId}/${event.id}\n<@&${config.bot.announcement_role}> Wir gucken ${movie} am ${date} um ${time}` + const body = `[Abstimmung beendet] für https://discord.com/events/${event.guildId}/${event.id}\n<@&${config.bot.announcement_role}> Wir gucken ${movie} am ${date} um ${time}` const options: MessageCreateOptions = { content: body, allowedMentions: { parse: ["roles"] } @@ -252,13 +275,14 @@ export default class VoteController { const announcementChannel = this.client.getAnnouncementChannelForGuild(guildId) logger.info("Sending vote closed message.", { guildId, requestId }) if (!announcementChannel) { - logger.error("Could not find announcement channel. Please fix!", { guildId, requestId }) - return + const errorMessages = "Could not find announcement channel. Please fix!" + logger.error(errorMessages, { guildId, requestId }) + throw errorMessages } - announcementChannel.send(options) + return announcementChannel.send(options) } - private extractMovieFromMessageByEmote(message: Message, emote: string): string { - const lines = message.cleanContent.split("\n") + private extractMovieFromMessageByEmote(lastMessages: Message, emote: string): string { + const lines = lastMessages.cleanContent.split("\n") const emoteLines = lines.filter(line => line.includes(emote)) if (!emoteLines) { -- 2.40.1 From 1e912b20ef710388c6de1a9cd29f1fec4cb6c4c9 Mon Sep 17 00:00:00 2001 From: kenobi Date: Sun, 13 Aug 2023 18:35:48 +0200 Subject: [PATCH 47/72] formatting for package.json --- package.json | 102 +++++++++++++++++++++++++-------------------------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/package.json b/package.json index b8961e2..a242129 100644 --- a/package.json +++ b/package.json @@ -1,52 +1,52 @@ { - "name": "node-jellyfin-discord-bot", - "version": "1.1.3", - "description": "A discord bot to sync jellyfin accounts with discord roles", - "main": "index.js", - "license": "MIT", - "dependencies": { - "@discordjs/rest": "^1.7.0", - "@tsconfig/recommended": "^1.0.2", - "@types/node": "^18.15.11", - "@types/node-cron": "^3.0.7", - "@types/request": "^2.48.8", - "@types/uuid": "^9.0.1", - "axios": "^1.3.5", - "date-fns": "^2.29.3", - "date-fns-tz": "^2.0.0", - "discord-api-types": "^0.37.38", - "discord.js": "^14.9.0", - "dotenv": "^16.0.3", - "jellyfin-apiclient": "^1.10.0", - "node-cron": "^3.0.2", - "sqlite3": "^5.1.6", - "ts-node": "^10.9.1", - "typescript": "^5.0.4", - "uuid": "^9.0.0", - "winston": "^3.8.2" - }, - "scripts": { - "build": "tsc", - "buildwatch": "tsc --watch", - "clean": "rimraf build", - "start": "node build/index.js", - "debuggable": "node build/index.js --inspect-brk", - "monitor": "nodemon build/index.js", - "lint": "eslint . --ext .ts", - "lint-fix": "eslint . --ext .ts --fix", - "test": "jest", - "test-watch": "jest --watch" - }, - "devDependencies": { - "@types/jest": "^29.5.2", - "@typescript-eslint/eslint-plugin": "^5.58.0", - "@typescript-eslint/parser": "^5.58.0", - "eslint": "^8.38.0", - "jest": "^29.5.0", - "jest-cli": "^29.5.0", - "mockdate": "^3.0.5", - "nodemon": "^2.0.22", - "rimraf": "^5.0.0", - "ts-jest": "^29.1.0" - } -} \ No newline at end of file + "name": "node-jellyfin-discord-bot", + "version": "1.1.3", + "description": "A discord bot to sync jellyfin accounts with discord roles", + "main": "index.js", + "license": "MIT", + "dependencies": { + "@discordjs/rest": "^1.7.0", + "@tsconfig/recommended": "^1.0.2", + "@types/node": "^18.15.11", + "@types/node-cron": "^3.0.7", + "@types/request": "^2.48.8", + "@types/uuid": "^9.0.1", + "axios": "^1.3.5", + "date-fns": "^2.29.3", + "date-fns-tz": "^2.0.0", + "discord-api-types": "^0.37.38", + "discord.js": "^14.9.0", + "dotenv": "^16.0.3", + "jellyfin-apiclient": "^1.10.0", + "node-cron": "^3.0.2", + "sqlite3": "^5.1.6", + "ts-node": "^10.9.1", + "typescript": "^5.0.4", + "uuid": "^9.0.0", + "winston": "^3.8.2" + }, + "scripts": { + "build": "tsc", + "buildwatch": "tsc --watch", + "clean": "rimraf build", + "start": "node build/index.js", + "debuggable": "node build/index.js --inspect-brk", + "monitor": "nodemon build/index.js", + "lint": "eslint . --ext .ts", + "lint-fix": "eslint . --ext .ts --fix", + "test": "jest --runInBand", + "test-watch": "jest --watch" + }, + "devDependencies": { + "@types/jest": "^29.5.2", + "@typescript-eslint/eslint-plugin": "^5.58.0", + "@typescript-eslint/parser": "^5.58.0", + "eslint": "^8.38.0", + "jest": "^29.5.0", + "jest-cli": "^29.5.0", + "mockdate": "^3.0.5", + "nodemon": "^2.0.22", + "rimraf": "^5.0.0", + "ts-jest": "^29.1.0" + } +} -- 2.40.1 From eef3a9c3589e238883efe47bcb146f4a6487dc1f Mon Sep 17 00:00:00 2001 From: kenobi Date: Sat, 21 Oct 2023 14:11:03 +0200 Subject: [PATCH 48/72] add missing role to test --- tests/discord/noneofthat.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/discord/noneofthat.test.ts b/tests/discord/noneofthat.test.ts index 5a83fef..a4b51ff 100644 --- a/tests/discord/noneofthat.test.ts +++ b/tests/discord/noneofthat.test.ts @@ -53,7 +53,7 @@ describe('vote controller - none_of_that functions', () => { allowedMentions: { parse: ["roles"] }, - content: `[Abstimmung beendet] für https://discord.com/events/${testGuildId}/${testEventId}\n<@&> Wir gucken MovieNew am 01.01. um 01:00` + content: `[Abstimmung beendet] für https://discord.com/events/${testGuildId}/${testEventId}\n<@&1117915290781626398> Wir gucken MovieNew am 01.01. um 01:00` }) }) // test('checkForPollsToClose', async () => { -- 2.40.1 From 599243990e1a73faaef4f2f8cb2f977fdfcceae5 Mon Sep 17 00:00:00 2001 From: kenobi Date: Sat, 21 Oct 2023 14:56:15 +0200 Subject: [PATCH 49/72] remove console.logs --- server/commands/echo.ts | 3 ++- server/commands/resetPassword.ts | 5 +++-- server/events/messageCreate.ts | 3 ++- server/helper/sendFailureDM.ts | 3 ++- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/server/commands/echo.ts b/server/commands/echo.ts index 4bf385b..98d7b79 100644 --- a/server/commands/echo.ts +++ b/server/commands/echo.ts @@ -1,6 +1,7 @@ import { ApplicationCommandOptionType } from 'discord.js' import { Command } from '../structures/command' import { RunOptions } from '../types/commandTypes' +import { logger } from '../logger' export default new Command({ name: 'echo', description: 'Echoes a text', @@ -13,7 +14,7 @@ export default new Command({ } ], run: async (interaction: RunOptions) => { - console.log('echo called') + logger.info('echo called') interaction.interaction.reply(interaction.toString()) } }) diff --git a/server/commands/resetPassword.ts b/server/commands/resetPassword.ts index 28eac2c..8aab706 100644 --- a/server/commands/resetPassword.ts +++ b/server/commands/resetPassword.ts @@ -2,15 +2,16 @@ import { v4 as uuid } from 'uuid' import { jellyfinHandler } from "../.." import { Command } from '../structures/command' import { RunOptions } from '../types/commandTypes' +import { logger } from '../logger' export default new Command({ name: 'passwort_reset', description: 'Ich vergebe dir ein neues Passwort und schicke es dir per DM zu. Kostet auch nix! Versprochen! 😉', options: [], run: async (interaction: RunOptions) => { - console.log('PasswortReset called') + logger.info('PasswortReset called') interaction.interaction.followUp('Yo, ich schick dir eins!') - console.log(JSON.stringify(interaction.interaction.member, null, 2)) + logger.info(JSON.stringify(interaction.interaction.member, null, 2)) jellyfinHandler.resetUserPasswort(interaction.interaction.member, uuid()) } }) diff --git a/server/events/messageCreate.ts b/server/events/messageCreate.ts index 3d3620a..bf92054 100644 --- a/server/events/messageCreate.ts +++ b/server/events/messageCreate.ts @@ -1,6 +1,7 @@ import { Message } from "discord.js" +import { logger } from "../logger" export const name = 'messageCreate' export function execute(message: Message) { - console.log(`${JSON.stringify(message)} has been created`) + logger.info(`${JSON.stringify(message)} has been created`) } diff --git a/server/helper/sendFailureDM.ts b/server/helper/sendFailureDM.ts index 78a5dfa..d1adf5f 100644 --- a/server/helper/sendFailureDM.ts +++ b/server/helper/sendFailureDM.ts @@ -1,10 +1,11 @@ import { CustomError, errorCodes } from "../interfaces" +import { logger } from "../logger" import { ExtendedClient } from "../structures/client" export async function sendFailureDM(creatorMessage: string, client: ExtendedClient, creatorId?: string): Promise { if (!creatorId) throw new CustomError('No creator ID present', errorCodes.no_creator_id) const creator = await client.users.fetch(creatorId) - console.log(`Creator ${JSON.stringify(creator)}`) + logger.info(`Creator ${JSON.stringify(creator)}`) if (creator) if (!creator.dmChannel) await creator.createDM() -- 2.40.1 From e66aebc88c772421d20db57a3f0ce984be8e2982 Mon Sep 17 00:00:00 2001 From: kenobi Date: Sat, 21 Oct 2023 14:56:33 +0200 Subject: [PATCH 50/72] make top pick retain optional during reroll via env var --- server/configuration.ts | 4 +++- server/helper/vote.controller.ts | 29 +++++++++++++++++------------ 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/server/configuration.ts b/server/configuration.ts index 832e48f..edfe08e 100644 --- a/server/configuration.ts +++ b/server/configuration.ts @@ -31,6 +31,7 @@ export interface Config { yavin_jellyfin_token: string yavin_jellyfin_collection_user: string random_movie_count: number + reroll_retains_top_picks: boolean } } export const config: Config = { @@ -71,6 +72,7 @@ export const config: Config = { yavin_jellyfin_token: process.env.YAVIN_TOKEN ?? "", yavin_jellyfin_collection_user: process.env.YAVIN_COLLECTION_USER ?? "", jf_user: process.env.JELLYFIN_USER ?? "", - random_movie_count: parseInt(process.env.RANDOM_MOVIE_COUNT ?? "5") ?? 5 + random_movie_count: parseInt(process.env.RANDOM_MOVIE_COUNT ?? "5") ?? 5, + reroll_retains_top_picks: process.env.REROLL_RETAIN === "true" } } diff --git a/server/helper/vote.controller.ts b/server/helper/vote.controller.ts index 30529d2..d3b10c7 100644 --- a/server/helper/vote.controller.ts +++ b/server/helper/vote.controller.ts @@ -52,7 +52,7 @@ export default class VoteController { logger.info(`No reroll`, { requestId, guildId }) else { logger.info('Starting poll reroll', { requestId, guildId }) - await this.handleReroll(reactedUponMessage, guild, guild.id, requestId) + await this.handleReroll(reactedUponMessage, guild.id, requestId) logger.info(`Finished handling NONE_OF_THAT vote`, { requestId, guildId }) } } @@ -68,20 +68,25 @@ export default class VoteController { logger.debug(`${vote.movie} : ${vote.count} -> above: ${aboveThreshold}`) return aboveThreshold } - public async handleReroll(voteMessage: VoteMessage, guild: Guild, guildId: string, requestId: string) { + public async handleReroll(voteMessage: VoteMessage, guildId: string, requestId: string) { //get movies that already had votes to give them a second chance const voteInfo: VoteMessageInfo = await this.parseVoteInfoFromVoteMessage(voteMessage, requestId) - const votedOnMovies = voteInfo.votes.filter(this.isAboveThreshold).filter(x => x.emote !== NONE_OF_THAT) - logger.info(`Found ${votedOnMovies.length} with votes`, { requestId, guildId }) - - // get movies from jellyfin to fill the remaining slots - const newMovieCount: number = config.bot.random_movie_count - votedOnMovies.length - logger.info(`Fetching ${newMovieCount} from jellyfin`) - const newMovies: string[] = await this.yavinJellyfinHandler.getRandomMovieNames(newMovieCount, guildId, requestId) - - // merge - const movies: string[] = newMovies.concat(votedOnMovies.map(x => x.movie)) + let movies: string[] = Array() + if (config.bot.reroll_retains_top_picks) { + const votedOnMovies = voteInfo.votes.filter(this.isAboveThreshold).filter(x => x.emote !== NONE_OF_THAT) + logger.info(`Found ${votedOnMovies.length} with votes`, { requestId, guildId }) + const newMovieCount: number = config.bot.random_movie_count - votedOnMovies.length + logger.info(`Fetching ${newMovieCount} from jellyfin`) + const newMovies: string[] = await this.yavinJellyfinHandler.getRandomMovieNames(newMovieCount, guildId, requestId) + // merge + movies = newMovies.concat(votedOnMovies.map(x => x.movie)) + } else { + // get movies from jellyfin to fill the remaining slots + const newMovieCount: number = config.bot.random_movie_count + logger.info(`Fetching ${newMovieCount} from jellyfin`) + movies = await this.yavinJellyfinHandler.getRandomMovieNames(newMovieCount, guildId, requestId) + } // create new message logger.info(`Creating new poll message with new movies: ${movies}`, { requestId, guildId }) -- 2.40.1 From c73cd20ccff37eb55626fb46578e297f8891f846 Mon Sep 17 00:00:00 2001 From: kenobi Date: Sat, 21 Oct 2023 15:05:25 +0200 Subject: [PATCH 51/72] add test-relevant fallback values for unit tests --- server/configuration.ts | 10 +++++----- tests/discord/noneofthat.test.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/server/configuration.ts b/server/configuration.ts index edfe08e..b43eef0 100644 --- a/server/configuration.ts +++ b/server/configuration.ts @@ -61,11 +61,11 @@ export const config: Config = { client_id: process.env.CLIENT_ID ?? "", jellfin_token: process.env.JELLYFIN_TOKEN ?? "", jellyfin_url: process.env.JELLYFIN_URL ?? "", - workaround_token: process.env.TOKEN ?? "", - watcher_role: process.env.WATCHER_ROLE ?? "", - jf_admin_role: process.env.ADMIN_ROLE ?? "", - announcement_role: process.env.WATCHPARTY_ANNOUNCEMENT_ROLE ?? "", - announcement_channel_id: process.env.CHANNEL_ID ?? "", + workaround_token: process.env.TOKEN ?? "TOKEN", + watcher_role: process.env.WATCHER_ROLE ?? "WATCHER_ROLE", + jf_admin_role: process.env.ADMIN_ROLE ?? "ADMIN_ROLE", + announcement_role: process.env.WATCHPARTY_ANNOUNCEMENT_ROLE ?? "ANNOUNCE_ROLE", + announcement_channel_id: process.env.CHANNEL_ID ?? "ANNOUNCE_CHANNEL", jf_collection_id: process.env.JELLYFIN_COLLECTION_ID ?? "", yavin_collection_id: process.env.YAVIN_COLLECTION_ID ?? "", yavin_jellyfin_url: process.env.YAVIN_JELLYFIN_URL ?? "", diff --git a/tests/discord/noneofthat.test.ts b/tests/discord/noneofthat.test.ts index a4b51ff..72782ff 100644 --- a/tests/discord/noneofthat.test.ts +++ b/tests/discord/noneofthat.test.ts @@ -53,7 +53,7 @@ describe('vote controller - none_of_that functions', () => { allowedMentions: { parse: ["roles"] }, - content: `[Abstimmung beendet] für https://discord.com/events/${testGuildId}/${testEventId}\n<@&1117915290781626398> Wir gucken MovieNew am 01.01. um 01:00` + content: `[Abstimmung beendet] für https://discord.com/events/${testGuildId}/${testEventId}\n<@&ANNOUNCE_ROLE> Wir gucken MovieNew am 01.01. um 01:00` }) }) // test('checkForPollsToClose', async () => { -- 2.40.1 From 9cdc6e193458870e99b542288c7c53e6038ecd3f Mon Sep 17 00:00:00 2001 From: kenobi Date: Tue, 24 Oct 2023 22:39:57 +0200 Subject: [PATCH 52/72] add fake env vars for unit tests --- jest.config.js | 33 ++++++++++++++++---------------- server/logger.ts | 3 ++- testenv.js | 15 +++++++++++++++ tests/discord/noneofthat.test.ts | 2 +- 4 files changed, 35 insertions(+), 18 deletions(-) create mode 100644 testenv.js diff --git a/jest.config.js b/jest.config.js index 4086ac8..b364ab7 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,18 +1,19 @@ module.exports = { - 'roots': [ - '/tests', - '/server' - ], - 'transform': { - '^.+\\.tsx?$': 'ts-jest' - }, - 'testRegex': '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$', - 'moduleFileExtensions': [ - 'ts', - 'tsx', - 'js', - 'jsx', - 'json', - 'node' - ], + 'roots': [ + '/tests', + '/server' + ], + 'transform': { + '^.+\\.tsx?$': 'ts-jest' + }, + 'testRegex': '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$', + 'setupFiles': ["/testenv.js"], + 'moduleFileExtensions': [ + 'ts', + 'tsx', + 'js', + 'jsx', + 'json', + 'node' + ], }; diff --git a/server/logger.ts b/server/logger.ts index 060c72a..b81d8ee 100644 --- a/server/logger.ts +++ b/server/logger.ts @@ -16,7 +16,8 @@ const logFormat = format.combine( const consoleTransports = [ new transports.Console({ - format: logFormat + format: logFormat, + silent: process.env.NODE_ENV === 'testing' }) ] export const logger = createLogger({ diff --git a/testenv.js b/testenv.js new file mode 100644 index 0000000..ca22600 --- /dev/null +++ b/testenv.js @@ -0,0 +1,15 @@ +process.env.CLIENT_ID = "CLIENT_ID" +process.env.SECRET = "SECRET" +process.env.BOT_TOKEN = "BOT_TOKEN" +process.env.WATCHER_ROLE = "WATCHER_ROLE" +process.env.ADMIN_ROLE = "ADMIN_ROLE" +process.env.CHANNEL_ID = "CHANNEL_ID" +process.env.WATCHPARTY_ANNOUNCEMENT_ROLE = "WATCHPARTY_ANNOUNCEMENT_ROLE" +process.env.YAVIN_JELLYFIN_URL = "YAVIN_JELLYFIN_URL" +process.env.YAVIN_COLLECTION_ID = "YAVIN_COLLECTION_ID" +process.env.YAVIN_COLLECTION_USER = "YAVIN_COLLECTION_USER" +process.env.YAVIN_TOKEN = "YAVIN_TOKEN" +process.env.TOKEN = "TOKEN" +process.env.JELLYFIN_USER = "JELLYFIN_USER" +process.env.JELLYFIN_COLLECTION_ID = "JELLYFIN_COLLECTION_ID" +process.env.JELLYFIN_URL = "JELLYFIN_URL" diff --git a/tests/discord/noneofthat.test.ts b/tests/discord/noneofthat.test.ts index 72782ff..c37c268 100644 --- a/tests/discord/noneofthat.test.ts +++ b/tests/discord/noneofthat.test.ts @@ -53,7 +53,7 @@ describe('vote controller - none_of_that functions', () => { allowedMentions: { parse: ["roles"] }, - content: `[Abstimmung beendet] für https://discord.com/events/${testGuildId}/${testEventId}\n<@&ANNOUNCE_ROLE> Wir gucken MovieNew am 01.01. um 01:00` + content: `[Abstimmung beendet] für https://discord.com/events/${testGuildId}/${testEventId}\n<@&WATCHPARTY_ANNOUNCEMENT_ROLE> Wir gucken MovieNew am 01.01. um 01:00` }) }) // test('checkForPollsToClose', async () => { -- 2.40.1 From f705b97804dd0de39157f8ca68dbe61cb9131494 Mon Sep 17 00:00:00 2001 From: kenobi Date: Tue, 24 Oct 2023 22:42:03 +0200 Subject: [PATCH 53/72] move testenv to correct location --- jest.config.js | 2 +- testenv.js => tests/testenv.js | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename testenv.js => tests/testenv.js (100%) diff --git a/jest.config.js b/jest.config.js index b364ab7..9db68a4 100644 --- a/jest.config.js +++ b/jest.config.js @@ -7,7 +7,7 @@ module.exports = { '^.+\\.tsx?$': 'ts-jest' }, 'testRegex': '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$', - 'setupFiles': ["/testenv.js"], + 'setupFiles': ["/tests/testenv.js"], 'moduleFileExtensions': [ 'ts', 'tsx', diff --git a/testenv.js b/tests/testenv.js similarity index 100% rename from testenv.js rename to tests/testenv.js -- 2.40.1 From 4600820889d046972bb264912f0ad929c8950dac Mon Sep 17 00:00:00 2001 From: kenobi Date: Sat, 18 Nov 2023 17:28:44 +0100 Subject: [PATCH 54/72] move preparation of vote Message sending into vote controller event only needs to supply information, text creation, sending and pinning happens in the vote controller --- server/events/autoCreateVoteByWPEvent.ts | 17 +++++++++++------ server/helper/vote.controller.ts | 11 ++++++++++- server/interfaces.ts | 9 ++++++++- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/server/events/autoCreateVoteByWPEvent.ts b/server/events/autoCreateVoteByWPEvent.ts index 00ee9d9..89cd3e0 100644 --- a/server/events/autoCreateVoteByWPEvent.ts +++ b/server/events/autoCreateVoteByWPEvent.ts @@ -4,7 +4,6 @@ import { client, yavinJellyfinHandler } from "../.."; import { Maybe } from "../interfaces"; import { logger } from "../logger"; - export const name = 'guildScheduledEventCreate' export async function execute(event: GuildScheduledEvent) { @@ -25,15 +24,21 @@ export async function execute(event: GuildScheduledEvent) { return } logger.debug(`Found channel ${JSON.stringify(announcementChannel, null, 2)}`, { guildId: event.guildId, requestId }) - if (!event.scheduledStartAt) { - logger.info("EVENT DOES NOT HAVE STARTDATE; CANCELLING", { guildId: event.guildId, requestId }) + logger.info("Event does not have a start date, cancelling", { guildId: event.guildId, requestId }) return } - const sentMessageText = client.voteController.createVoteMessageText(event.id, event.scheduledStartAt, movies, event.guild?.id ?? "", requestId) - const sentMessage = await client.voteController.sendVoteMessage(sentMessageText, movies.length, announcementChannel) + const sentMessage = await client.voteController.prepareAndSendVoteMessage({ + movies, + startDate: event.scheduledStartAt, + event, + announcementChannel, + pinAfterSending: true + }, + event.guildId, + requestId) - sentMessage.pin() + logger.debug(JSON.stringify(sentMessage)) } } diff --git a/server/helper/vote.controller.ts b/server/helper/vote.controller.ts index d3b10c7..528df06 100644 --- a/server/helper/vote.controller.ts +++ b/server/helper/vote.controller.ts @@ -5,7 +5,7 @@ import { getMembersWithRoleFromGuild } from "./roleFilter" import { config } from "../configuration" import { VoteMessage, isVoteEndedMessage, isVoteMessage } from "./messageIdentifiers" import { createDateStringFromEvent } from "./dateHelper" -import { Maybe } from "../interfaces" +import { Maybe, voteMessageInputInformation as prepareVoteMessageInput } from "../interfaces" import format from "date-fns/format" import toDate from "date-fns/toDate" import differenceInDays from "date-fns/differenceInDays" @@ -161,6 +161,14 @@ export default class VoteController { throw Error(`Could not find eventId in Vote Message`) } + public async prepareAndSendVoteMessage(inputInfo: prepareVoteMessageInput, guildId: string, requestId: string) { + const messageText = this.createVoteMessageText(inputInfo.event.id, inputInfo.startDate, inputInfo.movies, guildId, requestId) + const sentMessage = await this.sendVoteMessage(messageText, inputInfo.movies.length, inputInfo.announcementChannel) + if (inputInfo.pinAfterSending) + sentMessage.pin() + return sentMessage + } + public createVoteMessageText(eventId: string, eventStartDate: Date, movies: string[], guildId: string, requestId: string): string { let message = `[Abstimmung] für https://discord.com/events/${guildId}/${eventId} \n<@&${config.bot.announcement_role}> Es gibt eine neue Abstimmung für die nächste Watchparty ${createDateStringFromEvent(eventStartDate, guildId, requestId)}! Stimme hierunter für den nächsten Film ab!\n` @@ -171,6 +179,7 @@ export default class VoteController { return message } + public async sendVoteMessage(message: string, movieCount: number, announcementChannel: TextChannel) { const options: MessageCreateOptions = { diff --git a/server/interfaces.ts b/server/interfaces.ts index 982c1ec..a6fc009 100644 --- a/server/interfaces.ts +++ b/server/interfaces.ts @@ -1,5 +1,5 @@ import { Collection } from "@discordjs/collection" -import { Role } from "discord.js" +import { GuildScheduledEvent, Role, TextChannel } from "discord.js" export type Maybe = T | undefined | null export interface Player { @@ -39,3 +39,10 @@ export interface JellyfinConfig { collectionUser: string } export type PermissionLevel = "VIEWER" | "ADMIN" | "TEMPORARY" +export interface voteMessageInputInformation { + movies: string[], + startDate: Date, + event: GuildScheduledEvent, + announcementChannel: TextChannel, + pinAfterSending: boolean, +} -- 2.40.1 From 66507cb08fa50ba3a7be28388c55b21227fb2261 Mon Sep 17 00:00:00 2001 From: kenobi Date: Sat, 18 Nov 2023 17:40:50 +0100 Subject: [PATCH 55/72] msg -> message --- server/helper/vote.controller.ts | 8 ++++---- server/jellyfin/runtime.ts | 12 ++++++------ tests/discord/votes.test.ts | 8 ++++---- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/server/helper/vote.controller.ts b/server/helper/vote.controller.ts index 528df06..103d348 100644 --- a/server/helper/vote.controller.ts +++ b/server/helper/vote.controller.ts @@ -57,11 +57,11 @@ export default class VoteController { } } - private async removeMessage(msg: Message): Promise> { - if (msg.pinned) { - await msg.unpin() + private async removeMessage(message: Message): Promise> { + if (message.pinned) { + await message.unpin() } - return await msg.delete() + return await message.delete() } public isAboveThreshold(vote: Vote): boolean { const aboveThreshold = (vote.count - 1) >= 1 diff --git a/server/jellyfin/runtime.ts b/server/jellyfin/runtime.ts index 93efc22..c1494b8 100644 --- a/server/jellyfin/runtime.ts +++ b/server/jellyfin/runtime.ts @@ -253,22 +253,22 @@ function isFormData(value: any): value is FormData { export class ResponseError extends Error { override name: "ResponseError" = "ResponseError"; - constructor(public response: Response, msg?: string) { - super(msg); + constructor(public response: Response, errorMessage?: string) { + super(errorMessage); } } export class FetchError extends Error { override name: "FetchError" = "FetchError"; - constructor(public cause: Error, msg?: string) { - super(msg); + constructor(public cause: Error, errorMessage?: string) { + super(errorMessage); } } export class RequiredError extends Error { override name: "RequiredError" = "RequiredError"; - constructor(public field: string, msg?: string) { - super(msg); + constructor(public field: string, errorMessage?: string) { + super(errorMessage); } } diff --git a/tests/discord/votes.test.ts b/tests/discord/votes.test.ts index 754be96..14ae07e 100644 --- a/tests/discord/votes.test.ts +++ b/tests/discord/votes.test.ts @@ -32,7 +32,7 @@ test('parse votes from vote message', async () => { ] } - const msg: VoteMessage = { + const message: VoteMessage = { cleanContent: testMessage, guild: { id: testGuildId, @@ -58,7 +58,7 @@ test('parse votes from vote message', async () => { } } - const result = await voteController.parseVoteInfoFromVoteMessage(msg, 'requestId') + const result = await voteController.parseVoteInfoFromVoteMessage(message, 'requestId') console.log(JSON.stringify(result)) expect(Array.isArray(result)).toBe(false) expect(result.eventId).toEqual(testEventId) @@ -158,7 +158,7 @@ test.skip('handles complete none_of_that vote', () => { } } } - const msgReaction: MessageReaction = { + const messageReaction: MessageReaction = { message: reactedUponMessage } @@ -170,7 +170,7 @@ test.skip('handles complete none_of_that vote', () => { } }) - const res = voteController.handleNoneOfThatVote(msgReaction, reactedUponMessage, 'requestId', 'guildId') + const res = voteController.handleNoneOfThatVote(messageReaction, reactedUponMessage, 'requestId', 'guildId') }) -- 2.40.1 From 296a490e935cbdb79b70d73d2df9bc12a5774c53 Mon Sep 17 00:00:00 2001 From: kenobi Date: Sat, 18 Nov 2023 18:14:56 +0100 Subject: [PATCH 56/72] rename filter function --- server/helper/vote.controller.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/server/helper/vote.controller.ts b/server/helper/vote.controller.ts index 103d348..0093ace 100644 --- a/server/helper/vote.controller.ts +++ b/server/helper/vote.controller.ts @@ -63,10 +63,12 @@ export default class VoteController { } return await message.delete() } - public isAboveThreshold(vote: Vote): boolean { - const aboveThreshold = (vote.count - 1) >= 1 - logger.debug(`${vote.movie} : ${vote.count} -> above: ${aboveThreshold}`) - return aboveThreshold + + public hasAtLeastOneVote(vote: Vote): boolean { + // subtracting the bots initial vote + const overOneVote = (vote.count - 1) >= 1 + logger.debug(`${vote.movie} : ${vote.count} -> above: ${overOneVote}`) + return overOneVote } public async handleReroll(voteMessage: VoteMessage, guildId: string, requestId: string) { //get movies that already had votes to give them a second chance @@ -74,7 +76,7 @@ export default class VoteController { let movies: string[] = Array() if (config.bot.reroll_retains_top_picks) { - const votedOnMovies = voteInfo.votes.filter(this.isAboveThreshold).filter(x => x.emote !== NONE_OF_THAT) + const votedOnMovies = voteInfo.votes.filter(this.hasAtLeastOneVote).filter(x => x.emote !== NONE_OF_THAT) logger.info(`Found ${votedOnMovies.length} with votes`, { requestId, guildId }) const newMovieCount: number = config.bot.random_movie_count - votedOnMovies.length logger.info(`Fetching ${newMovieCount} from jellyfin`) -- 2.40.1 From 119343c916b023a926e534575ae803cdec5b9594 Mon Sep 17 00:00:00 2001 From: kenobi Date: Sat, 18 Nov 2023 18:15:13 +0100 Subject: [PATCH 57/72] fix comment --- server/helper/vote.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/helper/vote.controller.ts b/server/helper/vote.controller.ts index 0093ace..fb9342f 100644 --- a/server/helper/vote.controller.ts +++ b/server/helper/vote.controller.ts @@ -71,7 +71,7 @@ export default class VoteController { return overOneVote } public async handleReroll(voteMessage: VoteMessage, guildId: string, requestId: string) { - //get movies that already had votes to give them a second chance + // get the movies currently being voted on, their votes, the eventId and its date const voteInfo: VoteMessageInfo = await this.parseVoteInfoFromVoteMessage(voteMessage, requestId) let movies: string[] = Array() -- 2.40.1 From a455fd8ff7e6b8ffb032fb4aed9389da68ee513b Mon Sep 17 00:00:00 2001 From: kenobi Date: Sat, 18 Nov 2023 18:15:27 +0100 Subject: [PATCH 58/72] message -> messageText --- server/helper/vote.controller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/helper/vote.controller.ts b/server/helper/vote.controller.ts index fb9342f..f750f65 100644 --- a/server/helper/vote.controller.ts +++ b/server/helper/vote.controller.ts @@ -92,7 +92,7 @@ export default class VoteController { // create new message logger.info(`Creating new poll message with new movies: ${movies}`, { requestId, guildId }) - const message = this.createVoteMessageText(voteInfo.eventId, voteInfo.eventDate, movies, guildId, requestId) + const messageText = this.createVoteMessageText(voteInfo.eventId, voteInfo.eventDate, movies, guildId, requestId) const announcementChannel = this.client.getAnnouncementChannelForGuild(guildId) if (!announcementChannel) { logger.error(`No announcementChannel found for ${guildId}, can't post poll`) @@ -106,7 +106,7 @@ export default class VoteController { logger.error(`Error during removeMessage: ${err}`) } - const sentMessage = await this.sendVoteMessage(message, movies.length, announcementChannel) + const sentMessage = await this.sendVoteMessage(messageText, movies.length, announcementChannel) sentMessage.pin() logger.info(`Sent and pinned new poll message`, { requestId, guildId }) } -- 2.40.1 From 20da25f2bf9a473704f8b4660e5f05183679ba39 Mon Sep 17 00:00:00 2001 From: kenobi Date: Sat, 18 Nov 2023 18:22:11 +0100 Subject: [PATCH 59/72] comment filter function --- server/helper/vote.controller.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/helper/vote.controller.ts b/server/helper/vote.controller.ts index f750f65..44d1f55 100644 --- a/server/helper/vote.controller.ts +++ b/server/helper/vote.controller.ts @@ -64,7 +64,11 @@ export default class VoteController { return await message.delete() } - public hasAtLeastOneVote(vote: Vote): boolean { + /** + * returns true if a Vote object contains at least one vote + * @param {Vote} vote + */ + private hasAtLeastOneVote(vote: Vote): boolean { // subtracting the bots initial vote const overOneVote = (vote.count - 1) >= 1 logger.debug(`${vote.movie} : ${vote.count} -> above: ${overOneVote}`) -- 2.40.1 From fc64728a780f99b56aebff7f0a7c5d24a901d90d Mon Sep 17 00:00:00 2001 From: kenobi Date: Sat, 18 Nov 2023 18:26:45 +0100 Subject: [PATCH 60/72] msg -> message --- server/helper/messageIdentifiers.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/server/helper/messageIdentifiers.ts b/server/helper/messageIdentifiers.ts index 76832af..220b8cd 100644 --- a/server/helper/messageIdentifiers.ts +++ b/server/helper/messageIdentifiers.ts @@ -8,13 +8,13 @@ export type VoteMessage = Message & { readonly __brand: 'voteend' } export type DiscordMessage = VoteMessage | VoteEndMessage | AnnouncementMessage -export function isVoteMessage(msg: Message): msg is VoteMessage { - return msg.cleanContent.includes('[Abstimmung]') +export function isVoteMessage(message: Message): message is VoteMessage { + return message.cleanContent.includes('[Abstimmung]') } -export function isInitialAnnouncement(msg: Message): msg is AnnouncementMessage { - return msg.cleanContent.includes("[initial]") +export function isInitialAnnouncement(message: Message): message is AnnouncementMessage { + return message.cleanContent.includes("[initial]") } -export function isVoteEndedMessage(msg: Message): msg is VoteEndMessage { - return msg.cleanContent.includes("[Abstimmung beendet]") +export function isVoteEndedMessage(message: Message): message is VoteEndMessage { + return message.cleanContent.includes("[Abstimmung beendet]") } -- 2.40.1 From ca99987a20baeceda27cb5e206bff42a54f31b04 Mon Sep 17 00:00:00 2001 From: kenobi Date: Sun, 19 Nov 2023 18:21:51 +0100 Subject: [PATCH 61/72] clean up variable and function names --- server/helper/vote.controller.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/server/helper/vote.controller.ts b/server/helper/vote.controller.ts index 44d1f55..41188ae 100644 --- a/server/helper/vote.controller.ts +++ b/server/helper/vote.controller.ts @@ -186,11 +186,11 @@ export default class VoteController { return message } - public async sendVoteMessage(message: string, movieCount: number, announcementChannel: TextChannel) { + public async sendVoteMessage(messageText: string, movieCount: number, announcementChannel: TextChannel) { const options: MessageCreateOptions = { allowedMentions: { parse: ["roles"] }, - content: message, + content: messageText, } const sentMessage: Message = await (await announcementChannel.fetch()).send(options) @@ -237,12 +237,12 @@ export default class VoteController { logger.info("Deleting vote message") await lastMessage.delete() - const event = await this.getEvent(guild, guild.id, requestId) + const event = await this.getOpenEvent(guild, guild.id, requestId) if (event && votes?.length > 0) { - this.updateEvent(event, votes, guild, guildId, requestId) + this.updateOpenPollEventWithVoteResults(event, votes, guild, guildId, requestId) this.sendVoteClosedMessage(event, votes[0].movie, guildId, requestId) } - lastMessage.unpin() //todo: uncomment when bot has permission to pin/unpin + lastMessage.unpin() } /** * gets votes for the movies without the NONE_OF_THAT votes @@ -262,7 +262,7 @@ export default class VoteController { } return votes } - public async getEvent(guild: Guild, guildId: string, requestId: string): Promise { + public async getOpenEvent(guild: Guild, guildId: string, requestId: string): Promise { const voteEvents = (await guild.scheduledEvents.fetch()) .map((value) => value) .filter(event => event.name.toLowerCase().includes("voting offen")) @@ -274,7 +274,7 @@ export default class VoteController { } return voteEvents[0] } - public async updateEvent(voteEvent: GuildScheduledEvent, votes: Vote[], guild: Guild, guildId: string, requestId: string) { + public async updateOpenPollEventWithVoteResults(voteEvent: GuildScheduledEvent, votes: Vote[], guild: Guild, guildId: string, requestId: string) { logger.info(`Updating event with movie ${votes[0].movie}.`, { guildId, requestId }) const options: GuildScheduledEventEditOptions> = { name: votes[0].movie, @@ -285,8 +285,8 @@ export default class VoteController { voteEvent.edit(options) } public async sendVoteClosedMessage(event: GuildScheduledEvent, movie: string, guildId: string, requestId: string): Promise> { - const date = event.scheduledStartAt ? format(event.scheduledStartAt, "dd.MM.") : "Fehler, event hatte kein Datum" - const time = event.scheduledStartAt ? format(event.scheduledStartAt, "HH:mm") : "Fehler, event hatte kein Datum" + const date = event.scheduledStartAt ? format(event.scheduledStartAt, "dd.MM.") : "Fehler, Event hatte kein Datum" + const time = event.scheduledStartAt ? format(event.scheduledStartAt, "HH:mm") : "Fehler, Event hatte keine Uhrzeit" const body = `[Abstimmung beendet] für https://discord.com/events/${event.guildId}/${event.id}\n<@&${config.bot.announcement_role}> Wir gucken ${movie} am ${date} um ${time}` const options: MessageCreateOptions = { content: body, @@ -295,14 +295,14 @@ export default class VoteController { const announcementChannel = this.client.getAnnouncementChannelForGuild(guildId) logger.info("Sending vote closed message.", { guildId, requestId }) if (!announcementChannel) { - const errorMessages = "Could not find announcement channel. Please fix!" - logger.error(errorMessages, { guildId, requestId }) - throw errorMessages + const errorMessageText = "Could not find announcement channel. Please fix!" + logger.error(errorMessageText, { guildId, requestId }) + throw errorMessageText } return announcementChannel.send(options) } - private extractMovieFromMessageByEmote(lastMessages: Message, emote: string): string { - const lines = lastMessages.cleanContent.split("\n") + private extractMovieFromMessageByEmote(voteMessage: VoteMessage, emote: string): string { + const lines = voteMessage.cleanContent.split("\n") const emoteLines = lines.filter(line => line.includes(emote)) if (!emoteLines) { -- 2.40.1 From 081f3c62011fe1aac737e92b89deea907c26cd9a Mon Sep 17 00:00:00 2001 From: kenobi Date: Sun, 19 Nov 2023 18:24:13 +0100 Subject: [PATCH 62/72] fix incorrect branded type --- server/helper/messageIdentifiers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/helper/messageIdentifiers.ts b/server/helper/messageIdentifiers.ts index 220b8cd..79c90e9 100644 --- a/server/helper/messageIdentifiers.ts +++ b/server/helper/messageIdentifiers.ts @@ -2,9 +2,9 @@ import { Message } from "discord.js"; // branded types to differentiate objects of identical Type but different contents -export type VoteEndMessage = Message & { readonly __brand: 'vote' } +export type VoteEndMessage = Message & { readonly __brand: 'voteend' } export type AnnouncementMessage = Message & { readonly __brand: 'announcement' } -export type VoteMessage = Message & { readonly __brand: 'voteend' } +export type VoteMessage = Message & { readonly __brand: 'vote' } export type DiscordMessage = VoteMessage | VoteEndMessage | AnnouncementMessage -- 2.40.1 From fce90911140b95177dc4af28ca702a0dd175a21e Mon Sep 17 00:00:00 2001 From: kenobi Date: Sun, 19 Nov 2023 18:24:33 +0100 Subject: [PATCH 63/72] rename message type union to better reflect its intention --- server/helper/messageIdentifiers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/helper/messageIdentifiers.ts b/server/helper/messageIdentifiers.ts index 79c90e9..312e03a 100644 --- a/server/helper/messageIdentifiers.ts +++ b/server/helper/messageIdentifiers.ts @@ -6,7 +6,7 @@ export type VoteEndMessage = Message & { readonly __brand: 'voteend' } export type AnnouncementMessage = Message & { readonly __brand: 'announcement' } export type VoteMessage = Message & { readonly __brand: 'vote' } -export type DiscordMessage = VoteMessage | VoteEndMessage | AnnouncementMessage +export type KnownDiscordMessage = VoteMessage | VoteEndMessage | AnnouncementMessage export function isVoteMessage(message: Message): message is VoteMessage { return message.cleanContent.includes('[Abstimmung]') -- 2.40.1 From 1348abbd48627263a4f720b8e5d6a3da40723fd7 Mon Sep 17 00:00:00 2001 From: kenobi Date: Sun, 19 Nov 2023 18:55:51 +0100 Subject: [PATCH 64/72] make message identifiers actually work properly with LSP --- server/helper/messageIdentifiers.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/helper/messageIdentifiers.ts b/server/helper/messageIdentifiers.ts index 312e03a..fa6a0e1 100644 --- a/server/helper/messageIdentifiers.ts +++ b/server/helper/messageIdentifiers.ts @@ -2,9 +2,9 @@ import { Message } from "discord.js"; // branded types to differentiate objects of identical Type but different contents -export type VoteEndMessage = Message & { readonly __brand: 'voteend' } -export type AnnouncementMessage = Message & { readonly __brand: 'announcement' } -export type VoteMessage = Message & { readonly __brand: 'vote' } +export type VoteEndMessage = Message & { readonly __brand: 'voteend' } +export type AnnouncementMessage = Message & { readonly __brand: 'announcement' } +export type VoteMessage = Message & { readonly __brand: 'vote' } export type KnownDiscordMessage = VoteMessage | VoteEndMessage | AnnouncementMessage -- 2.40.1 From 68546b0b50b6902464b1ba4f4714a520d3bf78ab Mon Sep 17 00:00:00 2001 From: kenobi Date: Sun, 19 Nov 2023 18:56:08 +0100 Subject: [PATCH 65/72] adjust message identifier in test --- tests/discord/noneofthat.test.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/discord/noneofthat.test.ts b/tests/discord/noneofthat.test.ts index c37c268..95dcda4 100644 --- a/tests/discord/noneofthat.test.ts +++ b/tests/discord/noneofthat.test.ts @@ -3,6 +3,7 @@ import VoteController from "../../server/helper/vote.controller" import { JellyfinHandler } from "../../server/jellyfin/handler" import { ExtendedClient } from "../../server/structures/client" import { Emoji, NONE_OF_THAT } from "../../server/constants" +import { isVoteMessage } from "../../server/helper/messageIdentifiers" describe('vote controller - none_of_that functions', () => { const testEventId = '1234321' @@ -89,8 +90,10 @@ describe('vote controller - none_of_that functions', () => { }) } } - const result = await votes.getVotesByEmote(mockMessage, 'guildId', 'requestId') - expect(result.length).toEqual(5) - expect(result).toEqual(votesList.filter(x => x.movie != NONE_OF_THAT)) + if (isVoteMessage(mockMessage)) { + const result = await votes.getVotesByEmote(mockMessage, 'guildId', 'requestId') + expect(result.length).toEqual(5) + expect(result).toEqual(votesList.filter(x => x.movie != NONE_OF_THAT)) + } }) }) -- 2.40.1 From 976175242b3b619948c0af720d0e15cdddac8c51 Mon Sep 17 00:00:00 2001 From: kenobi Date: Sun, 19 Nov 2023 18:56:39 +0100 Subject: [PATCH 66/72] reorder close poll and use message identifier --- server/helper/vote.controller.ts | 38 +++++++++++++++++--------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/server/helper/vote.controller.ts b/server/helper/vote.controller.ts index 41188ae..c92e999 100644 --- a/server/helper/vote.controller.ts +++ b/server/helper/vote.controller.ts @@ -225,29 +225,31 @@ export default class VoteController { const lastMessage: Message = messages[0] - logger.debug(`Found messages: ${JSON.stringify(messages, null, 2)}`, { guildId, requestId }) - - logger.debug(`Last message: ${JSON.stringify(lastMessage, null, 2)}`, { guildId, requestId }) - - - const votes = (await this.getVotesByEmote(lastMessage, guildId, requestId)) - .sort((a, b) => b.count - a.count) - - logger.debug(`votes: ${JSON.stringify(votes, null, 2)}`, { guildId, requestId }) - - logger.info("Deleting vote message") - await lastMessage.delete() - const event = await this.getOpenEvent(guild, guild.id, requestId) - if (event && votes?.length > 0) { - this.updateOpenPollEventWithVoteResults(event, votes, guild, guildId, requestId) - this.sendVoteClosedMessage(event, votes[0].movie, guildId, requestId) + if (!isVoteMessage(lastMessage)) { + logger.error(`Found message that is not a vote message, can't proceed`, { guildId, requestId }) + logger.debug(`Found messages: ${JSON.stringify(messages, null, 2)}`, { guildId, requestId }) + logger.debug(`Last message: ${JSON.stringify(lastMessage, null, 2)}`, { guildId, requestId }) + } + else { + const votes = (await this.getVotesByEmote(lastMessage, guildId, requestId)) + .sort((a, b) => b.count - a.count) + + logger.debug(`votes: ${JSON.stringify(votes, null, 2)}`, { guildId, requestId }) + + logger.info("Deleting vote message") + lastMessage.unpin() + await lastMessage.delete() + const event = await this.getOpenEvent(guild, guild.id, requestId) + if (event && votes?.length > 0) { + this.updateOpenPollEventWithVoteResults(event, votes, guild, guildId, requestId) + this.sendVoteClosedMessage(event, votes[0].movie, guildId, requestId) + } } - lastMessage.unpin() } /** * gets votes for the movies without the NONE_OF_THAT votes */ - public async getVotesByEmote(message: Message, guildId: string, requestId: string): Promise { + public async getVotesByEmote(message: VoteMessage, guildId: string, requestId: string): Promise { const votes: Vote[] = [] logger.debug(`Number of items in emotes: ${Object.values(Emotes).length}`, { guildId, requestId }) for (let i = 0; i < Object.keys(Emotes).length / 2; i++) { -- 2.40.1 From 8df180898e23a0776a05c8bab50bd18576a0a9b6 Mon Sep 17 00:00:00 2001 From: kenobi Date: Sun, 19 Nov 2023 20:04:06 +0100 Subject: [PATCH 67/72] pad logging level to always be 5 characters --- server/logger.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/logger.ts b/server/logger.ts index b81d8ee..f25cc88 100644 --- a/server/logger.ts +++ b/server/logger.ts @@ -6,7 +6,7 @@ export const noGuildId = 'NoGuildId' const printFn = format.printf(({ guildId, level, message, errorCode, requestId, timestamp: logTimestamp }: { [k: string]: string }) => { - return `[${guildId ?? ''}][${level}][${logTimestamp}][${errorCode ?? ''}][${requestId ?? ''}]:${message}` + return `[${guildId ?? ''}][${level.padStart(5, " ")}][${logTimestamp}][${errorCode ?? ''}][${requestId ?? ''}]:${message}` }) const logFormat = format.combine( -- 2.40.1 From 7d794a8001a66d068f949c893d689a068c3caeed Mon Sep 17 00:00:00 2001 From: kenobi Date: Sun, 19 Nov 2023 20:04:30 +0100 Subject: [PATCH 68/72] refactor voteInfo to include event instead of eventid and startDate --- server/helper/dateHelper.ts | 4 +-- server/helper/vote.controller.ts | 59 ++++++++++++++++++-------------- tests/discord/noneofthat.test.ts | 30 ++++------------ tests/discord/votes.test.ts | 34 +++++++++++++----- 4 files changed, 67 insertions(+), 60 deletions(-) diff --git a/server/helper/dateHelper.ts b/server/helper/dateHelper.ts index 9da4c53..7c75b79 100644 --- a/server/helper/dateHelper.ts +++ b/server/helper/dateHelper.ts @@ -1,10 +1,10 @@ import { format, isToday } from "date-fns"; import { utcToZonedTime } from "date-fns-tz" -import { GuildScheduledEvent } from "discord.js"; import { logger } from "../logger"; import de from "date-fns/locale/de"; +import { Maybe } from "../interfaces"; -export function createDateStringFromEvent(eventStartDate:Date, requestId: string, guildId?: string): string { +export function createDateStringFromEvent(eventStartDate: Maybe, requestId: string, guildId?: string): string { if (!eventStartDate) { logger.error("Event has no start. Cannot create dateString.", { guildId, requestId }) return `"habe keinen Startzeitpunkt ermitteln können"` diff --git a/server/helper/vote.controller.ts b/server/helper/vote.controller.ts index c92e999..f510c51 100644 --- a/server/helper/vote.controller.ts +++ b/server/helper/vote.controller.ts @@ -21,8 +21,7 @@ export type Vote = { } export type VoteMessageInfo = { votes: Vote[], - eventId: string, - eventDate: Date + event: GuildScheduledEvent, } export default class VoteController { private client: ExtendedClient @@ -74,11 +73,8 @@ export default class VoteController { logger.debug(`${vote.movie} : ${vote.count} -> above: ${overOneVote}`) return overOneVote } - public async handleReroll(voteMessage: VoteMessage, guildId: string, requestId: string) { - // get the movies currently being voted on, their votes, the eventId and its date - const voteInfo: VoteMessageInfo = await this.parseVoteInfoFromVoteMessage(voteMessage, requestId) - let movies: string[] = Array() + public async generateRerollMovieList(voteInfo: VoteMessageInfo, guildId: string, requestId: string) { if (config.bot.reroll_retains_top_picks) { const votedOnMovies = voteInfo.votes.filter(this.hasAtLeastOneVote).filter(x => x.emote !== NONE_OF_THAT) logger.info(`Found ${votedOnMovies.length} with votes`, { requestId, guildId }) @@ -86,40 +82,53 @@ export default class VoteController { logger.info(`Fetching ${newMovieCount} from jellyfin`) const newMovies: string[] = await this.yavinJellyfinHandler.getRandomMovieNames(newMovieCount, guildId, requestId) // merge - movies = newMovies.concat(votedOnMovies.map(x => x.movie)) + return newMovies.concat(votedOnMovies.map(x => x.movie)) } else { // get movies from jellyfin to fill the remaining slots const newMovieCount: number = config.bot.random_movie_count logger.info(`Fetching ${newMovieCount} from jellyfin`) - movies = await this.yavinJellyfinHandler.getRandomMovieNames(newMovieCount, guildId, requestId) + return await this.yavinJellyfinHandler.getRandomMovieNames(newMovieCount, guildId, requestId) } - // create new message + } + + public async handleReroll(voteMessage: VoteMessage, guildId: string, requestId: string) { + // get the movies currently being voted on, their votes, the eventId and its date + const voteInfo: VoteMessageInfo = await this.parseVoteInfoFromVoteMessage(voteMessage, requestId) + if (!voteInfo.event.scheduledStartAt) { + logger.info("Event does not have a start date, cancelling", { guildId: voteInfo.event.guildId, requestId }) + return + } + + let movies: string[] = await this.generateRerollMovieList(voteInfo, guildId, requestId) - logger.info(`Creating new poll message with new movies: ${movies}`, { requestId, guildId }) - const messageText = this.createVoteMessageText(voteInfo.eventId, voteInfo.eventDate, movies, guildId, requestId) const announcementChannel = this.client.getAnnouncementChannelForGuild(guildId) if (!announcementChannel) { logger.error(`No announcementChannel found for ${guildId}, can't post poll`) return } - try { logger.info(`Trying to remove old vote Message`, { requestId, guildId }) this.removeMessage(voteMessage) } catch (err) { + // TODO: integrate failure DM to media Admin to inform about inability to delete old message logger.error(`Error during removeMessage: ${err}`) } - - const sentMessage = await this.sendVoteMessage(messageText, movies.length, announcementChannel) - sentMessage.pin() - logger.info(`Sent and pinned new poll message`, { requestId, guildId }) + const sentMessage = this.prepareAndSendVoteMessage({ + event: voteInfo.event, + movies, + announcementChannel, + startDate: voteInfo.event.scheduledStartAt, + pinAfterSending: true + }, + guildId, + requestId) + logger.debug(`Sent reroll message: ${JSON.stringify(sentMessage)}`, { requestId, guildId }) } - private async fetchEventStartDateByEventId(guild: Guild, eventId: string, requestId: string): Promise> { + private async fetchEventByEventId(guild: Guild, eventId: string, requestId: string): Promise> { const guildEvent: GuildScheduledEvent = await guild.scheduledEvents.fetch(eventId) if (!guildEvent) logger.error(`GuildScheduledEvent with id${eventId} could not be found`, { requestId, guildId: guild.id }) - if (guildEvent.scheduledStartAt) - return guildEvent.scheduledStartAt + return guildEvent } public async parseVoteInfoFromVoteMessage(message: VoteMessage, requestId: string): Promise { @@ -129,8 +138,7 @@ export default class VoteController { if (!message.guild) throw new Error(`Message ${message.id} not a guild message`) - let eventStartDate: Maybe = await this.fetchEventStartDateByEventId(message.guild, parsedIds.eventId, requestId) - if (!eventStartDate) eventStartDate = this.parseEventDateFromMessage(message.cleanContent, message.guild.id, requestId) + const event: Maybe = await this.fetchEventByEventId(message.guild, parsedIds.eventId, requestId) let votes: Vote[] = [] for (const line of lines) { @@ -149,7 +157,7 @@ export default class VoteController { } } } - return { eventId: parsedIds.eventId, eventDate: eventStartDate, votes } + return { event, votes } } public parseEventDateFromMessage(message: string, guildId: string, requestId: string): Date { logger.warn(`Falling back to RegEx parsing to get Event Date`, { guildId, requestId }) @@ -168,15 +176,15 @@ export default class VoteController { } public async prepareAndSendVoteMessage(inputInfo: prepareVoteMessageInput, guildId: string, requestId: string) { - const messageText = this.createVoteMessageText(inputInfo.event.id, inputInfo.startDate, inputInfo.movies, guildId, requestId) + const messageText = this.createVoteMessageText(inputInfo.event, inputInfo.movies, guildId, requestId) const sentMessage = await this.sendVoteMessage(messageText, inputInfo.movies.length, inputInfo.announcementChannel) if (inputInfo.pinAfterSending) sentMessage.pin() return sentMessage } - public createVoteMessageText(eventId: string, eventStartDate: Date, movies: string[], guildId: string, requestId: string): string { - let message = `[Abstimmung] für https://discord.com/events/${guildId}/${eventId} \n<@&${config.bot.announcement_role}> Es gibt eine neue Abstimmung für die nächste Watchparty ${createDateStringFromEvent(eventStartDate, guildId, requestId)}! Stimme hierunter für den nächsten Film ab!\n` + public createVoteMessageText(event: GuildScheduledEvent, movies: string[], guildId: string, requestId: string): string { + let message = `[Abstimmung] für https://discord.com/events/${guildId}/${event.id} \n<@&${config.bot.announcement_role}> Es gibt eine neue Abstimmung für die nächste Watchparty ${createDateStringFromEvent(event.scheduledStartAt, guildId, requestId)}! Stimme hierunter für den nächsten Film ab!\n` for (let i = 0; i < movies.length; i++) { message = message.concat(Emotes[i]).concat(": ").concat(movies[i]).concat("\n") @@ -186,6 +194,7 @@ export default class VoteController { return message } + // TODO: Refactor into separate message controller public async sendVoteMessage(messageText: string, movieCount: number, announcementChannel: TextChannel) { const options: MessageCreateOptions = { diff --git a/tests/discord/noneofthat.test.ts b/tests/discord/noneofthat.test.ts index 95dcda4..5469192 100644 --- a/tests/discord/noneofthat.test.ts +++ b/tests/discord/noneofthat.test.ts @@ -29,11 +29,16 @@ describe('vote controller - none_of_that functions', () => { id: 'mockId' } } + const mockEvent: GuildScheduledEvent = { + scheduledStartAt: testEventDate, + id: testEventId, + guild: testGuildId + } const mockJellyfinHandler: JellyfinHandler = { getRandomMovieNames: jest.fn().mockReturnValue(["movie1"]) } const votes = new VoteController(mockClient, mockJellyfinHandler) - const mockMessageContent = votes.createVoteMessageText(testEventId, testEventDate, testMovies, testGuildId, "requestId") + const mockMessageContent = votes.createVoteMessageText(mockEvent, testMovies, testGuildId, "requestId") test('sendVoteClosedMessage', async () => { mockClient.getAnnouncementChannelForGuild = jest.fn().mockReturnValue({ @@ -57,29 +62,6 @@ describe('vote controller - none_of_that functions', () => { content: `[Abstimmung beendet] für https://discord.com/events/${testGuildId}/${testEventId}\n<@&WATCHPARTY_ANNOUNCEMENT_ROLE> Wir gucken MovieNew am 01.01. um 01:00` }) }) - // test('checkForPollsToClose', async () => { - // - // const testGuild: Guild = { - // scheduledEvents: { - // fetch: jest.fn().mockImplementation(() => { - // return new Promise(resolve => { - // resolve([ - // { name: "Event Name" }, - // { name: "Event: VOTING OFFEN", scheduledStartTimestamp: "" }, - // { name: "another voting" }, - // ] - // ) - // }) - // }) - // } - // } - // - // const result = await votes.checkForPollsToClose(testGuild) - // - // - // - // - // }) test('getVotesByEmote', async () => { const mockMessage: Message = { diff --git a/tests/discord/votes.test.ts b/tests/discord/votes.test.ts index 14ae07e..4855428 100644 --- a/tests/discord/votes.test.ts +++ b/tests/discord/votes.test.ts @@ -1,9 +1,9 @@ import { Emoji, NONE_OF_THAT } from "../../server/constants" -import VoteController, { Vote, VoteMessageInfo } from "../../server/helper/vote.controller" +import VoteController, { VoteMessageInfo } from "../../server/helper/vote.controller" import { JellyfinHandler } from "../../server/jellyfin/handler" import { ExtendedClient } from "../../server/structures/client" import { VoteMessage } from "../../server/helper/messageIdentifiers" -import { Message, MessageReaction } from "discord.js" +import { GuildScheduledEvent, MessageReaction } from "discord.js" test('parse votes from vote message', async () => { const testMovies = [ 'Movie1', @@ -16,12 +16,16 @@ test('parse votes from vote message', async () => { const testEventDate = new Date('2023-01-01') const testGuildId = "888999888" const voteController: VoteController = new VoteController({}, {}) - const testMessage = voteController.createVoteMessageText(testEventId, testEventDate, testMovies, testGuildId, "requestId") + const mockEvent: GuildScheduledEvent = { + scheduledStartAt: testEventDate, + id: testEventId, + guild: testGuildId + } + const testMessage = voteController.createVoteMessageText(mockEvent, testMovies, testGuildId, "requestId") const expectedResult: VoteMessageInfo = { - eventId: testEventId, - eventDate: testEventDate, + event: mockEvent, votes: [ { emote: Emoji.one, count: 1, movie: testMovies[0] }, { emote: Emoji.two, count: 2, movie: testMovies[1] }, @@ -40,6 +44,8 @@ test('parse votes from vote message', async () => { fetch: jest.fn().mockImplementation((input: any) => { if (input === testEventId) return { + id: testEventId, + guild: testGuildId, scheduledStartAt: testEventDate } }) @@ -61,8 +67,8 @@ test('parse votes from vote message', async () => { const result = await voteController.parseVoteInfoFromVoteMessage(message, 'requestId') console.log(JSON.stringify(result)) expect(Array.isArray(result)).toBe(false) - expect(result.eventId).toEqual(testEventId) - expect(result.eventDate).toEqual(testEventDate) + expect(result.event.id).toEqual(testEventId) + expect(result.event.scheduledStartAt).toEqual(testEventDate) expect(result.votes.length).toEqual(expectedResult.votes.length) expect(result).toEqual(expectedResult) }) @@ -79,7 +85,12 @@ test('parse votes from vote message', () => { const testEventDate = new Date('2023-01-01') const testGuildId = "888999888" const voteController: VoteController = new VoteController({}, {}) - const testMessage = voteController.createVoteMessageText(testEventId, testEventDate, testMovies, testGuildId, "requestId") + const mockEvent: GuildScheduledEvent = { + scheduledStartAt: testEventDate, + id: testEventId, + guild: testGuildId + } + const testMessage = voteController.createVoteMessageText(mockEvent, testMovies, testGuildId, "requestId") const result = voteController.parseGuildIdAndEventIdFromWholeMessage(testMessage) expect(result).toEqual({ guildId: testGuildId, eventId: testEventId }) @@ -108,7 +119,12 @@ test.skip('handles complete none_of_that vote', () => { } } const voteController = new VoteController(mockClient, mockJellyfinHandler) - const mockMessageContent = voteController.createVoteMessageText(testEventId, testEventDate, testMovies, testGuildId, "requestId") + const mockEvent: GuildScheduledEvent = { + scheduledStartAt: testEventDate, + id: testEventId, + guild: testGuildId + } + const mockMessageContent = voteController.createVoteMessageText(mockEvent, testMovies, testGuildId, "requestId") const reactedUponMessage: VoteMessage = { cleanContent: mockMessageContent, guild: { -- 2.40.1 From 03b6a30ffa67afca9a4bc0563f7bf092bbcdd60b Mon Sep 17 00:00:00 2001 From: kenobi Date: Sun, 19 Nov 2023 20:11:03 +0100 Subject: [PATCH 69/72] remove unnecessary if --- server/events/handleMessageReactionAdd.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/server/events/handleMessageReactionAdd.ts b/server/events/handleMessageReactionAdd.ts index 8772fad..0349c5a 100644 --- a/server/events/handleMessageReactionAdd.ts +++ b/server/events/handleMessageReactionAdd.ts @@ -36,9 +36,6 @@ export async function execute(messageReaction: MessageReaction, user: User) { logger.info(`Reaction is NONE_OF_THAT on a vote message. Handling`, { requestId, guildId }) return client.voteController.handleNoneOfThatVote(messageReaction, reactedUponMessage, requestId, guildId) } - if (messageReaction.emoji.toString() === Emoji.one) { - // do something - } } else if (isInitialAnnouncement(reactedUponMessage)) { if (messageReaction.emoji.toString() === Emoji.ticket) { -- 2.40.1 From 4e9fe587b0af1b91f049b820023bd0f7e6280517 Mon Sep 17 00:00:00 2001 From: kenobi Date: Sun, 19 Nov 2023 20:13:49 +0100 Subject: [PATCH 70/72] rename to getOpenPollEvent --- server/helper/vote.controller.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/helper/vote.controller.ts b/server/helper/vote.controller.ts index f510c51..6938572 100644 --- a/server/helper/vote.controller.ts +++ b/server/helper/vote.controller.ts @@ -248,7 +248,7 @@ export default class VoteController { logger.info("Deleting vote message") lastMessage.unpin() await lastMessage.delete() - const event = await this.getOpenEvent(guild, guild.id, requestId) + const event = await this.getOpenPollEvent(guild, guild.id, requestId) if (event && votes?.length > 0) { this.updateOpenPollEventWithVoteResults(event, votes, guild, guildId, requestId) this.sendVoteClosedMessage(event, votes[0].movie, guildId, requestId) @@ -273,7 +273,7 @@ export default class VoteController { } return votes } - public async getOpenEvent(guild: Guild, guildId: string, requestId: string): Promise { + public async getOpenPollEvent(guild: Guild, guildId: string, requestId: string): Promise { const voteEvents = (await guild.scheduledEvents.fetch()) .map((value) => value) .filter(event => event.name.toLowerCase().includes("voting offen")) -- 2.40.1 From 6d40930dc126ba0581ffc5a0733caef93fd4cc60 Mon Sep 17 00:00:00 2001 From: kenobi Date: Sun, 19 Nov 2023 20:17:51 +0100 Subject: [PATCH 71/72] fix incorrect log regarding update cancellation, fixes return type of function to use Maybe --- server/helper/vote.controller.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/helper/vote.controller.ts b/server/helper/vote.controller.ts index 6938572..9c98ec4 100644 --- a/server/helper/vote.controller.ts +++ b/server/helper/vote.controller.ts @@ -273,15 +273,15 @@ export default class VoteController { } return votes } - public async getOpenPollEvent(guild: Guild, guildId: string, requestId: string): Promise { + public async getOpenPollEvent(guild: Guild, guildId: string, requestId: string): Promise> { const voteEvents = (await guild.scheduledEvents.fetch()) .map((value) => value) .filter(event => event.name.toLowerCase().includes("voting offen")) logger.debug(`Found events: ${JSON.stringify(voteEvents, null, 2)}`, { guildId, requestId }) if (!voteEvents || voteEvents.length <= 0) { - logger.error("Could not find vote event. Cancelling update!", { guildId, requestId }) - return null + logger.error("Could not find an open vote event.", { guildId, requestId }) + return } return voteEvents[0] } -- 2.40.1 From fb4ab59dc6de62d8f0fa28fb86e3d18078bf919f Mon Sep 17 00:00:00 2001 From: kenobi Date: Sun, 19 Nov 2023 20:22:14 +0100 Subject: [PATCH 72/72] rename emotes to validvoteemotes --- server/constants.ts | 3 ++- server/events/handleMessageReactionAdd.ts | 4 ++-- server/helper/vote.controller.ts | 12 ++++++------ 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/server/constants.ts b/server/constants.ts index d11ca88..ace6c75 100644 --- a/server/constants.ts +++ b/server/constants.ts @@ -1,6 +1,7 @@ -export enum Emotes { "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟" } +export enum ValidVoteEmotes { "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟" } export const NONE_OF_THAT = "❌" +// WIP export const Emoji = { "one": "\u0031\uFE0F\u20E3", "two": "\u0032\uFE0F\u20E3", diff --git a/server/events/handleMessageReactionAdd.ts b/server/events/handleMessageReactionAdd.ts index 0349c5a..38cb212 100644 --- a/server/events/handleMessageReactionAdd.ts +++ b/server/events/handleMessageReactionAdd.ts @@ -1,7 +1,7 @@ import { Message, MessageReaction, User } from "discord.js"; import { logger, newRequestId, noGuildId } from "../logger"; -import { Emoji, Emotes, NONE_OF_THAT } from "../constants"; +import { Emoji, ValidVoteEmotes, NONE_OF_THAT } from "../constants"; import { client } from "../.."; import { isInitialAnnouncement, isVoteMessage } from "../helper/messageIdentifiers"; @@ -26,7 +26,7 @@ export async function execute(messageReaction: MessageReaction, user: User) { logger.info(`emoji: ${messageReaction.emoji.toString()}`) - if (!Object.values(Emotes).includes(messageReaction.emoji.toString()) && messageReaction.emoji.toString() !== NONE_OF_THAT) { + if (!Object.values(ValidVoteEmotes).includes(messageReaction.emoji.toString()) && messageReaction.emoji.toString() !== NONE_OF_THAT) { logger.info(`${messageReaction.emoji.toString()} currently not handled`) return } diff --git a/server/helper/vote.controller.ts b/server/helper/vote.controller.ts index 9c98ec4..064aff1 100644 --- a/server/helper/vote.controller.ts +++ b/server/helper/vote.controller.ts @@ -1,5 +1,5 @@ import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildScheduledEventSetStatusArg, GuildScheduledEventStatus, Message, MessageCreateOptions, MessageReaction, TextChannel } from "discord.js" -import { Emotes, NONE_OF_THAT } from "../constants" +import { ValidVoteEmotes, NONE_OF_THAT } from "../constants" import { logger, newRequestId } from "../logger" import { getMembersWithRoleFromGuild } from "./roleFilter" import { config } from "../configuration" @@ -187,7 +187,7 @@ export default class VoteController { let message = `[Abstimmung] für https://discord.com/events/${guildId}/${event.id} \n<@&${config.bot.announcement_role}> Es gibt eine neue Abstimmung für die nächste Watchparty ${createDateStringFromEvent(event.scheduledStartAt, guildId, requestId)}! Stimme hierunter für den nächsten Film ab!\n` for (let i = 0; i < movies.length; i++) { - message = message.concat(Emotes[i]).concat(": ").concat(movies[i]).concat("\n") + message = message.concat(ValidVoteEmotes[i]).concat(": ").concat(movies[i]).concat("\n") } message = message.concat(NONE_OF_THAT).concat(": Wenn dir nichts davon gefällt.") @@ -205,7 +205,7 @@ export default class VoteController { const sentMessage: Message = await (await announcementChannel.fetch()).send(options) for (let i = 0; i < movieCount; i++) { - sentMessage.react(Emotes[i]) + sentMessage.react(ValidVoteEmotes[i]) } sentMessage.react(NONE_OF_THAT) @@ -260,9 +260,9 @@ export default class VoteController { */ public async getVotesByEmote(message: VoteMessage, guildId: string, requestId: string): Promise { const votes: Vote[] = [] - logger.debug(`Number of items in emotes: ${Object.values(Emotes).length}`, { guildId, requestId }) - for (let i = 0; i < Object.keys(Emotes).length / 2; i++) { - const emote = Emotes[i] + logger.debug(`Number of items in emotes: ${Object.values(ValidVoteEmotes).length}`, { guildId, requestId }) + for (let i = 0; i < Object.keys(ValidVoteEmotes).length / 2; i++) { + const emote = ValidVoteEmotes[i] logger.debug(`Getting reaction for emote ${emote}`, { guildId, requestId }) const reaction = message.reactions.resolve(emote) logger.debug(`Reaction for emote ${emote}: ${JSON.stringify(reaction, null, 2)}`, { guildId, requestId }) -- 2.40.1