Compare commits
	
		
			50 Commits
		
	
	
		
			eef3a9c358
			...
			feat/20-re
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| a60fc2db7e | |||
| a50ac1716f | |||
| ef39c6315d | |||
| 1f372b0aac | |||
| d1aacbb3d3 | |||
| 1ae8278fb8 | |||
| 417b24d408 | |||
| 88061c361c | |||
| f83f54749d | |||
| 90b0b07080 | |||
| 6d0eaed426 | |||
| 8f320cee5c | |||
| 016bb243cc | |||
| 2c8cd96ac7 | |||
| ba4aefed8e | |||
| 8efae12907 | |||
| fec0bc31f1 | |||
| 1bfcaa95f9 | |||
| fb4ab59dc6 | |||
| 6d40930dc1 | |||
| 4e9fe587b0 | |||
| 03b6a30ffa | |||
| 7d794a8001 | |||
| 8df180898e | |||
| 976175242b | |||
| 68546b0b50 | |||
| 1348abbd48 | |||
| fce9091114 | |||
| 081f3c6201 | |||
| ca99987a20 | |||
| fc64728a78 | |||
| 20da25f2bf | |||
| a455fd8ff7 | |||
| 119343c916 | |||
| 296a490e93 | |||
| 66507cb08f | |||
| 4600820889 | |||
| 4a3e8809be | |||
| 690ba697b6 | |||
| 71343d6742 | |||
| 3f6e558d39 | |||
| ca259c5f24 | |||
| b1c581ca6e | |||
| 96189c2392 | |||
| 700353cff4 | |||
| f705b97804 | |||
| 9cdc6e1934 | |||
| c73cd20ccf | |||
| e66aebc88c | |||
| 599243990e | 
@@ -7,6 +7,7 @@ module.exports = {
 | 
				
			|||||||
		'^.+\\.tsx?$': 'ts-jest'
 | 
							'^.+\\.tsx?$': 'ts-jest'
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	'testRegex': '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',
 | 
						'testRegex': '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',
 | 
				
			||||||
 | 
						'setupFiles': ["<rootDir>/tests/testenv.js"],
 | 
				
			||||||
	'moduleFileExtensions': [
 | 
						'moduleFileExtensions': [
 | 
				
			||||||
		'ts',
 | 
							'ts',
 | 
				
			||||||
		'tsx',
 | 
							'tsx',
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										4
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -1,12 +1,12 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
	"name": "node-jellyfin-discord-bot",
 | 
						"name": "node-jellyfin-discord-bot",
 | 
				
			||||||
  "version": "1.1.3",
 | 
						"version": "1.1.4",
 | 
				
			||||||
	"lockfileVersion": 2,
 | 
						"lockfileVersion": 2,
 | 
				
			||||||
	"requires": true,
 | 
						"requires": true,
 | 
				
			||||||
	"packages": {
 | 
						"packages": {
 | 
				
			||||||
		"": {
 | 
							"": {
 | 
				
			||||||
			"name": "node-jellyfin-discord-bot",
 | 
								"name": "node-jellyfin-discord-bot",
 | 
				
			||||||
      "version": "1.1.3",
 | 
								"version": "1.1.4",
 | 
				
			||||||
			"license": "MIT",
 | 
								"license": "MIT",
 | 
				
			||||||
			"dependencies": {
 | 
								"dependencies": {
 | 
				
			||||||
				"@discordjs/rest": "^1.7.0",
 | 
									"@discordjs/rest": "^1.7.0",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
	"name": "node-jellyfin-discord-bot",
 | 
						"name": "node-jellyfin-discord-bot",
 | 
				
			||||||
	"version": "1.1.3",
 | 
						"version": "1.1.4",
 | 
				
			||||||
	"description": "A discord bot to sync jellyfin accounts with discord roles",
 | 
						"description": "A discord bot to sync jellyfin accounts with discord roles",
 | 
				
			||||||
	"main": "index.js",
 | 
						"main": "index.js",
 | 
				
			||||||
	"license": "MIT",
 | 
						"license": "MIT",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -82,41 +82,5 @@ Für eine Erklärung wie das alles funktioniert mach einfach /mitgucken für ein
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function manageAnnouncementRoles(guild: Guild, reaction: MessageReaction, requestId: string) {
 | 
					 | 
				
			||||||
	const guildId = guild.id
 | 
					 | 
				
			||||||
	logger.info("Managing roles", { guildId, requestId })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const announcementRole: Role | undefined = (await guild.roles.fetch()).find(role => role.id === config.bot.announcement_role)
 | 
					 | 
				
			||||||
	if (!announcementRole) {
 | 
					 | 
				
			||||||
		logger.error(`Could not find announcement role! Aborting! Was looking for role with id: ${config.bot.announcement_role}`, { guildId, requestId })
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const usersWhoWantRole: User[] = (await reaction.users.fetch()).filter(user => !user.bot).map(user => user)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const allUsers = (await guild.members.fetch())
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const usersWhoHaveRole: GuildMember[] = allUsers
 | 
					 | 
				
			||||||
		.filter(member => member.roles.cache
 | 
					 | 
				
			||||||
			.find(role => role.id === config.bot.announcement_role) !== undefined)
 | 
					 | 
				
			||||||
		.map(member => member)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const usersWhoNeedRoleRevoked: GuildMember[] = usersWhoHaveRole
 | 
					 | 
				
			||||||
		.filter(userWhoHas => !usersWhoWantRole.map(wanter => wanter.id).includes(userWhoHas.id))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const usersWhoDontHaveRole: GuildMember[] = allUsers
 | 
					 | 
				
			||||||
		.filter(member => member.roles.cache
 | 
					 | 
				
			||||||
			.find(role => role.id === config.bot.announcement_role) === undefined)
 | 
					 | 
				
			||||||
		.map(member => member)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const usersWhoNeedRole: GuildMember[] = usersWhoDontHaveRole
 | 
					 | 
				
			||||||
		.filter(userWhoNeeds => usersWhoWantRole.map(wanter => wanter.id).includes(userWhoNeeds.id))
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	logger.debug(`Theses users will get the role removed: ${JSON.stringify(usersWhoNeedRoleRevoked)}`, { guildId, requestId })
 | 
					 | 
				
			||||||
	logger.debug(`Theses users will get the role added: ${JSON.stringify(usersWhoNeedRole)}`, { guildId, requestId })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	usersWhoNeedRoleRevoked.forEach(user => user.roles.remove(announcementRole))
 | 
					 | 
				
			||||||
	usersWhoNeedRole.forEach(user => user.roles.add(announcementRole))
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,7 @@
 | 
				
			|||||||
import { ApplicationCommandOptionType } from 'discord.js'
 | 
					import { ApplicationCommandOptionType } from 'discord.js'
 | 
				
			||||||
import { Command } from '../structures/command'
 | 
					import { Command } from '../structures/command'
 | 
				
			||||||
import { RunOptions } from '../types/commandTypes'
 | 
					import { RunOptions } from '../types/commandTypes'
 | 
				
			||||||
 | 
					import { logger } from '../logger'
 | 
				
			||||||
export default new Command({
 | 
					export default new Command({
 | 
				
			||||||
	name: 'echo',
 | 
						name: 'echo',
 | 
				
			||||||
	description: 'Echoes a text',
 | 
						description: 'Echoes a text',
 | 
				
			||||||
@@ -13,7 +14,7 @@ export default new Command({
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	],
 | 
						],
 | 
				
			||||||
	run: async (interaction: RunOptions) => {
 | 
						run: async (interaction: RunOptions) => {
 | 
				
			||||||
		console.log('echo called')
 | 
							logger.info('echo called')
 | 
				
			||||||
		interaction.interaction.reply(interaction.toString())
 | 
							interaction.interaction.reply(interaction.toString())
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,15 +2,16 @@ import { v4 as uuid } from 'uuid'
 | 
				
			|||||||
import { jellyfinHandler } from "../.."
 | 
					import { jellyfinHandler } from "../.."
 | 
				
			||||||
import { Command } from '../structures/command'
 | 
					import { Command } from '../structures/command'
 | 
				
			||||||
import { RunOptions } from '../types/commandTypes'
 | 
					import { RunOptions } from '../types/commandTypes'
 | 
				
			||||||
 | 
					import { logger } from '../logger'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default new Command({
 | 
					export default new Command({
 | 
				
			||||||
	name: 'passwort_reset',
 | 
						name: 'passwort_reset',
 | 
				
			||||||
	description: 'Ich vergebe dir ein neues Passwort und schicke es dir per DM zu. Kostet auch nix! Versprochen! 😉',
 | 
						description: 'Ich vergebe dir ein neues Passwort und schicke es dir per DM zu. Kostet auch nix! Versprochen! 😉',
 | 
				
			||||||
	options: [],
 | 
						options: [],
 | 
				
			||||||
	run: async (interaction: RunOptions) => {
 | 
						run: async (interaction: RunOptions) => {
 | 
				
			||||||
		console.log('PasswortReset called')
 | 
							logger.info('PasswortReset called')
 | 
				
			||||||
		interaction.interaction.followUp('Yo, ich schick dir eins!')
 | 
							interaction.interaction.followUp('Yo, ich schick dir eins!')
 | 
				
			||||||
		console.log(JSON.stringify(interaction.interaction.member, null, 2))
 | 
							logger.info(JSON.stringify(interaction.interaction.member, null, 2))
 | 
				
			||||||
		jellyfinHandler.resetUserPasswort(interaction.interaction.member, uuid())
 | 
							jellyfinHandler.resetUserPasswort(interaction.interaction.member, uuid())
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -31,6 +31,7 @@ export interface Config {
 | 
				
			|||||||
		yavin_jellyfin_token: string
 | 
							yavin_jellyfin_token: string
 | 
				
			||||||
		yavin_jellyfin_collection_user: string
 | 
							yavin_jellyfin_collection_user: string
 | 
				
			||||||
		random_movie_count: number
 | 
							random_movie_count: number
 | 
				
			||||||
 | 
							reroll_retains_top_picks: boolean
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
export const config: Config = {
 | 
					export const config: Config = {
 | 
				
			||||||
@@ -60,17 +61,18 @@ export const config: Config = {
 | 
				
			|||||||
		client_id: process.env.CLIENT_ID ?? "",
 | 
							client_id: process.env.CLIENT_ID ?? "",
 | 
				
			||||||
		jellfin_token: process.env.JELLYFIN_TOKEN ?? "",
 | 
							jellfin_token: process.env.JELLYFIN_TOKEN ?? "",
 | 
				
			||||||
		jellyfin_url: process.env.JELLYFIN_URL ?? "",
 | 
							jellyfin_url: process.env.JELLYFIN_URL ?? "",
 | 
				
			||||||
		workaround_token: process.env.TOKEN ?? "",
 | 
							workaround_token: process.env.TOKEN ?? "TOKEN",
 | 
				
			||||||
		watcher_role: process.env.WATCHER_ROLE ?? "",
 | 
							watcher_role: process.env.WATCHER_ROLE ?? "WATCHER_ROLE",
 | 
				
			||||||
		jf_admin_role: process.env.ADMIN_ROLE ?? "",
 | 
							jf_admin_role: process.env.ADMIN_ROLE ?? "ADMIN_ROLE",
 | 
				
			||||||
		announcement_role: process.env.WATCHPARTY_ANNOUNCEMENT_ROLE ?? "",
 | 
							announcement_role: process.env.WATCHPARTY_ANNOUNCEMENT_ROLE ?? "ANNOUNCE_ROLE",
 | 
				
			||||||
		announcement_channel_id: process.env.CHANNEL_ID ?? "",
 | 
							announcement_channel_id: process.env.CHANNEL_ID ?? "ANNOUNCE_CHANNEL",
 | 
				
			||||||
		jf_collection_id: process.env.JELLYFIN_COLLECTION_ID ?? "",
 | 
							jf_collection_id: process.env.JELLYFIN_COLLECTION_ID ?? "",
 | 
				
			||||||
		yavin_collection_id: process.env.YAVIN_COLLECTION_ID ?? "",
 | 
							yavin_collection_id: process.env.YAVIN_COLLECTION_ID ?? "",
 | 
				
			||||||
		yavin_jellyfin_url: process.env.YAVIN_JELLYFIN_URL ?? "",
 | 
							yavin_jellyfin_url: process.env.YAVIN_JELLYFIN_URL ?? "",
 | 
				
			||||||
		yavin_jellyfin_token: process.env.YAVIN_TOKEN ?? "",
 | 
							yavin_jellyfin_token: process.env.YAVIN_TOKEN ?? "",
 | 
				
			||||||
		yavin_jellyfin_collection_user: process.env.YAVIN_COLLECTION_USER ?? "",
 | 
							yavin_jellyfin_collection_user: process.env.YAVIN_COLLECTION_USER ?? "",
 | 
				
			||||||
		jf_user: process.env.JELLYFIN_USER ?? "",
 | 
							jf_user: process.env.JELLYFIN_USER ?? "",
 | 
				
			||||||
		random_movie_count: parseInt(process.env.RANDOM_MOVIE_COUNT ?? "5") ?? 5
 | 
							random_movie_count: parseInt(process.env.RANDOM_MOVIE_COUNT ?? "5") ?? 5,
 | 
				
			||||||
 | 
							reroll_retains_top_picks: process.env.REROLL_RETAIN === "true"
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export enum Emotes { "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟" }
 | 
					export enum ValidVoteEmotes { "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟" }
 | 
				
			||||||
export const NONE_OF_THAT = "❌"
 | 
					export const NONE_OF_THAT = "❌"
 | 
				
			||||||
 | 
					// WIP
 | 
				
			||||||
export const Emoji = {
 | 
					export const Emoji = {
 | 
				
			||||||
	"one": "\u0031\uFE0F\u20E3",
 | 
						"one": "\u0031\uFE0F\u20E3",
 | 
				
			||||||
	"two": "\u0032\uFE0F\u20E3",
 | 
						"two": "\u0032\uFE0F\u20E3",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,7 +4,6 @@ import { client, yavinJellyfinHandler } from "../..";
 | 
				
			|||||||
import { Maybe } from "../interfaces";
 | 
					import { Maybe } from "../interfaces";
 | 
				
			||||||
import { logger } from "../logger";
 | 
					import { logger } from "../logger";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
export const name = 'guildScheduledEventCreate'
 | 
					export const name = 'guildScheduledEventCreate'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function execute(event: GuildScheduledEvent) {
 | 
					export async function execute(event: GuildScheduledEvent) {
 | 
				
			||||||
@@ -25,15 +24,21 @@ export async function execute(event: GuildScheduledEvent) {
 | 
				
			|||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		logger.debug(`Found channel ${JSON.stringify(announcementChannel, null, 2)}`, { guildId: event.guildId, requestId })
 | 
							logger.debug(`Found channel ${JSON.stringify(announcementChannel, null, 2)}`, { guildId: event.guildId, requestId })
 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (!event.scheduledStartAt) {
 | 
							if (!event.scheduledStartAt) {
 | 
				
			||||||
			logger.info("EVENT DOES NOT HAVE STARTDATE; CANCELLING", { guildId: event.guildId, requestId })
 | 
								logger.info("Event does not have a start date, cancelling", { guildId: event.guildId, requestId })
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		const sentMessageText = client.voteController.createVoteMessageText(event.id, event.scheduledStartAt, movies, event.guild?.id ?? "", requestId)
 | 
							const sentMessage = await client.voteController.prepareAndSendVoteMessage({
 | 
				
			||||||
		const sentMessage = await client.voteController.sendVoteMessage(sentMessageText, movies.length, announcementChannel)
 | 
								movies,
 | 
				
			||||||
 | 
								startDate: event.scheduledStartAt,
 | 
				
			||||||
 | 
								event,
 | 
				
			||||||
 | 
								announcementChannel,
 | 
				
			||||||
 | 
								pinAfterSending: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
								event.guildId,
 | 
				
			||||||
 | 
								requestId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		sentMessage.pin()
 | 
							logger.debug(JSON.stringify(sentMessage))
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import { Message, MessageReaction, User } from "discord.js";
 | 
					import { Message, MessageReaction, User } from "discord.js";
 | 
				
			||||||
import { logger, newRequestId, noGuildId } from "../logger";
 | 
					import { logger, newRequestId, noGuildId } from "../logger";
 | 
				
			||||||
import { Emoji, Emotes, NONE_OF_THAT } from "../constants";
 | 
					import { Emoji, ValidVoteEmotes, NONE_OF_THAT } from "../constants";
 | 
				
			||||||
import { client } from "../..";
 | 
					import { client } from "../..";
 | 
				
			||||||
import { isInitialAnnouncement, isVoteMessage } from "../helper/messageIdentifiers";
 | 
					import { isInitialAnnouncement, isVoteMessage } from "../helper/messageIdentifiers";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -22,11 +22,10 @@ export async function execute(messageReaction: MessageReaction, user: User) {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	logger.info(`Got reaction on message`, { requestId, guildId })
 | 
						logger.info(`Got reaction on message`, { requestId, guildId })
 | 
				
			||||||
	//logger.debug(`reactedUponMessage payload: ${JSON.stringify(reactedUponMessage)}`)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	logger.info(`emoji: ${messageReaction.emoji.toString()}`)
 | 
						logger.info(`emoji: ${messageReaction.emoji.toString()}`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (!Object.values(Emotes).includes(messageReaction.emoji.toString()) && messageReaction.emoji.toString() !== NONE_OF_THAT) {
 | 
						if (!Object.values(ValidVoteEmotes).includes(messageReaction.emoji.toString()) && messageReaction.emoji.toString() !== NONE_OF_THAT) {
 | 
				
			||||||
		logger.info(`${messageReaction.emoji.toString()} currently not handled`)
 | 
							logger.info(`${messageReaction.emoji.toString()} currently not handled`)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -36,13 +35,11 @@ export async function execute(messageReaction: MessageReaction, user: User) {
 | 
				
			|||||||
			logger.info(`Reaction is NONE_OF_THAT on a vote message. Handling`, { requestId, guildId })
 | 
								logger.info(`Reaction is NONE_OF_THAT on a vote message. Handling`, { requestId, guildId })
 | 
				
			||||||
			return client.voteController.handleNoneOfThatVote(messageReaction, reactedUponMessage, requestId, guildId)
 | 
								return client.voteController.handleNoneOfThatVote(messageReaction, reactedUponMessage, requestId, guildId)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if (messageReaction.emoji.toString() === Emoji.one) {
 | 
					 | 
				
			||||||
			// do something
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	else if (isInitialAnnouncement(reactedUponMessage)) {
 | 
						else if (isInitialAnnouncement(reactedUponMessage)) {
 | 
				
			||||||
		if (messageReaction.emoji.toString() === Emoji.ticket) {
 | 
							if (messageReaction.emoji.toString() === Emoji.ticket) {
 | 
				
			||||||
			logger.error(`Got a role emoji. Not implemented yet. ${reactedUponMessage.id}`)
 | 
								logger.error(`Got a role emoji. ${reactedUponMessage.id}`)
 | 
				
			||||||
 | 
								return client.roleController.addMediaRoleToUser(user, messageReaction.message.guild, requestId)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										33
									
								
								server/events/handleMessageReactionRemove.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								server/events/handleMessageReactionRemove.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					import { Message, MessageReaction, User } from "discord.js";
 | 
				
			||||||
 | 
					import { logger, newRequestId, noGuildId } from "../logger";
 | 
				
			||||||
 | 
					import { Emoji } from "../constants";
 | 
				
			||||||
 | 
					import { client } from "../..";
 | 
				
			||||||
 | 
					import { isInitialAnnouncement } from "../helper/messageIdentifiers";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const name = 'messageReactionRemove'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function execute(messageReaction: MessageReaction, user: User) {
 | 
				
			||||||
 | 
						if (user.id == client.user?.id) {
 | 
				
			||||||
 | 
							logger.info('Skipping bot reaction')
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						const requestId = newRequestId()
 | 
				
			||||||
 | 
						const guildId = messageReaction.message.inGuild() ? messageReaction.message.guildId : noGuildId
 | 
				
			||||||
 | 
						const reactedUponMessage: Message = messageReaction.message.partial ? await messageReaction.message.fetch() : messageReaction.message
 | 
				
			||||||
 | 
						if (!messageReaction.message.guild) {
 | 
				
			||||||
 | 
							logger.warn(`Received messageReactionRemove on non-guild message.`, { requestId })
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						logger.info(`Got reaction on message`, { requestId, guildId })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						logger.info(`emoji: ${messageReaction.emoji.toString()}`)
 | 
				
			||||||
 | 
						if (isInitialAnnouncement(reactedUponMessage)) {
 | 
				
			||||||
 | 
							if (messageReaction.emoji.toString() === Emoji.ticket) {
 | 
				
			||||||
 | 
								logger.info(`User: ${user.id}, ${user.username} has removed a ticket reaction. Starting role management`, { requestId, guildId })
 | 
				
			||||||
 | 
								return client.roleController.removeMediaRoleFromUser(user, messageReaction.message.guild, requestId)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,6 +1,7 @@
 | 
				
			|||||||
import { Message } from "discord.js"
 | 
					import { Message } from "discord.js"
 | 
				
			||||||
 | 
					import { logger } from "../logger"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const name = 'messageCreate'
 | 
					export const name = 'messageCreate'
 | 
				
			||||||
export function execute(message: Message) {
 | 
					export function execute(message: Message) {
 | 
				
			||||||
	console.log(`${JSON.stringify(message)} has been created`)
 | 
						logger.info(`${JSON.stringify(message)} has been created`)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,10 +1,10 @@
 | 
				
			|||||||
import { format, isToday } from "date-fns";
 | 
					import { format, isToday } 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 { logger } from "../logger";
 | 
				
			||||||
import de from "date-fns/locale/de";
 | 
					import de from "date-fns/locale/de";
 | 
				
			||||||
 | 
					import { Maybe } from "../interfaces";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function createDateStringFromEvent(eventStartDate:Date, requestId: string, guildId?: string): string {
 | 
					export function createDateStringFromEvent(eventStartDate: Maybe<Date>, requestId: string, guildId?: string): string {
 | 
				
			||||||
	if (!eventStartDate) {
 | 
						if (!eventStartDate) {
 | 
				
			||||||
		logger.error("Event has no start. Cannot create dateString.", { guildId, requestId })
 | 
							logger.error("Event has no start. Cannot create dateString.", { guildId, requestId })
 | 
				
			||||||
		return `"habe keinen Startzeitpunkt ermitteln können"`
 | 
							return `"habe keinen Startzeitpunkt ermitteln können"`
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,19 +2,19 @@ import { Message } from "discord.js";
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// branded types to differentiate objects of identical Type but different contents
 | 
					// branded types to differentiate objects of identical Type but different contents
 | 
				
			||||||
export type VoteEndMessage = Message & { readonly __brand: 'vote' }
 | 
					export type VoteEndMessage = Message<true> & { readonly __brand: 'voteend' }
 | 
				
			||||||
export type AnnouncementMessage = Message & { readonly __brand: 'announcement' }
 | 
					export type AnnouncementMessage = Message<true> & { readonly __brand: 'announcement' }
 | 
				
			||||||
export type VoteMessage = Message & { readonly __brand: 'voteend' }
 | 
					export type VoteMessage = Message<true> & { readonly __brand: 'vote' }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type DiscordMessage = VoteMessage | VoteEndMessage | AnnouncementMessage
 | 
					export type KnownDiscordMessage = VoteMessage | VoteEndMessage | AnnouncementMessage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function isVoteMessage(msg: Message): msg is VoteMessage {
 | 
					export function isVoteMessage(message: Message): message is VoteMessage {
 | 
				
			||||||
	return msg.cleanContent.includes('[Abstimmung]')
 | 
						return message.cleanContent.includes('[Abstimmung]')
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
export function isInitialAnnouncement(msg: Message): msg is AnnouncementMessage {
 | 
					export function isInitialAnnouncement(message: Message): message is AnnouncementMessage {
 | 
				
			||||||
	return msg.cleanContent.includes("[initial]")
 | 
						return message.cleanContent.includes("[initial]")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
export function isVoteEndedMessage(msg: Message): msg is VoteEndMessage {
 | 
					export function isVoteEndedMessage(message: Message): message is VoteEndMessage {
 | 
				
			||||||
	return msg.cleanContent.includes("[Abstimmung beendet]")
 | 
						return message.cleanContent.includes("[Abstimmung beendet]")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										83
									
								
								server/helper/role.controller.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								server/helper/role.controller.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,83 @@
 | 
				
			|||||||
 | 
					import { Guild, MessageReaction, Role, User } from "discord.js";
 | 
				
			||||||
 | 
					import { GuildMember } from "discord.js";
 | 
				
			||||||
 | 
					import { logger } from "../logger";
 | 
				
			||||||
 | 
					import { config } from "../configuration";
 | 
				
			||||||
 | 
					import { Maybe } from "../interfaces";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default class RoleController {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						constructor() { }
 | 
				
			||||||
 | 
						private getAnnounceRoleIdForGuild(guildId: string): string {
 | 
				
			||||||
 | 
							const role = config.bot.announcement_role
 | 
				
			||||||
 | 
							if (!role) throw new Error(`No announcementRole defined for guild ${guildId}`)
 | 
				
			||||||
 | 
							return role
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						public async addRoleToUser(member: GuildMember, role: Role, guildId: string, requestId: string) {
 | 
				
			||||||
 | 
							logger.info(`Adding Role ${role.id} to user ${member.id}|${member.user.username}`, { requestId, guildId })
 | 
				
			||||||
 | 
							return await member.roles.add(role)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						private async removeRoleFromUser(member: GuildMember, role: Role, guildId: string, requestId: string) {
 | 
				
			||||||
 | 
							logger.info(`Removing Role ${role.id} from user ${member.id}|${member.user.username}`, { requestId, guildId })
 | 
				
			||||||
 | 
							return await member.roles.remove(role)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public async addMediaRoleToUser(user: User, guild: Guild, requestId: string) {
 | 
				
			||||||
 | 
							const roleToAdd = await this.getAnnouncementRoleForGuild(guild, requestId)
 | 
				
			||||||
 | 
							if (!roleToAdd) throw new Error(`No announcementRole found to add to user`)
 | 
				
			||||||
 | 
							const guildMember = await guild.members.fetch(user)
 | 
				
			||||||
 | 
							return this.addRoleToUser(guildMember, roleToAdd, guild.id, requestId)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						public async removeMediaRoleFromUser(user: User, guild: Guild, requestId: string) {
 | 
				
			||||||
 | 
							const roleToRemove = await this.getAnnouncementRoleForGuild(guild, requestId)
 | 
				
			||||||
 | 
							if (!roleToRemove) throw new Error(`No announcementRole found to remove from user`)
 | 
				
			||||||
 | 
							const guildMember = await guild.members.fetch(user)
 | 
				
			||||||
 | 
							return this.removeRoleFromUser(guildMember, roleToRemove, guild.id, requestId)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public async getAnnouncementRoleForGuild(guild: Guild, requestId: string): Promise<Role> {
 | 
				
			||||||
 | 
							const mediaRole = this.getAnnounceRoleIdForGuild(guild.id)
 | 
				
			||||||
 | 
							const announcement_role = await guild.roles.fetch()
 | 
				
			||||||
 | 
								.then(fetchedRoles => fetchedRoles.find(role => role.id === mediaRole))
 | 
				
			||||||
 | 
								.catch(error => {
 | 
				
			||||||
 | 
									logger.error(`Could not find announcement_role with id ${config.bot.announcement_role}. Error: ${error}`, { requestId, guildId: guild.id })
 | 
				
			||||||
 | 
									throw error
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							if (!announcement_role) throw new Error(`Could not find announcement_role with id ${config.bot.announcement_role}.`)
 | 
				
			||||||
 | 
							return announcement_role
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public async assignAnnouncementRolesFromReaction(guild: Guild, reaction: MessageReaction, requestId: string) {
 | 
				
			||||||
 | 
							const guildId = guild.id
 | 
				
			||||||
 | 
							logger.info("Managing roles", { guildId, requestId })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const announcementRole = await this.getAnnouncementRoleForGuild(guild, requestId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const usersWhoWantRole: User[] = (await reaction.users.fetch()).filter(user => !user.bot).map(user => user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const allUsers = await guild.members.fetch()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const usersWhoHaveRole: GuildMember[] = allUsers
 | 
				
			||||||
 | 
								.filter(member => member.roles.cache
 | 
				
			||||||
 | 
									.find(role => role.id === announcementRole.id) !== undefined)
 | 
				
			||||||
 | 
								.map(member => member)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const usersWhoNeedRoleRevoked: GuildMember[] = usersWhoHaveRole
 | 
				
			||||||
 | 
								.filter(userWhoHas => !usersWhoWantRole.map(wanter => wanter.id).includes(userWhoHas.id))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const usersWhoDontHaveRole: GuildMember[] = allUsers
 | 
				
			||||||
 | 
								.filter(member => member.roles.cache
 | 
				
			||||||
 | 
									.find(role => role.id === announcementRole.id) === undefined)
 | 
				
			||||||
 | 
								.map(member => member)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const usersWhoNeedRole: GuildMember[] = usersWhoDontHaveRole
 | 
				
			||||||
 | 
								.filter(userWhoNeeds => usersWhoWantRole.map(wanter => wanter.id).includes(userWhoNeeds.id))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							logger.debug(`Theses users will get the role removed: ${JSON.stringify(usersWhoNeedRoleRevoked)}`, { guildId, requestId })
 | 
				
			||||||
 | 
							logger.debug(`Theses users will get the role added: ${JSON.stringify(usersWhoNeedRole)}`, { guildId, requestId })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							usersWhoNeedRoleRevoked.forEach(user => this.removeRoleFromUser(user, announcementRole, guild.id, requestId))
 | 
				
			||||||
 | 
							usersWhoNeedRole.forEach(user => this.addRoleToUser(user, announcementRole, guild.id, requestId))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,10 +1,11 @@
 | 
				
			|||||||
import { CustomError, errorCodes } from "../interfaces"
 | 
					import { CustomError, errorCodes } from "../interfaces"
 | 
				
			||||||
 | 
					import { logger } from "../logger"
 | 
				
			||||||
import { ExtendedClient } from "../structures/client"
 | 
					import { ExtendedClient } from "../structures/client"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function sendFailureDM(creatorMessage: string, client: ExtendedClient, creatorId?: string): Promise<void> {
 | 
					export async function sendFailureDM(creatorMessage: string, client: ExtendedClient, creatorId?: string): Promise<void> {
 | 
				
			||||||
	if (!creatorId) throw new CustomError('No creator ID present', errorCodes.no_creator_id)
 | 
						if (!creatorId) throw new CustomError('No creator ID present', errorCodes.no_creator_id)
 | 
				
			||||||
	const creator = await client.users.fetch(creatorId)
 | 
						const creator = await client.users.fetch(creatorId)
 | 
				
			||||||
	console.log(`Creator ${JSON.stringify(creator)}`)
 | 
						logger.info(`Creator ${JSON.stringify(creator)}`)
 | 
				
			||||||
	if (creator)
 | 
						if (creator)
 | 
				
			||||||
		if (!creator.dmChannel)
 | 
							if (!creator.dmChannel)
 | 
				
			||||||
			await creator.createDM()
 | 
								await creator.createDM()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,11 +1,11 @@
 | 
				
			|||||||
import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildScheduledEventSetStatusArg, GuildScheduledEventStatus, Message, MessageCreateOptions, MessageReaction, TextChannel } from "discord.js"
 | 
					import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildScheduledEventSetStatusArg, GuildScheduledEventStatus, Message, MessageCreateOptions, MessageReaction, TextChannel } from "discord.js"
 | 
				
			||||||
import { Emotes, NONE_OF_THAT } from "../constants"
 | 
					import { ValidVoteEmotes, NONE_OF_THAT } from "../constants"
 | 
				
			||||||
import { logger, newRequestId } from "../logger"
 | 
					import { logger, newRequestId } from "../logger"
 | 
				
			||||||
import { getMembersWithRoleFromGuild } from "./roleFilter"
 | 
					import { getMembersWithRoleFromGuild } from "./roleFilter"
 | 
				
			||||||
import { config } from "../configuration"
 | 
					import { config } from "../configuration"
 | 
				
			||||||
import { VoteMessage, isVoteEndedMessage, isVoteMessage } from "./messageIdentifiers"
 | 
					import { VoteMessage, isVoteEndedMessage, isVoteMessage } from "./messageIdentifiers"
 | 
				
			||||||
import { createDateStringFromEvent } from "./dateHelper"
 | 
					import { createDateStringFromEvent } from "./dateHelper"
 | 
				
			||||||
import { Maybe } from "../interfaces"
 | 
					import { Maybe, prepareVoteMessageInput } from "../interfaces"
 | 
				
			||||||
import format from "date-fns/format"
 | 
					import format from "date-fns/format"
 | 
				
			||||||
import toDate from "date-fns/toDate"
 | 
					import toDate from "date-fns/toDate"
 | 
				
			||||||
import differenceInDays from "date-fns/differenceInDays"
 | 
					import differenceInDays from "date-fns/differenceInDays"
 | 
				
			||||||
@@ -21,8 +21,7 @@ export type Vote = {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
export type VoteMessageInfo = {
 | 
					export type VoteMessageInfo = {
 | 
				
			||||||
	votes: Vote[],
 | 
						votes: Vote[],
 | 
				
			||||||
	eventId: string,
 | 
						event: GuildScheduledEvent,
 | 
				
			||||||
	eventDate: Date
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
export default class VoteController {
 | 
					export default class VoteController {
 | 
				
			||||||
	private client: ExtendedClient
 | 
						private client: ExtendedClient
 | 
				
			||||||
@@ -52,63 +51,84 @@ export default class VoteController {
 | 
				
			|||||||
			logger.info(`No reroll`, { requestId, guildId })
 | 
								logger.info(`No reroll`, { requestId, guildId })
 | 
				
			||||||
		else {
 | 
							else {
 | 
				
			||||||
			logger.info('Starting poll reroll', { requestId, guildId })
 | 
								logger.info('Starting poll reroll', { requestId, guildId })
 | 
				
			||||||
			await this.handleReroll(reactedUponMessage, guild, guild.id, requestId)
 | 
								await this.handleReroll(reactedUponMessage, guild.id, requestId)
 | 
				
			||||||
			logger.info(`Finished handling NONE_OF_THAT vote`, { requestId, guildId })
 | 
								logger.info(`Finished handling NONE_OF_THAT vote`, { requestId, guildId })
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private async removeMessage(msg: Message): Promise<Message<boolean>> {
 | 
						private async removeMessage(message: Message): Promise<Message<boolean>> {
 | 
				
			||||||
		if (msg.pinned) {
 | 
							if (message.pinned) {
 | 
				
			||||||
			await msg.unpin()
 | 
								await message.unpin()
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		return await msg.delete()
 | 
							return await message.delete()
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	public isAboveThreshold(vote: Vote): boolean {
 | 
					 | 
				
			||||||
		const aboveThreshold = (vote.count - 1) >= 1
 | 
					 | 
				
			||||||
		logger.debug(`${vote.movie} : ${vote.count} -> above: ${aboveThreshold}`)
 | 
					 | 
				
			||||||
		return aboveThreshold
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	public async handleReroll(voteMessage: VoteMessage, guild: Guild, guildId: string, requestId: string) {
 | 
					 | 
				
			||||||
		//get movies that already had votes to give them a second chance
 | 
					 | 
				
			||||||
		const voteInfo: VoteMessageInfo = await this.parseVoteInfoFromVoteMessage(voteMessage, requestId)
 | 
					 | 
				
			||||||
		const votedOnMovies = voteInfo.votes.filter(this.isAboveThreshold).filter(x => x.emote !== NONE_OF_THAT)
 | 
					 | 
				
			||||||
		logger.info(`Found ${votedOnMovies.length} with votes`, { requestId, guildId })
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// get movies from jellyfin to fill the remaining slots
 | 
						/**
 | 
				
			||||||
 | 
						 * returns true if a Vote object contains at least one vote
 | 
				
			||||||
 | 
						 * @param {Vote} vote
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						private hasAtLeastOneVote(vote: Vote): boolean {
 | 
				
			||||||
 | 
							// subtracting the bots initial vote
 | 
				
			||||||
 | 
							const overOneVote = (vote.count - 1) >= 1
 | 
				
			||||||
 | 
							logger.debug(`${vote.movie} : ${vote.count} -> above: ${overOneVote}`)
 | 
				
			||||||
 | 
							return overOneVote
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public async generateRerollMovieList(voteInfo: VoteMessageInfo, guildId: string, requestId: string) {
 | 
				
			||||||
 | 
							if (config.bot.reroll_retains_top_picks) {
 | 
				
			||||||
 | 
								const votedOnMovies = voteInfo.votes.filter(this.hasAtLeastOneVote).filter(x => x.emote !== NONE_OF_THAT)
 | 
				
			||||||
 | 
								logger.info(`Found ${votedOnMovies.length} with votes`, { requestId, guildId })
 | 
				
			||||||
			const newMovieCount: number = config.bot.random_movie_count - votedOnMovies.length
 | 
								const newMovieCount: number = config.bot.random_movie_count - votedOnMovies.length
 | 
				
			||||||
			logger.info(`Fetching ${newMovieCount} from jellyfin`)
 | 
								logger.info(`Fetching ${newMovieCount} from jellyfin`)
 | 
				
			||||||
			const newMovies: string[] = await this.yavinJellyfinHandler.getRandomMovieNames(newMovieCount, guildId, requestId)
 | 
								const newMovies: string[] = await this.yavinJellyfinHandler.getRandomMovieNames(newMovieCount, guildId, requestId)
 | 
				
			||||||
 | 
					 | 
				
			||||||
			// merge
 | 
								// merge
 | 
				
			||||||
		const movies: string[] = newMovies.concat(votedOnMovies.map(x => x.movie))
 | 
								return newMovies.concat(votedOnMovies.map(x => x.movie))
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								// get movies from jellyfin to fill the remaining slots
 | 
				
			||||||
 | 
								const newMovieCount: number = config.bot.random_movie_count
 | 
				
			||||||
 | 
								logger.info(`Fetching ${newMovieCount} from jellyfin`)
 | 
				
			||||||
 | 
								return await this.yavinJellyfinHandler.getRandomMovieNames(newMovieCount, guildId, requestId)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// create new message
 | 
						public async handleReroll(voteMessage: VoteMessage, guildId: string, requestId: string) {
 | 
				
			||||||
 | 
							// get the movies currently being voted on, their votes, the eventId and its date
 | 
				
			||||||
 | 
							const voteInfo: VoteMessageInfo = await this.parseVoteInfoFromVoteMessage(voteMessage, requestId)
 | 
				
			||||||
 | 
							if (!voteInfo.event.scheduledStartAt) {
 | 
				
			||||||
 | 
								logger.info("Event does not have a start date, cancelling", { guildId: voteInfo.event.guildId, requestId })
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							let movies: string[] = await this.generateRerollMovieList(voteInfo, guildId, requestId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		logger.info(`Creating new poll message with new movies: ${movies}`, { requestId, guildId })
 | 
					 | 
				
			||||||
		const message = this.createVoteMessageText(voteInfo.eventId, voteInfo.eventDate, movies, guildId, requestId)
 | 
					 | 
				
			||||||
		const announcementChannel = this.client.getAnnouncementChannelForGuild(guildId)
 | 
							const announcementChannel = this.client.getAnnouncementChannelForGuild(guildId)
 | 
				
			||||||
		if (!announcementChannel) {
 | 
							if (!announcementChannel) {
 | 
				
			||||||
			logger.error(`No announcementChannel found for ${guildId}, can't post poll`)
 | 
								logger.error(`No announcementChannel found for ${guildId}, can't post poll`)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					 | 
				
			||||||
		try {
 | 
							try {
 | 
				
			||||||
			logger.info(`Trying to remove old vote Message`, { requestId, guildId })
 | 
								logger.info(`Trying to remove old vote Message`, { requestId, guildId })
 | 
				
			||||||
			this.removeMessage(voteMessage)
 | 
								this.removeMessage(voteMessage)
 | 
				
			||||||
		} catch (err) {
 | 
							} catch (err) {
 | 
				
			||||||
 | 
								// TODO: integrate failure DM to media Admin to inform about inability to delete old message
 | 
				
			||||||
			logger.error(`Error during removeMessage: ${err}`)
 | 
								logger.error(`Error during removeMessage: ${err}`)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							const sentMessage = this.prepareAndSendVoteMessage({
 | 
				
			||||||
		const sentMessage = await this.sendVoteMessage(message, movies.length, announcementChannel)
 | 
								event: voteInfo.event,
 | 
				
			||||||
		sentMessage.pin()
 | 
								movies,
 | 
				
			||||||
		logger.info(`Sent and pinned new poll message`, { requestId, guildId })
 | 
								announcementChannel,
 | 
				
			||||||
 | 
								startDate: voteInfo.event.scheduledStartAt,
 | 
				
			||||||
 | 
								pinAfterSending: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
								guildId,
 | 
				
			||||||
 | 
								requestId)
 | 
				
			||||||
 | 
							logger.debug(`Sent reroll message: ${JSON.stringify(sentMessage)}`, { requestId, guildId })
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private async fetchEventStartDateByEventId(guild: Guild, eventId: string, requestId: string): Promise<Maybe<Date>> {
 | 
						private async fetchEventByEventId(guild: Guild, eventId: string, requestId: string): Promise<Maybe<GuildScheduledEvent>> {
 | 
				
			||||||
		const guildEvent: GuildScheduledEvent = await guild.scheduledEvents.fetch(eventId)
 | 
							const guildEvent: GuildScheduledEvent = await guild.scheduledEvents.fetch(eventId)
 | 
				
			||||||
		if (!guildEvent) logger.error(`GuildScheduledEvent with id${eventId} could not be found`, { requestId, guildId: guild.id })
 | 
							if (!guildEvent) logger.error(`GuildScheduledEvent with id${eventId} could not be found`, { requestId, guildId: guild.id })
 | 
				
			||||||
		if (guildEvent.scheduledStartAt)
 | 
							return guildEvent
 | 
				
			||||||
			return guildEvent.scheduledStartAt
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public async parseVoteInfoFromVoteMessage(message: VoteMessage, requestId: string): Promise<VoteMessageInfo> {
 | 
						public async parseVoteInfoFromVoteMessage(message: VoteMessage, requestId: string): Promise<VoteMessageInfo> {
 | 
				
			||||||
@@ -118,8 +138,7 @@ export default class VoteController {
 | 
				
			|||||||
		if (!message.guild)
 | 
							if (!message.guild)
 | 
				
			||||||
			throw new Error(`Message ${message.id} not a guild message`)
 | 
								throw new Error(`Message ${message.id} not a guild message`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		let eventStartDate: Maybe<Date> = await this.fetchEventStartDateByEventId(message.guild, parsedIds.eventId, requestId)
 | 
							const event: Maybe<GuildScheduledEvent> = await this.fetchEventByEventId(message.guild, parsedIds.eventId, requestId)
 | 
				
			||||||
		if (!eventStartDate) eventStartDate = this.parseEventDateFromMessage(message.cleanContent, message.guild.id, requestId)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
		let votes: Vote[] = []
 | 
							let votes: Vote[] = []
 | 
				
			||||||
		for (const line of lines) {
 | 
							for (const line of lines) {
 | 
				
			||||||
@@ -138,7 +157,7 @@ export default class VoteController {
 | 
				
			|||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		return <VoteMessageInfo>{ eventId: parsedIds.eventId, eventDate: eventStartDate, votes }
 | 
							return <VoteMessageInfo>{ event, votes }
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	public parseEventDateFromMessage(message: string, guildId: string, requestId: string): Date {
 | 
						public parseEventDateFromMessage(message: string, guildId: string, requestId: string): Date {
 | 
				
			||||||
		logger.warn(`Falling back to RegEx parsing to get Event Date`, { guildId, requestId })
 | 
							logger.warn(`Falling back to RegEx parsing to get Event Date`, { guildId, requestId })
 | 
				
			||||||
@@ -156,27 +175,37 @@ export default class VoteController {
 | 
				
			|||||||
		throw Error(`Could not find eventId in Vote Message`)
 | 
							throw Error(`Could not find eventId in Vote Message`)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public createVoteMessageText(eventId: string, eventStartDate: Date, movies: string[], guildId: string, requestId: string): string {
 | 
						public async prepareAndSendVoteMessage(inputInfo: prepareVoteMessageInput, guildId: string, requestId: string) {
 | 
				
			||||||
		let message = `[Abstimmung] für https://discord.com/events/${guildId}/${eventId} \n<@&${config.bot.announcement_role}> Es gibt eine neue Abstimmung für die nächste Watchparty ${createDateStringFromEvent(eventStartDate, guildId, requestId)}! Stimme hierunter für den nächsten Film ab!\n`
 | 
							const messageText = this.createVoteMessageText(inputInfo.event, inputInfo.movies, guildId, requestId)
 | 
				
			||||||
 | 
							const sentMessage = await this.sendVoteMessage(messageText, inputInfo.movies.length, inputInfo.announcementChannel)
 | 
				
			||||||
 | 
							if (inputInfo.pinAfterSending)
 | 
				
			||||||
 | 
								sentMessage.pin()
 | 
				
			||||||
 | 
							return sentMessage
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public createVoteMessageText(event: GuildScheduledEvent, movies: string[], guildId: string, requestId: string): string {
 | 
				
			||||||
 | 
							let message = `[Abstimmung] für https://discord.com/events/${guildId}/${event.id} \n<@&${config.bot.announcement_role}> Es gibt eine neue Abstimmung für die nächste Watchparty ${createDateStringFromEvent(event.scheduledStartAt, guildId, requestId)}! Stimme hierunter für den nächsten Film ab!\n`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		for (let i = 0; i < movies.length; i++) {
 | 
							for (let i = 0; i < movies.length; i++) {
 | 
				
			||||||
			message = message.concat(Emotes[i]).concat(": ").concat(movies[i]).concat("\n")
 | 
								message = message.concat(ValidVoteEmotes[i]).concat(": ").concat(movies[i]).concat("\n")
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		message = message.concat(NONE_OF_THAT).concat(": Wenn dir nichts davon gefällt.")
 | 
							message = message.concat(NONE_OF_THAT).concat(": Wenn dir nichts davon gefällt.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return message
 | 
							return message
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	public async sendVoteMessage(message: string, movieCount: number, announcementChannel: TextChannel) {
 | 
					
 | 
				
			||||||
 | 
						// TODO: Refactor into separate message controller
 | 
				
			||||||
 | 
						public async sendVoteMessage(messageText: string, movieCount: number, announcementChannel: TextChannel) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const options: MessageCreateOptions = {
 | 
							const options: MessageCreateOptions = {
 | 
				
			||||||
			allowedMentions: { parse: ["roles"] },
 | 
								allowedMentions: { parse: ["roles"] },
 | 
				
			||||||
			content: message,
 | 
								content: messageText,
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const sentMessage: Message<true> = await (await announcementChannel.fetch()).send(options)
 | 
							const sentMessage: Message<true> = await (await announcementChannel.fetch()).send(options)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		for (let i = 0; i < movieCount; i++) {
 | 
							for (let i = 0; i < movieCount; i++) {
 | 
				
			||||||
			sentMessage.react(Emotes[i])
 | 
								sentMessage.react(ValidVoteEmotes[i])
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		sentMessage.react(NONE_OF_THAT)
 | 
							sentMessage.react(NONE_OF_THAT)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -205,33 +234,35 @@ export default class VoteController {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		const lastMessage: Message<true> = messages[0]
 | 
							const lastMessage: Message<true> = messages[0]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (!isVoteMessage(lastMessage)) {
 | 
				
			||||||
 | 
								logger.error(`Found message that is not a vote message, can't proceed`, { guildId, requestId })
 | 
				
			||||||
			logger.debug(`Found messages: ${JSON.stringify(messages, null, 2)}`, { guildId, requestId })
 | 
								logger.debug(`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 })
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							else {
 | 
				
			||||||
			const votes = (await this.getVotesByEmote(lastMessage, guildId, requestId))
 | 
								const votes = (await this.getVotesByEmote(lastMessage, guildId, requestId))
 | 
				
			||||||
				.sort((a, b) => b.count - a.count)
 | 
									.sort((a, b) => b.count - a.count)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			logger.debug(`votes: ${JSON.stringify(votes, null, 2)}`, { guildId, requestId })
 | 
								logger.debug(`votes: ${JSON.stringify(votes, null, 2)}`, { guildId, requestId })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			logger.info("Deleting vote message")
 | 
								logger.info("Deleting vote message")
 | 
				
			||||||
 | 
								lastMessage.unpin()
 | 
				
			||||||
			await lastMessage.delete()
 | 
								await lastMessage.delete()
 | 
				
			||||||
		const event = await this.getEvent(guild, guild.id, requestId)
 | 
								const event = await this.getOpenPollEvent(guild, guild.id, requestId)
 | 
				
			||||||
			if (event && votes?.length > 0) {
 | 
								if (event && votes?.length > 0) {
 | 
				
			||||||
			this.updateEvent(event, votes, guild, guildId, requestId)
 | 
									this.updateOpenPollEventWithVoteResults(event, votes, guild, guildId, requestId)
 | 
				
			||||||
				this.sendVoteClosedMessage(event, votes[0].movie, guildId, requestId)
 | 
									this.sendVoteClosedMessage(event, votes[0].movie, guildId, requestId)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		lastMessage.unpin() //todo: uncomment when bot has permission to pin/unpin
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * gets votes for the movies without the NONE_OF_THAT votes
 | 
						 * gets votes for the movies without the NONE_OF_THAT votes
 | 
				
			||||||
	*/
 | 
						*/
 | 
				
			||||||
	public async getVotesByEmote(message: Message, guildId: string, requestId: string): Promise<Vote[]> {
 | 
						public async getVotesByEmote(message: VoteMessage, guildId: string, requestId: string): Promise<Vote[]> {
 | 
				
			||||||
		const votes: Vote[] = []
 | 
							const votes: Vote[] = []
 | 
				
			||||||
		logger.debug(`Number of items in emotes: ${Object.values(Emotes).length}`, { guildId, requestId })
 | 
							logger.debug(`Number of items in emotes: ${Object.values(ValidVoteEmotes).length}`, { guildId, requestId })
 | 
				
			||||||
		for (let i = 0; i < Object.keys(Emotes).length / 2; i++) {
 | 
							for (let i = 0; i < Object.keys(ValidVoteEmotes).length / 2; i++) {
 | 
				
			||||||
			const emote = Emotes[i]
 | 
								const emote = ValidVoteEmotes[i]
 | 
				
			||||||
			logger.debug(`Getting reaction for emote ${emote}`, { guildId, requestId })
 | 
								logger.debug(`Getting reaction for emote ${emote}`, { guildId, requestId })
 | 
				
			||||||
			const reaction = message.reactions.resolve(emote)
 | 
								const reaction = message.reactions.resolve(emote)
 | 
				
			||||||
			logger.debug(`Reaction for emote ${emote}: ${JSON.stringify(reaction, null, 2)}`, { guildId, requestId })
 | 
								logger.debug(`Reaction for emote ${emote}: ${JSON.stringify(reaction, null, 2)}`, { guildId, requestId })
 | 
				
			||||||
@@ -242,19 +273,19 @@ export default class VoteController {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
		return votes
 | 
							return votes
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	public async getEvent(guild: Guild, guildId: string, requestId: string): Promise<GuildScheduledEvent | null> {
 | 
						public async getOpenPollEvent(guild: Guild, guildId: string, requestId: string): Promise<Maybe<GuildScheduledEvent>> {
 | 
				
			||||||
		const voteEvents = (await guild.scheduledEvents.fetch())
 | 
							const voteEvents = (await guild.scheduledEvents.fetch())
 | 
				
			||||||
			.map((value) => value)
 | 
								.map((value) => value)
 | 
				
			||||||
			.filter(event => event.name.toLowerCase().includes("voting offen"))
 | 
								.filter(event => event.name.toLowerCase().includes("voting offen"))
 | 
				
			||||||
		logger.debug(`Found events: ${JSON.stringify(voteEvents, null, 2)}`, { guildId, requestId })
 | 
							logger.debug(`Found events: ${JSON.stringify(voteEvents, null, 2)}`, { guildId, requestId })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (!voteEvents || voteEvents.length <= 0) {
 | 
							if (!voteEvents || voteEvents.length <= 0) {
 | 
				
			||||||
			logger.error("Could not find vote event. Cancelling update!", { guildId, requestId })
 | 
								logger.error("Could not find an open vote event.", { guildId, requestId })
 | 
				
			||||||
			return null
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		return voteEvents[0]
 | 
							return voteEvents[0]
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	public async updateEvent(voteEvent: GuildScheduledEvent, votes: Vote[], guild: Guild, guildId: string, requestId: string) {
 | 
						public async updateOpenPollEventWithVoteResults(voteEvent: GuildScheduledEvent, votes: Vote[], guild: Guild, guildId: string, requestId: string) {
 | 
				
			||||||
		logger.info(`Updating event with movie ${votes[0].movie}.`, { guildId, requestId })
 | 
							logger.info(`Updating event with movie ${votes[0].movie}.`, { guildId, requestId })
 | 
				
			||||||
		const options: GuildScheduledEventEditOptions<GuildScheduledEventStatus.Scheduled, GuildScheduledEventSetStatusArg<GuildScheduledEventStatus.Scheduled>> = {
 | 
							const options: GuildScheduledEventEditOptions<GuildScheduledEventStatus.Scheduled, GuildScheduledEventSetStatusArg<GuildScheduledEventStatus.Scheduled>> = {
 | 
				
			||||||
			name: votes[0].movie,
 | 
								name: votes[0].movie,
 | 
				
			||||||
@@ -265,8 +296,8 @@ export default class VoteController {
 | 
				
			|||||||
		voteEvent.edit(options)
 | 
							voteEvent.edit(options)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	public async sendVoteClosedMessage(event: GuildScheduledEvent, movie: string, guildId: string, requestId: string): Promise<Message<boolean>> {
 | 
						public async sendVoteClosedMessage(event: GuildScheduledEvent, movie: string, guildId: string, requestId: string): Promise<Message<boolean>> {
 | 
				
			||||||
		const date = event.scheduledStartAt ? format(event.scheduledStartAt, "dd.MM.") : "Fehler, event hatte kein Datum"
 | 
							const date = event.scheduledStartAt ? format(event.scheduledStartAt, "dd.MM.") : "Fehler, Event hatte kein Datum"
 | 
				
			||||||
		const time = event.scheduledStartAt ? format(event.scheduledStartAt, "HH:mm") : "Fehler, event hatte kein Datum"
 | 
							const time = event.scheduledStartAt ? format(event.scheduledStartAt, "HH:mm") : "Fehler, Event hatte keine Uhrzeit"
 | 
				
			||||||
		const body = `[Abstimmung beendet] für https://discord.com/events/${event.guildId}/${event.id}\n<@&${config.bot.announcement_role}> Wir gucken ${movie} am ${date} um ${time}`
 | 
							const 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 = {
 | 
							const options: MessageCreateOptions = {
 | 
				
			||||||
			content: body,
 | 
								content: body,
 | 
				
			||||||
@@ -275,14 +306,14 @@ export default class VoteController {
 | 
				
			|||||||
		const announcementChannel = this.client.getAnnouncementChannelForGuild(guildId)
 | 
							const announcementChannel = this.client.getAnnouncementChannelForGuild(guildId)
 | 
				
			||||||
		logger.info("Sending vote closed message.", { guildId, requestId })
 | 
							logger.info("Sending vote closed message.", { guildId, requestId })
 | 
				
			||||||
		if (!announcementChannel) {
 | 
							if (!announcementChannel) {
 | 
				
			||||||
			const errorMessages = "Could not find announcement channel. Please fix!"
 | 
								const errorMessageText = "Could not find announcement channel. Please fix!"
 | 
				
			||||||
			logger.error(errorMessages, { guildId, requestId })
 | 
								logger.error(errorMessageText, { guildId, requestId })
 | 
				
			||||||
			throw errorMessages
 | 
								throw errorMessageText
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		return announcementChannel.send(options)
 | 
							return announcementChannel.send(options)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	private extractMovieFromMessageByEmote(lastMessages: Message, emote: string): string {
 | 
						private extractMovieFromMessageByEmote(voteMessage: VoteMessage, emote: string): string {
 | 
				
			||||||
		const lines = lastMessages.cleanContent.split("\n")
 | 
							const lines = voteMessage.cleanContent.split("\n")
 | 
				
			||||||
		const emoteLines = lines.filter(line => line.includes(emote))
 | 
							const emoteLines = lines.filter(line => line.includes(emote))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (!emoteLines) {
 | 
							if (!emoteLines) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
import { Collection } from "@discordjs/collection"
 | 
					import { Collection } from "@discordjs/collection"
 | 
				
			||||||
import { Role } from "discord.js"
 | 
					import { GuildScheduledEvent, Role, TextChannel } from "discord.js"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type Maybe<T> = T | undefined | null
 | 
					export type Maybe<T> = T | undefined | null
 | 
				
			||||||
export interface Player {
 | 
					export interface Player {
 | 
				
			||||||
@@ -39,3 +39,10 @@ export interface JellyfinConfig {
 | 
				
			|||||||
	collectionUser: string
 | 
						collectionUser: string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
export type PermissionLevel = "VIEWER" | "ADMIN" | "TEMPORARY"
 | 
					export type PermissionLevel = "VIEWER" | "ADMIN" | "TEMPORARY"
 | 
				
			||||||
 | 
					export interface prepareVoteMessageInput {
 | 
				
			||||||
 | 
						movies: string[],
 | 
				
			||||||
 | 
						startDate: Date,
 | 
				
			||||||
 | 
						event: GuildScheduledEvent,
 | 
				
			||||||
 | 
						announcementChannel: TextChannel,
 | 
				
			||||||
 | 
						pinAfterSending: boolean,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -253,22 +253,22 @@ function isFormData(value: any): value is FormData {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export class ResponseError extends Error {
 | 
					export class ResponseError extends Error {
 | 
				
			||||||
	override name: "ResponseError" = "ResponseError";
 | 
						override name: "ResponseError" = "ResponseError";
 | 
				
			||||||
	constructor(public response: Response, msg?: string) {
 | 
						constructor(public response: Response, errorMessage?: string) {
 | 
				
			||||||
		super(msg);
 | 
							super(errorMessage);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class FetchError extends Error {
 | 
					export class FetchError extends Error {
 | 
				
			||||||
	override name: "FetchError" = "FetchError";
 | 
						override name: "FetchError" = "FetchError";
 | 
				
			||||||
	constructor(public cause: Error, msg?: string) {
 | 
						constructor(public cause: Error, errorMessage?: string) {
 | 
				
			||||||
		super(msg);
 | 
							super(errorMessage);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class RequiredError extends Error {
 | 
					export class RequiredError extends Error {
 | 
				
			||||||
	override name: "RequiredError" = "RequiredError";
 | 
						override name: "RequiredError" = "RequiredError";
 | 
				
			||||||
	constructor(public field: string, msg?: string) {
 | 
						constructor(public field: string, errorMessage?: string) {
 | 
				
			||||||
		super(msg);
 | 
							super(errorMessage);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,7 @@ export const noGuildId = 'NoGuildId'
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const printFn = format.printf(({ guildId, level, message, errorCode, requestId, timestamp: logTimestamp }: { [k: string]: string }) => {
 | 
					const printFn = format.printf(({ guildId, level, message, errorCode, requestId, timestamp: logTimestamp }: { [k: string]: string }) => {
 | 
				
			||||||
	return `[${guildId ?? ''}][${level}][${logTimestamp}][${errorCode ?? ''}][${requestId ?? ''}]:${message}`
 | 
						return `[${guildId ?? ''}][${level.padStart(5, " ")}][${logTimestamp}][${errorCode ?? ''}][${requestId ?? ''}]:${message}`
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const logFormat = format.combine(
 | 
					const logFormat = format.combine(
 | 
				
			||||||
@@ -16,7 +16,8 @@ const logFormat = format.combine(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const consoleTransports = [
 | 
					const consoleTransports = [
 | 
				
			||||||
	new transports.Console({
 | 
						new transports.Console({
 | 
				
			||||||
		format: logFormat
 | 
							format: logFormat,
 | 
				
			||||||
 | 
							silent: process.env.NODE_ENV === 'testing'
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
export const logger = createLogger({
 | 
					export const logger = createLogger({
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,6 @@ import { ApplicationCommandDataResolvable, Client, ClientOptions, Collection, Gu
 | 
				
			|||||||
import fs from 'fs';
 | 
					import fs from 'fs';
 | 
				
			||||||
import { ScheduledTask, schedule } from "node-cron";
 | 
					import { ScheduledTask, schedule } from "node-cron";
 | 
				
			||||||
import { v4 as uuid } from 'uuid';
 | 
					import { v4 as uuid } from 'uuid';
 | 
				
			||||||
import { manageAnnouncementRoles } from "../commands/announce";
 | 
					 | 
				
			||||||
import { config } from "../configuration";
 | 
					import { config } from "../configuration";
 | 
				
			||||||
import { Maybe } from "../interfaces";
 | 
					import { Maybe } from "../interfaces";
 | 
				
			||||||
import { JellyfinHandler } from "../jellyfin/handler";
 | 
					import { JellyfinHandler } from "../jellyfin/handler";
 | 
				
			||||||
@@ -11,6 +10,7 @@ import { CommandType } from "../types/commandTypes";
 | 
				
			|||||||
import { isInitialAnnouncement } from "../helper/messageIdentifiers";
 | 
					import { isInitialAnnouncement } from "../helper/messageIdentifiers";
 | 
				
			||||||
import VoteController from "../helper/vote.controller";
 | 
					import VoteController from "../helper/vote.controller";
 | 
				
			||||||
import { yavinJellyfinHandler } from "../..";
 | 
					import { yavinJellyfinHandler } from "../..";
 | 
				
			||||||
 | 
					import RoleController from "../helper/role.controller";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -19,6 +19,7 @@ export class ExtendedClient extends Client {
 | 
				
			|||||||
	private commandFilePath = `${__dirname}/../commands`
 | 
						private commandFilePath = `${__dirname}/../commands`
 | 
				
			||||||
	private jellyfin: JellyfinHandler
 | 
						private jellyfin: JellyfinHandler
 | 
				
			||||||
	public voteController: VoteController = new VoteController(this, yavinJellyfinHandler)
 | 
						public voteController: VoteController = new VoteController(this, yavinJellyfinHandler)
 | 
				
			||||||
 | 
						public roleController: RoleController = new RoleController()
 | 
				
			||||||
	public commands: Collection<string, CommandType> = new Collection()
 | 
						public commands: Collection<string, CommandType> = new Collection()
 | 
				
			||||||
	private announcementChannels: Collection<string, TextChannel> = new Collection() //guildId to TextChannel
 | 
						private announcementChannels: Collection<string, TextChannel> = new Collection() //guildId to TextChannel
 | 
				
			||||||
	private announcementRoleHandlerTask: Collection<string, ScheduledTask> = new Collection() //one task per guild
 | 
						private announcementRoleHandlerTask: Collection<string, ScheduledTask> = new Collection() //one task per guild
 | 
				
			||||||
@@ -172,10 +173,10 @@ export class ExtendedClient extends Client {
 | 
				
			|||||||
				}
 | 
									}
 | 
				
			||||||
				//logger.debug(`Message: ${JSON.stringify(message, null, 2)}`, { guildId: guild.id, requestId })
 | 
									//logger.debug(`Message: ${JSON.stringify(message, null, 2)}`, { guildId: guild.id, requestId })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				const reactions = message.reactions.resolve("🎫")
 | 
									const ticketReaction = message.reactions.resolve("🎫")
 | 
				
			||||||
				//logger.debug(`reactions: ${JSON.stringify(reactions, null, 2)}`, { guildId: guild.id, requestId })
 | 
									//logger.debug(`reactions: ${JSON.stringify(reactions, null, 2)}`, { guildId: guild.id, requestId })
 | 
				
			||||||
				if (reactions) {
 | 
									if (ticketReaction) {
 | 
				
			||||||
					manageAnnouncementRoles(message.guild, reactions, requestId)
 | 
										this.roleController.assignAnnouncementRolesFromReaction(message.guild, ticketReaction, requestId)
 | 
				
			||||||
				} else {
 | 
									} else {
 | 
				
			||||||
					logger.error("Did not get reactions! Aborting!", { guildId: guild.id, requestId })
 | 
										logger.error("Did not get reactions! Aborting!", { guildId: guild.id, requestId })
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,7 @@ import VoteController from "../../server/helper/vote.controller"
 | 
				
			|||||||
import { JellyfinHandler } from "../../server/jellyfin/handler"
 | 
					import { JellyfinHandler } from "../../server/jellyfin/handler"
 | 
				
			||||||
import { ExtendedClient } from "../../server/structures/client"
 | 
					import { ExtendedClient } from "../../server/structures/client"
 | 
				
			||||||
import { Emoji, NONE_OF_THAT } from "../../server/constants"
 | 
					import { Emoji, NONE_OF_THAT } from "../../server/constants"
 | 
				
			||||||
 | 
					import { isVoteMessage } from "../../server/helper/messageIdentifiers"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe('vote controller - none_of_that functions', () => {
 | 
					describe('vote controller - none_of_that functions', () => {
 | 
				
			||||||
	const testEventId = '1234321'
 | 
						const testEventId = '1234321'
 | 
				
			||||||
@@ -28,11 +29,16 @@ describe('vote controller - none_of_that functions', () => {
 | 
				
			|||||||
			id: 'mockId'
 | 
								id: 'mockId'
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						const mockEvent: GuildScheduledEvent = <GuildScheduledEvent><unknown>{
 | 
				
			||||||
 | 
							scheduledStartAt: testEventDate,
 | 
				
			||||||
 | 
							id: testEventId,
 | 
				
			||||||
 | 
							guild: testGuildId
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	const mockJellyfinHandler: JellyfinHandler = <JellyfinHandler><unknown>{
 | 
						const mockJellyfinHandler: JellyfinHandler = <JellyfinHandler><unknown>{
 | 
				
			||||||
		getRandomMovieNames: jest.fn().mockReturnValue(["movie1"])
 | 
							getRandomMovieNames: jest.fn().mockReturnValue(["movie1"])
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	const votes = new VoteController(mockClient, mockJellyfinHandler)
 | 
						const votes = new VoteController(mockClient, mockJellyfinHandler)
 | 
				
			||||||
	const mockMessageContent = votes.createVoteMessageText(testEventId, testEventDate, testMovies, testGuildId, "requestId")
 | 
						const mockMessageContent = votes.createVoteMessageText(mockEvent, testMovies, testGuildId, "requestId")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	test('sendVoteClosedMessage', async () => {
 | 
						test('sendVoteClosedMessage', async () => {
 | 
				
			||||||
		mockClient.getAnnouncementChannelForGuild = jest.fn().mockReturnValue({
 | 
							mockClient.getAnnouncementChannelForGuild = jest.fn().mockReturnValue({
 | 
				
			||||||
@@ -53,32 +59,9 @@ describe('vote controller - none_of_that functions', () => {
 | 
				
			|||||||
			allowedMentions: {
 | 
								allowedMentions: {
 | 
				
			||||||
				parse: ["roles"]
 | 
									parse: ["roles"]
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
			content: `[Abstimmung beendet] für https://discord.com/events/${testGuildId}/${testEventId}\n<@&1117915290781626398> Wir gucken MovieNew am 01.01. um 01:00`
 | 
								content: `[Abstimmung beendet] für https://discord.com/events/${testGuildId}/${testEventId}\n<@&WATCHPARTY_ANNOUNCEMENT_ROLE> Wir gucken MovieNew am 01.01. um 01:00`
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
	//	test('checkForPollsToClose', async () => {
 | 
					 | 
				
			||||||
	//
 | 
					 | 
				
			||||||
	//		const testGuild: Guild = <Guild><unknown>{
 | 
					 | 
				
			||||||
	//			scheduledEvents: {
 | 
					 | 
				
			||||||
	//				fetch: jest.fn().mockImplementation(() => {
 | 
					 | 
				
			||||||
	//					return new Promise(resolve => {
 | 
					 | 
				
			||||||
	//						resolve([
 | 
					 | 
				
			||||||
	//							{ name: "Event Name" },
 | 
					 | 
				
			||||||
	//							{ name: "Event: VOTING OFFEN", scheduledStartTimestamp: "" },
 | 
					 | 
				
			||||||
	//							{ name: "another voting" },
 | 
					 | 
				
			||||||
	//						]
 | 
					 | 
				
			||||||
	//						)
 | 
					 | 
				
			||||||
	//					})
 | 
					 | 
				
			||||||
	//				})
 | 
					 | 
				
			||||||
	//			}
 | 
					 | 
				
			||||||
	//		}
 | 
					 | 
				
			||||||
	//
 | 
					 | 
				
			||||||
	//		const result = await votes.checkForPollsToClose(testGuild)
 | 
					 | 
				
			||||||
	//
 | 
					 | 
				
			||||||
	//
 | 
					 | 
				
			||||||
	//
 | 
					 | 
				
			||||||
	//
 | 
					 | 
				
			||||||
	//	})
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	test('getVotesByEmote', async () => {
 | 
						test('getVotesByEmote', async () => {
 | 
				
			||||||
		const mockMessage: Message = <Message><unknown>{
 | 
							const mockMessage: Message = <Message><unknown>{
 | 
				
			||||||
@@ -89,8 +72,10 @@ describe('vote controller - none_of_that functions', () => {
 | 
				
			|||||||
				})
 | 
									})
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
							if (isVoteMessage(mockMessage)) {
 | 
				
			||||||
			const result = await votes.getVotesByEmote(mockMessage, 'guildId', 'requestId')
 | 
								const result = await votes.getVotesByEmote(mockMessage, 'guildId', 'requestId')
 | 
				
			||||||
			expect(result.length).toEqual(5)
 | 
								expect(result.length).toEqual(5)
 | 
				
			||||||
			expect(result).toEqual(votesList.filter(x => x.movie != NONE_OF_THAT))
 | 
								expect(result).toEqual(votesList.filter(x => x.movie != NONE_OF_THAT))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +1,9 @@
 | 
				
			|||||||
import { Emoji, NONE_OF_THAT } from "../../server/constants"
 | 
					import { Emoji, NONE_OF_THAT } from "../../server/constants"
 | 
				
			||||||
import VoteController, { Vote, VoteMessageInfo } from "../../server/helper/vote.controller"
 | 
					import VoteController, { VoteMessageInfo } from "../../server/helper/vote.controller"
 | 
				
			||||||
import { JellyfinHandler } from "../../server/jellyfin/handler"
 | 
					import { JellyfinHandler } from "../../server/jellyfin/handler"
 | 
				
			||||||
import { ExtendedClient } from "../../server/structures/client"
 | 
					import { ExtendedClient } from "../../server/structures/client"
 | 
				
			||||||
import { VoteMessage } from "../../server/helper/messageIdentifiers"
 | 
					import { VoteMessage } from "../../server/helper/messageIdentifiers"
 | 
				
			||||||
import { Message, MessageReaction } from "discord.js"
 | 
					import { GuildScheduledEvent, MessageReaction } from "discord.js"
 | 
				
			||||||
test('parse votes from vote message', async () => {
 | 
					test('parse votes from vote message', async () => {
 | 
				
			||||||
	const testMovies = [
 | 
						const testMovies = [
 | 
				
			||||||
		'Movie1',
 | 
							'Movie1',
 | 
				
			||||||
@@ -16,12 +16,16 @@ test('parse votes from vote message', async () => {
 | 
				
			|||||||
	const testEventDate = new Date('2023-01-01')
 | 
						const testEventDate = new Date('2023-01-01')
 | 
				
			||||||
	const testGuildId = "888999888"
 | 
						const testGuildId = "888999888"
 | 
				
			||||||
	const voteController: VoteController = new VoteController(<ExtendedClient>{}, <JellyfinHandler>{})
 | 
						const voteController: VoteController = new VoteController(<ExtendedClient>{}, <JellyfinHandler>{})
 | 
				
			||||||
	const testMessage = voteController.createVoteMessageText(testEventId, testEventDate, testMovies, testGuildId, "requestId")
 | 
						const mockEvent: GuildScheduledEvent = <GuildScheduledEvent><unknown>{
 | 
				
			||||||
 | 
							scheduledStartAt: testEventDate,
 | 
				
			||||||
 | 
							id: testEventId,
 | 
				
			||||||
 | 
							guild: testGuildId
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						const testMessage = voteController.createVoteMessageText(mockEvent, testMovies, testGuildId, "requestId")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const expectedResult: VoteMessageInfo = {
 | 
						const expectedResult: VoteMessageInfo = {
 | 
				
			||||||
		eventId: testEventId,
 | 
							event: mockEvent,
 | 
				
			||||||
		eventDate: testEventDate,
 | 
					 | 
				
			||||||
		votes: [
 | 
							votes: [
 | 
				
			||||||
			{ emote: Emoji.one, count: 1, movie: testMovies[0] },
 | 
								{ emote: Emoji.one, count: 1, movie: testMovies[0] },
 | 
				
			||||||
			{ emote: Emoji.two, count: 2, movie: testMovies[1] },
 | 
								{ emote: Emoji.two, count: 2, movie: testMovies[1] },
 | 
				
			||||||
@@ -32,7 +36,7 @@ test('parse votes from vote message', async () => {
 | 
				
			|||||||
		]
 | 
							]
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const msg: VoteMessage = <VoteMessage><unknown>{
 | 
						const message: VoteMessage = <VoteMessage><unknown>{
 | 
				
			||||||
		cleanContent: testMessage,
 | 
							cleanContent: testMessage,
 | 
				
			||||||
		guild: {
 | 
							guild: {
 | 
				
			||||||
			id: testGuildId,
 | 
								id: testGuildId,
 | 
				
			||||||
@@ -40,6 +44,8 @@ test('parse votes from vote message', async () => {
 | 
				
			|||||||
				fetch: jest.fn().mockImplementation((input: any) => {
 | 
									fetch: jest.fn().mockImplementation((input: any) => {
 | 
				
			||||||
					if (input === testEventId)
 | 
										if (input === testEventId)
 | 
				
			||||||
						return {
 | 
											return {
 | 
				
			||||||
 | 
												id: testEventId,
 | 
				
			||||||
 | 
												guild: testGuildId,
 | 
				
			||||||
							scheduledStartAt: testEventDate
 | 
												scheduledStartAt: testEventDate
 | 
				
			||||||
						}
 | 
											}
 | 
				
			||||||
				})
 | 
									})
 | 
				
			||||||
@@ -58,11 +64,11 @@ test('parse votes from vote message', async () => {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const result = await voteController.parseVoteInfoFromVoteMessage(msg, 'requestId')
 | 
						const result = await voteController.parseVoteInfoFromVoteMessage(message, 'requestId')
 | 
				
			||||||
	console.log(JSON.stringify(result))
 | 
						console.log(JSON.stringify(result))
 | 
				
			||||||
	expect(Array.isArray(result)).toBe(false)
 | 
						expect(Array.isArray(result)).toBe(false)
 | 
				
			||||||
	expect(result.eventId).toEqual(testEventId)
 | 
						expect(result.event.id).toEqual(testEventId)
 | 
				
			||||||
	expect(result.eventDate).toEqual(testEventDate)
 | 
						expect(result.event.scheduledStartAt).toEqual(testEventDate)
 | 
				
			||||||
	expect(result.votes.length).toEqual(expectedResult.votes.length)
 | 
						expect(result.votes.length).toEqual(expectedResult.votes.length)
 | 
				
			||||||
	expect(result).toEqual(expectedResult)
 | 
						expect(result).toEqual(expectedResult)
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
@@ -79,7 +85,12 @@ test('parse votes from vote message', () => {
 | 
				
			|||||||
	const testEventDate = new Date('2023-01-01')
 | 
						const testEventDate = new Date('2023-01-01')
 | 
				
			||||||
	const testGuildId = "888999888"
 | 
						const testGuildId = "888999888"
 | 
				
			||||||
	const voteController: VoteController = new VoteController(<ExtendedClient>{}, <JellyfinHandler>{})
 | 
						const voteController: VoteController = new VoteController(<ExtendedClient>{}, <JellyfinHandler>{})
 | 
				
			||||||
	const testMessage = voteController.createVoteMessageText(testEventId, testEventDate, testMovies, testGuildId, "requestId")
 | 
						const mockEvent: GuildScheduledEvent = <GuildScheduledEvent><unknown>{
 | 
				
			||||||
 | 
							scheduledStartAt: testEventDate,
 | 
				
			||||||
 | 
							id: testEventId,
 | 
				
			||||||
 | 
							guild: testGuildId
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						const testMessage = voteController.createVoteMessageText(mockEvent, testMovies, testGuildId, "requestId")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const result = voteController.parseGuildIdAndEventIdFromWholeMessage(testMessage)
 | 
						const result = voteController.parseGuildIdAndEventIdFromWholeMessage(testMessage)
 | 
				
			||||||
	expect(result).toEqual({ guildId: testGuildId, eventId: testEventId })
 | 
						expect(result).toEqual({ guildId: testGuildId, eventId: testEventId })
 | 
				
			||||||
@@ -108,7 +119,12 @@ test.skip('handles complete none_of_that vote', () => {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	const voteController = new VoteController(mockClient, mockJellyfinHandler)
 | 
						const voteController = new VoteController(mockClient, mockJellyfinHandler)
 | 
				
			||||||
	const mockMessageContent = voteController.createVoteMessageText(testEventId, testEventDate, testMovies, testGuildId, "requestId")
 | 
						const mockEvent: GuildScheduledEvent = <GuildScheduledEvent><unknown>{
 | 
				
			||||||
 | 
							scheduledStartAt: testEventDate,
 | 
				
			||||||
 | 
							id: testEventId,
 | 
				
			||||||
 | 
							guild: testGuildId
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						const mockMessageContent = voteController.createVoteMessageText(mockEvent, testMovies, testGuildId, "requestId")
 | 
				
			||||||
	const reactedUponMessage: VoteMessage = <VoteMessage><unknown>{
 | 
						const reactedUponMessage: VoteMessage = <VoteMessage><unknown>{
 | 
				
			||||||
		cleanContent: mockMessageContent,
 | 
							cleanContent: mockMessageContent,
 | 
				
			||||||
		guild: {
 | 
							guild: {
 | 
				
			||||||
@@ -158,7 +174,7 @@ test.skip('handles complete none_of_that vote', () => {
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	const msgReaction: MessageReaction = <MessageReaction><unknown>{
 | 
						const messageReaction: MessageReaction = <MessageReaction><unknown>{
 | 
				
			||||||
		message: reactedUponMessage
 | 
							message: reactedUponMessage
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -170,7 +186,7 @@ test.skip('handles complete none_of_that vote', () => {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const res = voteController.handleNoneOfThatVote(msgReaction, reactedUponMessage, 'requestId', 'guildId')
 | 
						const res = voteController.handleNoneOfThatVote(messageReaction, reactedUponMessage, 'requestId', 'guildId')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										15
									
								
								tests/testenv.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								tests/testenv.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					process.env.CLIENT_ID = "CLIENT_ID"
 | 
				
			||||||
 | 
					process.env.SECRET = "SECRET"
 | 
				
			||||||
 | 
					process.env.BOT_TOKEN = "BOT_TOKEN"
 | 
				
			||||||
 | 
					process.env.WATCHER_ROLE = "WATCHER_ROLE"
 | 
				
			||||||
 | 
					process.env.ADMIN_ROLE = "ADMIN_ROLE"
 | 
				
			||||||
 | 
					process.env.CHANNEL_ID = "CHANNEL_ID"
 | 
				
			||||||
 | 
					process.env.WATCHPARTY_ANNOUNCEMENT_ROLE = "WATCHPARTY_ANNOUNCEMENT_ROLE"
 | 
				
			||||||
 | 
					process.env.YAVIN_JELLYFIN_URL = "YAVIN_JELLYFIN_URL"
 | 
				
			||||||
 | 
					process.env.YAVIN_COLLECTION_ID = "YAVIN_COLLECTION_ID"
 | 
				
			||||||
 | 
					process.env.YAVIN_COLLECTION_USER = "YAVIN_COLLECTION_USER"
 | 
				
			||||||
 | 
					process.env.YAVIN_TOKEN = "YAVIN_TOKEN"
 | 
				
			||||||
 | 
					process.env.TOKEN = "TOKEN"
 | 
				
			||||||
 | 
					process.env.JELLYFIN_USER = "JELLYFIN_USER"
 | 
				
			||||||
 | 
					process.env.JELLYFIN_COLLECTION_ID = "JELLYFIN_COLLECTION_ID"
 | 
				
			||||||
 | 
					process.env.JELLYFIN_URL = "JELLYFIN_URL"
 | 
				
			||||||
		Reference in New Issue
	
	Block a user