diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..2d1d196 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +root = true +[*] +indent_style = tab +tab_width = 4 +[*.ts] +indent_style = tab +tab_width = 4 diff --git a/index.ts b/index.ts index 30ef40c..9a6fb79 100644 --- a/index.ts +++ b/index.ts @@ -5,21 +5,21 @@ import { JellyfinHandler } from "./server/jellyfin/handler" import { attachedImages } from "./server/assets/attachments" const requestId = 'startup' -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 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) export const attachmentImages = attachedImages async function init() { - try { - const users = await jellyfinHandler.getCurrentUsers("", requestId) - logger.info(`Fetched ${users.map(x => x.name).join(', ')} from JF`, { requestId }) - } catch (error) { - logger.error(`Error fetching existing users from Jellyfin`, { requestId }) - } - logger.info(`Starting client`, { requestId }) - client.start() + try { + const users = await jellyfinHandler.getCurrentUsers("", requestId) + logger.info(`Fetched ${users.map(x => x.name).join(', ')} from JF`, { requestId }) + } catch (error) { + logger.error(`Error fetching existing users from Jellyfin`, { requestId }) + } + logger.info(`Starting client`, { requestId }) + client.start() } init() diff --git a/package.json b/package.json index 715ae52..b8961e2 100644 --- a/package.json +++ b/package.json @@ -49,4 +49,4 @@ "rimraf": "^5.0.0", "ts-jest": "^29.1.0" } -} +} \ No newline at end of file diff --git a/server/commands/announce.ts b/server/commands/announce.ts index ca939b9..4a6eff3 100644 --- a/server/commands/announce.ts +++ b/server/commands/announce.ts @@ -8,114 +8,114 @@ import { Command } from '../structures/command' import { RunOptions } from '../types/commandTypes' export default new Command({ - name: 'announce', - description: 'Neues announcement im announcement Channel an alle senden.', - 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() - 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 }) + name: 'announce', + description: 'Neues announcement im announcement Channel an alle senden.', + 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() + 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 }) - if(!announcementType) { - logger.error("Did not get an announcement type!", { guildId, requestId }) - return - } + 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 }) - return - } else { - logger.info(`User ${command.member.displayName} seems to be admin`) - } + 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`) + } - if((announcementType.value).includes("initial")) { - sendInitialAnnouncement(guildId, requestId) - command.followUp("Ist rausgeschickt!") - } else { - command.followUp(`${announcementType.value} ist aktuell noch nicht implementiert`) - } - } + if ((announcementType.value).includes("initial")) { + sendInitialAnnouncement(guildId, requestId) + command.followUp("Ist rausgeschickt!") + } else { + command.followUp(`${announcementType.value} ist aktuell noch nicht implementiert`) + } + } }) 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: Maybe = client.getAnnouncementChannelForGuild(guildId) - if(!announcementChannel) { - logger.error("Could not find announcement channel. Aborting", { guildId, requestId }) - return - } + logger.info("Sending initial announcement") + 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()) - currentPinnedAnnouncementMessages.forEach(message => message.delete()) + 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 = `[initial] 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 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.` - const options: MessageCreateOptions = { - allowedMentions: { parse: ['everyone'] }, - content: body - } - const message: Message = await announcementChannel.send(options) - await message.react("🎫") - await message.pin() + const options: MessageCreateOptions = { + allowedMentions: { parse: ['everyone'] }, + content: body + } + const message: Message = await announcementChannel.send(options) + await message.react("🎫") + await message.pin() } export async function manageAnnouncementRoles(guild: Guild, reaction: MessageReaction, requestId: string) { - const guildId = guild.id - logger.info("Managing roles", { guildId, requestId }) + 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 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 usersWhoWantRole: User[] = (await reaction.users.fetch()).filter(user => !user.bot).map(user => user) - const allUsers = (await guild.members.fetch()) + 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 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 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 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)) + 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}) + 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)) + usersWhoNeedRoleRevoked.forEach(user => user.roles.remove(announcementRole)) + usersWhoNeedRole.forEach(user => user.roles.add(announcementRole)) } diff --git a/server/commands/closepoll.ts b/server/commands/closepoll.ts index 8d0a3ba..7cf05c2 100644 --- a/server/commands/closepoll.ts +++ b/server/commands/closepoll.ts @@ -10,175 +10,175 @@ import { Command } from '../structures/command' import { RunOptions } from '../types/commandTypes' export default new Command({ - name: 'closepoll', - description: 'Aktuelle Umfrage für nächste Watchparty beenden und Gewinner in Event eintragen.', - options: [], - run: async (interaction: RunOptions) => { - const command = interaction.interaction - const requestId = uuid() - if (!command.guild) { - 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 }) + name: 'closepoll', + description: 'Aktuelle Umfrage für nächste Watchparty beenden und Gewinner in Event eintragen.', + options: [], + run: async (interaction: RunOptions) => { + const command = interaction.interaction + const requestId = uuid() + if (!command.guild) { + 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) - } + command.followUp("Alles klar, beende die Umfrage :)") + closePoll(command.guild, requestId) + } }) export async function closePoll(guild: Guild, requestId: string) { - const guildId = guild.id - logger.info("stopping poll", { guildId, requestId }) + const guildId = guild.id + logger.info("stopping poll", { guildId, requestId }) - const announcementChannel: Maybe = client.getAnnouncementChannelForGuild(guildId) - if (!announcementChannel) { - logger.error("Could not find the textchannel. Unable to close poll.", { guildId, requestId }) - return - } + const announcementChannel: Maybe = client.getAnnouncementChannelForGuild(guildId) + if (!announcementChannel) { + logger.error("Could not find the textchannel. Unable to close poll.", { guildId, requestId }) + return + } - const messages: Message[] = (await announcementChannel.messages.fetch()) //todo: fetch only pinned messages - .map((value) => value) - .filter(message => !message.cleanContent.includes("[Abstimmung beendet]") && message.cleanContent.includes("[Abstimmung]")) - .sort((a, b) => b.createdTimestamp - a.createdTimestamp) + const messages: Message[] = (await announcementChannel.messages.fetch()) //todo: fetch only pinned messages + .map((value) => value) + .filter(message => !message.cleanContent.includes("[Abstimmung beendet]") && message.cleanContent.includes("[Abstimmung]")) + .sort((a, b) => b.createdTimestamp - a.createdTimestamp) - if (!messages || messages.length <= 0) { - logger.info("Could not find any vote messages. Cancelling pollClose", { guildId, requestId }) - return - } + if (!messages || messages.length <= 0) { + logger.info("Could not find any vote messages. Cancelling pollClose", { guildId, requestId }) + return + } - const lastMessage: Message = messages[0] + const lastMessage: Message = messages[0] - logger.debug(`Found messages: ${JSON.stringify(messages, null, 2)}`, { guildId, requestId }) + logger.debug(`Found messages: ${JSON.stringify(messages, null, 2)}`, { guildId, requestId }) - logger.debug(`Last message: ${JSON.stringify(lastMessage, null, 2)}`, { guildId, requestId }) + logger.debug(`Last message: ${JSON.stringify(lastMessage, null, 2)}`, { guildId, requestId }) - const votes = await (await getVotesByEmote(lastMessage, guildId, requestId)) - .sort((a, b) => b.count - a.count) + const votes = await (await getVotesByEmote(lastMessage, guildId, requestId)) + .sort((a, b) => b.count - a.count) - logger.debug(`votes: ${JSON.stringify(votes, null, 2)}`, { guildId, requestId }) + logger.debug(`votes: ${JSON.stringify(votes, null, 2)}`, { guildId, requestId }) - logger.info("Deleting vote message") - await lastMessage.delete() - const event = await getEvent(guild, guild.id, requestId) - if (event) { - updateEvent(event, votes, guild, guildId, requestId) - sendVoteClosedMessage(event, votes[0].movie, guildId, requestId) - } + 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 + //lastMessage.unpin() //todo: uncomment when bot has permission to pin/unpin } async function sendVoteClosedMessage(event: GuildScheduledEvent, movie: string, guildId: string, requestId: string) { - const date = event.scheduledStartAt ? format(event.scheduledStartAt, "dd.MM") : "Fehler, event hatte kein Datum" - const time = event.scheduledStartAt ? format(event.scheduledStartAt, "HH:mm") : "Fehler, event hatte kein Datum" - const body = `[Abstimmung beendet] für https://discord.com/events/${event.guildId}/${event.id}\n<@&${config.bot.announcement_role}> Wir gucken ${movie} am ${date} um ${time}` - const options: MessageCreateOptions = { - content: body, - allowedMentions: { parse: ["roles"] } - } - const announcementChannel = client.getAnnouncementChannelForGuild(guildId) - logger.info("Sending vote closed message.", { guildId, requestId }) - if (!announcementChannel) { - logger.error("Could not find announcement channel. Please fix!", { guildId, requestId }) - return - } - announcementChannel.send(options) + const date = event.scheduledStartAt ? format(event.scheduledStartAt, "dd.MM") : "Fehler, event hatte kein Datum" + const time = event.scheduledStartAt ? format(event.scheduledStartAt, "HH:mm") : "Fehler, event hatte kein Datum" + const body = `[Abstimmung beendet] für https://discord.com/events/${event.guildId}/${event.id}\n<@&${config.bot.announcement_role}> Wir gucken ${movie} am ${date} um ${time}` + const options: MessageCreateOptions = { + content: body, + allowedMentions: { parse: ["roles"] } + } + const announcementChannel = client.getAnnouncementChannelForGuild(guildId) + logger.info("Sending vote closed message.", { guildId, requestId }) + if (!announcementChannel) { + logger.error("Could not find announcement channel. Please fix!", { guildId, requestId }) + return + } + announcementChannel.send(options) } async function updateEvent(voteEvent: GuildScheduledEvent, votes: Vote[], guild: Guild, guildId: string, requestId: string) { - logger.info(`Updating event with movie ${votes[0].movie}.`, { guildId, requestId }) - const options: GuildScheduledEventEditOptions> = { - name: votes[0].movie, - description: `!wp\nNummer 2: ${votes[1].movie} mit ${votes[1].count - 1} Stimmen\nNummer 3: ${votes[2].movie} mit ${votes[2].count - 1} Stimmen` - } - logger.debug(`Updating event: ${JSON.stringify(voteEvent, null, 2)}`, { guildId, requestId }) - logger.info("Updating event.", { guildId, requestId }) - voteEvent.edit(options) + logger.info(`Updating event with movie ${votes[0].movie}.`, { guildId, requestId }) + const options: GuildScheduledEventEditOptions> = { + name: votes[0].movie, + description: `!wp\nNummer 2: ${votes[1].movie} mit ${votes[1].count - 1} Stimmen\nNummer 3: ${votes[2].movie} mit ${votes[2].count - 1} Stimmen` + } + logger.debug(`Updating event: ${JSON.stringify(voteEvent, null, 2)}`, { guildId, requestId }) + logger.info("Updating event.", { guildId, requestId }) + voteEvent.edit(options) } async function getEvent(guild: Guild, guildId: string, requestId: string): Promise { - const voteEvents = (await guild.scheduledEvents.fetch()) - .map((value) => value) - .filter(event => event.name.toLowerCase().includes("voting offen")) - logger.debug(`Found events: ${JSON.stringify(voteEvents, null, 2)}`, { guildId, requestId }) + 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] + if (!voteEvents || voteEvents.length <= 0) { + logger.error("Could not find vote event. Cancelling update!", { guildId, requestId }) + return null + } + return voteEvents[0] } type Vote = { - emote: string, //todo habs nicht hinbekommen hier Emotes zu nutzen - count: number, - movie: string + emote: string, //todo habs nicht hinbekommen hier Emotes zu nutzen + count: number, + movie: string } async function getVotesByEmote(message: Message, guildId: string, requestId: string): Promise { - const votes: Vote[] = [] - logger.debug(`Number of items in emotes: ${Object.values(Emotes).length}`, { guildId, requestId }) - for (let i = 0; i < Object.keys(Emotes).length / 2; i++) { - const emote = Emotes[i] - logger.debug(`Getting reaction for emote ${emote}`, { guildId, requestId }) - const reaction = await message.reactions.resolve(emote) - logger.debug(`Reaction for emote ${emote}: ${JSON.stringify(reaction, null, 2)}`, { guildId, requestId }) - if (reaction) { - const vote: Vote = { emote: emote, count: reaction.count, movie: extractMovieFromMessageByEmote(message, emote) } - votes.push(vote) - } - } - return votes + const votes: Vote[] = [] + logger.debug(`Number of items in emotes: ${Object.values(Emotes).length}`, { guildId, requestId }) + for (let i = 0; i < Object.keys(Emotes).length / 2; i++) { + const emote = Emotes[i] + logger.debug(`Getting reaction for emote ${emote}`, { guildId, requestId }) + const reaction = await message.reactions.resolve(emote) + logger.debug(`Reaction for emote ${emote}: ${JSON.stringify(reaction, null, 2)}`, { guildId, requestId }) + if (reaction) { + const vote: Vote = { emote: emote, count: reaction.count, movie: extractMovieFromMessageByEmote(message, emote) } + votes.push(vote) + } + } + return votes } function extractMovieFromMessageByEmote(message: Message, emote: string): string { - const lines = message.cleanContent.split("\n") - const emoteLines = lines.filter(line => line.includes(emote)) + const lines = message.cleanContent.split("\n") + const emoteLines = lines.filter(line => line.includes(emote)) - if (!emoteLines) { - return "" - } - const movie = emoteLines[0].substring(emoteLines[0].indexOf(emote) + emote.length + 2) // plus colon and space + if (!emoteLines) { + return "" + } + const movie = emoteLines[0].substring(emoteLines[0].indexOf(emote) + emote.length + 2) // plus colon and space - return movie + 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 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 updatedEvent = events[0] //add two hours because of different timezones in discord api and Date.now() + if (!updatedEvent.scheduledStartTimestamp) { + logger.error("Event does not have a scheduled start time. Cancelling", { guildId: guild.id, requestId }) + return + } - const createDate: Date = toDate(updatedEvent.createdTimestamp) - const eventDate: Date = toDate(updatedEvent.scheduledStartTimestamp) - const difference: number = differenceInDays(createDate, eventDate) + const createDate: Date = toDate(updatedEvent.createdTimestamp) + const eventDate: Date = toDate(updatedEvent.scheduledStartTimestamp) + const difference: number = differenceInDays(createDate, eventDate) - if (difference <= 2) { - logger.info("Less than two days between event create and event start. Not closing poll.", { guildId: guild.id, requestId }) - return - } + if (difference <= 2) { + logger.info("Less than two days between event create and event start. Not closing poll.", { guildId: guild.id, requestId }) + return + } - const closePollDate: Date = addDays(eventDate, -2) + 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 + if (isAfter(Date.now(), closePollDate)) { + logger.info("Less than two days until event. Closing poll", { guildId: guild.id, requestId }) + closePoll(guild, requestId) + } else { + logger.info(`ScheduledStart: ${closePollDate}. Now: ${toDate(Date.now())}`, { guildId: guild.id, requestId }) + } +} diff --git a/server/commands/guides.ts b/server/commands/guides.ts index bc281cc..caa21a8 100644 --- a/server/commands/guides.ts +++ b/server/commands/guides.ts @@ -8,76 +8,76 @@ import { RunOptions } from '../types/commandTypes' import { configureServer, explainRole, installation, loginInfo, useSyncgroup } from './mitgucken' export default new Command({ - name: 'guides', - description: 'Bekomme eine Auswahl von Guides per DM', - options: [], - run: async (interaction: RunOptions) => { - const requestId = uuid() - const guildId = interaction.interaction.guild?.id - logger.info(`Starting guides interaction for user ${interaction.interaction.user.id}`, { requestId, guildId }) + name: 'guides', + description: 'Bekomme eine Auswahl von Guides per DM', + options: [], + run: async (interaction: RunOptions) => { + const requestId = uuid() + const guildId = interaction.interaction.guild?.id + logger.info(`Starting guides interaction for user ${interaction.interaction.user.id}`, { requestId, guildId }) - const mediaPlayerGuideButton = new ButtonBuilder() - .setCustomId('jfInstallation') - .setLabel('Media Player Installation') - .setStyle(ButtonStyle.Primary) + const mediaPlayerGuideButton = new ButtonBuilder() + .setCustomId('jfInstallation') + .setLabel('Media Player Installation') + .setStyle(ButtonStyle.Primary) - const accountSetupGuideButton = new ButtonBuilder() - .setCustomId('configureServer') - .setLabel('Server einstellen') - .setStyle(ButtonStyle.Primary) + const accountSetupGuideButton = new ButtonBuilder() + .setCustomId('configureServer') + .setLabel('Server einstellen') + .setStyle(ButtonStyle.Primary) - const loginGuideButton = new ButtonBuilder() - .setCustomId('login') - .setLabel('Einloggen') - .setStyle(ButtonStyle.Primary) + const loginGuideButton = new ButtonBuilder() + .setCustomId('login') + .setLabel('Einloggen') + .setStyle(ButtonStyle.Primary) - const useSyncGroupGuideButton = new ButtonBuilder() - .setCustomId('useSyncGroup') - .setLabel('Watch Parties nutzen') - .setStyle(ButtonStyle.Primary) + const useSyncGroupGuideButton = new ButtonBuilder() + .setCustomId('useSyncGroup') + .setLabel('Watch Parties nutzen') + .setStyle(ButtonStyle.Primary) - const roleExplanationButton = new ButtonBuilder() - .setCustomId('explainRoles') - .setLabel('Wie bekomme ich Zugang') - .setStyle(ButtonStyle.Primary) + const roleExplanationButton = new ButtonBuilder() + .setCustomId('explainRoles') + .setLabel('Wie bekomme ich Zugang') + .setStyle(ButtonStyle.Primary) - const row = new ActionRowBuilder() - .addComponents(mediaPlayerGuideButton, accountSetupGuideButton, loginGuideButton, useSyncGroupGuideButton, roleExplanationButton) + const row = new ActionRowBuilder() + .addComponents(mediaPlayerGuideButton, accountSetupGuideButton, loginGuideButton, useSyncGroupGuideButton, roleExplanationButton) - //const userDMchannel = await interaction.interaction.user.createDM() - const response = await interaction.interaction.followUp({ - content: `Hier ist eine Auswahl von Guides.`, - components: [row] - }) + //const userDMchannel = await interaction.interaction.user.createDM() + const response = await interaction.interaction.followUp({ + content: `Hier ist eine Auswahl von Guides.`, + components: [row] + }) - try { + try { - const guideSelection = await response.awaitMessageComponent({ time: 60_000 }) + const guideSelection = await response.awaitMessageComponent({ time: 60_000 }) - if (guideSelection.customId === 'jfInstallation') { - const userDMChannel = await guideSelection.user.createDM() - userDMChannel.send({ embeds: installation(), files: [splashScreen] }) - } else if (guideSelection.customId === 'configureServer') { - const userDMChannel = await guideSelection.user.createDM() - userDMChannel.send({ embeds: configureServer(), files: [startScreen, serverConnection] }) - } else if (guideSelection.customId === 'login') { - const userDMChannel = await guideSelection.user.createDM() - userDMChannel.send({ embeds: loginInfo(), files: [accountChoice, loginScreen] }) - } else if (guideSelection.customId === 'useSyncGroup') { - const userDMChannel = await guideSelection.user.createDM() - userDMChannel.send({ embeds: useSyncgroup(), files: [overview, joingroup, resume, leavegroup] }) - } else if (guideSelection.customId === 'explainRoles') { - const userDMChannel = await guideSelection.user.createDM() - userDMChannel.send({ embeds: explainRole() }) - } + if (guideSelection.customId === 'jfInstallation') { + const userDMChannel = await guideSelection.user.createDM() + userDMChannel.send({ embeds: installation(), files: [splashScreen] }) + } else if (guideSelection.customId === 'configureServer') { + const userDMChannel = await guideSelection.user.createDM() + userDMChannel.send({ embeds: configureServer(), files: [startScreen, serverConnection] }) + } else if (guideSelection.customId === 'login') { + const userDMChannel = await guideSelection.user.createDM() + userDMChannel.send({ embeds: loginInfo(), files: [accountChoice, loginScreen] }) + } else if (guideSelection.customId === 'useSyncGroup') { + const userDMChannel = await guideSelection.user.createDM() + userDMChannel.send({ embeds: useSyncgroup(), files: [overview, joingroup, resume, leavegroup] }) + } else if (guideSelection.customId === 'explainRoles') { + const userDMChannel = await guideSelection.user.createDM() + userDMChannel.send({ embeds: explainRole() }) + } - guideSelection.update({ content: "Hab ich dir per DM geschickt :)", components: [] }) + guideSelection.update({ content: "Hab ich dir per DM geschickt :)", components: [] }) - } catch (error) { - await interaction.interaction.editReply({ content: 'Das dauert mir zu lange, frag mich nochmal wenn du nen Guide brauchst', components: [] }); - } + } catch (error) { + await interaction.interaction.editReply({ content: 'Das dauert mir zu lange, frag mich nochmal wenn du nen Guide brauchst', components: [] }); + } - } + } }) diff --git a/server/commands/mitgucken.ts b/server/commands/mitgucken.ts index c90a7f8..4cd6e04 100644 --- a/server/commands/mitgucken.ts +++ b/server/commands/mitgucken.ts @@ -7,139 +7,139 @@ import { attachmentImages } from '../..' const color = 0x0099FF export default new Command({ - name: 'mitgucken', - description: 'Erfahre wie die Verbindung mit Jellyfin funktioniert und eine WatchTogether Gruppe funktioniert.', - options: [], - run: async (interaction: RunOptions) => { - const requestId = uuid() - interaction.interaction.followUp('Ich schicke dir einen Guide per DM!') - const embedList: APIEmbed[] = [] - embedList.push(...installation()) - embedList.push(...configureServer()) - embedList.push(...explainRole()) - embedList.push(...loginInfo()) - embedList.push(...useSyncgroup()) + name: 'mitgucken', + description: 'Erfahre wie die Verbindung mit Jellyfin funktioniert und eine WatchTogether Gruppe funktioniert.', + options: [], + run: async (interaction: RunOptions) => { + const requestId = uuid() + interaction.interaction.followUp('Ich schicke dir einen Guide per DM!') + const embedList: APIEmbed[] = [] + embedList.push(...installation()) + embedList.push(...configureServer()) + embedList.push(...explainRole()) + embedList.push(...loginInfo()) + embedList.push(...useSyncgroup()) - //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 }) - const userDMchannel = await interaction.interaction.user.createDM() - userDMchannel.send({ embeds: embedList, files: attachmentImages }) - } + //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 }) + const userDMchannel = await interaction.interaction.user.createDM() + userDMchannel.send({ embeds: embedList, files: attachmentImages }) + } }) export function explainRole(): APIEmbed[] { - return [{ - color, - title: "Wie du an einen Account kommst", - description: roleExplanation - }] + return [{ + color, + title: "Wie du an einen Account kommst", + description: roleExplanation + }] } export function installation(): APIEmbed[] { - const embedList: APIEmbed[] = [] - // DownloadLink and installation - embedList.push({ - color, - title: 'Jellyfin Media Player Installation', - description: 'Du kannst den Jellyfin Media Player von github herunterladen.\n Der Mediaplayer muss genutzt werden, da ein Schauen direkt über das Webinterface den Server zum Schmelzen bringt.\nFühre die Datei aus und installiere den Jellyfin Media Player an den Ort deiner Wahl.', - fields: [ - { name: "Windows", value: "https://github.com/jellyfin/jellyfin-media-player/releases/download/v1.9.1/JellyfinMediaPlayer-1.9.1-windows-x64.exe" }, - { name: "Mac", value: "https://github.com/jellyfin/jellyfin-media-player/releases/download/v1.9.1/JellyfinMediaPlayer-1.9.1-macos-notarized.dmg" } - ], - image: { - url: 'attachment://set_splashscreen.png' - } - }) - return embedList + const embedList: APIEmbed[] = [] + // DownloadLink and installation + embedList.push({ + color, + title: 'Jellyfin Media Player Installation', + description: 'Du kannst den Jellyfin Media Player von github herunterladen.\n Der Mediaplayer muss genutzt werden, da ein Schauen direkt über das Webinterface den Server zum Schmelzen bringt.\nFühre die Datei aus und installiere den Jellyfin Media Player an den Ort deiner Wahl.', + fields: [ + { name: "Windows", value: "https://github.com/jellyfin/jellyfin-media-player/releases/download/v1.9.1/JellyfinMediaPlayer-1.9.1-windows-x64.exe" }, + { name: "Mac", value: "https://github.com/jellyfin/jellyfin-media-player/releases/download/v1.9.1/JellyfinMediaPlayer-1.9.1-macos-notarized.dmg" } + ], + image: { + url: 'attachment://set_splashscreen.png' + } + }) + return embedList } export function configureServer(): APIEmbed[] { - const embedList: APIEmbed[] = [] - // Login - embedList.push({ - color, - title: "Server Auswahl", - description: "Die Jellyfin App kann sich mit mehreren Servern verbinden.\n Hattest du noch nie eine Server Verbindung wähle hier 'Server hinzufügen'.", - image: { - url: 'attachment://start_screen.png' - } - }) - // Server Address - embedList.push({ - color, - title: "Server Verbindung", - description: "Stelle eine Verbindung zum Hartzarett Jellyfin Server her", - fields: [ - { name: "Server Adresse", value: "`https://media.hartzarett.ruhr`" } - ], - image: { - url: 'attachment://server_verbindung.png' - } - }) - return embedList + const embedList: APIEmbed[] = [] + // Login + embedList.push({ + color, + title: "Server Auswahl", + description: "Die Jellyfin App kann sich mit mehreren Servern verbinden.\n Hattest du noch nie eine Server Verbindung wähle hier 'Server hinzufügen'.", + image: { + url: 'attachment://start_screen.png' + } + }) + // Server Address + embedList.push({ + color, + title: "Server Verbindung", + description: "Stelle eine Verbindung zum Hartzarett Jellyfin Server her", + fields: [ + { name: "Server Adresse", value: "`https://media.hartzarett.ruhr`" } + ], + image: { + url: 'attachment://server_verbindung.png' + } + }) + return embedList } export function loginInfo(): APIEmbed[] { - const embedList: APIEmbed[] = [] - // Account choice - embedList.push({ - color, - title: "Account Auswahl", - description: "In der Regel sind die Accounts aus Datenschutzgründen versteckt.\nWähle 'Manuelle Anmeldung' aus", - image: { - url: 'attachment://auswahl_anmeldung.png' - } - }) - // password screen - 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 `/passwort_reset` aus :)", - image: { - url: 'attachment://login_screen.png' - } - }) - return embedList + const embedList: APIEmbed[] = [] + // Account choice + embedList.push({ + color, + title: "Account Auswahl", + description: "In der Regel sind die Accounts aus Datenschutzgründen versteckt.\nWähle 'Manuelle Anmeldung' aus", + image: { + url: 'attachment://auswahl_anmeldung.png' + } + }) + // password screen + 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 `/passwort_reset` aus :)", + image: { + url: 'attachment://login_screen.png' + } + }) + return embedList } export function useSyncgroup(): APIEmbed[] { - const embedList: APIEmbed[] = [] - embedList.push({ - color, - title: "SyncPlay Menü", - image: { - url: 'attachment://jellyfin_ubersicht.png' - }, - description: "Im Hauptbildschirm findest du die 'SyncPlay' Einstellungen oben rechts.", - }) - // join group - embedList.push({ - color, - title: "Gruppe beitreten", - image: { - url: 'attachment://gruppe_beitreten.png' - }, - description: "Suche dir aus dem Dropdown die SyncPlay Gruppe aus, die zu deinem Event gehört.", - }) - // leave group - embedList.push({ - color, - title: "Gruppe verlassen", - image: { - url: 'attachment://gruppe_verlassen.png' - }, - description: "Wenn du die Watchparty verlassen möchtest, kannst du das ebenfalls über das Menü oben rechts tun.", - }) - //resume playback - embedList.push({ - color, - title: "Wiedergabe fortsetzen", - image: { - url: 'attachment://wiedergabe_fortsetzen.png' - }, - description: "Wenn du aus der Watchparty rausgeflogen bist, oder die Wiedergabe verlassen hast, kannst du über das Menü oben rechts auch wieder zurückkehren.", - }) - return embedList + const embedList: APIEmbed[] = [] + embedList.push({ + color, + title: "SyncPlay Menü", + image: { + url: 'attachment://jellyfin_ubersicht.png' + }, + description: "Im Hauptbildschirm findest du die 'SyncPlay' Einstellungen oben rechts.", + }) + // join group + embedList.push({ + color, + title: "Gruppe beitreten", + image: { + url: 'attachment://gruppe_beitreten.png' + }, + description: "Suche dir aus dem Dropdown die SyncPlay Gruppe aus, die zu deinem Event gehört.", + }) + // leave group + embedList.push({ + color, + title: "Gruppe verlassen", + image: { + url: 'attachment://gruppe_verlassen.png' + }, + description: "Wenn du die Watchparty verlassen möchtest, kannst du das ebenfalls über das Menü oben rechts tun.", + }) + //resume playback + embedList.push({ + color, + title: "Wiedergabe fortsetzen", + image: { + url: 'attachment://wiedergabe_fortsetzen.png' + }, + description: "Wenn du aus der Watchparty rausgeflogen bist, oder die Wiedergabe verlassen hast, kannst du über das Menü oben rechts auch wieder zurückkehren.", + }) + return embedList } 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 diff --git a/server/configuration.ts b/server/configuration.ts index 181e26b..1e5f848 100644 --- a/server/configuration.ts +++ b/server/configuration.ts @@ -2,73 +2,73 @@ import dotenv from "dotenv" dotenv.config() interface options { - [k: string]: boolean | number | string | undefined + [k: string]: boolean | number | string | undefined } interface bodyParserOptions { - urlEncodedOptions: options, - jsonOptions: options + urlEncodedOptions: options, + jsonOptions: options } export interface Config { - server: { bodyParser: bodyParserOptions }, - bot: { - debug: boolean - silent: boolean - token: string - guild_id: string - client_id: string - jellfin_token: string - jellyfin_url: string - port: number - workaround_token: string - watcher_role: string - jf_admin_role: string - 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 - } + server: { bodyParser: bodyParserOptions }, + bot: { + debug: boolean + silent: boolean + token: string + guild_id: string + client_id: string + jellfin_token: string + jellyfin_url: string + port: number + workaround_token: string + watcher_role: string + jf_admin_role: string + 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 = { - server: { - bodyParser: { - urlEncodedOptions: { - inflate: true, - limit: '5mb', - type: 'application/x-www-form-urlencoded', - extended: true, - parameterLimit: 1000 - }, - jsonOptions: { - inflate: true, - limit: '5mb', - type: 'application/json', - strict: true - } - } - }, - bot: { - debug: true, - silent: false, - port: 1234, - token: process.env.BOT_TOKEN ?? "", - guild_id: process.env.GUILD_ID ?? "", - client_id: process.env.CLIENT_ID ?? "", - jellfin_token: process.env.JELLYFIN_TOKEN ?? "", - jellyfin_url: process.env.JELLYFIN_URL ?? "", - workaround_token: process.env.TOKEN ?? "", - watcher_role: process.env.WATCHER_ROLE ?? "", - jf_admin_role: process.env.ADMIN_ROLE ?? "", - announcement_role: process.env.WATCHPARTY_ANNOUNCEMENT_ROLE ?? "", - announcement_channel_id: process.env.CHANNEL_ID ?? "", - 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 ?? "" - } + server: { + bodyParser: { + urlEncodedOptions: { + inflate: true, + limit: '5mb', + type: 'application/x-www-form-urlencoded', + extended: true, + parameterLimit: 1000 + }, + jsonOptions: { + inflate: true, + limit: '5mb', + type: 'application/json', + strict: true + } + } + }, + bot: { + debug: true, + silent: false, + port: 1234, + token: process.env.BOT_TOKEN ?? "", + guild_id: process.env.GUILD_ID ?? "", + client_id: process.env.CLIENT_ID ?? "", + jellfin_token: process.env.JELLYFIN_TOKEN ?? "", + jellyfin_url: process.env.JELLYFIN_URL ?? "", + workaround_token: process.env.TOKEN ?? "", + watcher_role: process.env.WATCHER_ROLE ?? "", + jf_admin_role: process.env.ADMIN_ROLE ?? "", + announcement_role: process.env.WATCHPARTY_ANNOUNCEMENT_ROLE ?? "", + announcement_channel_id: process.env.CHANNEL_ID ?? "", + 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/announceManualWatchparty.ts b/server/events/announceManualWatchparty.ts index da29f13..acaccf6 100644 --- a/server/events/announceManualWatchparty.ts +++ b/server/events/announceManualWatchparty.ts @@ -10,39 +10,39 @@ import { logger } from "../logger"; 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 - } + 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!", { guildId, requestId }) - if(event.description.includes("!private")) { - logger.info("Event description contains \"!private\". Won't announce.", { guildId, requestId }) - return - } + 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) + const channel: Maybe = client.getAnnouncementChannelForGuild(guildId) - if (!channel) { - logger.error("Could not obtain announcement channel. Aborting announcement.", { guildId, requestId }) - return - } + 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)}` + 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.", { guildId, requestId }) - } + channel.send(message) + } else { + logger.debug("Got GuildScheduledEventCreate event but no !wp in description. Not creating manual wp announcement.", { guildId, requestId }) + } - } catch (error) { - // sendFailureDM(error) - logger.error(error, { guildId, requestId }) - } + } 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 6377928..8ccdddb 100644 --- a/server/events/autoCreateVoteByWPEvent.ts +++ b/server/events/autoCreateVoteByWPEvent.ts @@ -16,48 +16,48 @@ export const NONE_OF_THAT = "❌" export let task: ScheduledTask | undefined export async function execute(event: GuildScheduledEvent) { - const requestId = uuid() - - 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 }) - event.edit({ name: "Watchparty - Voting offen" }) - const movies = await yavinJellyfinHandler.getRandomMovieNames(5, event.guildId, requestId) + const requestId = uuid() - logger.info(`Got ${movies.length} random movies. Creating voting`, { guildId: event.guildId, requestId }) - logger.debug(`Movies: ${JSON.stringify(movies)}`, { 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 }) + event.edit({ name: "Watchparty - Voting offen" }) + const movies = await yavinJellyfinHandler.getRandomMovieNames(5, event.guildId, requestId) - 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 }) + logger.info(`Got ${movies.length} random movies. Creating voting`, { guildId: event.guildId, requestId }) + logger.debug(`Movies: ${JSON.stringify(movies)}`, { guildId: event.guildId, requestId }) - if(!event.scheduledStartAt) { - logger.info("EVENT DOES NOT HAVE STARTDATE; CANCELLING", {guildId: event.guildId, requestId}) - return - } - let message = `[Abstimmung] für https://discord.com/events/${event.guildId}/${event.id}\n<@&${config.bot.announcement_role}> Es gibt eine neue Abstimmung für die nächste Watchparty ${createDateStringFromEvent(event, event.guildId, requestId)}! Stimme hierunter für den nächsten Film ab!\n` + const 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 }) - 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.") + if (!event.scheduledStartAt) { + logger.info("EVENT DOES NOT HAVE STARTDATE; CANCELLING", { guildId: event.guildId, requestId }) + return + } + let message = `[Abstimmung] für https://discord.com/events/${event.guildId}/${event.id}\n<@&${config.bot.announcement_role}> Es gibt eine neue Abstimmung für die nächste Watchparty ${createDateStringFromEvent(event, event.guildId, requestId)}! Stimme hierunter für den nächsten Film ab!\n` - const options: MessageCreateOptions = { - allowedMentions: { parse: ["roles"]}, - content: message, - } + 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 sentMessage: Message = await (await announcementChannel.fetch()).send(options) + const options: MessageCreateOptions = { + allowedMentions: { parse: ["roles"] }, + content: message, + } - for (let i = 0; i < movies.length; i++) { - sentMessage.react(Emotes[i]) - } - sentMessage.react(NONE_OF_THAT) + const sentMessage: Message = await (await announcementChannel.fetch()).send(options) - // sentMessage.pin() //todo: uncomment when bot has permission to pin messages. Also update closepoll.ts to only fetch pinned messages - } + 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 + } } diff --git a/server/events/deleteAnnouncementsWhenWPEnds.ts b/server/events/deleteAnnouncementsWhenWPEnds.ts index df35f4b..8d7acfe 100644 --- a/server/events/deleteAnnouncementsWhenWPEnds.ts +++ b/server/events/deleteAnnouncementsWhenWPEnds.ts @@ -7,46 +7,46 @@ 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 + 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 - } + 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 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 }) - } + 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 + 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 +} diff --git a/server/events/handlePermJFAccountByRole.ts b/server/events/handlePermJFAccountByRole.ts index 6de7cf2..2c8d1d7 100644 --- a/server/events/handlePermJFAccountByRole.ts +++ b/server/events/handlePermJFAccountByRole.ts @@ -6,24 +6,24 @@ import { v4 as uuid } from "uuid" export const name = 'guildMemberUpdate' export async function execute(oldMember: GuildMember, newMember: GuildMember) { - try { - const requestId = uuid() - const changedRoles: ChangedRoles = filterRolesFromMemberUpdate(oldMember, newMember) - const triggerRoleIds: Collection = getGuildSpecificTriggerRoleId() + try { + const requestId = uuid() + const changedRoles: ChangedRoles = filterRolesFromMemberUpdate(oldMember, newMember) + const triggerRoleIds: Collection = getGuildSpecificTriggerRoleId() - triggerRoleIds.forEach((level, key) => { - const addedRoleMatches = changedRoles.addedRoles.find(aRole => aRole.id === key) - if (addedRoleMatches) { - jellyfinHandler.upsertUser(newMember, level, requestId) - } - const removedRoleMatches = changedRoles.removedRoles.find(rRole => rRole.id === key) - if (removedRoleMatches) { - jellyfinHandler.removeUser(newMember, level, requestId) - } - }) - } catch (error) { - console.error(error) - } + triggerRoleIds.forEach((level, key) => { + const addedRoleMatches = changedRoles.addedRoles.find(aRole => aRole.id === key) + if (addedRoleMatches) { + jellyfinHandler.upsertUser(newMember, level, requestId) + } + const removedRoleMatches = changedRoles.removedRoles.find(rRole => rRole.id === key) + if (removedRoleMatches) { + jellyfinHandler.removeUser(newMember, level, requestId) + } + }) + } catch (error) { + console.error(error) + } } diff --git a/server/events/handleTempJFUserByVoiceEvent.ts b/server/events/handleTempJFUserByVoiceEvent.ts index ac4ecee..3ed7c72 100644 --- a/server/events/handleTempJFUserByVoiceEvent.ts +++ b/server/events/handleTempJFUserByVoiceEvent.ts @@ -9,51 +9,51 @@ export const name = 'voiceStateUpdate' export async function execute(oldState: VoiceState, newState: VoiceState) { - try { - logger.info(JSON.stringify(newState, null, 2)) - //ignore events like mute/unmute - if(newState.channel?.id === oldState.channel?.id) { - logger.info("Not handling VoiceState event because channelid of old and new was the same (i.e. mute/unmute event)") - return - } + try { + logger.info(JSON.stringify(newState, null, 2)) + //ignore events like mute/unmute + if (newState.channel?.id === oldState.channel?.id) { + logger.info("Not handling VoiceState event because channelid of old and new was the same (i.e. mute/unmute event)") + return + } - const scheduledEvents = (await newState.guild.scheduledEvents.fetch()) - .filter((key) => key.description?.toLowerCase().includes("!wp") && key.isActive()) - .map((key) => key) + const scheduledEvents = (await newState.guild.scheduledEvents.fetch()) + .filter((key) => key.description?.toLowerCase().includes("!wp") && key.isActive()) + .map((key) => key) - const scheduledEventUsers = (await Promise.all(scheduledEvents.map(event => event.fetchSubscribers({withMember: true})))) + const scheduledEventUsers = (await Promise.all(scheduledEvents.map(event => event.fetchSubscribers({ withMember: true })))) - //Dont handle users, that are already subscribed to the event. We only want to handle unsubscribed users here - let userFound = false; - scheduledEventUsers.forEach(collection => { - collection.each(key => { - logger.info(JSON.stringify(key, null, 2)) - if(key.member.user.id === newState.member?.user.id) - userFound = true; - }) - }) - if(userFound) { - logger.info(`Not handling VoiceState event because user was already subscribed and got an account from there. User: ${JSON.stringify(newState.member, null, 2)}`) - return - } + //Dont handle users, that are already subscribed to the event. We only want to handle unsubscribed users here + let userFound = false; + scheduledEventUsers.forEach(collection => { + collection.each(key => { + logger.info(JSON.stringify(key, null, 2)) + if (key.member.user.id === newState.member?.user.id) + userFound = true; + }) + }) + if (userFound) { + logger.info(`Not handling VoiceState event because user was already subscribed and got an account from there. User: ${JSON.stringify(newState.member, null, 2)}`) + return + } - if (scheduledEvents.find(event => event.channelId === newState.channelId)) { - if(newState.member){ - logger.info("YO! Da ist jemand dem Channel mit dem Event beigetreten, ich kümmer mich mal um nen Account!") - const result = await jellyfinHandler.upsertUser(newState.member, "TEMPORARY", uuid()) - if (result === UserUpsertResult.created) { - newState.member.createDM().then(channel => channel.send(`Hey! Du bist unserer Watchparty beigetreten, ich hab dir gerade die Zugangsdaten für den Mediaserver geschickt!`)) - } else { - newState.member.createDM().then(channel => channel.send(`Hey! Du bist unserer Watchparty beigetreten aber du hast bereits einen Account. Falls du ein neues Passwort brauchst nutze /reset_passwort!`)) - } - } else { - logger.error("WTF? Expected Member?? When doing things") - } - } else { - logger.info("VoiceState channelId was not the id of any channel with events") - } - }catch(error){ - logger.error(error) - } -} \ No newline at end of file + if (scheduledEvents.find(event => event.channelId === newState.channelId)) { + if (newState.member) { + logger.info("YO! Da ist jemand dem Channel mit dem Event beigetreten, ich kümmer mich mal um nen Account!") + const result = await jellyfinHandler.upsertUser(newState.member, "TEMPORARY", uuid()) + if (result === UserUpsertResult.created) { + newState.member.createDM().then(channel => channel.send(`Hey! Du bist unserer Watchparty beigetreten, ich hab dir gerade die Zugangsdaten für den Mediaserver geschickt!`)) + } else { + newState.member.createDM().then(channel => channel.send(`Hey! Du bist unserer Watchparty beigetreten aber du hast bereits einen Account. Falls du ein neues Passwort brauchst nutze /reset_passwort!`)) + } + } else { + logger.error("WTF? Expected Member?? When doing things") + } + } else { + logger.info("VoiceState channelId was not the id of any channel with events") + } + } catch (error) { + logger.error(error) + } +} diff --git a/server/events/handleTempJFUsersByWPEvents.ts b/server/events/handleTempJFUsersByWPEvents.ts index 42a719c..eebc40e 100644 --- a/server/events/handleTempJFUsersByWPEvents.ts +++ b/server/events/handleTempJFUsersByWPEvents.ts @@ -8,51 +8,51 @@ import { logger } from "../logger"; 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 }) - if (!newEvent.guild) { - logger.error("Event has no guild, aborting.", { guildId: newEvent.guildId, requestId }) - return - } + try { + const requestId = uuid() + // 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().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 ?? []) + if (newEvent.description?.toLowerCase().includes("!wp") && [GuildScheduledEventStatus.Active, GuildScheduledEventStatus.Completed].includes(newEvent.status)) { + 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)) - members.push(member) - } - + const members: GuildMember[] = [] + 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 { - 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.`)) - }) - deleteJFUsers(newEvent.guildId, requestId) - } - } - } catch (error) { - logger.error(error) - } + if (newEvent.status === GuildScheduledEventStatus.Active) + createJFUsers(members, newEvent.name, requestId) + else { + + 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.`)) + }) + deleteJFUsers(newEvent.guildId, requestId) + } + } + } catch (error) { + logger.error(error) + } } async function createJFUsers(members: GuildMember[], movieName: string, requestId?: string) { - logger.info(`Creating users for: \n ${JSON.stringify(members, null, 2)}`) - members.forEach(member => { - member.createDM().then(channel => channel.send(`Hey! Du hast dich für die Watchparty von ${movieName} angemeldet! Es geht gleich los!`)) - jellyfinHandler.upsertUser(member, "TEMPORARY", requestId) - }) + logger.info(`Creating users for: \n ${JSON.stringify(members, null, 2)}`) + members.forEach(member => { + member.createDM().then(channel => channel.send(`Hey! Du hast dich für die Watchparty von ${movieName} angemeldet! Es geht gleich los!`)) + jellyfinHandler.upsertUser(member, "TEMPORARY", requestId) + }) } async function deleteJFUsers(guildId: string, requestId?: string) { - logger.info(`Watchparty ended, deleting tmp users`, { guildId, requestId }) - jellyfinHandler.purge(guildId, requestId) -} \ No newline at end of file + logger.info(`Watchparty ended, deleting tmp users`, { guildId, requestId }) + jellyfinHandler.purge(guildId, requestId) +} diff --git a/server/events/interactionCreate.ts b/server/events/interactionCreate.ts index c483f8f..27e5ff9 100644 --- a/server/events/interactionCreate.ts +++ b/server/events/interactionCreate.ts @@ -5,17 +5,17 @@ import { logger } from "../logger" export const name = 'interactionCreate' export async function execute(interaction: ExtendedInteraction) { - //console.dir(interaction, { depth: null }) - if (interaction.isCommand()) { - logger.info(`Interaction is a command.`, { guildId: interaction.guild?.id }) - await interaction.deferReply({ ephemeral: true }) - const command = client.commands.get(interaction.commandName) - if (!command) - return interaction.followUp('Invalid command') - command.run({ - args: interaction.options as CommandInteractionOptionResolver, - client, - interaction - }) - } + //console.dir(interaction, { depth: null }) + if (interaction.isCommand()) { + logger.info(`Interaction is a command.`, { guildId: interaction.guild?.id }) + await interaction.deferReply({ ephemeral: true }) + const command = client.commands.get(interaction.commandName) + if (!command) + return interaction.followUp('Invalid command') + command.run({ + args: interaction.options as CommandInteractionOptionResolver, + client, + interaction + }) + } } diff --git a/server/helper/dateHelper.ts b/server/helper/dateHelper.ts index af93486..dd9c5b2 100644 --- a/server/helper/dateHelper.ts +++ b/server/helper/dateHelper.ts @@ -1,23 +1,23 @@ import { format, isToday, toDate } from "date-fns"; -import {utcToZonedTime} from "date-fns-tz" +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) { - logger.error("Event has no start. Cannot create dateString.", {guildId, requestId}) - return `"habe keinen Startzeitpunkt ermitteln können"` - } + if (!event.scheduledStartAt) { + logger.error("Event has no start. Cannot create dateString.", { guildId, requestId }) + return `"habe keinen Startzeitpunkt ermitteln können"` + } - const timeZone = 'Europe/Berlin' - const zonedDateTime = utcToZonedTime(event.scheduledStartAt, timeZone) - const time = format(zonedDateTime, "HH:mm", {locale: de}) - - if(isToday(zonedDateTime)) { - return `heute um ${time}` - } + const timeZone = 'Europe/Berlin' + const zonedDateTime = utcToZonedTime(event.scheduledStartAt, timeZone) + const time = format(zonedDateTime, "HH:mm", { locale: de }) - const date = format(zonedDateTime, "eeee dd.MM", {locale: de}) - return `am ${date} um ${time}` -} \ No newline at end of file + if (isToday(zonedDateTime)) { + return `heute um ${time}` + } + + const date = format(zonedDateTime, "eeee dd.MM", { locale: de }) + return `am ${date} um ${time}` +} diff --git a/server/helper/roleFilter.ts b/server/helper/roleFilter.ts index 2779737..23ea237 100644 --- a/server/helper/roleFilter.ts +++ b/server/helper/roleFilter.ts @@ -5,20 +5,20 @@ import { config } from "../configuration" export function filterRolesFromMemberUpdate(oldMember: GuildMember, newMember: GuildMember): ChangedRoles { - const oldRoles = oldMember.roles.cache - const newRoles = newMember.roles.cache + const oldRoles = oldMember.roles.cache + const newRoles = newMember.roles.cache - const removedRoles = oldRoles.filter(x => newRoles.find(y => y.id === x.id) == undefined) - const addedRoles = newRoles.filter(x => oldRoles.find(y => y.id === x.id) == undefined) - logger.info(`Member ${oldMember.id} RemovedRoles: ${removedRoles.map(x => x.name)}`, { guildId: oldMember.guild.id }) - logger.info(`Member ${oldMember.id} AddedRoles: ${addedRoles.map(x => x.name)}`, { guildId: oldMember.guild.id }) + const removedRoles = oldRoles.filter(x => newRoles.find(y => y.id === x.id) == undefined) + const addedRoles = newRoles.filter(x => oldRoles.find(y => y.id === x.id) == undefined) + logger.info(`Member ${oldMember.id} RemovedRoles: ${removedRoles.map(x => x.name)}`, { guildId: oldMember.guild.id }) + logger.info(`Member ${oldMember.id} AddedRoles: ${addedRoles.map(x => x.name)}`, { guildId: oldMember.guild.id }) - return { addedRoles, removedRoles } + return { addedRoles, removedRoles } } export function getGuildSpecificTriggerRoleId(): Collection { - const outVal = new Collection() - outVal.set(config.bot.watcher_role, "VIEWER") - outVal.set(config.bot.jf_admin_role, "ADMIN") - return outVal + const outVal = new Collection() + outVal.set(config.bot.watcher_role, "VIEWER") + outVal.set(config.bot.jf_admin_role, "ADMIN") + return outVal } diff --git a/server/helper/sendFailureDM.ts b/server/helper/sendFailureDM.ts index b18c39c..78a5dfa 100644 --- a/server/helper/sendFailureDM.ts +++ b/server/helper/sendFailureDM.ts @@ -2,11 +2,11 @@ import { CustomError, errorCodes } from "../interfaces" import { ExtendedClient } from "../structures/client" export async function sendFailureDM(creatorMessage: string, client: ExtendedClient, creatorId?: string): Promise { - if (!creatorId) throw new CustomError('No creator ID present', errorCodes.no_creator_id) - const creator = await client.users.fetch(creatorId) - console.log(`Creator ${JSON.stringify(creator)}`) - if (creator) - if (!creator.dmChannel) - await creator.createDM() - await creator.dmChannel?.send(creatorMessage) + if (!creatorId) throw new CustomError('No creator ID present', errorCodes.no_creator_id) + const creator = await client.users.fetch(creatorId) + console.log(`Creator ${JSON.stringify(creator)}`) + if (creator) + if (!creator.dmChannel) + await creator.createDM() + await creator.dmChannel?.send(creatorMessage) } diff --git a/server/interfaces.ts b/server/interfaces.ts index 84e93b1..982c1ec 100644 --- a/server/interfaces.ts +++ b/server/interfaces.ts @@ -3,39 +3,39 @@ import { Role } from "discord.js" export type Maybe = T | undefined | null export interface Player { - name: string + name: string } export type supported_languages = "german" | "english" export interface localized_string { - [k: string]: { - [k in supported_languages]: string - } + [k: string]: { + [k in supported_languages]: string + } } export class CustomError extends Error { - private code: string - public constructor(message: string, errorCode: string) { - super(message) - this.code = errorCode - } - public getCode() { return this.code } + private code: string + public constructor(message: string, errorCode: string) { + super(message) + this.code = errorCode + } + public getCode() { return this.code } } export const errorCodes = { - no_end_date: 'no_end_date', - no_string_present: 'no_string_present', - no_schedule: 'no_schedule', - schedule_not_supported: 'schedule_not_supported', - no_repetition_amount: 'no_repetition_amount', - invalid_repetition_string: 'invalid_repetition_string', - no_creator_id: "no_creator_id", + no_end_date: 'no_end_date', + no_string_present: 'no_string_present', + no_schedule: 'no_schedule', + schedule_not_supported: 'schedule_not_supported', + no_repetition_amount: 'no_repetition_amount', + invalid_repetition_string: 'invalid_repetition_string', + no_creator_id: "no_creator_id", } export interface ChangedRoles { - addedRoles: Collection - removedRoles: Collection + addedRoles: Collection + removedRoles: Collection } export interface JellyfinConfig { - jellyfinUrl: string, - jellyfinToken: string, - movieCollectionId: string, - collectionUser: string + 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 8dc1dc9..11e9d91 100644 --- a/server/jellyfin/handler.ts +++ b/server/jellyfin/handler.ts @@ -9,275 +9,275 @@ import { Configuration, ConfigurationParameters } from "./runtime"; export class JellyfinHandler { - private userApi: UserApi - private systemApi: SystemApi - private moviesApi: ItemsApi - private token: string - private authHeader: { headers: { 'X-Emby-Authorization': string } } - private config: JellyfinConfig - private serverName = ""; + private userApi: UserApi + private systemApi: SystemApi + private moviesApi: ItemsApi + private token: string + private authHeader: { headers: { 'X-Emby-Authorization': string } } + private config: JellyfinConfig + private serverName = ""; - constructor(_config: JellyfinConfig, _userApi?: UserApi, _systemApi?: SystemApi, _itemsApi?: ItemsApi) { - this.config = _config - this.token = this.config.jellyfinToken - this.authHeader = { - headers: { - "X-Emby-Authorization": this.config.jellyfinToken - } - } - const userApiConfigurationParams: ConfigurationParameters = { - basePath: this.config.jellyfinUrl, - headers: this.authHeader.headers - } - const systemApiConfigurationParams: ConfigurationParameters = { - basePath: this.config.jellyfinUrl, - headers: this.authHeader.headers - } - const libraryApiConfigurationParams: ConfigurationParameters = { - basePath: this.config.jellyfinUrl, - headers: this.authHeader.headers - } + constructor(_config: JellyfinConfig, _userApi?: UserApi, _systemApi?: SystemApi, _itemsApi?: ItemsApi) { + this.config = _config + this.token = this.config.jellyfinToken + this.authHeader = { + headers: { + "X-Emby-Authorization": this.config.jellyfinToken + } + } + const userApiConfigurationParams: ConfigurationParameters = { + basePath: this.config.jellyfinUrl, + headers: this.authHeader.headers + } + const systemApiConfigurationParams: ConfigurationParameters = { + basePath: this.config.jellyfinUrl, + headers: this.authHeader.headers + } + const libraryApiConfigurationParams: ConfigurationParameters = { + basePath: this.config.jellyfinUrl, + headers: this.authHeader.headers + } - this.userApi = _userApi ?? new UserApi(new Configuration(userApiConfigurationParams)) - this.systemApi = _systemApi ?? new SystemApi(new Configuration(systemApiConfigurationParams)) - this.moviesApi = _itemsApi ?? new ItemsApi(new Configuration(libraryApiConfigurationParams)) - logger.info(`Initialized Jellyfin handler`, { requestId: 'Init' }) - } + this.userApi = _userApi ?? new UserApi(new Configuration(userApiConfigurationParams)) + this.systemApi = _systemApi ?? new SystemApi(new Configuration(systemApiConfigurationParams)) + this.moviesApi = _itemsApi ?? new ItemsApi(new Configuration(libraryApiConfigurationParams)) + logger.info(`Initialized Jellyfin handler`, { requestId: 'Init' }) + } - private generateJFUserName(discordUser: GuildMember, level: PermissionLevel): string { - return `${discordUser.displayName}${level == "TEMPORARY" ? "_tmp" : ""}` - } + private generateJFUserName(discordUser: GuildMember, level: PermissionLevel): string { + return `${discordUser.displayName}${level == "TEMPORARY" ? "_tmp" : ""}` + } - private generatePasswordForUser(): string { - return (Math.random() * 10000 + 10000).toFixed(0) - } + private generatePasswordForUser(): string { + return (Math.random() * 10000 + 10000).toFixed(0) + } - 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() - } - } - 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 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() + } + } + 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 + 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 operation: UpdateUserPolicyRequest = { + ...user.policy, + enableVideoPlaybackTranscoding: false + } - const request: UpdateUserPolicyOperationRequest = { - userId: user.id, - updateUserPolicyRequest: operation - } - this.userApi.updateUserPolicy(request) - } + 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 }) - return jfuser !== undefined - } + 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 }) + return jfuser !== undefined + } - public async getCurrentUsers(guildId: string, requestId?: string): Promise { - try { - logger.info(`Fetching current users from Jellyfin`, { requestId, guildId }) - const result = await this.userApi.getUsers(undefined, this.authHeader) - return result - } catch (error) { - logger.error(`Could not fetch current users from jellyfin`, { guildId, requestId }) - } - return [] - } + public async getCurrentUsers(guildId: string, requestId?: string): Promise { + try { + logger.info(`Fetching current users from Jellyfin`, { requestId, guildId }) + const result = await this.userApi.getUsers(undefined, this.authHeader) + return result + } catch (error) { + logger.error(`Could not fetch current users from jellyfin`, { guildId, requestId }) + } + return [] + } - public async getUser(discordUser: GuildMember, requestId?: string): Promise> { - logger.info(`Getting user for discord member ${discordUser.displayName}`, { requestId, guildId: discordUser.guild.id }) - const jfUsers = await this.getCurrentUsers(discordUser.guild.id, requestId) - const foundUser = jfUsers.find(x => x.name?.includes(discordUser.displayName)) - return foundUser - } + public async getUser(discordUser: GuildMember, requestId?: string): Promise> { + logger.info(`Getting user for discord member ${discordUser.displayName}`, { requestId, guildId: discordUser.guild.id }) + const jfUsers = await this.getCurrentUsers(discordUser.guild.id, requestId) + const foundUser = jfUsers.find(x => x.name?.includes(discordUser.displayName)) + return foundUser + } - public async removeUser(newMember: GuildMember, level: PermissionLevel, requestId?: string) { - logger.info(`${level == "TEMPORARY" ? "Deleting" : "Disabling"} user ${newMember.displayName}, but method is not implemented`, { requestId, guildId: newMember.guild.id }) - const jfuser = await this.getUser(newMember, requestId) - if (jfuser && jfuser.id) { - if (level === "TEMPORARY") { - const r: DeleteUserRequest = { - userId: jfuser.id - } - this.userApi.deleteUser(r) - } - else - await this.disableUser(jfuser, newMember.guild.id, requestId) - } - } + public async removeUser(newMember: GuildMember, level: PermissionLevel, requestId?: string) { + logger.info(`${level == "TEMPORARY" ? "Deleting" : "Disabling"} user ${newMember.displayName}, but method is not implemented`, { requestId, guildId: newMember.guild.id }) + const jfuser = await this.getUser(newMember, requestId) + if (jfuser && jfuser.id) { + if (level === "TEMPORARY") { + const r: DeleteUserRequest = { + userId: jfuser.id + } + this.userApi.deleteUser(r) + } + else + await this.disableUser(jfuser, newMember.guild.id, requestId) + } + } - public async purge(guildId: string, requestId?: string) { - logger.info("Deleting tmp users", { requestId, guildId }) - const users = (await this.userApi.getUsers()).filter(user => user.name?.endsWith("_tmp")) + public async purge(guildId: string, requestId?: string) { + logger.info("Deleting tmp users", { requestId, guildId }) + const users = (await this.userApi.getUsers()).filter(user => user.name?.endsWith("_tmp")) - users.forEach(user => { - if (user.id) { - const r: DeleteUserRequest = { - userId: user.id - } - this.userApi.deleteUser(r) - } - }) - } + users.forEach(user => { + if (user.id) { + const r: DeleteUserRequest = { + userId: user.id + } + this.userApi.deleteUser(r) + } + }) + } - public async resetUserPasswort(member: GuildMember, requestId?: string) { - logger.info(`Resetting password for user ${member.displayName}`, { requestId, guildId: member.guild.id }) - const jfUser = await this.getUser(member, requestId) - if (jfUser && jfUser.id) { + public async resetUserPasswort(member: GuildMember, requestId?: string) { + logger.info(`Resetting password for user ${member.displayName}`, { requestId, guildId: member.guild.id }) + const jfUser = await this.getUser(member, requestId) + if (jfUser && jfUser.id) { - // const reset: UpdateUserPasswordRequest = { - // resetPassword: true - // } + // const reset: UpdateUserPasswordRequest = { + // resetPassword: true + // } - // const shit: UpdateUserPasswordOperationRequest = { - // updateUserPasswordRequest: reset, - // userId: jfUser.id - // } + // const shit: UpdateUserPasswordOperationRequest = { + // updateUserPasswordRequest: reset, + // userId: jfUser.id + // } - // logger.info(JSON.stringify(jfUser.policy, null, 2)) + // logger.info(JSON.stringify(jfUser.policy, null, 2)) - // logger.info("Resetting password", {requestId}) - // await this.userApi.updateUserPassword(shit); + // logger.info("Resetting password", {requestId}) + // await this.userApi.updateUserPassword(shit); - const password = this.generatePasswordForUser() - const passwordRequest: UpdateUserPasswordRequest = { - // resetPassword: true, - currentPw: "", - newPw: password - } + const password = this.generatePasswordForUser() + const passwordRequest: UpdateUserPasswordRequest = { + // resetPassword: true, + currentPw: "", + newPw: password + } - const passwordOperationRequest: UpdateUserPasswordOperationRequest = { - updateUserPasswordRequest: passwordRequest, - userId: jfUser.id - } + const passwordOperationRequest: UpdateUserPasswordOperationRequest = { + updateUserPasswordRequest: passwordRequest, + userId: jfUser.id + } - logger.info("Setting new password", { requestId, guildId: member.guild.id }) - await this.userApi.updateUserPassword(passwordOperationRequest); + logger.info("Setting new password", { requestId, guildId: member.guild.id }) + await this.userApi.updateUserPassword(passwordOperationRequest); - (await member.createDM()).send(`Hier ist dein neues Passwort: ${password}`) - } else { - (await member.createDM()).send("Ich konnte leider keinen User von dir auf Jellyfin finden. Bitte melde dich bei Markus oder Samantha!") - } + (await member.createDM()).send(`Hier ist dein neues Passwort: ${password}`) + } else { + (await member.createDM()).send("Ich konnte leider keinen User von dir auf Jellyfin finden. Bitte melde dich bei Markus oder Samantha!") + } - } + } - public async disableUser(user: UserDto, guildId?: string, requestId?: string): Promise { - if (user.id) { - const jfUser = await this.getUser({ displayName: user.name, guild: { id: guildId } }, requestId) - logger.info(`Trying to disable user: ${user.name}|${user.id}|${JSON.stringify(jfUser, null, 2)}`, { guildId, requestId }) - const r: UpdateUserPolicyOperationRequest = { - userId: user.id ?? "", - updateUserPolicyRequest: { - ...jfUser?.policy, - isDisabled: true, - } - } - await this.userApi.updateUserPolicy(r) - logger.info(`Succeeded with disabling user: ${user.name}`, { guildId, requestId }) - } - else { - logger.error(`Can not disable user ${JSON.stringify(user)}, has no id?!`, { requestId, guildId }) - } - } + public async disableUser(user: UserDto, guildId?: string, requestId?: string): Promise { + if (user.id) { + const jfUser = await this.getUser({ displayName: user.name, guild: { id: guildId } }, requestId) + logger.info(`Trying to disable user: ${user.name}|${user.id}|${JSON.stringify(jfUser, null, 2)}`, { guildId, requestId }) + const r: UpdateUserPolicyOperationRequest = { + userId: user.id ?? "", + updateUserPolicyRequest: { + ...jfUser?.policy, + isDisabled: true, + } + } + await this.userApi.updateUserPolicy(r) + logger.info(`Succeeded with disabling user: ${user.name}`, { guildId, requestId }) + } + else { + logger.error(`Can not disable user ${JSON.stringify(user)}, has no id?!`, { requestId, guildId }) + } + } - public async enableUser(user: UserDto, guildId: string, requestId?: string): Promise { - if (user.id) { - const jfUser = await this.getUser({ displayName: user.name, guild: { id: guildId } }, requestId) - logger.info(`Trying to enable user: ${user.name}|${user.id}|${JSON.stringify(jfUser, null, 2)}`, { guildId, requestId }) - const r: UpdateUserPolicyOperationRequest = { - userId: user.id ?? "", - updateUserPolicyRequest: { - ...jfUser?.policy, - isDisabled: false, - } - } - await this.userApi.updateUserPolicy(r) - logger.info(`Succeeded with enabling user: ${user.name}`, { guildId, requestId }) - } - else { - logger.error(`Can not enable user ${JSON.stringify(user)}, has no id?!`, { requestId, guildId }) - } - } + public async enableUser(user: UserDto, guildId: string, requestId?: string): Promise { + if (user.id) { + const jfUser = await this.getUser({ displayName: user.name, guild: { id: guildId } }, requestId) + logger.info(`Trying to enable user: ${user.name}|${user.id}|${JSON.stringify(jfUser, null, 2)}`, { guildId, requestId }) + const r: UpdateUserPolicyOperationRequest = { + userId: user.id ?? "", + updateUserPolicyRequest: { + ...jfUser?.policy, + isDisabled: false, + } + } + await this.userApi.updateUserPolicy(r) + logger.info(`Succeeded with enabling user: ${user.name}`, { guildId, requestId }) + } + else { + logger.error(`Can not enable user ${JSON.stringify(user)}, has no id?!`, { requestId, guildId }) + } + } - public async upsertUser(newMember: GuildMember, level: PermissionLevel, requestId?: string): Promise { - logger.info(`Trying to upsert user ${newMember.displayName}, with permissionLevel ${level}`, { guildId: newMember.guild.id, requestId }) - const jfuser = await this.getUser(newMember, requestId) - if (jfuser && !jfuser.policy?.isDisabled) { - logger.info(`User with name ${newMember.displayName} is already present`, { guildId: newMember.guild.id, requestId }) - await this.enableUser(jfuser, newMember.guild.id, requestId) - return UserUpsertResult.enabled - } else { - this.createUserAccountForDiscordUser(newMember, level, newMember.guild.id, requestId) - return UserUpsertResult.created - } - } + public async upsertUser(newMember: GuildMember, level: PermissionLevel, requestId?: string): Promise { + logger.info(`Trying to upsert user ${newMember.displayName}, with permissionLevel ${level}`, { guildId: newMember.guild.id, requestId }) + const jfuser = await this.getUser(newMember, requestId) + if (jfuser && !jfuser.policy?.isDisabled) { + logger.info(`User with name ${newMember.displayName} is already present`, { guildId: newMember.guild.id, requestId }) + await this.enableUser(jfuser, newMember.guild.id, requestId) + return UserUpsertResult.enabled + } else { + this.createUserAccountForDiscordUser(newMember, level, newMember.guild.id, requestId) + return UserUpsertResult.created + } + } - public async getAllMovies(guildId: string, requestId: string): Promise { - logger.info("requesting all movies from jellyfin", { guildId, requestId }) + public async getAllMovies(guildId: string, requestId: string): Promise { + logger.info("requesting all movies from jellyfin", { guildId, requestId }) - const searchParams: GetItemsRequest = { - 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 }) - logger.info(`Found ${movies?.length} movies in total`, { guildId, requestId }) - return movies ?? [] - } + const searchParams: GetItemsRequest = { + 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 }) + logger.info(`Found ${movies?.length} movies in total`, { guildId, requestId }) + return movies ?? [] + } - public async getRandomMovies(count: number, guildId: string, requestId: string): Promise { - logger.info(`${count} random movies requested.`, { guildId, requestId }) - const allMovies = await this.getAllMovies(guildId, requestId) - if (count >= allMovies.length) { - logger.info(`${count} random movies requested but found only ${allMovies.length}. Returning all Movies.`, { guildId, requestId }) - return allMovies - } - const movies: BaseItemDto[] = [] - for (let i = 0; i < count; i++) { - const index = Math.floor(Math.random() * allMovies.length) - movies.push(...allMovies.splice(index, 1)) // maybe out of bounds? ? - } - return movies - } + public async getRandomMovies(count: number, guildId: string, requestId: string): Promise { + logger.info(`${count} random movies requested.`, { guildId, requestId }) + const allMovies = await this.getAllMovies(guildId, requestId) + if (count >= allMovies.length) { + logger.info(`${count} random movies requested but found only ${allMovies.length}. Returning all Movies.`, { guildId, requestId }) + return allMovies + } + const movies: BaseItemDto[] = [] + for (let i = 0; i < count; i++) { + 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 }) + 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 - } + 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 + } } diff --git a/server/jellyfin/runtime.ts b/server/jellyfin/runtime.ts index d0d1643..93efc22 100644 --- a/server/jellyfin/runtime.ts +++ b/server/jellyfin/runtime.ts @@ -16,72 +16,72 @@ export const BASE_PATH = "http://localhost".replace(/\/+$/, ""); export interface ConfigurationParameters { - basePath?: string; // override base path - fetchApi?: FetchAPI; // override for fetch implementation - middleware?: Middleware[]; // middleware to apply before/after fetch requests - queryParamsStringify?: (params: HTTPQuery) => string; // stringify function for query strings - username?: string; // parameter for basic security - password?: string; // parameter for basic security - apiKey?: string | ((name: string) => string); // parameter for apiKey security - accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string | Promise); // parameter for oauth2 security - headers?: HTTPHeaders; //header params we want to use on every request - credentials?: RequestCredentials; //value for the credentials param we want to use on each request + basePath?: string; // override base path + fetchApi?: FetchAPI; // override for fetch implementation + middleware?: Middleware[]; // middleware to apply before/after fetch requests + queryParamsStringify?: (params: HTTPQuery) => string; // stringify function for query strings + username?: string; // parameter for basic security + password?: string; // parameter for basic security + apiKey?: string | ((name: string) => string); // parameter for apiKey security + accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string | Promise); // parameter for oauth2 security + headers?: HTTPHeaders; //header params we want to use on every request + credentials?: RequestCredentials; //value for the credentials param we want to use on each request } export class Configuration { - constructor(private configuration: ConfigurationParameters = {}) {} + constructor(private configuration: ConfigurationParameters = {}) { } - set config(configuration: Configuration) { - this.configuration = configuration; - } + set config(configuration: Configuration) { + this.configuration = configuration; + } - get basePath(): string { - return this.configuration.basePath != null ? this.configuration.basePath : BASE_PATH; - } + get basePath(): string { + return this.configuration.basePath != null ? this.configuration.basePath : BASE_PATH; + } - get fetchApi(): FetchAPI | undefined { - return this.configuration.fetchApi; - } + get fetchApi(): FetchAPI | undefined { + return this.configuration.fetchApi; + } - get middleware(): Middleware[] { - return this.configuration.middleware || []; - } + get middleware(): Middleware[] { + return this.configuration.middleware || []; + } - get queryParamsStringify(): (params: HTTPQuery) => string { - return this.configuration.queryParamsStringify || querystring; - } + get queryParamsStringify(): (params: HTTPQuery) => string { + return this.configuration.queryParamsStringify || querystring; + } - get username(): string | undefined { - return this.configuration.username; - } + get username(): string | undefined { + return this.configuration.username; + } - get password(): string | undefined { - return this.configuration.password; - } + get password(): string | undefined { + return this.configuration.password; + } - get apiKey(): ((name: string) => string) | undefined { - const apiKey = this.configuration.apiKey; - if (apiKey) { - return typeof apiKey === 'function' ? apiKey : () => apiKey; - } - return undefined; - } + get apiKey(): ((name: string) => string) | undefined { + const apiKey = this.configuration.apiKey; + if (apiKey) { + return typeof apiKey === 'function' ? apiKey : () => apiKey; + } + return undefined; + } - get accessToken(): ((name?: string, scopes?: string[]) => string | Promise) | undefined { - const accessToken = this.configuration.accessToken; - if (accessToken) { - return typeof accessToken === 'function' ? accessToken : async () => accessToken; - } - return undefined; - } + get accessToken(): ((name?: string, scopes?: string[]) => string | Promise) | undefined { + const accessToken = this.configuration.accessToken; + if (accessToken) { + return typeof accessToken === 'function' ? accessToken : async () => accessToken; + } + return undefined; + } - get headers(): HTTPHeaders | undefined { - return this.configuration.headers; - } + get headers(): HTTPHeaders | undefined { + return this.configuration.headers; + } - get credentials(): RequestCredentials | undefined { - return this.configuration.credentials; - } + get credentials(): RequestCredentials | undefined { + return this.configuration.credentials; + } } export const DefaultConfig = new Configuration(); @@ -91,192 +91,192 @@ export const DefaultConfig = new Configuration(); */ export class BaseAPI { - private static readonly jsonRegex = new RegExp('^(:?application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(:?;.*)?$', 'i'); - private middleware: Middleware[]; + private static readonly jsonRegex = new RegExp('^(:?application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(:?;.*)?$', 'i'); + private middleware: Middleware[]; - constructor(protected configuration = DefaultConfig) { - this.middleware = configuration.middleware; - } + constructor(protected configuration = DefaultConfig) { + this.middleware = configuration.middleware; + } - withMiddleware(this: T, ...middlewares: Middleware[]) { - const next = this.clone(); - next.middleware = next.middleware.concat(...middlewares); - return next; - } + withMiddleware(this: T, ...middlewares: Middleware[]) { + const next = this.clone(); + next.middleware = next.middleware.concat(...middlewares); + return next; + } - withPreMiddleware(this: T, ...preMiddlewares: Array) { - const middlewares = preMiddlewares.map((pre) => ({ pre })); - return this.withMiddleware(...middlewares); - } + withPreMiddleware(this: T, ...preMiddlewares: Array) { + const middlewares = preMiddlewares.map((pre) => ({ pre })); + return this.withMiddleware(...middlewares); + } - withPostMiddleware(this: T, ...postMiddlewares: Array) { - const middlewares = postMiddlewares.map((post) => ({ post })); - return this.withMiddleware(...middlewares); - } + withPostMiddleware(this: T, ...postMiddlewares: Array) { + const middlewares = postMiddlewares.map((post) => ({ post })); + return this.withMiddleware(...middlewares); + } - /** - * Check if the given MIME is a JSON MIME. - * JSON MIME examples: - * application/json - * application/json; charset=UTF8 - * APPLICATION/JSON - * application/vnd.company+json - * @param mime - MIME (Multipurpose Internet Mail Extensions) - * @return True if the given MIME is JSON, false otherwise. - */ - protected isJsonMime(mime: string | null | undefined): boolean { - if (!mime) { - return false; - } - return BaseAPI.jsonRegex.test(mime); - } + /** + * Check if the given MIME is a JSON MIME. + * JSON MIME examples: + * application/json + * application/json; charset=UTF8 + * APPLICATION/JSON + * application/vnd.company+json + * @param mime - MIME (Multipurpose Internet Mail Extensions) + * @return True if the given MIME is JSON, false otherwise. + */ + protected isJsonMime(mime: string | null | undefined): boolean { + if (!mime) { + return false; + } + return BaseAPI.jsonRegex.test(mime); + } - protected async request(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction): Promise { - const { url, init } = await this.createFetchParams(context, initOverrides); - const response = await this.fetchApi(url, init); - if (response && (response.status >= 200 && response.status < 300)) { - return response; - } - throw new ResponseError(response, 'Response returned an error code'); - } + protected async request(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction): Promise { + const { url, init } = await this.createFetchParams(context, initOverrides); + const response = await this.fetchApi(url, init); + if (response && (response.status >= 200 && response.status < 300)) { + return response; + } + throw new ResponseError(response, 'Response returned an error code'); + } - private async createFetchParams(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction) { - let url = this.configuration.basePath + context.path; - if (context.query !== undefined && Object.keys(context.query).length !== 0) { - // only add the querystring to the URL if there are query parameters. - // this is done to avoid urls ending with a "?" character which buggy webservers - // do not handle correctly sometimes. - url += '?' + this.configuration.queryParamsStringify(context.query); - } + private async createFetchParams(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction) { + let url = this.configuration.basePath + context.path; + if (context.query !== undefined && Object.keys(context.query).length !== 0) { + // only add the querystring to the URL if there are query parameters. + // this is done to avoid urls ending with a "?" character which buggy webservers + // do not handle correctly sometimes. + url += '?' + this.configuration.queryParamsStringify(context.query); + } - const headers = Object.assign({}, this.configuration.headers, context.headers); - Object.keys(headers).forEach(key => headers[key] === undefined ? delete headers[key] : {}); + const headers = Object.assign({}, this.configuration.headers, context.headers); + Object.keys(headers).forEach(key => headers[key] === undefined ? delete headers[key] : {}); - const initOverrideFn = - typeof initOverrides === "function" - ? initOverrides - : async () => initOverrides; + const initOverrideFn = + typeof initOverrides === "function" + ? initOverrides + : async () => initOverrides; - const initParams = { - method: context.method, - headers, - body: context.body, - credentials: this.configuration.credentials, - }; + const initParams = { + method: context.method, + headers, + body: context.body, + credentials: this.configuration.credentials, + }; - const overriddenInit: RequestInit = { - ...initParams, - ...(await initOverrideFn({ - init: initParams, - context, - })) - }; + const overriddenInit: RequestInit = { + ...initParams, + ...(await initOverrideFn({ + init: initParams, + context, + })) + }; - const init: RequestInit = { - ...overriddenInit, - body: - isFormData(overriddenInit.body) || - overriddenInit.body instanceof URLSearchParams || - isBlob(overriddenInit.body) - ? overriddenInit.body - : JSON.stringify(overriddenInit.body), - }; + const init: RequestInit = { + ...overriddenInit, + body: + isFormData(overriddenInit.body) || + overriddenInit.body instanceof URLSearchParams || + isBlob(overriddenInit.body) + ? overriddenInit.body + : JSON.stringify(overriddenInit.body), + }; - return { url, init }; - } + return { url, init }; + } - private fetchApi = async (url: string, init: RequestInit) => { - let fetchParams = { url, init }; - for (const middleware of this.middleware) { - if (middleware.pre) { - fetchParams = await middleware.pre({ - fetch: this.fetchApi, - ...fetchParams, - }) || fetchParams; - } - } - let response: Response | undefined = undefined; - try { - response = await (this.configuration.fetchApi || fetch)(fetchParams.url, fetchParams.init); - } catch (e) { - for (const middleware of this.middleware) { - if (middleware.onError) { - response = await middleware.onError({ - fetch: this.fetchApi, - url: fetchParams.url, - init: fetchParams.init, - error: e, - response: response ? response.clone() : undefined, - }) || response; - } - } - if (response === undefined) { - if (e instanceof Error) { - throw new FetchError(e, 'The request failed and the interceptors did not return an alternative response'); - } else { - throw e; - } - } - } - for (const middleware of this.middleware) { - if (middleware.post) { - response = await middleware.post({ - fetch: this.fetchApi, - url: fetchParams.url, - init: fetchParams.init, - response: response.clone(), - }) || response; - } - } - return response; - } + private fetchApi = async (url: string, init: RequestInit) => { + let fetchParams = { url, init }; + for (const middleware of this.middleware) { + if (middleware.pre) { + fetchParams = await middleware.pre({ + fetch: this.fetchApi, + ...fetchParams, + }) || fetchParams; + } + } + let response: Response | undefined = undefined; + try { + response = await (this.configuration.fetchApi || fetch)(fetchParams.url, fetchParams.init); + } catch (e) { + for (const middleware of this.middleware) { + if (middleware.onError) { + response = await middleware.onError({ + fetch: this.fetchApi, + url: fetchParams.url, + init: fetchParams.init, + error: e, + response: response ? response.clone() : undefined, + }) || response; + } + } + if (response === undefined) { + if (e instanceof Error) { + throw new FetchError(e, 'The request failed and the interceptors did not return an alternative response'); + } else { + throw e; + } + } + } + for (const middleware of this.middleware) { + if (middleware.post) { + response = await middleware.post({ + fetch: this.fetchApi, + url: fetchParams.url, + init: fetchParams.init, + response: response.clone(), + }) || response; + } + } + return response; + } - /** - * Create a shallow clone of `this` by constructing a new instance - * and then shallow cloning data members. - */ - private clone(this: T): T { - const constructor = this.constructor as any; - const next = new constructor(this.configuration); - next.middleware = this.middleware.slice(); - return next; - } + /** + * Create a shallow clone of `this` by constructing a new instance + * and then shallow cloning data members. + */ + private clone(this: T): T { + const constructor = this.constructor as any; + const next = new constructor(this.configuration); + next.middleware = this.middleware.slice(); + return next; + } }; function isBlob(value: any): value is Blob { - return typeof Blob !== 'undefined' && value instanceof Blob; + return typeof Blob !== 'undefined' && value instanceof Blob; } function isFormData(value: any): value is FormData { - return typeof FormData !== "undefined" && value instanceof FormData; + return typeof FormData !== "undefined" && value instanceof FormData; } export class ResponseError extends Error { - override name: "ResponseError" = "ResponseError"; - constructor(public response: Response, msg?: string) { - super(msg); - } + override name: "ResponseError" = "ResponseError"; + constructor(public response: Response, msg?: string) { + super(msg); + } } export class FetchError extends Error { - override name: "FetchError" = "FetchError"; - constructor(public cause: Error, msg?: string) { - super(msg); - } + override name: "FetchError" = "FetchError"; + constructor(public cause: Error, msg?: string) { + super(msg); + } } export class RequiredError extends Error { - override name: "RequiredError" = "RequiredError"; - constructor(public field: string, msg?: string) { - super(msg); - } + override name: "RequiredError" = "RequiredError"; + constructor(public field: string, msg?: string) { + super(msg); + } } export const COLLECTION_FORMATS = { - csv: ",", - ssv: " ", - tsv: "\t", - pipes: "|", + csv: ",", + ssv: " ", + tsv: "\t", + pipes: "|", }; export type FetchAPI = WindowOrWorkerGlobalScope['fetch']; @@ -292,134 +292,134 @@ export type ModelPropertyNaming = 'camelCase' | 'snake_case' | 'PascalCase' | 'o export type InitOverrideFunction = (requestContext: { init: HTTPRequestInit, context: RequestOpts }) => Promise export interface FetchParams { - url: string; - init: RequestInit; + url: string; + init: RequestInit; } export interface RequestOpts { - path: string; - method: HTTPMethod; - headers: HTTPHeaders; - query?: HTTPQuery; - body?: HTTPBody; + path: string; + method: HTTPMethod; + headers: HTTPHeaders; + query?: HTTPQuery; + body?: HTTPBody; } export function exists(json: any, key: string) { - const value = json[key]; - return value !== null && value !== undefined; + const value = json[key]; + return value !== null && value !== undefined; } export function querystring(params: HTTPQuery, prefix: string = ''): string { - return Object.keys(params) - .map(key => querystringSingleKey(key, params[key], prefix)) - .filter(part => part.length > 0) - .join('&'); + return Object.keys(params) + .map(key => querystringSingleKey(key, params[key], prefix)) + .filter(part => part.length > 0) + .join('&'); } function querystringSingleKey(key: string, value: string | number | null | undefined | boolean | Array | Set | HTTPQuery, keyPrefix: string = ''): string { - const fullKey = keyPrefix + (keyPrefix.length ? `[${key}]` : key); - if (value instanceof Array) { - const multiValue = value.map(singleValue => encodeURIComponent(String(singleValue))) - .join(`&${encodeURIComponent(fullKey)}=`); - return `${encodeURIComponent(fullKey)}=${multiValue}`; - } - if (value instanceof Set) { - const valueAsArray = Array.from(value); - return querystringSingleKey(key, valueAsArray, keyPrefix); - } - if (value instanceof Date) { - return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`; - } - if (value instanceof Object) { - return querystring(value as HTTPQuery, fullKey); - } - return `${encodeURIComponent(fullKey)}=${encodeURIComponent(String(value))}`; + const fullKey = keyPrefix + (keyPrefix.length ? `[${key}]` : key); + if (value instanceof Array) { + const multiValue = value.map(singleValue => encodeURIComponent(String(singleValue))) + .join(`&${encodeURIComponent(fullKey)}=`); + return `${encodeURIComponent(fullKey)}=${multiValue}`; + } + if (value instanceof Set) { + const valueAsArray = Array.from(value); + return querystringSingleKey(key, valueAsArray, keyPrefix); + } + if (value instanceof Date) { + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`; + } + if (value instanceof Object) { + return querystring(value as HTTPQuery, fullKey); + } + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(String(value))}`; } export function mapValues(data: any, fn: (item: any) => any) { - return Object.keys(data).reduce( - (acc, key) => ({ ...acc, [key]: fn(data[key]) }), - {} - ); + return Object.keys(data).reduce( + (acc, key) => ({ ...acc, [key]: fn(data[key]) }), + {} + ); } export function canConsumeForm(consumes: Consume[]): boolean { - for (const consume of consumes) { - if ('multipart/form-data' === consume.contentType) { - return true; - } - } - return false; + for (const consume of consumes) { + if ('multipart/form-data' === consume.contentType) { + return true; + } + } + return false; } export interface Consume { - contentType: string; + contentType: string; } export interface RequestContext { - fetch: FetchAPI; - url: string; - init: RequestInit; + fetch: FetchAPI; + url: string; + init: RequestInit; } export interface ResponseContext { - fetch: FetchAPI; - url: string; - init: RequestInit; - response: Response; + fetch: FetchAPI; + url: string; + init: RequestInit; + response: Response; } export interface ErrorContext { - fetch: FetchAPI; - url: string; - init: RequestInit; - error: unknown; - response?: Response; + fetch: FetchAPI; + url: string; + init: RequestInit; + error: unknown; + response?: Response; } export interface Middleware { - pre?(context: RequestContext): Promise; - post?(context: ResponseContext): Promise; - onError?(context: ErrorContext): Promise; + pre?(context: RequestContext): Promise; + post?(context: ResponseContext): Promise; + onError?(context: ErrorContext): Promise; } export interface ApiResponse { - raw: Response; - value(): Promise; + raw: Response; + value(): Promise; } export interface ResponseTransformer { - (json: any): T; + (json: any): T; } export class JSONApiResponse { - constructor(public raw: Response, private transformer: ResponseTransformer = (jsonValue: any) => jsonValue) {} + constructor(public raw: Response, private transformer: ResponseTransformer = (jsonValue: any) => jsonValue) { } - async value(): Promise { - return this.transformer(await this.raw.json()); - } + async value(): Promise { + return this.transformer(await this.raw.json()); + } } export class VoidApiResponse { - constructor(public raw: Response) {} + constructor(public raw: Response) { } - async value(): Promise { - return undefined; - } + async value(): Promise { + return undefined; + } } export class BlobApiResponse { - constructor(public raw: Response) {} + constructor(public raw: Response) { } - async value(): Promise { - return await this.raw.blob(); - }; + async value(): Promise { + return await this.raw.blob(); + }; } export class TextApiResponse { - constructor(public raw: Response) {} + constructor(public raw: Response) { } - async value(): Promise { - return await this.raw.text(); - }; + async value(): Promise { + return await this.raw.text(); + }; } diff --git a/server/logger.ts b/server/logger.ts index 5497ea9..6b53f3b 100644 --- a/server/logger.ts +++ b/server/logger.ts @@ -3,22 +3,22 @@ import { config } from "./configuration" const printFn = format.printf(({ guildId, level, message, errorCode, requestId, timestamp: logTimestamp }: { [k: string]: string }) => { - return `[${guildId ?? ''}][${level}][${logTimestamp}][${errorCode ?? ''}][${requestId ?? ''}]:${message}` + return `[${guildId ?? ''}][${level}][${logTimestamp}][${errorCode ?? ''}][${requestId ?? ''}]:${message}` }) const logFormat = format.combine( - format.timestamp(), - printFn + format.timestamp(), + printFn ) const consoleTransports = [ - new transports.Console({ - format: logFormat - }) + new transports.Console({ + format: logFormat + }) ] export const logger = createLogger({ - level: config.bot.debug ? 'debug' : 'info', - format: logFormat, - silent: config.bot.silent, - transports: consoleTransports + level: config.bot.debug ? 'debug' : 'info', + format: logFormat, + silent: config.bot.silent, + transports: consoleTransports }) diff --git a/server/structures/client.ts b/server/structures/client.ts index fae18b3..10be4af 100644 --- a/server/structures/client.ts +++ b/server/structures/client.ts @@ -13,169 +13,169 @@ import { checkForPollsToClose } from "../commands/closepoll"; export class ExtendedClient extends Client { - private eventFilePath = `${__dirname}/../events` - 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 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) - const options: ClientOptions = { intents } - super(options) - this.jellyfin = jf - } - public async start() { - if (process.env.NODE_ENV === 'test') return - const promises: Promise[] = [] - promises.push(this.registerSlashCommands()) - promises.push(this.registerEventCallback()) - Promise.all(promises).then(() => { - this.login(config.bot.token) - }) - } - private async importFile(filepath: string): Promise { - logger.debug(`Importing ${filepath}`) - const imported = await import(filepath) - logger.debug(`Imported ${JSON.stringify(imported)}`) - return imported.default ?? imported - } - public async registerCommands(cmds: ApplicationCommandDataResolvable[], guildIds: Collection) { - if (guildIds) { - guildIds.forEach(guild => { - this.guilds.cache.get(guild.id)?.commands.set(cmds) - logger.info(`Registering commands to ${guild.name}|${guild.id}`) - }) - } else { - this.application?.commands.set(cmds) - logger.info(`Registering global commands`) - } - return - } - public async registerSlashCommands(): Promise { - try { - const slashCommands: ApplicationCommandDataResolvable[] = [] - const commandFiles = fs.readdirSync(this.commandFilePath).filter(file => file.endsWith('.ts') || file.endsWith('.js')) - for (const commandFile of commandFiles) { - const filePath = `${this.commandFilePath}/${commandFile}` - const command = await this.importFile(filePath) - logger.debug(JSON.stringify(command)) - if (!command.name) return - this.commands.set(command.name, command) - slashCommands.push(command) - } - this.on("ready", async (client: Client) => { - //logger.info(`Ready processing ${JSON.stringify(client)}`) - logger.info(`SlashCommands: ${JSON.stringify(slashCommands)}`) - const guilds = client.guilds.cache + private eventFilePath = `${__dirname}/../events` + 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 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) + const options: ClientOptions = { intents } + super(options) + this.jellyfin = jf + } + public async start() { + if (process.env.NODE_ENV === 'test') return + const promises: Promise[] = [] + promises.push(this.registerSlashCommands()) + promises.push(this.registerEventCallback()) + Promise.all(promises).then(() => { + this.login(config.bot.token) + }) + } + private async importFile(filepath: string): Promise { + logger.debug(`Importing ${filepath}`) + const imported = await import(filepath) + logger.debug(`Imported ${JSON.stringify(imported)}`) + return imported.default ?? imported + } + public async registerCommands(cmds: ApplicationCommandDataResolvable[], guildIds: Collection) { + if (guildIds) { + guildIds.forEach(guild => { + this.guilds.cache.get(guild.id)?.commands.set(cmds) + logger.info(`Registering commands to ${guild.name}|${guild.id}`) + }) + } else { + this.application?.commands.set(cmds) + logger.info(`Registering global commands`) + } + return + } + public async registerSlashCommands(): Promise { + try { + const slashCommands: ApplicationCommandDataResolvable[] = [] + const commandFiles = fs.readdirSync(this.commandFilePath).filter(file => file.endsWith('.ts') || file.endsWith('.js')) + for (const commandFile of commandFiles) { + const filePath = `${this.commandFilePath}/${commandFile}` + const command = await this.importFile(filePath) + logger.debug(JSON.stringify(command)) + if (!command.name) return + this.commands.set(command.name, command) + slashCommands.push(command) + } + 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) - await this.cacheAnnouncementServer(guilds) - this.startAnnouncementRoleBackgroundTask(guilds) - this.startPollCloseBackgroundTasks() - }) - } catch (error) { - logger.info(`Error refreshing slash commands: ${error}`) - } - } - 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) + this.registerCommands(slashCommands, guilds) + this.cacheUsers(guilds) + await this.cacheAnnouncementServer(guilds) + this.startAnnouncementRoleBackgroundTask(guilds) + this.startPollCloseBackgroundTasks() + }) + } catch (error) { + logger.info(`Error refreshing slash commands: ${error}`) + } + } + 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) - if (!channels || channels.length != 1) { - logger.error(`Could not find announcement channel for guild ${guild.name} with guildId ${guild.id}. Found ${channels}`) - continue - } - logger.info(`Fetched announcement channel: ${JSON.stringify(channels[0])}`) - this.announcementChannels.set(guild.id, channels[0]) - } - } - public getAnnouncementChannelForGuild(guildId: string): Maybe { - return this.announcementChannels.get(guildId) - } - public async cacheUsers(guilds: Collection) { - guilds.forEach((guild: Guild, id: Snowflake) => { - logger.info(`Fetching members for ${guild.name}|${id}`) - guild.members.fetch() - logger.info(`Fetched: ${guild.memberCount} members`) - }) - } - public async registerEventCallback() { - try { - const eventFiles = fs.readdirSync(this.eventFilePath).filter(file => file.endsWith('.ts') || file.endsWith('.js')); - for (const file of eventFiles) { - const filePath = `${this.eventFilePath}/${file}` - const event = await this.importFile(filePath) - if (event.once) { - logger.info(`Registering once ${file}`) - this.once(event.name, (...args: any[]) => event.execute(...args)) - } - else { - logger.info(`Registering on ${file}`) - this.on(event.name, (...args: any[]) => event.execute(...args)) - } - } - logger.info(`Registered event names ${this.eventNames()}`) - } catch (error) { - logger.error(error) - } - } + if (!channels || channels.length != 1) { + logger.error(`Could not find announcement channel for guild ${guild.name} with guildId ${guild.id}. Found ${channels}`) + continue + } + logger.info(`Fetched announcement channel: ${JSON.stringify(channels[0])}`) + this.announcementChannels.set(guild.id, channels[0]) + } + } + public getAnnouncementChannelForGuild(guildId: string): Maybe { + return this.announcementChannels.get(guildId) + } + public async cacheUsers(guilds: Collection) { + guilds.forEach((guild: Guild, id: Snowflake) => { + logger.info(`Fetching members for ${guild.name}|${id}`) + guild.members.fetch() + logger.info(`Fetched: ${guild.memberCount} members`) + }) + } + public async registerEventCallback() { + try { + const eventFiles = fs.readdirSync(this.eventFilePath).filter(file => file.endsWith('.ts') || file.endsWith('.js')); + for (const file of eventFiles) { + const filePath = `${this.eventFilePath}/${file}` + const event = await this.importFile(filePath) + if (event.once) { + logger.info(`Registering once ${file}`) + this.once(event.name, (...args: any[]) => event.execute(...args)) + } + else { + logger.info(`Registering on ${file}`) + this.on(event.name, (...args: any[]) => event.execute(...args)) + } + } + logger.info(`Registered event names ${this.eventNames()}`) + } catch (error) { + 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: 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]")) + public async startAnnouncementRoleBackgroundTask(guilds: Collection) { + for (const guild of guilds.values()) { + logger.info("Starting background task for announcement role", { guildId: 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 - } + 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() - 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 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("🎫") - //logger.debug(`reactions: ${JSON.stringify(reactions, null, 2)}`, { guildId: guild.id, requestId }) - if (reactions) { - manageAnnouncementRoles(message.guild, reactions, requestId) - } else { - logger.error("Did not get reactions! Aborting!", { guildId: guild.id, requestId }) - } - })) - } - } + 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 { + logger.error("Did not get reactions! Aborting!", { guildId: guild.id, requestId }) + } + })) + } + } - 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 - } - task.stop() - } + 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 + } + task.stop() + } - private async startPollCloseBackgroundTasks() { - for(const guild of this.guilds.cache) { - this.pollCloseBackgroundTasks.set(guild[1].id, schedule("0 * * * * *", () => checkForPollsToClose(guild[1]))) - } - } + private async startPollCloseBackgroundTasks() { + for (const guild of this.guilds.cache) { + this.pollCloseBackgroundTasks.set(guild[1].id, schedule("0 * * * * *", () => checkForPollsToClose(guild[1]))) + } + } } diff --git a/server/structures/event.ts b/server/structures/event.ts index 192ecfc..367b104 100644 --- a/server/structures/event.ts +++ b/server/structures/event.ts @@ -1,8 +1,8 @@ import { ClientEvents } from "discord.js"; export class Event{ - constructor( - public event: Key, - public run: (...args: ClientEvents[Key]) => unknown - ) { } + constructor( + public event: Key, + public run: (...args: ClientEvents[Key]) => unknown + ) { } }