From e50cb10c5b7d0621410d22ecc923eb6397e5c9b6 Mon Sep 17 00:00:00 2001 From: Sammy Date: Mon, 12 Jun 2023 22:21:10 +0200 Subject: [PATCH 01/50] implement slash command for announcement --- server/commands/announce.ts | 38 +++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 server/commands/announce.ts diff --git a/server/commands/announce.ts b/server/commands/announce.ts new file mode 100644 index 0000000..d825164 --- /dev/null +++ b/server/commands/announce.ts @@ -0,0 +1,38 @@ +import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildScheduledEventSetStatusArg, GuildScheduledEventStatus, Message, MessageCreateOptions, MessageEditOptions, TextChannel, messageLink } from 'discord.js' +import { v4 as uuid } from 'uuid' +import { config } from '../configuration' +import { Emotes } from '../events/guildScheduledEventCreate' +import { logger } from '../logger' +import { Command } from '../structures/command' +import { RunOptions } from '../types/commandTypes' +import { client } from '../..' + +export default new Command({ + name: 'announce', + description: 'Neues announcement im announcement Channel an alle senden.', + options: [], + run: async (interaction: RunOptions) => { + const command = interaction.interaction + const requestId = uuid() + const guildId = command.guildId! + logger.info("Got command for announcing!", { guildId, requestId }) + const announcementChannel: TextChannel = client.getAnnouncementChannelForGuild(guildId) + + const body = `Hey! @everyone! Hier ist der Watchparty Bot vom Hartzarett. + +Wir machen in Zukunft regelmäßig Watchparties! Falls du mitmachen möchtest, reagiere einfach auf diesen Post mit 🎫, dann bekommst du automatisch eine Rolle zugewiesen und wirst benachrichtigt sobald es in der Zukunft weitere Watchparties und Filme zum abstimmen gibt. + +Für eine Erklärung wie das alles funktioniert mach einfach /mitgucken für eine lange Erklärung am Stück oder /guides wenn du auswählen möchtest wozu du Infos bekommst.` + + const options: MessageCreateOptions = { + allowedMentions: { parse: ['everyone'] }, + content: body + } + + const message: Message = await announcementChannel.send(options) + + message.react("🎫") + + command.followUp("Ist rausgeschickt!") + } +}) \ No newline at end of file From a2c55ad676ffb1cad9c1dd641ec0927dd4938cd1 Mon Sep 17 00:00:00 2001 From: Sammy Date: Mon, 12 Jun 2023 22:34:39 +0200 Subject: [PATCH 02/50] restrict announcements to admins --- server/commands/announce.ts | 43 ++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/server/commands/announce.ts b/server/commands/announce.ts index d825164..67897f4 100644 --- a/server/commands/announce.ts +++ b/server/commands/announce.ts @@ -1,4 +1,4 @@ -import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildScheduledEventSetStatusArg, GuildScheduledEventStatus, Message, MessageCreateOptions, MessageEditOptions, TextChannel, messageLink } from 'discord.js' +import { Guild, GuildMember, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildScheduledEventSetStatusArg, GuildScheduledEventStatus, Message, MessageCreateOptions, MessageEditOptions, TextChannel, messageLink } from 'discord.js' import { v4 as uuid } from 'uuid' import { config } from '../configuration' import { Emotes } from '../events/guildScheduledEventCreate' @@ -16,23 +16,36 @@ export default new Command({ const requestId = uuid() const guildId = command.guildId! logger.info("Got command for announcing!", { guildId, requestId }) - const announcementChannel: TextChannel = client.getAnnouncementChannelForGuild(guildId) - const body = `Hey! @everyone! Hier ist der Watchparty Bot vom Hartzarett. - + if (!isAdmin(command.member)) { + logger.info(`Announcement was requested by ${command.member.displayName} but they are not an admin! Not sending announcement.`, { guildId, requestId }) + return + } else { + logger.info(`User ${command.member.displayName} seems to be admin`) + } + sendAnnouncement(guildId, requestId) + command.followUp("Ist rausgeschickt!") + } +}) + +function isAdmin(member: GuildMember): boolean { + return member.roles.cache.find((role, _) => role.id === config.bot.jf_admin_role) != undefined +} + +async function sendAnnouncement(guildId: string, requestId: string): Promise { + logger.info("Sending announcement") + const announcementChannel: TextChannel = client.getAnnouncementChannelForGuild(guildId) + + const body = `Hey! @everyone! Hier ist der Watchparty Bot vom Hartzarett. + Wir machen in Zukunft regelmäßig Watchparties! Falls du mitmachen möchtest, reagiere einfach auf diesen Post mit 🎫, dann bekommst du automatisch eine Rolle zugewiesen und wirst benachrichtigt sobald es in der Zukunft weitere Watchparties und Filme zum abstimmen gibt. Für eine Erklärung wie das alles funktioniert mach einfach /mitgucken für eine lange Erklärung am Stück oder /guides wenn du auswählen möchtest wozu du Infos bekommst.` - const options: MessageCreateOptions = { - allowedMentions: { parse: ['everyone'] }, - content: body - } - - const message: Message = await announcementChannel.send(options) - - message.react("🎫") - - command.followUp("Ist rausgeschickt!") + const options: MessageCreateOptions = { + allowedMentions: { parse: ['everyone'] }, + content: body } -}) \ No newline at end of file + const message: Message = await announcementChannel.send(options) + message.react("🎫") +} \ No newline at end of file From 24754decf47dd4f4b5da1930698465cd46b437df Mon Sep 17 00:00:00 2001 From: Sammy Date: Tue, 13 Jun 2023 18:18:26 +0200 Subject: [PATCH 03/50] implement announcement role management by reaction --- server/commands/announce.ts | 65 +++++++++++++++++++++++++++++++++---- server/configuration.ts | 2 ++ 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/server/commands/announce.ts b/server/commands/announce.ts index 67897f4..328855e 100644 --- a/server/commands/announce.ts +++ b/server/commands/announce.ts @@ -1,11 +1,14 @@ -import { Guild, GuildMember, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildScheduledEventSetStatusArg, GuildScheduledEventStatus, Message, MessageCreateOptions, MessageEditOptions, TextChannel, messageLink } from 'discord.js' +import { Guild, GuildMember, Message, MessageCreateOptions, MessageReaction, Role, TextChannel, User } from 'discord.js' import { v4 as uuid } from 'uuid' +import { client } from '../..' import { config } from '../configuration' -import { Emotes } from '../events/guildScheduledEventCreate' import { logger } from '../logger' import { Command } from '../structures/command' import { RunOptions } from '../types/commandTypes' -import { client } from '../..' +import { off } from 'process' +import { ScheduledTask, schedule } from 'node-cron' + +let task: ScheduledTask export default new Command({ name: 'announce', @@ -29,7 +32,7 @@ export default new Command({ }) function isAdmin(member: GuildMember): boolean { - return member.roles.cache.find((role, _) => role.id === config.bot.jf_admin_role) != undefined + return member.roles.cache.find((role, _) => role.id === config.bot.jf_admin_role) !== undefined } async function sendAnnouncement(guildId: string, requestId: string): Promise { @@ -47,5 +50,55 @@ Für eine Erklärung wie das alles funktioniert mach einfach /mitgucken für ein content: body } const message: Message = await announcementChannel.send(options) - message.react("🎫") -} \ No newline at end of file + await message.react("🎫") + // await message.pin() + + task = schedule("* * * * * *", async () => { + const reactions = await message.reactions.resolve("🎫") + if (reactions) { + manageAnnouncementRoles(message.guild, reactions) + } else { + logger.error("Did not get reactions! Aborting!", { guildId, requestId }) + } + }) +} + +async function manageAnnouncementRoles(guild: Guild, reaction: MessageReaction) { + const requestId = uuid() + const guildId = guild.id + logger.info("Managing roles", { guildId, requestId }) + + const announcementRole: Role | undefined = (await guild.roles.fetch()).find(role => role.id === config.bot.announcement_role) + if (!announcementRole) { + logger.error(`Could not find announcement role! Aborting! Was looking for role with id: ${config.bot.announcement_role}`, { guildId, requestId }) + return + } + + const usersWhoWantRole: User[] = (await reaction.users.fetch()).filter(user => !user.bot).map(user => user) + + const allUsers = (await guild.members.fetch()) + + const usersWhoHaveRole: GuildMember[] = allUsers + .filter(member=> member.roles.cache + .find(role => role.id === config.bot.announcement_role) !== undefined) + .map(member => member) + + const usersWhoNeedRoleRevoked: GuildMember[] = usersWhoHaveRole + .filter(userWhoHas => !usersWhoWantRole.map(wanter => wanter.id).includes(userWhoHas.id)) + + const usersWhoDontHaveRole: GuildMember[] = allUsers + .filter(member => member.roles.cache + .find(role=> role.id === config.bot.announcement_role) === undefined) + .map(member => member) + + const usersWhoNeedRole: GuildMember[] = usersWhoDontHaveRole + .filter(userWhoNeeds => usersWhoWantRole.map(wanter => wanter.id).includes(userWhoNeeds.id)) + + + logger.debug(`Theses users will get the role removed: ${JSON.stringify(usersWhoNeedRoleRevoked)}`, {guildId, requestId}) + logger.debug(`Theses users will get the role added: ${JSON.stringify(usersWhoNeedRole)}`, {guildId, requestId}) + + usersWhoNeedRoleRevoked.forEach(user => user.roles.remove(announcementRole)) + usersWhoNeedRole.forEach(user => user.roles.add(announcementRole)) +} + diff --git a/server/configuration.ts b/server/configuration.ts index 5807c47..144901b 100644 --- a/server/configuration.ts +++ b/server/configuration.ts @@ -23,6 +23,7 @@ export interface Config { workaround_token: string watcher_role: string jf_admin_role: string + announcement_role: string announcement_channel_id: string jf_collection_id: string } @@ -57,6 +58,7 @@ export const config: Config = { 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 ?? "", jf_collection_id: process.env.JELLYFIN_COLLECTION_ID ?? "" } From e774474a554cd574285947dffa2f442fe87144a1 Mon Sep 17 00:00:00 2001 From: Sammy Date: Tue, 13 Jun 2023 18:58:41 +0200 Subject: [PATCH 04/50] Put role handling in background task scheduled at startup --- server/commands/announce.ts | 19 ++++++---------- server/structures/client.ts | 45 +++++++++++++++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/server/commands/announce.ts b/server/commands/announce.ts index 328855e..631351b 100644 --- a/server/commands/announce.ts +++ b/server/commands/announce.ts @@ -39,7 +39,11 @@ async function sendAnnouncement(guildId: string, requestId: string): Promise message.cleanContent.includes("[announcement]")) + currentPinnedAnnouncementMessages.forEach(async (message) => await message.unpin()) + currentPinnedAnnouncementMessages.forEach(message => message.delete()) + + const body = `[announcement] Hey! @everyone! Hier ist der Watchparty Bot vom Hartzarett. Wir machen in Zukunft regelmäßig Watchparties! Falls du mitmachen möchtest, reagiere einfach auf diesen Post mit 🎫, dann bekommst du automatisch eine Rolle zugewiesen und wirst benachrichtigt sobald es in der Zukunft weitere Watchparties und Filme zum abstimmen gibt. @@ -51,20 +55,11 @@ Für eine Erklärung wie das alles funktioniert mach einfach /mitgucken für ein } const message: Message = await announcementChannel.send(options) await message.react("🎫") - // await message.pin() + await message.pin() - task = schedule("* * * * * *", async () => { - const reactions = await message.reactions.resolve("🎫") - if (reactions) { - manageAnnouncementRoles(message.guild, reactions) - } else { - logger.error("Did not get reactions! Aborting!", { guildId, requestId }) - } - }) } -async function manageAnnouncementRoles(guild: Guild, reaction: MessageReaction) { - const requestId = uuid() +export async function manageAnnouncementRoles(guild: Guild, reaction: MessageReaction, requestId: string) { const guildId = guild.id logger.info("Managing roles", { guildId, requestId }) diff --git a/server/structures/client.ts b/server/structures/client.ts index d0b58cb..3ab1e63 100644 --- a/server/structures/client.ts +++ b/server/structures/client.ts @@ -4,6 +4,10 @@ import fs from 'fs' import { config } from "../configuration"; import { logger } from "../logger"; import { JellyfinHandler } from "../jellyfin/handler"; +import { ScheduledTask, schedule } from "node-cron"; +import { manageAnnouncementRoles } from "../commands/announce"; +import { v4 as uuid } from 'uuid' +import { task } from "../events/guildScheduledEventCreate"; @@ -13,6 +17,7 @@ export class ExtendedClient extends Client { 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 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) @@ -60,14 +65,15 @@ export class ExtendedClient extends Client { this.commands.set(command.name, command) slashCommands.push(command) } - this.on("ready", (client: Client) => { + this.on("ready", async (client: Client) => { //logger.info(`Ready processing ${JSON.stringify(client)}`) logger.info(`SlashCommands: ${JSON.stringify(slashCommands)}`) const guilds = client.guilds.cache this.registerCommands(slashCommands, guilds) this.cacheUsers(guilds) - this.cacheAnnouncementServer(guilds) + await this.cacheAnnouncementServer(guilds) + this.startAnnouncementRoleBackgroundTask(guilds) }) } catch (error) { logger.info(`Error refreshing slash commands: ${error}`) @@ -117,4 +123,39 @@ export class ExtendedClient extends Client { logger.error(error) } } + + public async startAnnouncementRoleBackgroundTask(guilds: Collection) { + for (const guild of guilds.values()) { + logger.info("Starting background task for announcement role", { guildId: guild.id }) + const textChannel: TextChannel = this.getAnnouncementChannelForGuild(guild.id) + this.announcementRoleHandlerTask.set(guild.id, schedule("*/10 * * * * *", async () => { + const requestId = uuid() + const messages = (await textChannel.messages.fetchPinned()).filter(message => message.cleanContent.includes("[announcement]")) + + 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 }) + } else if (messages.size == 0) { + logger.error("Could not find any pinned announcement messages. Unable to manage roles!", { guildId: guild.id, requestId }) + } + + const message = messages.at(0)! + + const reactions = await message.reactions.resolve("🎫") + if (reactions) { + manageAnnouncementRoles(message.guild, reactions, requestId) + } else { + logger.error("Did not get reactions! Aborting!", { guildId: guild.id, requestId }) + } + })) + } + } + public stopAnnouncementRoleBackgroundTask(guild: string | Guild, requestId: string) { + const guildId: string = guild instanceof Guild ? guild.id : guild + const task: ScheduledTask | undefined = guild instanceof Guild ? this.announcementRoleHandlerTask.get(guild.id) : this.announcementRoleHandlerTask.get(guild) + if (!task) { + logger.error(`No task found for guildID ${guildId}.`, { guildId, requestId }) + return + } + task.stop() + } } From a5eab2f7be5c6d3ac0e4d481b301f89ed72f6cd9 Mon Sep 17 00:00:00 2001 From: Sammy Date: Tue, 13 Jun 2023 20:13:13 +0200 Subject: [PATCH 05/50] fix bug that reactions are not loaded after restart the message needed to be fetched again. Probably something with caches.. --- server/structures/client.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/server/structures/client.ts b/server/structures/client.ts index 3ab1e63..ce37605 100644 --- a/server/structures/client.ts +++ b/server/structures/client.ts @@ -130,7 +130,7 @@ export class ExtendedClient extends Client { const textChannel: TextChannel = this.getAnnouncementChannelForGuild(guild.id) this.announcementRoleHandlerTask.set(guild.id, schedule("*/10 * * * * *", async () => { const requestId = uuid() - const messages = (await textChannel.messages.fetchPinned()).filter(message => message.cleanContent.includes("[announcement]")) + const messages = (await textChannel.messages.fetchPinned()).filter(message => message.cleanContent.includes("[initial]")) 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 }) @@ -138,9 +138,11 @@ export class ExtendedClient extends Client { logger.error("Could not find any pinned announcement messages. Unable to manage roles!", { guildId: guild.id, requestId }) } - const message = messages.at(0)! + const message = await messages.at(0)!.fetch() + //logger.debug(`Message: ${JSON.stringify(message, null, 2)}`, { guildId: guild.id, requestId }) - const reactions = await message.reactions.resolve("🎫") + const reactions = message.reactions.resolve("🎫") + //logger.debug(`reactions: ${JSON.stringify(reactions, null, 2)}`, { guildId: guild.id, requestId }) if (reactions) { manageAnnouncementRoles(message.guild, reactions, requestId) } else { @@ -149,6 +151,7 @@ export class ExtendedClient extends Client { })) } } + public stopAnnouncementRoleBackgroundTask(guild: string | Guild, requestId: string) { const guildId: string = guild instanceof Guild ? guild.id : guild const task: ScheduledTask | undefined = guild instanceof Guild ? this.announcementRoleHandlerTask.get(guild.id) : this.announcementRoleHandlerTask.get(guild) From baefcf9bb9976080ddab893f170965004b9d1386 Mon Sep 17 00:00:00 2001 From: Sammy Date: Tue, 13 Jun 2023 21:12:32 +0200 Subject: [PATCH 06/50] add options for announcements --- server/commands/announce.ts | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/server/commands/announce.ts b/server/commands/announce.ts index 631351b..4fac196 100644 --- a/server/commands/announce.ts +++ b/server/commands/announce.ts @@ -1,4 +1,4 @@ -import { Guild, GuildMember, Message, MessageCreateOptions, MessageReaction, Role, TextChannel, User } from 'discord.js' +import { ApplicationCommandOptionType, Guild, GuildMember, Message, MessageCreateOptions, MessageReaction, Role, TextChannel, User } from 'discord.js' import { v4 as uuid } from 'uuid' import { client } from '../..' import { config } from '../configuration' @@ -13,12 +13,24 @@ let task: ScheduledTask export default new Command({ name: 'announce', description: 'Neues announcement im announcement Channel an alle senden.', - options: [], + options: [{ + name: "typ", + type: ApplicationCommandOptionType.String, + description:"Was für ein announcement?", + choices: [{name: "initial", value:"initial"},{name: "votepls", value:"votepls"},{name: "cancel", value:"cancel"}], + required: true + }], run: async (interaction: RunOptions) => { const command = interaction.interaction const requestId = uuid() const guildId = command.guildId! - logger.info("Got command for announcing!", { guildId, requestId }) + const announcementType = command.options.data.find(option => option.name.includes("typ")) + logger.info(`Got command for announcing ${announcementType?.value}!`, { guildId, requestId }) + + if(!announcementType) { + logger.error("Did not get an announcement type!", { guildId, requestId }) + return + } if (!isAdmin(command.member)) { logger.info(`Announcement was requested by ${command.member.displayName} but they are not an admin! Not sending announcement.`, { guildId, requestId }) @@ -26,8 +38,13 @@ export default new Command({ } else { logger.info(`User ${command.member.displayName} seems to be admin`) } - sendAnnouncement(guildId, requestId) - command.followUp("Ist rausgeschickt!") + + if((announcementType.value).includes("initial")) { + sendInitialAnnouncement(guildId, requestId) + command.followUp("Ist rausgeschickt!") + } else { + command.followUp(`${announcementType.value} ist aktuell noch nicht implementiert`) + } } }) @@ -35,15 +52,15 @@ function isAdmin(member: GuildMember): boolean { return member.roles.cache.find((role, _) => role.id === config.bot.jf_admin_role) !== undefined } -async function sendAnnouncement(guildId: string, requestId: string): Promise { - logger.info("Sending announcement") +async function sendInitialAnnouncement(guildId: string, requestId: string): Promise { + logger.info("Sending initial announcement") const announcementChannel: TextChannel = client.getAnnouncementChannelForGuild(guildId) - const currentPinnedAnnouncementMessages = (await announcementChannel.messages.fetchPinned()).filter(message => message.cleanContent.includes("[announcement]")) + const currentPinnedAnnouncementMessages = (await announcementChannel.messages.fetchPinned()).filter(message => message.cleanContent.includes("[initial]")) currentPinnedAnnouncementMessages.forEach(async (message) => await message.unpin()) currentPinnedAnnouncementMessages.forEach(message => message.delete()) - const body = `[announcement] Hey! @everyone! Hier ist der Watchparty Bot vom Hartzarett. + const body = `[initial] Hey! @everyone! Hier ist der Watchparty Bot vom Hartzarett. Wir machen in Zukunft regelmäßig Watchparties! Falls du mitmachen möchtest, reagiere einfach auf diesen Post mit 🎫, dann bekommst du automatisch eine Rolle zugewiesen und wirst benachrichtigt sobald es in der Zukunft weitere Watchparties und Filme zum abstimmen gibt. From 198a25d145eab98f5d9af679c3148310ed200609 Mon Sep 17 00:00:00 2001 From: Sammy Date: Tue, 13 Jun 2023 23:15:03 +0200 Subject: [PATCH 07/50] ping watch role when voting starts and closes --- server/commands/closepoll.ts | 5 +++-- server/events/guildScheduledEventCreate.ts | 12 +++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/server/commands/closepoll.ts b/server/commands/closepoll.ts index 1290882..6997726 100644 --- a/server/commands/closepoll.ts +++ b/server/commands/closepoll.ts @@ -61,15 +61,16 @@ export async function closePoll(guild: Guild, requestId: string) { } async function updateMessage(movie: string, message: Message, guildId: string, requestId: string) { + const role = (await message.guild!.roles.fetch()).find(role => role.id === config.bot.announcement_role) const body = `[Abstimmung beendet] Gewonnen hat: ${movie}` - .concat(message.cleanContent.substring("[Abstimmung]".length)) + .concat(message.content.substring("[Abstimmung]".length)) const options: MessageEditOptions = { content: body, + allowedMentions: { parse: ["roles"] } } logger.info("Updating message.", { guildId, requestId }) message.edit(options) - } async function updateEvent(votes: Vote[], guild: Guild, guildId: string, requestId: string) { diff --git a/server/events/guildScheduledEventCreate.ts b/server/events/guildScheduledEventCreate.ts index bdee85b..3ac411d 100644 --- a/server/events/guildScheduledEventCreate.ts +++ b/server/events/guildScheduledEventCreate.ts @@ -1,4 +1,4 @@ -import { GuildScheduledEvent, Message, TextChannel } from "discord.js"; +import { GuildScheduledEvent, Message, MessageCreateOptions, TextChannel } from "discord.js"; import { ScheduledTask, schedule } from "node-cron"; import { v4 as uuid } from "uuid"; import { client, jellyfinHandler } from "../.."; @@ -30,14 +30,20 @@ export async function execute(event: GuildScheduledEvent) { const announcementChannel: TextChannel = client.getAnnouncementChannelForGuild(event.guildId) logger.debug(`Found channel ${JSON.stringify(announcementChannel, null, 2)}`, { guildId: event.guildId, requestId }) + const role = (await event.guild!.roles.fetch()).find(role => role.id === config.bot.announcement_role) - let message = "[Abstimmung]\nEs gibt eine neue Abstimmung für die nächste Watchparty! Stimme hierunter für den nächsten Film ab!\n" + let message = `[Abstimmung]\n<@&${role ? role.id : "hab die Rolle nicht gefunden"}> Es gibt eine neue Abstimmung für die nächste Watchparty! 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!).concat("\n") } - const sentMessage: Message = await (await announcementChannel.fetch()).send(message) + 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]) From 220f9dc8ef7ab83bfb6e23f9589faab8b8ade77f Mon Sep 17 00:00:00 2001 From: Sammy Date: Wed, 14 Jun 2023 19:45:33 +0200 Subject: [PATCH 08/50] undo fetching role to get roleID We already have the role id?!?!? --- server/events/guildScheduledEventCreate.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/events/guildScheduledEventCreate.ts b/server/events/guildScheduledEventCreate.ts index 3ac411d..7c160f2 100644 --- a/server/events/guildScheduledEventCreate.ts +++ b/server/events/guildScheduledEventCreate.ts @@ -30,9 +30,8 @@ export async function execute(event: GuildScheduledEvent) { const announcementChannel: TextChannel = client.getAnnouncementChannelForGuild(event.guildId) logger.debug(`Found channel ${JSON.stringify(announcementChannel, null, 2)}`, { guildId: event.guildId, requestId }) - const role = (await event.guild!.roles.fetch()).find(role => role.id === config.bot.announcement_role) - let message = `[Abstimmung]\n<@&${role ? role.id : "hab die Rolle nicht gefunden"}> Es gibt eine neue Abstimmung für die nächste Watchparty! Stimme hierunter für den nächsten Film ab!\n` + let message = `[Abstimmung]\n<@&${config.bot.announcement_role}> Es gibt eine neue Abstimmung für die nächste Watchparty! 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!).concat("\n") From 9420eb436655a2bfd8771ddcfb043ea85df17ac4 Mon Sep 17 00:00:00 2001 From: Sammy Date: Wed, 14 Jun 2023 22:24:39 +0200 Subject: [PATCH 09/50] Change announcements All announcements but initial will be deleted upon event end. Vote announcement will be deleted upon vote end Vote and vote end announcement now contain date and time --- server/commands/announce.ts | 6 ++- server/commands/closepoll.ts | 63 ++++++++++++---------- server/events/guildScheduledEventCreate.ts | 10 +++- server/events/guildScheduledEventUpdate.ts | 5 +- 4 files changed, 53 insertions(+), 31 deletions(-) diff --git a/server/commands/announce.ts b/server/commands/announce.ts index 4fac196..28d7562 100644 --- a/server/commands/announce.ts +++ b/server/commands/announce.ts @@ -23,7 +23,11 @@ export default new Command({ run: async (interaction: RunOptions) => { const command = interaction.interaction const requestId = uuid() - const guildId = command.guildId! + if(!command.guildId) { + logger.error("COMMAND DOES NOT HAVE A GUILD ID; CANCELLING!!!", {requestId}) + return + } + const guildId = command.guildId const announcementType = command.options.data.find(option => option.name.includes("typ")) logger.info(`Got command for announcing ${announcementType?.value}!`, { guildId, requestId }) diff --git a/server/commands/closepoll.ts b/server/commands/closepoll.ts index 6997726..511afa5 100644 --- a/server/commands/closepoll.ts +++ b/server/commands/closepoll.ts @@ -1,11 +1,12 @@ -import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildScheduledEventSetStatusArg, GuildScheduledEventStatus, Message, MessageEditOptions, TextChannel } from 'discord.js' +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 { logger } from '../logger' import { Command } from '../structures/command' import { RunOptions } from '../types/commandTypes' -import { client } from '../..' +import { format } from 'date-fns' export default new Command({ name: 'closepoll', @@ -49,43 +50,38 @@ 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 }) - - updateEvent(votes, guild!, guildId, requestId) - updateMessage(votes[0].movie, lastMessage, 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 updateMessage(movie: string, message: Message, guildId: string, requestId: string) { - const role = (await message.guild!.roles.fetch()).find(role => role.id === config.bot.announcement_role) - const body = `[Abstimmung beendet] Gewonnen hat: ${movie}` - .concat(message.content.substring("[Abstimmung]".length)) - - const options: MessageEditOptions = { +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] <@&${config.bot.announcement_role}> Wir gucken ${movie} am ${date} um ${time}` + const options: MessageCreateOptions = { content: body, allowedMentions: { parse: ["roles"] } } - logger.info("Updating message.", { guildId, requestId }) - message.edit(options) + const announcementChannel = client.getAnnouncementChannelForGuild(guildId) + logger.info("Sending vote closed message.", { guildId, requestId }) + announcementChannel.send(options) } -async function updateEvent(votes: Vote[], guild: Guild, guildId: string, requestId: string) { +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 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 - } - - const voteEvent: GuildScheduledEvent = voteEvents[0] 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` @@ -95,6 +91,19 @@ async function updateEvent(votes: Vote[], guild: Guild, guildId: string, request 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, diff --git a/server/events/guildScheduledEventCreate.ts b/server/events/guildScheduledEventCreate.ts index 7c160f2..7b818bc 100644 --- a/server/events/guildScheduledEventCreate.ts +++ b/server/events/guildScheduledEventCreate.ts @@ -6,7 +6,7 @@ import { closePoll } from "../commands/closepoll"; import { config } from "../configuration"; import { logger } from "../logger"; import toDate from "date-fns/fp/toDate"; -import { addDays, isAfter, isBefore } from "date-fns"; +import { addDays, format, isAfter, isBefore } from "date-fns"; export const name = 'guildScheduledEventCreate' @@ -31,7 +31,13 @@ export async function execute(event: GuildScheduledEvent) { const announcementChannel: TextChannel = client.getAnnouncementChannelForGuild(event.guildId) logger.debug(`Found channel ${JSON.stringify(announcementChannel, null, 2)}`, { guildId: event.guildId, requestId }) - let message = `[Abstimmung]\n<@&${config.bot.announcement_role}> Es gibt eine neue Abstimmung für die nächste Watchparty! Stimme hierunter für den nächsten Film ab!\n` + if(!event.scheduledStartAt) { + logger.info("EVENT DOES NOT HAVE STARTDATE; CANCELLING", {guildId: event.guildId, requestId}) + return + } + const date = format(event.scheduledStartAt, "dd.MM") + const time = format(event.scheduledStartAt, "HH:mm") + 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!).concat("\n") diff --git a/server/events/guildScheduledEventUpdate.ts b/server/events/guildScheduledEventUpdate.ts index b479101..f183740 100644 --- a/server/events/guildScheduledEventUpdate.ts +++ b/server/events/guildScheduledEventUpdate.ts @@ -1,8 +1,9 @@ import { GuildMember, GuildScheduledEvent, GuildScheduledEventStatus } from "discord.js"; import { v4 as uuid } from "uuid"; -import { jellyfinHandler } from "../.."; +import { client, jellyfinHandler } from "../.."; import { getGuildSpecificTriggerRoleId } from "../helper/roleFilter"; import { logger } from "../logger"; +import { manageAnnouncementRoles } from "../commands/announce"; export const name = 'guildScheduledEventUpdate' @@ -27,6 +28,8 @@ export async function execute(oldEvent: GuildScheduledEvent, newEvent: GuildSche if (newEvent.status === GuildScheduledEventStatus.Active) createJFUsers(members, newEvent.name, requestId) else { + const announcements = (await client.getAnnouncementChannelForGuild(newEvent.guild!.id).messages.fetch()).filter(message => !message.pinned) + announcements.forEach(message => message.delete()) members.forEach(member => { member.createDM().then(channel => channel.send(`Die Watchparty ist vorbei, dein Account wurde wieder gelöscht. Wenn du einen permanenten Account haben möchtest, melde dich bei Samantha oder Marukus.`)) }) From 251756c6221ed43b0bca13c6d3dcc1563b0ed7a9 Mon Sep 17 00:00:00 2001 From: mightypanders Date: Wed, 14 Jun 2023 22:43:37 +0200 Subject: [PATCH 10/50] feat/cicd (#22) Co-authored-by: magnetotail Reviewed-on: https://gitea.brudi.xyz/kenobi/jellyfin-discord-bot/pulls/22 Co-authored-by: mightypanders Co-committed-by: mightypanders --- .gitea/workflows/docker-build.yaml | 23 +++++++++++++++++++++++ Dockerfile | 11 +++++++++++ 2 files changed, 34 insertions(+) create mode 100644 .gitea/workflows/docker-build.yaml create mode 100644 Dockerfile diff --git a/.gitea/workflows/docker-build.yaml b/.gitea/workflows/docker-build.yaml new file mode 100644 index 0000000..995fa23 --- /dev/null +++ b/.gitea/workflows/docker-build.yaml @@ -0,0 +1,23 @@ +name: Build a docker image for node-jellyfin-role-bot +run-name: ${{ gitea.actor }} is building an image +on: [push] +env: + REGISTRY: gitea.brudi.xyz + IMAGE_NAME: ${{ gitea.repository }} + USER: ${{ gitea.actor }} +jobs: + build-docker-image: + runs-on: ubuntu-latest + container: catthehacker/ubuntu:act-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: Log in to the Container registry + run: docker login -u ${{ env.USER }} -p ${{ secrets.TOKEN }} ${{ env.REGISTRY }} + - name: Build Container + run: docker build -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" . + - name: Push Container + run: docker push "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e765732 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM node:alpine as Build +ENV NODE_ENV=production +WORKDIR /app + +COPY [ "package-lock.json", "package.json", "index.ts", "tsconfig.json", "./" ] +COPY server ./server + +RUN npm ci --omit=dev + +RUN npm run build +CMD ["npm","run","start"] From 5b99c843b4ea2292b0c1984310723a344afa3506 Mon Sep 17 00:00:00 2001 From: Sammy Date: Thu, 15 Jun 2023 21:56:15 +0200 Subject: [PATCH 11/50] Fix PR and linting issues --- server/commands/announce.ts | 13 ++++--- server/commands/closepoll.ts | 29 ++++++++++----- server/commands/resetPassword.ts | 5 +-- server/configuration.ts | 1 - server/events/guildScheduledEventCreate.ts | 15 +++++--- server/events/guildScheduledEventUpdate.ts | 26 ++++++++----- server/events/ready.ts | 4 -- server/events/voiceStateUpdate.ts | 4 +- server/helper/roleFilter.ts | 2 +- server/jellyfin/handler.ts | 8 +--- server/structures/client.ts | 43 +++++++++++++--------- 11 files changed, 86 insertions(+), 64 deletions(-) delete mode 100644 server/events/ready.ts diff --git a/server/commands/announce.ts b/server/commands/announce.ts index 28d7562..85f3b43 100644 --- a/server/commands/announce.ts +++ b/server/commands/announce.ts @@ -2,13 +2,10 @@ import { ApplicationCommandOptionType, Guild, GuildMember, Message, MessageCreat 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 { off } from 'process' -import { ScheduledTask, schedule } from 'node-cron' - -let task: ScheduledTask export default new Command({ name: 'announce', @@ -53,12 +50,16 @@ export default new Command({ }) function isAdmin(member: GuildMember): boolean { - return member.roles.cache.find((role, _) => role.id === config.bot.jf_admin_role) !== undefined + return member.roles.cache.find((role) => role.id === config.bot.jf_admin_role) !== undefined } async function sendInitialAnnouncement(guildId: string, requestId: string): Promise { logger.info("Sending initial announcement") - const announcementChannel: TextChannel = client.getAnnouncementChannelForGuild(guildId) + const announcementChannel: Maybe = client.getAnnouncementChannelForGuild(guildId) + if(!announcementChannel) { + logger.error("Could not find announcement channel. Aborting", { guildId, requestId }) + return + } const currentPinnedAnnouncementMessages = (await announcementChannel.messages.fetchPinned()).filter(message => message.cleanContent.includes("[initial]")) currentPinnedAnnouncementMessages.forEach(async (message) => await message.unpin()) diff --git a/server/commands/closepoll.ts b/server/commands/closepoll.ts index 511afa5..b254f38 100644 --- a/server/commands/closepoll.ts +++ b/server/commands/closepoll.ts @@ -7,6 +7,7 @@ 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', @@ -15,14 +16,14 @@ export default new Command({ run: async (interaction: RunOptions) => { const command = interaction.interaction const requestId = uuid() - const guildId = command.guildId! - logger.info("Got command for closing poll!", { guildId, requestId }) if (!command.guild) { - logger.error("No guild found in interaction. Cancelling closing request", { guildId, requestId }) + logger.error("No guild found in interaction. Cancelling closing request", { requestId }) command.followUp("Es gab leider ein Problem. Ich konnte deine Anfrage nicht bearbeiten :(") return } - + const guildId = command.guildId + logger.info("Got command for closing poll!", { guildId, requestId }) + command.followUp("Alles klar, beende die Umfrage :)") closePoll(command.guild, requestId) } @@ -32,10 +33,14 @@ export async function closePoll(guild: Guild, requestId: string) { const guildId = guild.id logger.info("stopping poll", { guildId, requestId }) - const announcementChannel: TextChannel = client.getAnnouncementChannelForGuild(guildId) + 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) + .map((value) => value) .filter(message => !message.cleanContent.includes("[Abstimmung beendet]") && message.cleanContent.includes("[Abstimmung]")) .sort((a, b) => b.createdTimestamp - a.createdTimestamp) @@ -60,7 +65,7 @@ export async function closePoll(guild: Guild, requestId: string) { await lastMessage.delete() const event = await getEvent(guild, guild.id, requestId) if(event) { - updateEvent(event, votes, guild!, guildId, requestId) + updateEvent(event, votes, guild, guildId, requestId) sendVoteClosedMessage(event, votes[0].movie, guildId, requestId) } @@ -77,6 +82,10 @@ async function sendVoteClosedMessage(event: GuildScheduledEvent, movie: string, } 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) } @@ -93,7 +102,7 @@ async function updateEvent(voteEvent: GuildScheduledEvent, votes: Vote[], guild: async function getEvent(guild: Guild, guildId: string, requestId: string): Promise { const voteEvents = (await guild.scheduledEvents.fetch()) - .map((value, _) => value) + .map((value) => value) .filter(event => event.name.toLowerCase().includes("voting offen")) logger.debug(`Found events: ${JSON.stringify(voteEvents, null, 2)}`, { guildId, requestId }) @@ -119,14 +128,14 @@ async function getVotesByEmote(message: Message, guildId: string, requestId: str 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, guildId, requestId) } + const vote: Vote = { emote: emote, count: reaction.count, movie: extractMovieFromMessageByEmote(message, emote) } votes.push(vote) } } return votes } -function extractMovieFromMessageByEmote(message: Message, emote: string, guildId: string, requestId: string): string { +function extractMovieFromMessageByEmote(message: Message, emote: string): string { const lines = message.cleanContent.split("\n") const emoteLines = lines.filter(line => line.includes(emote)) diff --git a/server/commands/resetPassword.ts b/server/commands/resetPassword.ts index 479e819..28eac2c 100644 --- a/server/commands/resetPassword.ts +++ b/server/commands/resetPassword.ts @@ -1,8 +1,7 @@ -import { ApplicationCommandOptionType, BurstHandlerMajorIdKey } from 'discord.js' +import { v4 as uuid } from 'uuid' +import { jellyfinHandler } from "../.." import { Command } from '../structures/command' import { RunOptions } from '../types/commandTypes' -import { jellyfinHandler } from "../.." -import { v4 as uuid } from 'uuid' export default new Command({ name: 'passwort_reset', diff --git a/server/configuration.ts b/server/configuration.ts index 144901b..1ad75d6 100644 --- a/server/configuration.ts +++ b/server/configuration.ts @@ -1,5 +1,4 @@ import dotenv from "dotenv" -import { AddListingProviderRequestToJSON } from "./jellyfin" dotenv.config() interface options { diff --git a/server/events/guildScheduledEventCreate.ts b/server/events/guildScheduledEventCreate.ts index 7b818bc..7d36221 100644 --- a/server/events/guildScheduledEventCreate.ts +++ b/server/events/guildScheduledEventCreate.ts @@ -1,3 +1,5 @@ +import { addDays, format, isAfter } from "date-fns"; +import toDate from "date-fns/fp/toDate"; import { GuildScheduledEvent, Message, MessageCreateOptions, TextChannel } from "discord.js"; import { ScheduledTask, schedule } from "node-cron"; import { v4 as uuid } from "uuid"; @@ -5,8 +7,7 @@ import { client, jellyfinHandler } from "../.."; import { closePoll } from "../commands/closepoll"; import { config } from "../configuration"; import { logger } from "../logger"; -import toDate from "date-fns/fp/toDate"; -import { addDays, format, isAfter, isBefore } from "date-fns"; +import { Maybe } from "../interfaces"; export const name = 'guildScheduledEventCreate' @@ -28,7 +29,11 @@ export async function execute(event: GuildScheduledEvent) { 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 }) - const announcementChannel: TextChannel = client.getAnnouncementChannelForGuild(event.guildId) + const announcementChannel: Maybe = client.getAnnouncementChannelForGuild(event.guildId) + if(!announcementChannel) { + logger.error("Could not find announcement channel. Aborting", { guildId: event.guildId, requestId }) + return + } logger.debug(`Found channel ${JSON.stringify(announcementChannel, null, 2)}`, { guildId: event.guildId, requestId }) if(!event.scheduledStartAt) { @@ -40,7 +45,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!).concat("\n") + message = message.concat(Emotes[i]).concat(": ").concat(movies[i].name ?? "Film hatte keinen Namen :(").concat("\n") } const options: MessageCreateOptions = { @@ -72,7 +77,7 @@ async function checkForPollsToClose(event: GuildScheduledEvent): Promise { //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) + .map((value) => value) if (!events || events.length <= 0) { logger.info("Did not find any events. Cancelling", { guildId: event.guildId, requestId }) diff --git a/server/events/guildScheduledEventUpdate.ts b/server/events/guildScheduledEventUpdate.ts index f183740..b3cd8b1 100644 --- a/server/events/guildScheduledEventUpdate.ts +++ b/server/events/guildScheduledEventUpdate.ts @@ -3,7 +3,6 @@ import { v4 as uuid } from "uuid"; import { client, jellyfinHandler } from "../.."; import { getGuildSpecificTriggerRoleId } from "../helper/roleFilter"; import { logger } from "../logger"; -import { manageAnnouncementRoles } from "../commands/announce"; export const name = 'guildScheduledEventUpdate' @@ -11,24 +10,33 @@ export const name = 'guildScheduledEventUpdate' export async function execute(oldEvent: GuildScheduledEvent, newEvent: GuildScheduledEvent) { try { const requestId = uuid() - logger.debug(`Got scheduledEvent update. New Event: ${JSON.stringify(newEvent, null, 2)}`,{guildId: newEvent.guildId, requestId}) + logger.debug(`Got scheduledEvent update. New Event: ${JSON.stringify(newEvent, null, 2)}`, { guildId: newEvent.guildId, requestId }) + if (!newEvent.guild) { + logger.error("Event has no guild, aborting.", { guildId: newEvent.guildId, requestId }) + return + } if (newEvent.description?.toLowerCase().includes("!wp") && [GuildScheduledEventStatus.Active, GuildScheduledEventStatus.Completed].includes(newEvent.status)) { - const roles = getGuildSpecificTriggerRoleId(newEvent.guildId).map((key, value)=> value) - const eventMembers = (await newEvent.fetchSubscribers({ withMember: true })).filter(member => !member.member.roles.cache.hasAny(...roles)).map((value, _) => value.member) - const channelMembers = newEvent.channel?.members.filter(member => !member.roles.cache.hasAny(...roles)).map((value, _) => value ) + const roles = getGuildSpecificTriggerRoleId().map((key, value) => value) + const eventMembers = (await newEvent.fetchSubscribers({ withMember: true })).filter(member => !member.member.roles.cache.hasAny(...roles)).map((value) => value.member) + const channelMembers = newEvent.channel?.members.filter(member => !member.roles.cache.hasAny(...roles)).map((value) => value) const allMembers = eventMembers.concat(channelMembers ?? []) const members: GuildMember[] = [] - for(const member of allMembers){ - if(!members.find(x => x.id == member.id)) + for (const member of allMembers) { + if (!members.find(x => x.id == member.id)) members.push(member) } - + if (newEvent.status === GuildScheduledEventStatus.Active) createJFUsers(members, newEvent.name, requestId) else { - const announcements = (await client.getAnnouncementChannelForGuild(newEvent.guild!.id).messages.fetch()).filter(message => !message.pinned) + const announcementChannel = await client.getAnnouncementChannelForGuild(newEvent.guild.id) + if(!announcementChannel) { + logger.error("Could not find announcement channel. Aborting", { guildId: newEvent.guild.id, requestId }) + return + } + const announcements = (await announcementChannel.messages.fetch()).filter(message => !message.pinned) announcements.forEach(message => message.delete()) members.forEach(member => { member.createDM().then(channel => channel.send(`Die Watchparty ist vorbei, dein Account wurde wieder gelöscht. Wenn du einen permanenten Account haben möchtest, melde dich bei Samantha oder Marukus.`)) diff --git a/server/events/ready.ts b/server/events/ready.ts deleted file mode 100644 index 91cef65..0000000 --- a/server/events/ready.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const name = 'ready' -export function execute(client: any) { - //console.log(`Processing ready: ${JSON.stringify(client)} has been created.`) -} diff --git a/server/events/voiceStateUpdate.ts b/server/events/voiceStateUpdate.ts index d4ef660..ac4ecee 100644 --- a/server/events/voiceStateUpdate.ts +++ b/server/events/voiceStateUpdate.ts @@ -18,8 +18,8 @@ export async function execute(oldState: VoiceState, newState: VoiceState) { } const scheduledEvents = (await newState.guild.scheduledEvents.fetch()) - .filter((key, value) => key.description?.toLowerCase().includes("!wp") && key.isActive()) - .map((key, value) => key) + .filter((key) => key.description?.toLowerCase().includes("!wp") && key.isActive()) + .map((key) => key) const scheduledEventUsers = (await Promise.all(scheduledEvents.map(event => event.fetchSubscribers({withMember: true})))) diff --git a/server/helper/roleFilter.ts b/server/helper/roleFilter.ts index f36f577..2779737 100644 --- a/server/helper/roleFilter.ts +++ b/server/helper/roleFilter.ts @@ -16,7 +16,7 @@ export function filterRolesFromMemberUpdate(oldMember: GuildMember, newMember: G return { addedRoles, removedRoles } } -export function getGuildSpecificTriggerRoleId(guildId: string): Collection { +export function getGuildSpecificTriggerRoleId(): Collection { const outVal = new Collection() outVal.set(config.bot.watcher_role, "VIEWER") outVal.set(config.bot.jf_admin_role, "ADMIN") diff --git a/server/jellyfin/handler.ts b/server/jellyfin/handler.ts index 27e26ab..7b777d2 100644 --- a/server/jellyfin/handler.ts +++ b/server/jellyfin/handler.ts @@ -2,7 +2,7 @@ import { GuildMember } from "discord.js"; import { Config } from "../configuration"; import { Maybe, PermissionLevel } from "../interfaces"; import { logger } from "../logger"; -import { CreateUserByNameOperationRequest, DeleteUserRequest, GetItemsRequest, GetMovieRemoteSearchResultsOperationRequest, ItemLookupApi, ItemsApi, LibraryApi, SystemApi, UpdateUserPasswordOperationRequest, UpdateUserPolicyOperationRequest, UserApi } from "./apis"; +import { CreateUserByNameOperationRequest, DeleteUserRequest, GetItemsRequest, ItemsApi, SystemApi, UpdateUserPasswordOperationRequest, UpdateUserPolicyOperationRequest, UserApi } from "./apis"; import { BaseItemDto, UpdateUserPasswordRequest } from "./models"; import { UserDto } from "./models/UserDto"; import { Configuration, ConfigurationParameters } from "./runtime"; @@ -56,10 +56,6 @@ export class JellyfinHandler { return `${discordUser.displayName}${level == "TEMPORARY" ? "_tmp" : ""}` } - public async addPermissionsToUserAccount(jfUserAccount: UserDto, guildId: string, requestId: string): Promise { - throw new Error("Method not implemented."); - } - private generatePasswordForUser(): string { return (Math.random() * 10000 + 10000).toFixed(0) } @@ -253,7 +249,7 @@ export class JellyfinHandler { } const movies: BaseItemDto[] = [] for (let i = 0; i < count; i++) { - const index = Math.random() * allMovies.length + const index = Math.floor(Math.random() * allMovies.length) movies.push(...allMovies.splice(index, 1)) // maybe out of bounds? ? } diff --git a/server/structures/client.ts b/server/structures/client.ts index ce37605..9ac2c1a 100644 --- a/server/structures/client.ts +++ b/server/structures/client.ts @@ -1,13 +1,13 @@ -import { ApplicationCommandDataResolvable, Client, ClientOptions, Collection, GatewayIntentBits, Guild, IntentsBitField, Snowflake, TextChannel } from "discord.js"; -import { CommandType } from "../types/commandTypes"; -import fs from 'fs' -import { config } from "../configuration"; -import { logger } from "../logger"; -import { JellyfinHandler } from "../jellyfin/handler"; +import { ApplicationCommandDataResolvable, Client, ClientOptions, Collection, Guild, IntentsBitField, Snowflake, TextChannel } from "discord.js"; +import fs from 'fs'; import { ScheduledTask, schedule } from "node-cron"; +import { v4 as uuid } from 'uuid'; import { manageAnnouncementRoles } from "../commands/announce"; -import { v4 as uuid } from 'uuid' -import { task } from "../events/guildScheduledEventCreate"; +import { config } from "../configuration"; +import { Maybe } from "../interfaces"; +import { JellyfinHandler } from "../jellyfin/handler"; +import { logger } from "../logger"; +import { CommandType } from "../types/commandTypes"; @@ -82,8 +82,8 @@ export class ExtendedClient extends Client { private async cacheAnnouncementServer(guilds: Collection) { for (const guild of guilds.values()) { const channels: TextChannel[] = (await guild.channels.fetch()) - ?.filter(channel => channel!.id === config.bot.announcement_channel_id) - .map((value, _) => value) + ?.filter(channel => channel?.id === config.bot.announcement_channel_id) + .map((value) => value) if (!channels || channels.length != 1) { logger.error(`Could not find announcement channel for guild ${guild.name} with guildId ${guild.id}. Found ${channels}`) @@ -93,8 +93,8 @@ export class ExtendedClient extends Client { this.announcementChannels.set(guild.id, channels[0]) } } - public getAnnouncementChannelForGuild(guildId: string): TextChannel { - return this.announcementChannels.get(guildId)! //we set the channel by ourselves only if we find one, I think this is sage (mark my words) + public getAnnouncementChannelForGuild(guildId: string): Maybe { + return this.announcementChannels.get(guildId) } public async cacheUsers(guilds: Collection) { guilds.forEach((guild: Guild, id: Snowflake) => { @@ -127,18 +127,28 @@ export class ExtendedClient extends Client { public async startAnnouncementRoleBackgroundTask(guilds: Collection) { for (const guild of guilds.values()) { logger.info("Starting background task for announcement role", { guildId: guild.id }) - const textChannel: TextChannel = this.getAnnouncementChannelForGuild(guild.id) + const textChannel: Maybe = this.getAnnouncementChannelForGuild(guild.id) + if(!textChannel) { + logger.error("Could not find announcement channel. Aborting", { guildId: guild.id }) + return + } this.announcementRoleHandlerTask.set(guild.id, schedule("*/10 * * * * *", async () => { const requestId = uuid() const messages = (await textChannel.messages.fetchPinned()).filter(message => message.cleanContent.includes("[initial]")) 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 }) + return } else if (messages.size == 0) { logger.error("Could not find any pinned announcement messages. Unable to manage roles!", { guildId: guild.id, requestId }) + return } - const message = await messages.at(0)!.fetch() + const message = await messages.at(0)?.fetch() + if (!message) { + logger.error(`No pinned message found`, { guildId: guild.id, requestId }) + return + } //logger.debug(`Message: ${JSON.stringify(message, null, 2)}`, { guildId: guild.id, requestId }) const reactions = message.reactions.resolve("🎫") @@ -152,9 +162,8 @@ export class ExtendedClient extends Client { } } - public stopAnnouncementRoleBackgroundTask(guild: string | Guild, requestId: string) { - const guildId: string = guild instanceof Guild ? guild.id : guild - const task: ScheduledTask | undefined = guild instanceof Guild ? this.announcementRoleHandlerTask.get(guild.id) : this.announcementRoleHandlerTask.get(guild) + public stopAnnouncementRoleBackgroundTask(guildId: string, requestId: string) { + const task: Maybe = this.announcementRoleHandlerTask.get(guildId) if (!task) { logger.error(`No task found for guildID ${guildId}.`, { guildId, requestId }) return From d22e38efbf6968c42b4d819c3b7b79b3f939c244 Mon Sep 17 00:00:00 2001 From: Sammy Date: Thu, 15 Jun 2023 22:02:37 +0200 Subject: [PATCH 12/50] fix build --- server/events/guildMemberUpdate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/events/guildMemberUpdate.ts b/server/events/guildMemberUpdate.ts index 4b0d773..6de7cf2 100644 --- a/server/events/guildMemberUpdate.ts +++ b/server/events/guildMemberUpdate.ts @@ -9,7 +9,7 @@ export async function execute(oldMember: GuildMember, newMember: GuildMember) { try { const requestId = uuid() const changedRoles: ChangedRoles = filterRolesFromMemberUpdate(oldMember, newMember) - const triggerRoleIds: Collection = getGuildSpecificTriggerRoleId(oldMember.guild.id) + const triggerRoleIds: Collection = getGuildSpecificTriggerRoleId() triggerRoleIds.forEach((level, key) => { const addedRoleMatches = changedRoles.addedRoles.find(aRole => aRole.id === key) From 71ffc6ba50aa3b5cd41fab27614b7172eac7c4c5 Mon Sep 17 00:00:00 2001 From: Sammy Date: Thu, 15 Jun 2023 22:33:22 +0200 Subject: [PATCH 13/50] use yavin to get random movies --- index.ts | 3 ++- server/configuration.ts | 12 ++++++++- server/events/guildScheduledEventCreate.ts | 6 ++--- server/interfaces.ts | 6 +++++ server/jellyfin/handler.ts | 30 ++++++++-------------- server/structures/client.ts | 1 - 6 files changed, 32 insertions(+), 26 deletions(-) diff --git a/index.ts b/index.ts index 49b529f..30ef40c 100644 --- a/index.ts +++ b/index.ts @@ -5,7 +5,8 @@ import { JellyfinHandler } from "./server/jellyfin/handler" import { attachedImages } from "./server/assets/attachments" const requestId = 'startup' -export const jellyfinHandler = new JellyfinHandler(config) +export const jellyfinHandler = new JellyfinHandler({jellyfinToken: config.bot.workaround_token, jellyfinUrl: config.bot.jellyfin_url, movieCollectionId: config.bot.jf_collection_id, collectionUser: config.bot.jf_user}) +export const yavinJellyfinHandler = new JellyfinHandler({jellyfinToken: config.bot.yavin_jellyfin_token, jellyfinUrl: config.bot.yavin_jellyfin_url, movieCollectionId: config.bot.yavin_collection_id, collectionUser: config.bot.yavin_jellyfin_collection_user}) export const client = new ExtendedClient(jellyfinHandler) diff --git a/server/configuration.ts b/server/configuration.ts index 1ad75d6..181e26b 100644 --- a/server/configuration.ts +++ b/server/configuration.ts @@ -25,6 +25,11 @@ export interface Config { announcement_role: string announcement_channel_id: string jf_collection_id: string + jf_user: string + yavin_collection_id: string + yavin_jellyfin_url: string + yavin_jellyfin_token: string + yavin_jellyfin_collection_user: string } } export const config: Config = { @@ -59,6 +64,11 @@ export const config: Config = { jf_admin_role: process.env.ADMIN_ROLE ?? "", announcement_role: process.env.WATCHPARTY_ANNOUNCEMENT_ROLE ?? "", announcement_channel_id: process.env.CHANNEL_ID ?? "", - jf_collection_id: process.env.JELLYFIN_COLLECTION_ID ?? "" + jf_collection_id: process.env.JELLYFIN_COLLECTION_ID ?? "", + yavin_collection_id: process.env.YAVIN_COLLECTION_ID ?? "", + 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 ?? "" } } diff --git a/server/events/guildScheduledEventCreate.ts b/server/events/guildScheduledEventCreate.ts index 7d36221..d14721e 100644 --- a/server/events/guildScheduledEventCreate.ts +++ b/server/events/guildScheduledEventCreate.ts @@ -3,11 +3,11 @@ import toDate from "date-fns/fp/toDate"; import { GuildScheduledEvent, Message, MessageCreateOptions, TextChannel } from "discord.js"; import { ScheduledTask, schedule } from "node-cron"; import { v4 as uuid } from "uuid"; -import { client, jellyfinHandler } from "../.."; +import { client, yavinJellyfinHandler } from "../.."; import { closePoll } from "../commands/closepoll"; import { config } from "../configuration"; -import { logger } from "../logger"; import { Maybe } from "../interfaces"; +import { logger } from "../logger"; export const name = 'guildScheduledEventCreate' @@ -24,7 +24,7 @@ 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 jellyfinHandler.getRandomMovies(5, event.guildId, requestId) + const movies = await yavinJellyfinHandler.getRandomMovies(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 }) diff --git a/server/interfaces.ts b/server/interfaces.ts index e9807d3..84e93b1 100644 --- a/server/interfaces.ts +++ b/server/interfaces.ts @@ -32,4 +32,10 @@ export interface ChangedRoles { addedRoles: Collection removedRoles: Collection } +export interface JellyfinConfig { + jellyfinUrl: string, + jellyfinToken: string, + movieCollectionId: string, + collectionUser: string +} export type PermissionLevel = "VIEWER" | "ADMIN" | "TEMPORARY" diff --git a/server/jellyfin/handler.ts b/server/jellyfin/handler.ts index 7b777d2..a1af178 100644 --- a/server/jellyfin/handler.ts +++ b/server/jellyfin/handler.ts @@ -1,6 +1,5 @@ import { GuildMember } from "discord.js"; -import { Config } from "../configuration"; -import { Maybe, PermissionLevel } from "../interfaces"; +import { JellyfinConfig, Maybe, PermissionLevel } from "../interfaces"; import { logger } from "../logger"; import { CreateUserByNameOperationRequest, DeleteUserRequest, GetItemsRequest, ItemsApi, SystemApi, UpdateUserPasswordOperationRequest, UpdateUserPolicyOperationRequest, UserApi } from "./apis"; import { BaseItemDto, UpdateUserPasswordRequest } from "./models"; @@ -15,34 +14,27 @@ export class JellyfinHandler { private moviesApi: ItemsApi private token: string private authHeader: { headers: { 'X-Emby-Authorization': string } } - private config: Config + private config: JellyfinConfig private serverName = ""; - public async ServerName(): Promise { - if (this.serverName === "") { - const info = await this.systemApi.getSystemInfo(this.authHeader) - this.serverName = info.serverName ?? this.config.bot.jellyfin_url - } - return this.serverName - } - constructor(_config: Config, _userApi?: UserApi, _systemApi?: SystemApi, _itemsApi?: ItemsApi) { + constructor(_config: JellyfinConfig, _userApi?: UserApi, _systemApi?: SystemApi, _itemsApi?: ItemsApi) { this.config = _config - this.token = this.config.bot.jellfin_token + this.token = this.config.jellyfinToken this.authHeader = { headers: { - "X-Emby-Authorization": this.config.bot.workaround_token + "X-Emby-Authorization": this.config.jellyfinToken } } const userApiConfigurationParams: ConfigurationParameters = { - basePath: this.config.bot.jellyfin_url, + basePath: this.config.jellyfinUrl, headers: this.authHeader.headers } const systemApiConfigurationParams: ConfigurationParameters = { - basePath: this.config.bot.jellyfin_url, + basePath: this.config.jellyfinUrl, headers: this.authHeader.headers } const libraryApiConfigurationParams: ConfigurationParameters = { - basePath: this.config.bot.jellyfin_url, + basePath: this.config.jellyfinUrl, headers: this.authHeader.headers } @@ -228,11 +220,9 @@ export class JellyfinHandler { public async getAllMovies(guildId: string, requestId: string): Promise { logger.info("requesting all movies from jellyfin", { guildId, requestId }) - const liloJfUser = await this.getUser({ guild: { id: guildId }, displayName: "lilo" }, requestId) - const searchParams: GetItemsRequest = { - userId: liloJfUser?.id, - parentId: this.config.bot.jf_collection_id // collection ID for all movies + userId: this.config.collectionUser, + parentId: this.config.movieCollectionId // collection ID for all movies } const movies = (await (this.moviesApi.getItems(searchParams))).items?.filter(item => !item.isFolder) // logger.debug(JSON.stringify(movies, null, 2), { guildId: guildId, requestId }) diff --git a/server/structures/client.ts b/server/structures/client.ts index 9ac2c1a..6ecaf6b 100644 --- a/server/structures/client.ts +++ b/server/structures/client.ts @@ -33,7 +33,6 @@ export class ExtendedClient extends Client { Promise.all(promises).then(() => { this.login(config.bot.token) }) - logger.info(`Connected with ${await this.jellyfin.ServerName()}`) } private async importFile(filepath: string): Promise { logger.debug(`Importing ${filepath}`) From 2fae61fc1fb1bc77965816f2eb6f590a90e0cb63 Mon Sep 17 00:00:00 2001 From: Sammy Date: Thu, 15 Jun 2023 22:33:42 +0200 Subject: [PATCH 14/50] change announcement message to specify MOVIE watchparties --- server/commands/announce.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/commands/announce.ts b/server/commands/announce.ts index 85f3b43..ca939b9 100644 --- a/server/commands/announce.ts +++ b/server/commands/announce.ts @@ -67,7 +67,7 @@ async function sendInitialAnnouncement(guildId: string, requestId: string): Prom const body = `[initial] Hey! @everyone! Hier ist der Watchparty Bot vom Hartzarett. -Wir machen in Zukunft regelmäßig Watchparties! Falls du mitmachen möchtest, reagiere einfach auf diesen Post mit 🎫, dann bekommst du automatisch eine Rolle zugewiesen und wirst benachrichtigt sobald es in der Zukunft weitere Watchparties und Filme zum abstimmen gibt. +Wir machen in Zukunft regelmäßig Watchparties in denen wir zusammen Filme gucken! Falls du mitmachen möchtest, reagiere einfach auf diesen Post mit 🎫, dann bekommst du automatisch eine Rolle zugewiesen und wirst benachrichtigt sobald es in der Zukunft weitere Watchparties und Filme zum abstimmen gibt. Für eine Erklärung wie das alles funktioniert mach einfach /mitgucken für eine lange Erklärung am Stück oder /guides wenn du auswählen möchtest wozu du Infos bekommst.` From f78e4c3e3e21d775f3b6de6fd5fd5535c46e90d3 Mon Sep 17 00:00:00 2001 From: Sammy Date: Thu, 15 Jun 2023 23:30:13 +0200 Subject: [PATCH 15/50] 1.0.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c453438..cb6444d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-jellyfin-discord-bot", - "version": "0.0.1", + "version": "1.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "node-jellyfin-discord-bot", - "version": "0.0.1", + "version": "1.0.0", "license": "MIT", "dependencies": { "@discordjs/rest": "^1.7.0", diff --git a/package.json b/package.json index c70ccba..b59b69b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-jellyfin-discord-bot", - "version": "0.0.1", + "version": "1.0.0", "description": "A discord bot to sync jellyfin accounts with discord roles", "main": "index.js", "license": "MIT", From d6300e8bec85d6791bb9159dc442ea04af6e6d7b Mon Sep 17 00:00:00 2001 From: Sammy Date: Fri, 16 Jun 2023 20:15:36 +0200 Subject: [PATCH 16/50] improve movie name resolving When creating the poll the bot will now only request movie names instead of movies This improves the errorhandling because the movie names cannot be null Also the movieName function filters empty movienames and will guarantee the requested number of names --- server/events/guildScheduledEventCreate.ts | 6 +++--- server/jellyfin/handler.ts | 13 ++++++++++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/server/events/guildScheduledEventCreate.ts b/server/events/guildScheduledEventCreate.ts index d14721e..7a28fee 100644 --- a/server/events/guildScheduledEventCreate.ts +++ b/server/events/guildScheduledEventCreate.ts @@ -24,10 +24,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 +45,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 = { 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 } From 26c2d912527ed053b8c4228ec40f4bf14ecffea9 Mon Sep 17 00:00:00 2001 From: mightypanders Date: Sat, 17 Jun 2023 01:13:33 +0200 Subject: [PATCH 17/50] fix and refactor mitgucken guide - reorder account acquisition in front of login - rebox explainrole function - adjust external reference to function --- server/commands/guides.ts | 4 ++-- server/commands/mitgucken.ts | 24 +++++++++++++----------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/server/commands/guides.ts b/server/commands/guides.ts index 43c1302..bc281cc 100644 --- a/server/commands/guides.ts +++ b/server/commands/guides.ts @@ -5,7 +5,7 @@ import { accountChoice, joingroup, leavegroup, loginScreen, overview, resume, se import { logger } from '../logger' import { Command } from '../structures/command' import { RunOptions } from '../types/commandTypes' -import { configureServer, explainRoles, installation, loginInfo, useSyncgroup } from './mitgucken' +import { configureServer, explainRole, installation, loginInfo, useSyncgroup } from './mitgucken' export default new Command({ name: 'guides', @@ -70,7 +70,7 @@ export default new Command({ userDMChannel.send({ embeds: useSyncgroup(), files: [overview, joingroup, resume, leavegroup] }) } else if (guideSelection.customId === 'explainRoles') { const userDMChannel = await guideSelection.user.createDM() - userDMChannel.send(explainRoles()) + userDMChannel.send({ embeds: explainRole() }) } guideSelection.update({ content: "Hab ich dir per DM geschickt :)", components: [] }) diff --git a/server/commands/mitgucken.ts b/server/commands/mitgucken.ts index 68af7c9..e3a67ba 100644 --- a/server/commands/mitgucken.ts +++ b/server/commands/mitgucken.ts @@ -16,13 +16,9 @@ export default new Command({ const embedList: APIEmbed[] = [] embedList.push(...installation()) embedList.push(...configureServer()) + embedList.push(...explainRole()) embedList.push(...loginInfo()) embedList.push(...useSyncgroup()) - embedList.push({ - color, - title: "Wie du an einen Account kommst", - description: explainRoles() - }) //logger.info(`Trying to use ${splashScreen.name}`, { requestId, guildId: interaction.interaction.guild?.id }) logger.info(`Sending guide to ${interaction.interaction.user.id}`, { requestId, guildId: interaction.interaction.guild?.id }) @@ -32,6 +28,13 @@ export default new Command({ }) +export function explainRole(): APIEmbed[] { + return [{ + color, + title: "Wie du an einen Account kommst", + description: roleExplanation + }] +} export function installation(): APIEmbed[] { const embedList: APIEmbed[] = [] // DownloadLink and installation @@ -65,9 +68,9 @@ export function configureServer(): APIEmbed[] { embedList.push({ color, title: "Server Verbindung", - description: "Stelle eine Verbindung zum Hartzarett Jellyfin Server her", + description: "Stelle eine Verbindung zum Hartzarett Jellyfin Server her\nDie Adresse lautet:\nhttps://media.hartzarett.ruhr\n\n", fields: [ - { name: "Server Adresse", value: "https://media.hartzarett.ruhr" } + { name: "Server Adresse", value: "`https://media.hartzarett.ruhr`" } ], image: { url: 'attachment://server_verbindung.png' @@ -91,7 +94,7 @@ export function loginInfo(): APIEmbed[] { embedList.push({ color, title: "Login", - description: "Melde dich mit dem Usernamen und Passwort an, welches dir von mir zugeschickt wird. Falls du ein neues brauchst führe einmal /reset_passwort aus :)", + description: "Melde dich mit dem Usernamen und Passwort an, welches dir von mir zugeschickt wird. Falls du ein neues brauchst führe einmal `/passwort_reset` aus :)", image: { url: 'attachment://login_screen.png' } @@ -139,8 +142,7 @@ export function useSyncgroup(): APIEmbed[] { return embedList } -export function explainRoles(): string { - return `Mit einer Rolle kann dafür gesorgt werden, dass du einen dauerhaften Account auf dem Mediaserver hast. Wende dich bei Bedarf an Samantha oder Markus.\n +const roleExplanation = `Mit einer Rolle kann dafür gesorgt werden, dass du einen dauerhaften Account auf dem Mediaserver hast. Wende dich bei Bedarf an Samantha oder Markus.\n Für eine watchparty bekommst du allerdings automatisch einen Account. Hierfür melde einfach Interesse an dem Event an. Wenn du für das Event Interesse angemeldet hast bekommst du automatisch beim Start des Events einen Benutzernamen und das dazugehörige Passwort zugesendet.\n Hast du kein Interesse angemeldet bekommst du automatisch einen Nutzernamen und Passwort zugeschickt wenn du den Channel betrittst in dem das Event stattfindet.` -} \ No newline at end of file + From 7899aac5ce1e0f7f0315400e82a3c877c3073b45 Mon Sep 17 00:00:00 2001 From: mightypanders Date: Sat, 17 Jun 2023 01:20:01 +0200 Subject: [PATCH 18/50] adjust server connection message remove extraneous repetition of the server address --- server/commands/mitgucken.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/commands/mitgucken.ts b/server/commands/mitgucken.ts index e3a67ba..c90a7f8 100644 --- a/server/commands/mitgucken.ts +++ b/server/commands/mitgucken.ts @@ -68,7 +68,7 @@ export function configureServer(): APIEmbed[] { embedList.push({ color, title: "Server Verbindung", - description: "Stelle eine Verbindung zum Hartzarett Jellyfin Server her\nDie Adresse lautet:\nhttps://media.hartzarett.ruhr\n\n", + description: "Stelle eine Verbindung zum Hartzarett Jellyfin Server her", fields: [ { name: "Server Adresse", value: "`https://media.hartzarett.ruhr`" } ], From 7c8072b295029894f03eb3665be629019942dca7 Mon Sep 17 00:00:00 2001 From: mightypanders Date: Sat, 17 Jun 2023 01:20:54 +0200 Subject: [PATCH 19/50] add separate compile step should be run on every push to a branch to check for compilability --- .gitea/workflows/compile.yaml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .gitea/workflows/compile.yaml diff --git a/.gitea/workflows/compile.yaml b/.gitea/workflows/compile.yaml new file mode 100644 index 0000000..0309d61 --- /dev/null +++ b/.gitea/workflows/compile.yaml @@ -0,0 +1,17 @@ +name: Compile the repository +on: [push] +env: + REGISTRY: gitea.brudi.xyz + IMAGE_NAME: ${{ gitea.repository }} + USER: ${{ gitea.actor }} +jobs: + compile: + runs-on: ubuntu-latest + container: catthehacker/ubuntu:act-latest + permissions: + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: Build Container + run: docker build -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" . From ce4441cee3a669d778e1c87617a8f6bd299722bb Mon Sep 17 00:00:00 2001 From: mightypanders Date: Sat, 17 Jun 2023 01:22:53 +0200 Subject: [PATCH 20/50] add restriction to master branch on docker push job --- .gitea/workflows/docker-build.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitea/workflows/docker-build.yaml b/.gitea/workflows/docker-build.yaml index 995fa23..d7dbeac 100644 --- a/.gitea/workflows/docker-build.yaml +++ b/.gitea/workflows/docker-build.yaml @@ -8,6 +8,7 @@ env: jobs: build-docker-image: runs-on: ubuntu-latest + if: gitea.ref == 'refs/heads/master' container: catthehacker/ubuntu:act-latest permissions: contents: read From 2c09033c3f79675c0b5f45c54137f2eac05ea28c Mon Sep 17 00:00:00 2001 From: mightypanders Date: Sat, 17 Jun 2023 01:27:49 +0200 Subject: [PATCH 21/50] add new tag restriction to docker push job --- .gitea/workflows/docker-build.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitea/workflows/docker-build.yaml b/.gitea/workflows/docker-build.yaml index d7dbeac..eb3985f 100644 --- a/.gitea/workflows/docker-build.yaml +++ b/.gitea/workflows/docker-build.yaml @@ -1,6 +1,9 @@ name: Build a docker image for node-jellyfin-role-bot run-name: ${{ gitea.actor }} is building an image -on: [push] +on: + push: + tags: + - '*' env: REGISTRY: gitea.brudi.xyz IMAGE_NAME: ${{ gitea.repository }} From 1e6a75687a8e635706cc4e83a446542e62de81b4 Mon Sep 17 00:00:00 2001 From: mightypanders Date: Sat, 17 Jun 2023 01:31:03 +0200 Subject: [PATCH 22/50] add package version extraction to docker push job --- .gitea/workflows/docker-build.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitea/workflows/docker-build.yaml b/.gitea/workflows/docker-build.yaml index eb3985f..fe5fe7a 100644 --- a/.gitea/workflows/docker-build.yaml +++ b/.gitea/workflows/docker-build.yaml @@ -19,9 +19,11 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v3 + - name: Get Package Version + run: VERSION = node -p "require('./package.json').version" - name: Log in to the Container registry run: docker login -u ${{ env.USER }} -p ${{ secrets.TOKEN }} ${{ env.REGISTRY }} - name: Build Container - run: docker build -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" . + run: docker build -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}". - name: Push Container run: docker push "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" From 07849d331acd3985248af91282e2e6ad21618277 Mon Sep 17 00:00:00 2001 From: Sammy Date: Sat, 17 Jun 2023 12:00:14 +0200 Subject: [PATCH 23/50] move scheduling of pollclose task to startup Also moved check function to closepoll.ts --- server/commands/closepoll.ts | 32 ++++++++++++++- server/events/guildScheduledEventCreate.ts | 45 +--------------------- server/events/guildScheduledEventUpdate.ts | 1 + server/structures/client.ts | 13 ++++++- 4 files changed, 44 insertions(+), 47 deletions(-) diff --git a/server/commands/closepoll.ts b/server/commands/closepoll.ts index b254f38..1de84bb 100644 --- a/server/commands/closepoll.ts +++ b/server/commands/closepoll.ts @@ -1,13 +1,13 @@ +import { addDays, 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', @@ -145,4 +145,32 @@ 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 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: 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 7a28fee..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"; @@ -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/structures/client.ts b/server/structures/client.ts index 6ecaf6b..6d74663 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.startPollCloceBackgroundTasks() }) } catch (error) { logger.info(`Error refreshing slash commands: ${error}`) @@ -169,4 +172,10 @@ export class ExtendedClient extends Client { } task.stop() } + + private async startPollCloceBackgroundTasks() { + for(const guild of this.guilds.cache) { + this.pollCloseBackgroundTasks.set(guild[1].id, schedule("0 * * * * *", () => checkForPollsToClose(guild[1]))) + } + } } From 4cc332820fa06c25bc4c2fea5ba1b138462eaf23 Mon Sep 17 00:00:00 2001 From: Sammy Date: Sat, 17 Jun 2023 13:03:48 +0200 Subject: [PATCH 24/50] prevent poll close if event is less than 24h old --- server/commands/closepoll.ts | 8 ++++++++ server/structures/client.ts | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/server/commands/closepoll.ts b/server/commands/closepoll.ts index 1de84bb..88c5f90 100644 --- a/server/commands/closepoll.ts +++ b/server/commands/closepoll.ts @@ -164,6 +164,14 @@ export async function checkForPollsToClose(guild: Guild): Promise { return } + const createDate: Date = toDate(updatedEvent.createdTimestamp) + const closePollMinDate: Date = addDays(createDate, 1) + + if(isAfter(closePollMinDate, Date.now())) { + logger.info("Event is less than 24h old. Not closing poll!", { guildId: guild.id, requestId }) + return + } + const eventDate: Date = toDate(updatedEvent.scheduledStartTimestamp) const closePollDate: Date = addDays(eventDate, -2) diff --git a/server/structures/client.ts b/server/structures/client.ts index 6d74663..fae18b3 100644 --- a/server/structures/client.ts +++ b/server/structures/client.ts @@ -75,7 +75,7 @@ export class ExtendedClient extends Client { this.cacheUsers(guilds) await this.cacheAnnouncementServer(guilds) this.startAnnouncementRoleBackgroundTask(guilds) - this.startPollCloceBackgroundTasks() + this.startPollCloseBackgroundTasks() }) } catch (error) { logger.info(`Error refreshing slash commands: ${error}`) @@ -173,7 +173,7 @@ export class ExtendedClient extends Client { task.stop() } - private async startPollCloceBackgroundTasks() { + private async startPollCloseBackgroundTasks() { for(const guild of this.guilds.cache) { this.pollCloseBackgroundTasks.set(guild[1].id, schedule("0 * * * * *", () => checkForPollsToClose(guild[1]))) } From 670a64af22ce6b22ec458cd69c912aea0e920cf9 Mon Sep 17 00:00:00 2001 From: Sammy Date: Sat, 17 Jun 2023 13:18:52 +0200 Subject: [PATCH 25/50] Check if less than 2 days between create and start for deciding if to close --- server/commands/closepoll.ts | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/server/commands/closepoll.ts b/server/commands/closepoll.ts index 88c5f90..78a2a0b 100644 --- a/server/commands/closepoll.ts +++ b/server/commands/closepoll.ts @@ -1,4 +1,4 @@ -import { addDays, format, isAfter, toDate } from 'date-fns' +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 '../..' @@ -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 } @@ -151,10 +151,10 @@ 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) { + 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) { + } else if (events.length == 0) { logger.info("Could not find any events. Cancelling", { guildId: guild.id, requestId }) } @@ -165,14 +165,14 @@ export async function checkForPollsToClose(guild: Guild): Promise { } const createDate: Date = toDate(updatedEvent.createdTimestamp) - const closePollMinDate: Date = addDays(createDate, 1) + const eventDate: Date = toDate(updatedEvent.scheduledStartTimestamp) + const difference: number = differenceInDays(createDate, eventDate) - if(isAfter(closePollMinDate, Date.now())) { - logger.info("Event is less than 24h old. Not closing poll!", { guildId: guild.id, requestId }) + if (difference <= 2) { + logger.info("Less than two days between event create and event start. Not closing poll.", { guildId: guild.id, requestId }) return } - - const eventDate: Date = toDate(updatedEvent.scheduledStartTimestamp) + const closePollDate: Date = addDays(eventDate, -2) if (isAfter(Date.now(), closePollDate)) { From 0b67b126dd4235388e251418b3f878429c24be8a Mon Sep 17 00:00:00 2001 From: mightypanders Date: Mon, 19 Jun 2023 23:19:40 +0200 Subject: [PATCH 26/50] 1.0.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index cb6444d..65cc6d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-jellyfin-discord-bot", - "version": "1.0.0", + "version": "1.0.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "node-jellyfin-discord-bot", - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "dependencies": { "@discordjs/rest": "^1.7.0", diff --git a/package.json b/package.json index b59b69b..1a8568b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-jellyfin-discord-bot", - "version": "1.0.0", + "version": "1.0.1", "description": "A discord bot to sync jellyfin accounts with discord roles", "main": "index.js", "license": "MIT", From 8a7973a2e3ffd91ae60c874100b26f72511c17a6 Mon Sep 17 00:00:00 2001 From: mightypanders Date: Mon, 19 Jun 2023 23:26:41 +0200 Subject: [PATCH 27/50] compile only on PR --- .gitea/workflows/compile.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/compile.yaml b/.gitea/workflows/compile.yaml index 0309d61..199daf9 100644 --- a/.gitea/workflows/compile.yaml +++ b/.gitea/workflows/compile.yaml @@ -1,5 +1,5 @@ name: Compile the repository -on: [push] +on: [pull_request] env: REGISTRY: gitea.brudi.xyz IMAGE_NAME: ${{ gitea.repository }} From c00453d3d32255963792dbb88d51d8c44ea5f278 Mon Sep 17 00:00:00 2001 From: mightypanders Date: Mon, 19 Jun 2023 23:26:56 +0200 Subject: [PATCH 28/50] push all tags of an image --- .gitea/workflows/docker-build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/docker-build.yaml b/.gitea/workflows/docker-build.yaml index fe5fe7a..26efd1d 100644 --- a/.gitea/workflows/docker-build.yaml +++ b/.gitea/workflows/docker-build.yaml @@ -26,4 +26,4 @@ jobs: - name: Build Container run: docker build -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}". - name: Push Container - run: docker push "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" + run: docker push --all-tags "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" From 111ccaa8804f735a9c0df7a69981bf5c56c2c7c9 Mon Sep 17 00:00:00 2001 From: mightypanders Date: Mon, 19 Jun 2023 23:28:39 +0200 Subject: [PATCH 29/50] simplify the test compile --- .gitea/workflows/compile.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/compile.yaml b/.gitea/workflows/compile.yaml index 199daf9..2fa0821 100644 --- a/.gitea/workflows/compile.yaml +++ b/.gitea/workflows/compile.yaml @@ -14,4 +14,4 @@ jobs: - name: Checkout repository uses: actions/checkout@v3 - name: Build Container - run: docker build -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" . + run: docker build . From 777ae330ad326041de4447b431804dc12b46333f Mon Sep 17 00:00:00 2001 From: mightypanders Date: Mon, 19 Jun 2023 23:34:25 +0200 Subject: [PATCH 30/50] remove master branch restriction on docker-build since the pipeline is only ever called on a tag, which will by convention only be committed to the master branch, we can be reasonably sure that we are on the correct branch. Tags are independent on branches which makes it impossible to check for tag AND master branch ref at the same time. --- .gitea/workflows/docker-build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/docker-build.yaml b/.gitea/workflows/docker-build.yaml index 26efd1d..2a07f26 100644 --- a/.gitea/workflows/docker-build.yaml +++ b/.gitea/workflows/docker-build.yaml @@ -11,7 +11,7 @@ env: jobs: build-docker-image: runs-on: ubuntu-latest - if: gitea.ref == 'refs/heads/master' + #if: gitea.ref == 'refs/heads/master' container: catthehacker/ubuntu:act-latest permissions: contents: read From 8569a3e1e6c65b71aca3ef261234539034209804 Mon Sep 17 00:00:00 2001 From: Sammy Date: Thu, 22 Jun 2023 18:49:01 +0200 Subject: [PATCH 31/50] Create option "none of that" in voting --- server/events/guildScheduledEventCreate.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/events/guildScheduledEventCreate.ts b/server/events/guildScheduledEventCreate.ts index 985bb5c..1d61636 100644 --- a/server/events/guildScheduledEventCreate.ts +++ b/server/events/guildScheduledEventCreate.ts @@ -6,11 +6,13 @@ import { client, yavinJellyfinHandler } from "../.."; import { config } from "../configuration"; import { Maybe } from "../interfaces"; import { logger } from "../logger"; +import { SemanticClassificationFormat } from "typescript"; 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 @@ -40,11 +42,12 @@ export async function execute(event: GuildScheduledEvent) { } const date = format(event.scheduledStartAt, "dd.MM") const time = format(event.scheduledStartAt, "HH:mm") - 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` + 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]).concat("\n") } + message = message.concat(NONE_OF_THAT).concat(": Wenn dir nichts davon gefällt.") const options: MessageCreateOptions = { allowedMentions: { parse: ["roles"]}, @@ -56,6 +59,7 @@ export async function execute(event: GuildScheduledEvent) { 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 } From e8c58d5ff82e2c164e07282b201a063f3ea38ea8 Mon Sep 17 00:00:00 2001 From: Sammy Date: Thu, 22 Jun 2023 19:35:47 +0200 Subject: [PATCH 32/50] add event box to create message --- server/events/guildScheduledEventCreate.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/server/events/guildScheduledEventCreate.ts b/server/events/guildScheduledEventCreate.ts index 1d61636..74071b2 100644 --- a/server/events/guildScheduledEventCreate.ts +++ b/server/events/guildScheduledEventCreate.ts @@ -6,7 +6,6 @@ import { client, yavinJellyfinHandler } from "../.."; import { config } from "../configuration"; import { Maybe } from "../interfaces"; import { logger } from "../logger"; -import { SemanticClassificationFormat } from "typescript"; export const name = 'guildScheduledEventCreate' @@ -42,7 +41,7 @@ export async function execute(event: GuildScheduledEvent) { } const date = format(event.scheduledStartAt, "dd.MM") const time = format(event.scheduledStartAt, "HH:mm") - 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` + 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 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]).concat("\n") @@ -51,7 +50,7 @@ export async function execute(event: GuildScheduledEvent) { const options: MessageCreateOptions = { allowedMentions: { parse: ["roles"]}, - content: message + content: message, } const sentMessage: Message = await (await announcementChannel.fetch()).send(options) From 9da8f47784645b093718c3aec9176ffacd5e45fe Mon Sep 17 00:00:00 2001 From: Sammy Date: Thu, 22 Jun 2023 19:38:59 +0200 Subject: [PATCH 33/50] add event box to vote closed message --- server/commands/closepoll.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/commands/closepoll.ts b/server/commands/closepoll.ts index 78a2a0b..ff3dfd8 100644 --- a/server/commands/closepoll.ts +++ b/server/commands/closepoll.ts @@ -75,7 +75,7 @@ export async function closePoll(guild: Guild, requestId: string) { 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] <@&${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"] } From d61457cb5f2aa04f7e1333f06c849b880f9d8ebe Mon Sep 17 00:00:00 2001 From: Sammy Date: Thu, 22 Jun 2023 23:05:47 +0200 Subject: [PATCH 34/50] 1.0.2 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 65cc6d7..8acccd3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-jellyfin-discord-bot", - "version": "1.0.1", + "version": "1.0.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "node-jellyfin-discord-bot", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "dependencies": { "@discordjs/rest": "^1.7.0", diff --git a/package.json b/package.json index 1a8568b..62c4c85 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-jellyfin-discord-bot", - "version": "1.0.1", + "version": "1.0.2", "description": "A discord bot to sync jellyfin accounts with discord roles", "main": "index.js", "license": "MIT", From b9f65125dcf72ef6f6f4e5b6482a77828911b803 Mon Sep 17 00:00:00 2001 From: Sammy Date: Thu, 22 Jun 2023 23:18:06 +0200 Subject: [PATCH 35/50] Hopefully fix ci/cd --- .gitea/workflows/docker-build.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.gitea/workflows/docker-build.yaml b/.gitea/workflows/docker-build.yaml index 2a07f26..4cce4d2 100644 --- a/.gitea/workflows/docker-build.yaml +++ b/.gitea/workflows/docker-build.yaml @@ -19,11 +19,9 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v3 - - name: Get Package Version - run: VERSION = node -p "require('./package.json').version" - name: Log in to the Container registry run: docker login -u ${{ env.USER }} -p ${{ secrets.TOKEN }} ${{ env.REGISTRY }} - name: Build Container - run: docker build -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}". + run: docker build -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ node -p "require('./package.json').version" }}". - name: Push Container run: docker push --all-tags "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" From a18406e7e4d2733327e44cd7a64b557717ea74c6 Mon Sep 17 00:00:00 2001 From: Sammy Date: Thu, 22 Jun 2023 23:18:41 +0200 Subject: [PATCH 36/50] 1.0.3 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8acccd3..cced63c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-jellyfin-discord-bot", - "version": "1.0.2", + "version": "1.0.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "node-jellyfin-discord-bot", - "version": "1.0.2", + "version": "1.0.3", "license": "MIT", "dependencies": { "@discordjs/rest": "^1.7.0", diff --git a/package.json b/package.json index 62c4c85..b8ee888 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-jellyfin-discord-bot", - "version": "1.0.2", + "version": "1.0.3", "description": "A discord bot to sync jellyfin accounts with discord roles", "main": "index.js", "license": "MIT", From 9af847f2349763c9e619bc7751d6e3c75b35186e Mon Sep 17 00:00:00 2001 From: Sammy Date: Thu, 22 Jun 2023 23:23:38 +0200 Subject: [PATCH 37/50] Fix docker-build for good --- .gitea/workflows/docker-build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/docker-build.yaml b/.gitea/workflows/docker-build.yaml index 4cce4d2..586bfb7 100644 --- a/.gitea/workflows/docker-build.yaml +++ b/.gitea/workflows/docker-build.yaml @@ -24,4 +24,4 @@ jobs: - name: Build Container run: docker build -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ node -p "require('./package.json').version" }}". - name: Push Container - run: docker push --all-tags "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" + run: docker push --all-tags "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" From ee363e065cde45671e55fd662583ede95c4f2d27 Mon Sep 17 00:00:00 2001 From: Sammy Date: Thu, 22 Jun 2023 23:23:56 +0200 Subject: [PATCH 38/50] 1.0.4 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index cced63c..5c1a027 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-jellyfin-discord-bot", - "version": "1.0.3", + "version": "1.0.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "node-jellyfin-discord-bot", - "version": "1.0.3", + "version": "1.0.4", "license": "MIT", "dependencies": { "@discordjs/rest": "^1.7.0", diff --git a/package.json b/package.json index b8ee888..536ee7a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-jellyfin-discord-bot", - "version": "1.0.3", + "version": "1.0.4", "description": "A discord bot to sync jellyfin accounts with discord roles", "main": "index.js", "license": "MIT", From 5b98c9bf2fe955d09ef8d7be50d24988ec5b09a1 Mon Sep 17 00:00:00 2001 From: Sammy Date: Fri, 23 Jun 2023 14:37:29 +0200 Subject: [PATCH 39/50] Rename event files to specific case We can add multiple eventhandlers per eventname. To avoid confusion and large files and to improve concise file names the event files were renamed --- server/commands/closepoll.ts | 2 +- server/events/announceManualWatchparty.ts | 26 +++++++++++++++++++ ...ntCreate.ts => autoCreateVoteByWPEvent.ts} | 3 +-- ...Update.ts => handlePermJFAccountByRole.ts} | 0 ...ate.ts => handleTempJFUserByVoiceEvent.ts} | 0 ...date.ts => handleTempJFUsersByWPEvents.ts} | 0 6 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 server/events/announceManualWatchparty.ts rename server/events/{guildScheduledEventCreate.ts => autoCreateVoteByWPEvent.ts} (96%) rename server/events/{guildMemberUpdate.ts => handlePermJFAccountByRole.ts} (100%) rename server/events/{voiceStateUpdate.ts => handleTempJFUserByVoiceEvent.ts} (100%) rename server/events/{guildScheduledEventUpdate.ts => handleTempJFUsersByWPEvents.ts} (100%) diff --git a/server/commands/closepoll.ts b/server/commands/closepoll.ts index ff3dfd8..8d0a3ba 100644 --- a/server/commands/closepoll.ts +++ b/server/commands/closepoll.ts @@ -3,7 +3,7 @@ import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildSchedu import { v4 as uuid } from 'uuid' import { client } from '../..' import { config } from '../configuration' -import { Emotes } from '../events/guildScheduledEventCreate' +import { Emotes } from '../events/autoCreateVoteByWPEvent' import { Maybe } from '../interfaces' import { logger } from '../logger' import { Command } from '../structures/command' diff --git a/server/events/announceManualWatchparty.ts b/server/events/announceManualWatchparty.ts new file mode 100644 index 0000000..e5b0e8f --- /dev/null +++ b/server/events/announceManualWatchparty.ts @@ -0,0 +1,26 @@ +import { format } from "date-fns"; +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"; +import { Maybe } from "../interfaces"; +import { logger } from "../logger"; + + +export const name = 'guildScheduledEventCreate' + +export async function execute(event: GuildScheduledEvent) { + const guildId = event.guildId + const requestId = uuid() + + if(event.description?.includes("!wp")) { + logger.info("Got manual create event of watchparty event!") + + + + + + } + +} \ No newline at end of file diff --git a/server/events/guildScheduledEventCreate.ts b/server/events/autoCreateVoteByWPEvent.ts similarity index 96% rename from server/events/guildScheduledEventCreate.ts rename to server/events/autoCreateVoteByWPEvent.ts index 74071b2..5ea0e5f 100644 --- a/server/events/guildScheduledEventCreate.ts +++ b/server/events/autoCreateVoteByWPEvent.ts @@ -17,8 +17,7 @@ export let task: ScheduledTask | undefined export async function execute(event: GuildScheduledEvent) { const requestId = uuid() - logger.debug(`New event created: ${JSON.stringify(event, null, 2)}`, { guildId: event.guildId, requestId }) - + if (event.name.toLowerCase().includes("!nextwp")) { 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 }) diff --git a/server/events/guildMemberUpdate.ts b/server/events/handlePermJFAccountByRole.ts similarity index 100% rename from server/events/guildMemberUpdate.ts rename to server/events/handlePermJFAccountByRole.ts diff --git a/server/events/voiceStateUpdate.ts b/server/events/handleTempJFUserByVoiceEvent.ts similarity index 100% rename from server/events/voiceStateUpdate.ts rename to server/events/handleTempJFUserByVoiceEvent.ts diff --git a/server/events/guildScheduledEventUpdate.ts b/server/events/handleTempJFUsersByWPEvents.ts similarity index 100% rename from server/events/guildScheduledEventUpdate.ts rename to server/events/handleTempJFUsersByWPEvents.ts From 31e440434e7d26651edfec1ab7a8175d7376c05a Mon Sep 17 00:00:00 2001 From: Sammy Date: Fri, 23 Jun 2023 15:51:07 +0200 Subject: [PATCH 40/50] properly clean up wp announcements without event only --- server/events/announceManualWatchparty.ts | 30 ++++++++--- server/events/autoCreateVoteByWPEvent.ts | 6 +-- .../events/deleteAnnouncementsWhenWPEnds.ts | 52 +++++++++++++++++++ server/events/handleTempJFUsersByWPEvents.ts | 12 ++--- server/helper/dateHelper.ts | 16 ++++++ 5 files changed, 97 insertions(+), 19 deletions(-) create mode 100644 server/events/deleteAnnouncementsWhenWPEnds.ts create mode 100644 server/helper/dateHelper.ts diff --git a/server/events/announceManualWatchparty.ts b/server/events/announceManualWatchparty.ts index e5b0e8f..a37f5d3 100644 --- a/server/events/announceManualWatchparty.ts +++ b/server/events/announceManualWatchparty.ts @@ -1,9 +1,8 @@ -import { format } from "date-fns"; -import { GuildScheduledEvent, Message, MessageCreateOptions, TextChannel } from "discord.js"; -import { ScheduledTask } from "node-cron"; +import { GuildScheduledEvent, TextChannel } from "discord.js"; import { v4 as uuid } from "uuid"; -import { client, yavinJellyfinHandler } from "../.."; +import { client } from "../.."; import { config } from "../configuration"; +import { createDateStringFromEvent } from "../helper/dateHelper"; import { Maybe } from "../interfaces"; import { logger } from "../logger"; @@ -13,14 +12,33 @@ export const name = 'guildScheduledEventCreate' export async function execute(event: GuildScheduledEvent) { const guildId = event.guildId const requestId = uuid() + try { + if (!event.description) { + logger.debug("Got GuildScheduledEventCreate event. But has no description. Aborting.") + return + } - if(event.description?.includes("!wp")) { - logger.info("Got manual create event of watchparty event!") + if (event.description.includes("!wp")) { + logger.info("Got manual create event of watchparty event!", { guildId, requestId }) + const channel: Maybe = client.getAnnouncementChannelForGuild(guildId) + if (!channel) { + logger.error("Could not obtain announcement channel. Aborting announcement.", { guildId, requestId }) + 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)}` + channel.send(message) + } else { + logger.debug("Got GuildScheduledEventCreate event but no !wp in description. Not creating manual wp announcement.") + } + } catch (error) { + // sendFailureDM(error) + logger.error(error, { guildId, requestId }) } + } \ No newline at end of file diff --git a/server/events/autoCreateVoteByWPEvent.ts b/server/events/autoCreateVoteByWPEvent.ts index 5ea0e5f..6377928 100644 --- a/server/events/autoCreateVoteByWPEvent.ts +++ b/server/events/autoCreateVoteByWPEvent.ts @@ -1,9 +1,9 @@ -import { format } from "date-fns"; 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"; +import { createDateStringFromEvent } from "../helper/dateHelper"; import { Maybe } from "../interfaces"; import { logger } from "../logger"; @@ -38,9 +38,7 @@ export async function execute(event: GuildScheduledEvent) { logger.info("EVENT DOES NOT HAVE STARTDATE; CANCELLING", {guildId: event.guildId, requestId}) return } - const date = format(event.scheduledStartAt, "dd.MM") - const time = format(event.scheduledStartAt, "HH:mm") - 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 am ${date} um ${time}! Stimme hierunter für den nächsten Film ab!\n` + 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") diff --git a/server/events/deleteAnnouncementsWhenWPEnds.ts b/server/events/deleteAnnouncementsWhenWPEnds.ts new file mode 100644 index 0000000..df35f4b --- /dev/null +++ b/server/events/deleteAnnouncementsWhenWPEnds.ts @@ -0,0 +1,52 @@ +import { Collection, GuildScheduledEvent, GuildScheduledEventStatus, Message } from "discord.js"; +import { v4 as uuid } from "uuid"; +import { client } from "../.."; +import { logger } from "../logger"; + + +export const name = 'guildScheduledEventUpdate' + +export async function execute(oldEvent: GuildScheduledEvent, newEvent: GuildScheduledEvent) { + const requestId = uuid() + try { + if (!newEvent.guild) { + logger.error("Event has no guild, aborting.", { guildId: newEvent.guildId, requestId }) + return + } + const guildId = newEvent.guildId + + if (newEvent.description?.toLowerCase().includes("!wp") && newEvent.status === GuildScheduledEventStatus.Completed) { + logger.info("A watchparty ended. Cleaning up announcements!", { guildId, requestId }) + const announcementChannel = client.getAnnouncementChannelForGuild(newEvent.guild.id) + if (!announcementChannel) { + logger.error("Could not find announcement channel. Aborting", { guildId: newEvent.guild.id, requestId }) + return + } + + const events = await newEvent.guild.scheduledEvents.fetch() + + const wpAnnouncements = (await announcementChannel.messages.fetch()).filter(message => !message.cleanContent.includes("[initial]")) + const announcementsWithoutEvent = filterAnnouncementsByPendingWPs(wpAnnouncements, events) + logger.info(`Deleting ${announcementsWithoutEvent.length} announcements.`, {guildId, requestId}) + announcementsWithoutEvent.forEach(message => message.delete()) + } + } catch (error) { + logger.error(error, { guildId: newEvent.guildId, requestId }) + } +} + +function filterAnnouncementsByPendingWPs(messages: Collection>, events: Collection>): Message[] { + const filteredMessages: Message[] = [] + for (const message of messages.values()) { + let foundEventForMessage = false + for (const event of events.values()) { + if (message.cleanContent.includes(event.id)) { //announcement always has eventid because of eventbox + foundEventForMessage = true + } + } + if(!foundEventForMessage){ + filteredMessages.push(message) + } + } + return filteredMessages +} \ No newline at end of file diff --git a/server/events/handleTempJFUsersByWPEvents.ts b/server/events/handleTempJFUsersByWPEvents.ts index eed87e7..42a719c 100644 --- a/server/events/handleTempJFUsersByWPEvents.ts +++ b/server/events/handleTempJFUsersByWPEvents.ts @@ -1,6 +1,6 @@ import { GuildMember, GuildScheduledEvent, GuildScheduledEventStatus } from "discord.js"; import { v4 as uuid } from "uuid"; -import { client, jellyfinHandler } from "../.."; +import { jellyfinHandler } from "../.."; import { getGuildSpecificTriggerRoleId } from "../helper/roleFilter"; import { logger } from "../logger"; @@ -10,7 +10,7 @@ export const name = 'guildScheduledEventUpdate' export async function execute(oldEvent: GuildScheduledEvent, newEvent: GuildScheduledEvent) { try { const requestId = uuid() - logger.debug(`Got scheduledEvent update. New Event: ${JSON.stringify(newEvent, null, 2)}`, { guildId: newEvent.guildId, requestId }) + // logger.debug(`Got scheduledEvent update. New Event: ${JSON.stringify(newEvent, null, 2)}`, { guildId: newEvent.guildId, requestId }) if (!newEvent.guild) { logger.error("Event has no guild, aborting.", { guildId: newEvent.guildId, requestId }) return @@ -32,13 +32,7 @@ export async function execute(oldEvent: GuildScheduledEvent, newEvent: GuildSche if (newEvent.status === GuildScheduledEventStatus.Active) createJFUsers(members, newEvent.name, requestId) else { - const announcementChannel = await client.getAnnouncementChannelForGuild(newEvent.guild.id) - if(!announcementChannel) { - logger.error("Could not find announcement channel. Aborting", { guildId: newEvent.guild.id, requestId }) - return - } - const announcements = (await announcementChannel.messages.fetch()).filter(message => !message.pinned) - announcements.forEach(message => message.delete()) + members.forEach(member => { member.createDM().then(channel => channel.send(`Die Watchparty ist vorbei, dein Account wurde wieder gelöscht. Wenn du einen permanenten Account haben möchtest, melde dich bei Samantha oder Marukus.`)) }) diff --git a/server/helper/dateHelper.ts b/server/helper/dateHelper.ts new file mode 100644 index 0000000..f51752a --- /dev/null +++ b/server/helper/dateHelper.ts @@ -0,0 +1,16 @@ +import { format } from "date-fns"; +import { GuildScheduledEvent } from "discord.js"; +import { logger } from "../logger"; + +export function createDateStringFromEvent(event: GuildScheduledEvent, requestId: string, guildId?: string): string { + if(!event.scheduledStartAt) { + logger.error("Event has no start. Cannot create dateString.", {guildId, requestId}) + return "habe keinen Startzeitpunkt ermitteln können" + } + + const date = format(event.scheduledStartAt, "dd.MM") + const time = format(event.scheduledStartAt, "HH:mm") + + + return `am ${date} um ${time}` +} \ No newline at end of file From 45d87275bfd91981f98af4661ec03e34509624c8 Mon Sep 17 00:00:00 2001 From: Sammy Date: Fri, 23 Jun 2023 15:56:36 +0200 Subject: [PATCH 41/50] prevent announcement when description contains !private --- 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 a37f5d3..da29f13 100644 --- a/server/events/announceManualWatchparty.ts +++ b/server/events/announceManualWatchparty.ts @@ -20,6 +20,10 @@ export async function execute(event: GuildScheduledEvent) { if (event.description.includes("!wp")) { logger.info("Got manual create event of watchparty event!", { guildId, requestId }) + if(event.description.includes("!private")) { + logger.info("Event description contains \"!private\". Won't announce.", { guildId, requestId }) + return + } const channel: Maybe = client.getAnnouncementChannelForGuild(guildId) @@ -32,7 +36,7 @@ export async function execute(event: GuildScheduledEvent) { channel.send(message) } else { - logger.debug("Got GuildScheduledEventCreate event but no !wp in description. Not creating manual wp announcement.") + logger.debug("Got GuildScheduledEventCreate event but no !wp in description. Not creating manual wp announcement.", { guildId, requestId }) } } catch (error) { From 3a5ea5d4ff2abf4dd3416f698361e6073e1e51fa Mon Sep 17 00:00:00 2001 From: Sammy Date: Fri, 23 Jun 2023 19:30:17 +0200 Subject: [PATCH 42/50] improve message clarity when no start date in event found --- server/helper/dateHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/helper/dateHelper.ts b/server/helper/dateHelper.ts index f51752a..914f533 100644 --- a/server/helper/dateHelper.ts +++ b/server/helper/dateHelper.ts @@ -5,7 +5,7 @@ import { logger } from "../logger"; export function createDateStringFromEvent(event: GuildScheduledEvent, requestId: string, guildId?: string): string { if(!event.scheduledStartAt) { logger.error("Event has no start. Cannot create dateString.", {guildId, requestId}) - return "habe keinen Startzeitpunkt ermitteln können" + return `"habe keinen Startzeitpunkt ermitteln können"` } const date = format(event.scheduledStartAt, "dd.MM") From a4d7c57d102c401d812d4e9630ec86876208f44c Mon Sep 17 00:00:00 2001 From: Sammy Date: Fri, 23 Jun 2023 19:33:31 +0200 Subject: [PATCH 43/50] 1.1.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5c1a027..e5191de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-jellyfin-discord-bot", - "version": "1.0.4", + "version": "1.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "node-jellyfin-discord-bot", - "version": "1.0.4", + "version": "1.1.0", "license": "MIT", "dependencies": { "@discordjs/rest": "^1.7.0", diff --git a/package.json b/package.json index 536ee7a..08a1f63 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-jellyfin-discord-bot", - "version": "1.0.4", + "version": "1.1.0", "description": "A discord bot to sync jellyfin accounts with discord roles", "main": "index.js", "license": "MIT", From f314b2f355f49e3b001200119a30cc79140b7491 Mon Sep 17 00:00:00 2001 From: mightypanders Date: Fri, 23 Jun 2023 19:44:44 +0200 Subject: [PATCH 44/50] maybe fix a docker build typo --- .gitea/workflows/docker-build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/docker-build.yaml b/.gitea/workflows/docker-build.yaml index 586bfb7..2757a1a 100644 --- a/.gitea/workflows/docker-build.yaml +++ b/.gitea/workflows/docker-build.yaml @@ -22,6 +22,6 @@ jobs: - name: Log in to the Container registry run: docker login -u ${{ env.USER }} -p ${{ secrets.TOKEN }} ${{ env.REGISTRY }} - name: Build Container - run: docker build -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ node -p "require('./package.json').version" }}". + run: docker build -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ node -p "require('./package.json').version" }}" . - name: Push Container run: docker push --all-tags "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" From 51ebf2e939d5cd8b65f4c7e3641a128ba0a8c3cf Mon Sep 17 00:00:00 2001 From: mightypanders Date: Fri, 23 Jun 2023 19:44:58 +0200 Subject: [PATCH 45/50] 1.1.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e5191de..f82596e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-jellyfin-discord-bot", - "version": "1.1.0", + "version": "1.1.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "node-jellyfin-discord-bot", - "version": "1.1.0", + "version": "1.1.1", "license": "MIT", "dependencies": { "@discordjs/rest": "^1.7.0", diff --git a/package.json b/package.json index 08a1f63..443ce4f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-jellyfin-discord-bot", - "version": "1.1.0", + "version": "1.1.1", "description": "A discord bot to sync jellyfin accounts with discord roles", "main": "index.js", "license": "MIT", From d5d82043f022f29bb797b4d2ebaef5809296623b Mon Sep 17 00:00:00 2001 From: mightypanders Date: Fri, 23 Jun 2023 19:46:06 +0200 Subject: [PATCH 46/50] temporarily remove second tag on docker build --- .gitea/workflows/docker-build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/docker-build.yaml b/.gitea/workflows/docker-build.yaml index 2757a1a..5700667 100644 --- a/.gitea/workflows/docker-build.yaml +++ b/.gitea/workflows/docker-build.yaml @@ -22,6 +22,6 @@ jobs: - name: Log in to the Container registry run: docker login -u ${{ env.USER }} -p ${{ secrets.TOKEN }} ${{ env.REGISTRY }} - name: Build Container - run: docker build -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ node -p "require('./package.json').version" }}" . + run: docker build -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" . - name: Push Container run: docker push --all-tags "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" From c1a449bafe4382be0470e291f416a314de9ff7de Mon Sep 17 00:00:00 2001 From: mightypanders Date: Fri, 23 Jun 2023 19:46:20 +0200 Subject: [PATCH 47/50] 1.1.2 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index f82596e..87a673f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-jellyfin-discord-bot", - "version": "1.1.1", + "version": "1.1.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "node-jellyfin-discord-bot", - "version": "1.1.1", + "version": "1.1.2", "license": "MIT", "dependencies": { "@discordjs/rest": "^1.7.0", diff --git a/package.json b/package.json index 443ce4f..822f062 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-jellyfin-discord-bot", - "version": "1.1.1", + "version": "1.1.2", "description": "A discord bot to sync jellyfin accounts with discord roles", "main": "index.js", "license": "MIT", From fa9998e92c01ee7b7bc81a41a809729f30e1a58c Mon Sep 17 00:00:00 2001 From: Sammy Date: Fri, 23 Jun 2023 21:23:54 +0200 Subject: [PATCH 48/50] Unallow transcoding per default for new users --- server/jellyfin/handler.ts | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/server/jellyfin/handler.ts b/server/jellyfin/handler.ts index e47ac6c..8dc1dc9 100644 --- a/server/jellyfin/handler.ts +++ b/server/jellyfin/handler.ts @@ -2,7 +2,7 @@ import { GuildMember } from "discord.js"; import { JellyfinConfig, Maybe, PermissionLevel } from "../interfaces"; import { logger } from "../logger"; import { CreateUserByNameOperationRequest, DeleteUserRequest, GetItemsRequest, ItemsApi, SystemApi, UpdateUserPasswordOperationRequest, UpdateUserPolicyOperationRequest, UserApi } from "./apis"; -import { BaseItemDto, UpdateUserPasswordRequest } from "./models"; +import { BaseItemDto, UpdateUserPasswordRequest, UpdateUserPolicyRequest } from "./models"; import { UserDto } from "./models/UserDto"; import { Configuration, ConfigurationParameters } from "./runtime"; @@ -52,24 +52,46 @@ export class JellyfinHandler { return (Math.random() * 10000 + 10000).toFixed(0) } - public async createUserAccountForDiscordUser(discordUser: GuildMember, level: PermissionLevel, guildId?: string, requestId?: string): Promise { + public async createUserAccountForDiscordUser(discordUser: GuildMember, level: PermissionLevel, requestId: string, guildId?: string): Promise { const newUserName = this.generateJFUserName(discordUser, level) logger.info(`New Username for ${discordUser.displayName}: ${newUserName}`, { guildId, requestId }) const req: CreateUserByNameOperationRequest = { createUserByNameRequest: { name: newUserName, - password: this.generatePasswordForUser(), + password: this.generatePasswordForUser() } } logger.debug(JSON.stringify(req), { requestId, guildId }) const createResult = await this.userApi.createUserByName(req) if (createResult) { + if(createResult.policy) { + this.setUserPermissions(createResult, requestId, guildId) + } (await discordUser.createDM()).send(`Ich hab dir mal nen Account angelegt :)\nDein Username ist ${createResult.name}, dein Password ist "${req.createUserByNameRequest.password}"!`) return createResult } else throw new Error('Could not create User in Jellyfin') } + public async setUserPermissions(user: UserDto, requestId: string, guildId?: string) { + if(!user.policy || !user.id) { + logger.error(`Cannot update user policy. User ${user.name} has no policy to modify`, {guildId, requestId}) + return + } + user.policy.enableVideoPlaybackTranscoding = false + + const operation: UpdateUserPolicyRequest = { + ...user.policy, + enableVideoPlaybackTranscoding: false + } + + const request: UpdateUserPolicyOperationRequest = { + userId: user.id, + updateUserPolicyRequest: operation + } + this.userApi.updateUserPolicy(request) + } + public async isUserAlreadyPresent(discordUser: GuildMember, requestId?: string): Promise { const jfuser = await this.getUser(discordUser, requestId) logger.debug(`Presence for DiscordUser ${discordUser.id}:${jfuser !== undefined}`, { guildId: discordUser.guild.id, requestId }) From 61544feabaa3e3a8f8e0e3c446a1a112987c6a25 Mon Sep 17 00:00:00 2001 From: Sammy Date: Fri, 23 Jun 2023 23:46:11 +0200 Subject: [PATCH 49/50] Fix stupid timezone issues --- package-lock.json | 15 +++++++++++++++ package.json | 1 + server/helper/dateHelper.ts | 15 +++++++++++---- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 87a673f..da7f6db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@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", @@ -2626,6 +2627,14 @@ "url": "https://opencollective.com/date-fns" } }, + "node_modules/date-fns-tz": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-2.0.0.tgz", + "integrity": "sha512-OAtcLdB9vxSXTWHdT8b398ARImVwQMyjfYGkKD2zaGpHseG2UPHbHjXELReErZFxWdSLph3c2zOaaTyHfOhERQ==", + "peerDependencies": { + "date-fns": ">=2.0.0" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -8905,6 +8914,12 @@ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==" }, + "date-fns-tz": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-2.0.0.tgz", + "integrity": "sha512-OAtcLdB9vxSXTWHdT8b398ARImVwQMyjfYGkKD2zaGpHseG2UPHbHjXELReErZFxWdSLph3c2zOaaTyHfOhERQ==", + "requires": {} + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", diff --git a/package.json b/package.json index 822f062..51b1333 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@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", diff --git a/server/helper/dateHelper.ts b/server/helper/dateHelper.ts index 914f533..af93486 100644 --- a/server/helper/dateHelper.ts +++ b/server/helper/dateHelper.ts @@ -1,6 +1,8 @@ -import { format } from "date-fns"; +import { format, isToday, toDate } 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"; export function createDateStringFromEvent(event: GuildScheduledEvent, requestId: string, guildId?: string): string { if(!event.scheduledStartAt) { @@ -8,9 +10,14 @@ export function createDateStringFromEvent(event: GuildScheduledEvent, requestId: return `"habe keinen Startzeitpunkt ermitteln können"` } - const date = format(event.scheduledStartAt, "dd.MM") - const time = format(event.scheduledStartAt, "HH:mm") - + const timeZone = 'Europe/Berlin' + const zonedDateTime = utcToZonedTime(event.scheduledStartAt, timeZone) + const time = format(zonedDateTime, "HH:mm", {locale: de}) + + if(isToday(zonedDateTime)) { + return `heute um ${time}` + } + const date = format(zonedDateTime, "eeee dd.MM", {locale: de}) return `am ${date} um ${time}` } \ No newline at end of file From e52e845851b52165909120ac1e6f7ae2523b442e Mon Sep 17 00:00:00 2001 From: Sammy Date: Fri, 23 Jun 2023 23:46:52 +0200 Subject: [PATCH 50/50] 1.1.3 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index da7f6db..f7c99e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "node-jellyfin-discord-bot", - "version": "1.1.2", + "version": "1.1.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "node-jellyfin-discord-bot", - "version": "1.1.2", + "version": "1.1.3", "license": "MIT", "dependencies": { "@discordjs/rest": "^1.7.0", diff --git a/package.json b/package.json index 51b1333..d4f0a57 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-jellyfin-discord-bot", - "version": "1.1.2", + "version": "1.1.3", "description": "A discord bot to sync jellyfin accounts with discord roles", "main": "index.js", "license": "MIT",