diff --git a/server/commands/closepoll.ts b/server/commands/closepoll.ts index b254f38..78a2a0b 100644 --- a/server/commands/closepoll.ts +++ b/server/commands/closepoll.ts @@ -1,13 +1,13 @@ +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 { Emotes } from '../events/guildScheduledEventCreate' +import { Maybe } from '../interfaces' import { logger } from '../logger' import { Command } from '../structures/command' import { RunOptions } from '../types/commandTypes' -import { format } from 'date-fns' -import { Maybe } from '../interfaces' export default new Command({ name: 'closepoll', @@ -23,7 +23,7 @@ export default new Command({ } const guildId = command.guildId logger.info("Got command for closing poll!", { guildId, requestId }) - + command.followUp("Alles klar, beende die Umfrage :)") closePoll(command.guild, requestId) } @@ -34,7 +34,7 @@ export async function closePoll(guild: Guild, requestId: string) { logger.info("stopping poll", { guildId, requestId }) const announcementChannel: Maybe = client.getAnnouncementChannelForGuild(guildId) - if(!announcementChannel) { + if (!announcementChannel) { logger.error("Could not find the textchannel. Unable to close poll.", { guildId, requestId }) return } @@ -55,16 +55,16 @@ export async function closePoll(guild: Guild, requestId: string) { 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) - + .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) { + if (event) { updateEvent(event, votes, guild, guildId, requestId) sendVoteClosedMessage(event, votes[0].movie, guildId, requestId) } @@ -82,7 +82,7 @@ async function sendVoteClosedMessage(event: GuildScheduledEvent, movie: string, } const announcementChannel = client.getAnnouncementChannelForGuild(guildId) logger.info("Sending vote closed message.", { guildId, requestId }) - if(!announcementChannel) { + if (!announcementChannel) { logger.error("Could not find announcement channel. Please fix!", { guildId, requestId }) return } @@ -145,4 +145,40 @@ function extractMovieFromMessageByEmote(message: Message, emote: string): string 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 }) + } } \ No newline at end of file diff --git a/server/events/guildScheduledEventCreate.ts b/server/events/guildScheduledEventCreate.ts index d14721e..985bb5c 100644 --- a/server/events/guildScheduledEventCreate.ts +++ b/server/events/guildScheduledEventCreate.ts @@ -1,10 +1,8 @@ -import { addDays, format, isAfter } from "date-fns"; -import toDate from "date-fns/fp/toDate"; +import { format } from "date-fns"; import { GuildScheduledEvent, Message, MessageCreateOptions, TextChannel } from "discord.js"; -import { ScheduledTask, schedule } from "node-cron"; +import { ScheduledTask } from "node-cron"; import { v4 as uuid } from "uuid"; import { client, yavinJellyfinHandler } from "../.."; -import { closePoll } from "../commands/closepoll"; import { config } from "../configuration"; import { Maybe } from "../interfaces"; import { logger } from "../logger"; @@ -24,10 +22,10 @@ export async function execute(event: GuildScheduledEvent) { logger.info("Event was a placeholder event to start a new watchparty and voting. Creating vote!", { guildId: event.guildId, requestId }) logger.debug("Renaming event", { guildId: event.guildId, requestId }) event.edit({ name: "Watchparty - Voting offen" }) - const movies = await yavinJellyfinHandler.getRandomMovies(5, event.guildId, requestId) + const movies = await yavinJellyfinHandler.getRandomMovieNames(5, event.guildId, requestId) logger.info(`Got ${movies.length} random movies. Creating voting`, { guildId: event.guildId, requestId }) - logger.debug(`Movies: ${JSON.stringify(movies.map(movie => movie.name))}`, { guildId: event.guildId, requestId }) + logger.debug(`Movies: ${JSON.stringify(movies)}`, { guildId: event.guildId, requestId }) const announcementChannel: Maybe = client.getAnnouncementChannelForGuild(event.guildId) if(!announcementChannel) { @@ -45,7 +43,7 @@ export async function execute(event: GuildScheduledEvent) { let message = `[Abstimmung]\n<@&${config.bot.announcement_role}> Es gibt eine neue Abstimmung für die nächste Watchparty am ${date} um ${time}}! 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].name ?? "Film hatte keinen Namen :(").concat("\n") + message = message.concat(Emotes[i]).concat(": ").concat(movies[i]).concat("\n") } const options: MessageCreateOptions = { @@ -59,46 +57,7 @@ export async function execute(event: GuildScheduledEvent) { sentMessage.react(Emotes[i]) } - if (!task) { - task = schedule("0 * * * * *", () => checkForPollsToClose(event)) - } - // sentMessage.pin() //todo: uncomment when bot has permission to pin messages. Also update closepoll.ts to only fetch pinned messages } } -async function checkForPollsToClose(event: GuildScheduledEvent): Promise { - const requestId = uuid() - logger.info(`Automatic check for poll closing.`, { guildId: event.guildId, requestId }) - if (!event.guild) { - logger.error("No guild in event. Cancelling.", { guildId: event.guildId, requestId }) - return - } - //refetch event in case the time changed or the poll is already closed - const events = (await event.guild.scheduledEvents.fetch()) - .filter(event => event.name.toLowerCase().includes("voting offen")) - .map((value) => value) - - if (!events || events.length <= 0) { - logger.info("Did not find any events. Cancelling", { guildId: event.guildId, requestId }) - return - } else if (events.length > 1) { - logger.error(`More than one event found. Don't know which one is the right one :( Events: ${JSON.stringify(events, null, 2)}`, { guildId: event.guildId, requestId }) - return - } - 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: event.guildId, requestId }) - return - } - - const eventDate: Date = toDate(updatedEvent.scheduledStartTimestamp) - const closePollDate: Date = addDays(eventDate, -2) - - if (isAfter(Date.now(), closePollDate)) { - logger.info("Less than two days until event. Closing poll", { guildId: event.guildId, requestId }) - closePoll(event.guild, requestId) - } else { - logger.info(`ScheduledStart: ${closePollDate}. Now: ${toDate(Date.now())}`, { guildId: event.guildId, requestId }) - } -} \ No newline at end of file diff --git a/server/events/guildScheduledEventUpdate.ts b/server/events/guildScheduledEventUpdate.ts index b3cd8b1..eed87e7 100644 --- a/server/events/guildScheduledEventUpdate.ts +++ b/server/events/guildScheduledEventUpdate.ts @@ -27,6 +27,7 @@ export async function execute(oldEvent: GuildScheduledEvent, newEvent: GuildSche if (!members.find(x => x.id == member.id)) members.push(member) } + if (newEvent.status === GuildScheduledEventStatus.Active) createJFUsers(members, newEvent.name, requestId) diff --git a/server/jellyfin/handler.ts b/server/jellyfin/handler.ts index a1af178..e47ac6c 100644 --- a/server/jellyfin/handler.ts +++ b/server/jellyfin/handler.ts @@ -242,10 +242,21 @@ export class JellyfinHandler { const index = Math.floor(Math.random() * allMovies.length) movies.push(...allMovies.splice(index, 1)) // maybe out of bounds? ? } - return movies } + public async getRandomMovieNames(count: number, guildId: string, requestId: string): Promise { + logger.info(`${count} random movie names requested`, { guildId, requestId }) + + let movieCount = 0 + let movieNames: string[] + do { + movieNames = (await this.getRandomMovies(count, guildId, requestId)).filter(movie => movie.name && movie.name.length > 0).map(movie => movie.name) + movieCount = movieNames.length + } while (movieCount < count) + return movieNames + } + } export enum UserUpsertResult { enabled, created } diff --git a/server/structures/client.ts b/server/structures/client.ts index 6ecaf6b..fae18b3 100644 --- a/server/structures/client.ts +++ b/server/structures/client.ts @@ -8,6 +8,7 @@ import { Maybe } from "../interfaces"; import { JellyfinHandler } from "../jellyfin/handler"; import { logger } from "../logger"; import { CommandType } from "../types/commandTypes"; +import { checkForPollsToClose } from "../commands/closepoll"; @@ -16,8 +17,9 @@ export class ExtendedClient extends Client { private commandFilePath = `${__dirname}/../commands` private jellyfin: JellyfinHandler public commands: Collection = new Collection() - private announcementChannels: Collection = new Collection //guildId to TextChannel - private announcementRoleHandlerTask: Collection = new Collection //one task per guild + private announcementChannels: Collection = new Collection() //guildId to TextChannel + private announcementRoleHandlerTask: Collection = new Collection() //one task per guild + 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) @@ -73,6 +75,7 @@ export class ExtendedClient extends Client { this.cacheUsers(guilds) await this.cacheAnnouncementServer(guilds) this.startAnnouncementRoleBackgroundTask(guilds) + this.startPollCloseBackgroundTasks() }) } catch (error) { logger.info(`Error refreshing slash commands: ${error}`) @@ -169,4 +172,10 @@ export class ExtendedClient extends Client { } task.stop() } + + private async startPollCloseBackgroundTasks() { + for(const guild of this.guilds.cache) { + this.pollCloseBackgroundTasks.set(guild[1].id, schedule("0 * * * * *", () => checkForPollsToClose(guild[1]))) + } + } }