Compare commits
	
		
			95 Commits
		
	
	
		
			b1c581ca6e
			...
			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 | |||
| f705b97804 | |||
| 9cdc6e1934 | |||
| c73cd20ccf | |||
| e66aebc88c | |||
| 599243990e | |||
| eef3a9c358 | |||
| 1e912b20ef | |||
| ce4dc81f7d | |||
| b76df79d2a | |||
| 4e563d57fd | |||
| b6a1e06b03 | |||
| 2ebc7fbdbe | |||
| 8ff5aeff03 | |||
| 1101a84501 | |||
| 91ec2ece7e | |||
| 5e58765cf4 | |||
| a2adef808f | |||
| dc66c277b2 | |||
| c022cc32d5 | |||
| e763e76413 | |||
| 137d156981 | |||
| fdfe7ce404 | |||
| 146848b759 | |||
| e54f03292e | |||
| fe45445811 | |||
| 8f02e11dba | |||
| 878c81bfa7 | |||
| ca19168cf4 | |||
| e8893646f0 | |||
| e61b3a7b16 | |||
| 9383cee4a0 | |||
| 0748097a1f | |||
| ffba737e5a | |||
| 4cd9c771f0 | |||
| 8c3cf7829b | |||
| 1a13638ed9 | |||
| c351e27fdd | |||
| 6d3bea169e | |||
| 3f071c8a4e | |||
| 98d1ca73b5 | |||
| ee742018e9 | |||
| 8ad651c753 | |||
| a4a834ad27 | |||
| e8dcfd8340 | |||
| d9d1d74ef9 | |||
| 331ff89060 | |||
| f6476c609b | |||
| 6220268b14 | |||
| b6034d4fb7 | |||
| ca0a9e3cb8 | |||
| b8a32aab40 | |||
| e3e755011d | |||
| 5a6c66cb3e | 
							
								
								
									
										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",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,6 +6,7 @@ import { Maybe } from '../interfaces'
 | 
				
			|||||||
import { logger } from '../logger'
 | 
					import { logger } from '../logger'
 | 
				
			||||||
import { Command } from '../structures/command'
 | 
					import { Command } from '../structures/command'
 | 
				
			||||||
import { RunOptions } from '../types/commandTypes'
 | 
					import { RunOptions } from '../types/commandTypes'
 | 
				
			||||||
 | 
					import { isInitialAnnouncement } from '../helper/messageIdentifiers'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default new Command({
 | 
					export default new Command({
 | 
				
			||||||
	name: 'announce',
 | 
						name: 'announce',
 | 
				
			||||||
@@ -61,7 +62,7 @@ async function sendInitialAnnouncement(guildId: string, requestId: string): Prom
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const currentPinnedAnnouncementMessages = (await announcementChannel.messages.fetchPinned()).filter(message => message.cleanContent.includes("[initial]"))
 | 
						const currentPinnedAnnouncementMessages = (await announcementChannel.messages.fetchPinned()).filter(message => isInitialAnnouncement(message))
 | 
				
			||||||
	currentPinnedAnnouncementMessages.forEach(async (message) => await message.unpin())
 | 
						currentPinnedAnnouncementMessages.forEach(async (message) => await message.unpin())
 | 
				
			||||||
	currentPinnedAnnouncementMessages.forEach(message => message.delete())
 | 
						currentPinnedAnnouncementMessages.forEach(message => message.delete())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -81,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,10 +1,5 @@
 | 
				
			|||||||
import { addDays, differenceInDays, format, isAfter, toDate } from 'date-fns'
 | 
					 | 
				
			||||||
import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildScheduledEventSetStatusArg, GuildScheduledEventStatus, Message, MessageCreateOptions, TextChannel } from 'discord.js'
 | 
					 | 
				
			||||||
import { v4 as uuid } from 'uuid'
 | 
					import { v4 as uuid } from 'uuid'
 | 
				
			||||||
import { client } from '../..'
 | 
					import { client } from '../..'
 | 
				
			||||||
import { config } from '../configuration'
 | 
					 | 
				
			||||||
import { Emotes } from '../events/autoCreateVoteByWPEvent'
 | 
					 | 
				
			||||||
import { Maybe } from '../interfaces'
 | 
					 | 
				
			||||||
import { logger } from '../logger'
 | 
					import { logger } from '../logger'
 | 
				
			||||||
import { Command } from '../structures/command'
 | 
					import { Command } from '../structures/command'
 | 
				
			||||||
import { RunOptions } from '../types/commandTypes'
 | 
					import { RunOptions } from '../types/commandTypes'
 | 
				
			||||||
@@ -25,160 +20,6 @@ export default new Command({
 | 
				
			|||||||
		logger.info("Got command for closing poll!", { guildId, requestId })
 | 
							logger.info("Got command for closing poll!", { guildId, requestId })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		command.followUp("Alles klar, beende die Umfrage :)")
 | 
							command.followUp("Alles klar, beende die Umfrage :)")
 | 
				
			||||||
		closePoll(command.guild, requestId)
 | 
							client.voteController.closePoll(command.guild, requestId)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function closePoll(guild: Guild, requestId: string) {
 | 
					 | 
				
			||||||
	const guildId = guild.id
 | 
					 | 
				
			||||||
	logger.info("stopping poll", { guildId, requestId })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const announcementChannel: Maybe<TextChannel> = client.getAnnouncementChannelForGuild(guildId)
 | 
					 | 
				
			||||||
	if (!announcementChannel) {
 | 
					 | 
				
			||||||
		logger.error("Could not find the textchannel. Unable to close poll.", { guildId, requestId })
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const messages: Message<true>[] = (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
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const lastMessage: Message<true> = messages[0]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	logger.debug(`Found messages: ${JSON.stringify(messages, null, 2)}`, { guildId, requestId })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	logger.debug(`Last message: ${JSON.stringify(lastMessage, null, 2)}`, { guildId, requestId })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const votes = await (await getVotesByEmote(lastMessage, guildId, requestId))
 | 
					 | 
				
			||||||
		.sort((a, b) => b.count - a.count)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	logger.debug(`votes: ${JSON.stringify(votes, null, 2)}`, { guildId, requestId })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	logger.info("Deleting vote message")
 | 
					 | 
				
			||||||
	await lastMessage.delete()
 | 
					 | 
				
			||||||
	const event = await getEvent(guild, guild.id, requestId)
 | 
					 | 
				
			||||||
	if (event) {
 | 
					 | 
				
			||||||
		updateEvent(event, votes, guild, guildId, requestId)
 | 
					 | 
				
			||||||
		sendVoteClosedMessage(event, votes[0].movie, guildId, requestId)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	//lastMessage.unpin() //todo: uncomment when bot has permission to pin/unpin
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function sendVoteClosedMessage(event: GuildScheduledEvent, movie: string, guildId: string, requestId: string) {
 | 
					 | 
				
			||||||
	const date = event.scheduledStartAt ? format(event.scheduledStartAt, "dd.MM") : "Fehler, event hatte kein Datum"
 | 
					 | 
				
			||||||
	const time = event.scheduledStartAt ? format(event.scheduledStartAt, "HH:mm") : "Fehler, event hatte kein Datum"
 | 
					 | 
				
			||||||
	const body = `[Abstimmung beendet] für https://discord.com/events/${event.guildId}/${event.id}\n<@&${config.bot.announcement_role}> Wir gucken  ${movie} am ${date} um ${time}`
 | 
					 | 
				
			||||||
	const options: MessageCreateOptions = {
 | 
					 | 
				
			||||||
		content: body,
 | 
					 | 
				
			||||||
		allowedMentions: { parse: ["roles"] }
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	const announcementChannel = client.getAnnouncementChannelForGuild(guildId)
 | 
					 | 
				
			||||||
	logger.info("Sending vote closed message.", { guildId, requestId })
 | 
					 | 
				
			||||||
	if (!announcementChannel) {
 | 
					 | 
				
			||||||
		logger.error("Could not find announcement channel. Please fix!", { guildId, requestId })
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	announcementChannel.send(options)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function updateEvent(voteEvent: GuildScheduledEvent, votes: Vote[], guild: Guild, guildId: string, requestId: string) {
 | 
					 | 
				
			||||||
	logger.info(`Updating event with movie ${votes[0].movie}.`, { guildId, requestId })
 | 
					 | 
				
			||||||
	const options: GuildScheduledEventEditOptions<GuildScheduledEventStatus.Scheduled, GuildScheduledEventSetStatusArg<GuildScheduledEventStatus.Scheduled>> = {
 | 
					 | 
				
			||||||
		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<GuildScheduledEvent | null> {
 | 
					 | 
				
			||||||
	const voteEvents = (await guild.scheduledEvents.fetch())
 | 
					 | 
				
			||||||
		.map((value) => value)
 | 
					 | 
				
			||||||
		.filter(event => event.name.toLowerCase().includes("voting offen"))
 | 
					 | 
				
			||||||
	logger.debug(`Found events: ${JSON.stringify(voteEvents, null, 2)}`, { guildId, requestId })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if (!voteEvents || voteEvents.length <= 0) {
 | 
					 | 
				
			||||||
		logger.error("Could not find vote event. Cancelling update!", { guildId, requestId })
 | 
					 | 
				
			||||||
		return null
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return voteEvents[0]
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type Vote = {
 | 
					 | 
				
			||||||
	emote: string, //todo habs nicht hinbekommen hier Emotes zu nutzen
 | 
					 | 
				
			||||||
	count: number,
 | 
					 | 
				
			||||||
	movie: string
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function getVotesByEmote(message: Message, guildId: string, requestId: string): Promise<Vote[]> {
 | 
					 | 
				
			||||||
	const votes: Vote[] = []
 | 
					 | 
				
			||||||
	logger.debug(`Number of items in emotes: ${Object.values(Emotes).length}`, { guildId, requestId })
 | 
					 | 
				
			||||||
	for (let i = 0; i < Object.keys(Emotes).length / 2; i++) {
 | 
					 | 
				
			||||||
		const emote = Emotes[i]
 | 
					 | 
				
			||||||
		logger.debug(`Getting reaction for emote ${emote}`, { guildId, requestId })
 | 
					 | 
				
			||||||
		const reaction = await message.reactions.resolve(emote)
 | 
					 | 
				
			||||||
		logger.debug(`Reaction for emote ${emote}: ${JSON.stringify(reaction, null, 2)}`, { guildId, requestId })
 | 
					 | 
				
			||||||
		if (reaction) {
 | 
					 | 
				
			||||||
			const vote: Vote = { emote: emote, count: reaction.count, movie: extractMovieFromMessageByEmote(message, emote) }
 | 
					 | 
				
			||||||
			votes.push(vote)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return votes
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function extractMovieFromMessageByEmote(message: Message, emote: string): string {
 | 
					 | 
				
			||||||
	const lines = message.cleanContent.split("\n")
 | 
					 | 
				
			||||||
	const emoteLines = lines.filter(line => line.includes(emote))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if (!emoteLines) {
 | 
					 | 
				
			||||||
		return ""
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	const movie = emoteLines[0].substring(emoteLines[0].indexOf(emote) + emote.length + 2) // plus colon and space
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return movie
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function checkForPollsToClose(guild: Guild): Promise<void> {
 | 
					 | 
				
			||||||
	const requestId = uuid()
 | 
					 | 
				
			||||||
	logger.info(`Automatic check for poll closing.`, { guildId: guild.id, requestId })
 | 
					 | 
				
			||||||
	const events = (await guild.scheduledEvents.fetch()).filter(event => event.name.toLocaleLowerCase().includes("voting offen")).map(event => event)
 | 
					 | 
				
			||||||
	if (events.length > 1) {
 | 
					 | 
				
			||||||
		logger.error("Handling more than one Event is not implemented yet. Found more than one poll to close")
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	} else if (events.length == 0) {
 | 
					 | 
				
			||||||
		logger.info("Could not find any events. Cancelling", { guildId: guild.id, requestId })
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const updatedEvent = events[0]             //add two hours because of different timezones in discord api and Date.now()
 | 
					 | 
				
			||||||
	if (!updatedEvent.scheduledStartTimestamp) {
 | 
					 | 
				
			||||||
		logger.error("Event does not have a scheduled start time. Cancelling", { guildId: guild.id, requestId })
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const createDate: Date = toDate(updatedEvent.createdTimestamp)
 | 
					 | 
				
			||||||
	const eventDate: Date = toDate(updatedEvent.scheduledStartTimestamp)
 | 
					 | 
				
			||||||
	const difference: number = differenceInDays(createDate, eventDate)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if (difference <= 2) {
 | 
					 | 
				
			||||||
		logger.info("Less than two days between event create and event start. Not closing poll.", { guildId: guild.id, requestId })
 | 
					 | 
				
			||||||
		return
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const closePollDate: Date = addDays(eventDate, -2)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if (isAfter(Date.now(), closePollDate)) {
 | 
					 | 
				
			||||||
		logger.info("Less than two days until event. Closing poll", { guildId: guild.id, requestId })
 | 
					 | 
				
			||||||
		closePoll(guild, requestId)
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		logger.info(`ScheduledStart: ${closePollDate}. Now: ${toDate(Date.now())}`, { guildId: guild.id, requestId })
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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())
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,6 +30,8 @@ export interface Config {
 | 
				
			|||||||
		yavin_jellyfin_url: string
 | 
							yavin_jellyfin_url: string
 | 
				
			||||||
		yavin_jellyfin_token: string
 | 
							yavin_jellyfin_token: string
 | 
				
			||||||
		yavin_jellyfin_collection_user: string
 | 
							yavin_jellyfin_collection_user: string
 | 
				
			||||||
 | 
							random_movie_count: number
 | 
				
			||||||
 | 
							reroll_retains_top_picks: boolean
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
export const config: Config = {
 | 
					export const config: Config = {
 | 
				
			||||||
@@ -59,16 +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,
 | 
				
			||||||
 | 
							reroll_retains_top_picks: process.env.REROLL_RETAIN === "true"
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										17
									
								
								server/constants.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								server/constants.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					export enum ValidVoteEmotes { "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟" }
 | 
				
			||||||
 | 
					export const NONE_OF_THAT = "❌"
 | 
				
			||||||
 | 
					// WIP
 | 
				
			||||||
 | 
					export const Emoji = {
 | 
				
			||||||
 | 
						"one": "\u0031\uFE0F\u20E3",
 | 
				
			||||||
 | 
						"two": "\u0032\uFE0F\u20E3",
 | 
				
			||||||
 | 
						"three": "\u0033\uFE0F\u20E3",
 | 
				
			||||||
 | 
						"four": "\u0034\uFE0F\u20E3",
 | 
				
			||||||
 | 
						"five": "\u0035\uFE0F\u20E3",
 | 
				
			||||||
 | 
						"six": "\u0036\uFE0F\u20E3",
 | 
				
			||||||
 | 
						"seven": "\u0037\uFE0F\u20E3",
 | 
				
			||||||
 | 
						"eight": "\u0038\uFE0F\u20E3",
 | 
				
			||||||
 | 
						"nine": "\u0039\uFE0F\u20E3",
 | 
				
			||||||
 | 
						"ten": "\uD83D\uDD1F",
 | 
				
			||||||
 | 
						"ticket": "🎫"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -32,7 +32,11 @@ export async function execute(event: GuildScheduledEvent) {
 | 
				
			|||||||
				return
 | 
									return
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const message = `[Watchparty] https://discord.com/events/${event.guildId}/${event.id} \nHey <@&${config.bot.announcement_role}>, wir gucken ${event.name} ${createDateStringFromEvent(event, guildId, requestId)}`
 | 
								if (!event.scheduledStartAt) {
 | 
				
			||||||
 | 
									logger.error('Event has no start date, bailing out')
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								const message = `[Watchparty] https://discord.com/events/${event.guildId}/${event.id} \nHey <@&${config.bot.announcement_role}>, wir gucken ${event.name} ${createDateStringFromEvent(event.scheduledStartAt, guildId, requestId)}`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			channel.send(message)
 | 
								channel.send(message)
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,20 +1,11 @@
 | 
				
			|||||||
import { GuildScheduledEvent, Message, MessageCreateOptions, TextChannel } from "discord.js";
 | 
					import { GuildScheduledEvent, TextChannel } from "discord.js";
 | 
				
			||||||
import { ScheduledTask } from "node-cron";
 | 
					 | 
				
			||||||
import { v4 as uuid } from "uuid";
 | 
					import { v4 as uuid } from "uuid";
 | 
				
			||||||
import { client, yavinJellyfinHandler } from "../..";
 | 
					import { client, yavinJellyfinHandler } from "../..";
 | 
				
			||||||
import { config } from "../configuration";
 | 
					 | 
				
			||||||
import { createDateStringFromEvent } from "../helper/dateHelper";
 | 
					 | 
				
			||||||
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 enum Emotes { "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟" }
 | 
					 | 
				
			||||||
export const NONE_OF_THAT = "❌"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export let task: ScheduledTask | undefined
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function execute(event: GuildScheduledEvent) {
 | 
					export async function execute(event: GuildScheduledEvent) {
 | 
				
			||||||
	const requestId = uuid()
 | 
						const requestId = uuid()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -33,31 +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
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		let message = `[Abstimmung] für https://discord.com/events/${event.guildId}/${event.id}\n<@&${config.bot.announcement_role}> Es gibt eine neue Abstimmung für die nächste Watchparty ${createDateStringFromEvent(event, event.guildId, requestId)}! Stimme hierunter für den nächsten Film ab!\n`
 | 
							const sentMessage = await client.voteController.prepareAndSendVoteMessage({
 | 
				
			||||||
 | 
								movies,
 | 
				
			||||||
 | 
								startDate: event.scheduledStartAt,
 | 
				
			||||||
 | 
								event,
 | 
				
			||||||
 | 
								announcementChannel,
 | 
				
			||||||
 | 
								pinAfterSending: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
								event.guildId,
 | 
				
			||||||
 | 
								requestId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		for (let i = 0; i < movies.length; i++) {
 | 
							logger.debug(JSON.stringify(sentMessage))
 | 
				
			||||||
			message = message.concat(Emotes[i]).concat(": ").concat(movies[i]).concat("\n")
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		message = message.concat(NONE_OF_THAT).concat(": Wenn dir nichts davon gefällt.")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		const options: MessageCreateOptions = {
 | 
					 | 
				
			||||||
			allowedMentions: { parse: ["roles"] },
 | 
					 | 
				
			||||||
			content: message,
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		const sentMessage: Message<true> = await (await announcementChannel.fetch()).send(options)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		for (let i = 0; i < movies.length; i++) {
 | 
					 | 
				
			||||||
			sentMessage.react(Emotes[i])
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		sentMessage.react(NONE_OF_THAT)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// sentMessage.pin() //todo: uncomment when bot has permission to pin messages. Also update closepoll.ts to only fetch pinned messages
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@ import { Collection, GuildScheduledEvent, GuildScheduledEventStatus, Message } f
 | 
				
			|||||||
import { v4 as uuid } from "uuid";
 | 
					import { v4 as uuid } from "uuid";
 | 
				
			||||||
import { client } from "../..";
 | 
					import { client } from "../..";
 | 
				
			||||||
import { logger } from "../logger";
 | 
					import { logger } from "../logger";
 | 
				
			||||||
 | 
					import { isInitialAnnouncement } from "../helper/messageIdentifiers";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const name = 'guildScheduledEventUpdate'
 | 
					export const name = 'guildScheduledEventUpdate'
 | 
				
			||||||
@@ -25,7 +26,7 @@ export async function execute(oldEvent: GuildScheduledEvent, newEvent: GuildSche
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
			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 wpAnnouncements = (await announcementChannel.messages.fetch()).filter(message => !isInitialAnnouncement(message))
 | 
				
			||||||
			const announcementsWithoutEvent = filterAnnouncementsByPendingWPs(wpAnnouncements, events)
 | 
								const announcementsWithoutEvent = filterAnnouncementsByPendingWPs(wpAnnouncements, events)
 | 
				
			||||||
			logger.info(`Deleting ${announcementsWithoutEvent.length} announcements.`, { guildId, requestId })
 | 
								logger.info(`Deleting ${announcementsWithoutEvent.length} announcements.`, { guildId, requestId })
 | 
				
			||||||
			announcementsWithoutEvent.forEach(message => message.delete())
 | 
								announcementsWithoutEvent.forEach(message => message.delete())
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										46
									
								
								server/events/handleMessageReactionAdd.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								server/events/handleMessageReactionAdd.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					import { Message, MessageReaction, User } from "discord.js";
 | 
				
			||||||
 | 
					import { logger, newRequestId, noGuildId } from "../logger";
 | 
				
			||||||
 | 
					import { Emoji, ValidVoteEmotes, NONE_OF_THAT } from "../constants";
 | 
				
			||||||
 | 
					import { client } from "../..";
 | 
				
			||||||
 | 
					import { isInitialAnnouncement, isVoteMessage } from "../helper/messageIdentifiers";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const name = 'messageReactionAdd'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 messageReactionAdd on non-guild message.`, { requestId })
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						logger.info(`Got reaction on message`, { requestId, guildId })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						logger.info(`emoji: ${messageReaction.emoji.toString()}`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!Object.values(ValidVoteEmotes).includes(messageReaction.emoji.toString()) && messageReaction.emoji.toString() !== NONE_OF_THAT) {
 | 
				
			||||||
 | 
							logger.info(`${messageReaction.emoji.toString()} currently not handled`)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						logger.info(`Found a match for ${messageReaction.emoji.toString()}`)
 | 
				
			||||||
 | 
						if (isVoteMessage(reactedUponMessage)) {
 | 
				
			||||||
 | 
							if (messageReaction.emoji.toString() === NONE_OF_THAT) {
 | 
				
			||||||
 | 
								logger.info(`Reaction is NONE_OF_THAT on a vote message. Handling`, { requestId, guildId })
 | 
				
			||||||
 | 
								return client.voteController.handleNoneOfThatVote(messageReaction, reactedUponMessage, requestId, guildId)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						else if (isInitialAnnouncement(reactedUponMessage)) {
 | 
				
			||||||
 | 
							if (messageReaction.emoji.toString() === Emoji.ticket) {
 | 
				
			||||||
 | 
								logger.error(`Got a role emoji. ${reactedUponMessage.id}`)
 | 
				
			||||||
 | 
								return client.roleController.addMediaRoleToUser(user, messageReaction.message.guild, requestId)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							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,23 +1,23 @@
 | 
				
			|||||||
import { format, isToday, toDate } 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(event: GuildScheduledEvent, requestId: string, guildId?: string): string {
 | 
					export function createDateStringFromEvent(eventStartDate: Maybe<Date>, requestId: string, guildId?: string): string {
 | 
				
			||||||
	if (!event.scheduledStartAt) {
 | 
						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"`
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const timeZone = 'Europe/Berlin'
 | 
						const timeZone = 'Europe/Berlin'
 | 
				
			||||||
	const zonedDateTime = utcToZonedTime(event.scheduledStartAt, timeZone)
 | 
						const zonedDateTime = utcToZonedTime(eventStartDate, timeZone)
 | 
				
			||||||
	const time = format(zonedDateTime, "HH:mm", { locale: de })
 | 
						const time = format(zonedDateTime, "HH:mm", { locale: de })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (isToday(zonedDateTime)) {
 | 
						if (isToday(zonedDateTime)) {
 | 
				
			||||||
		return `heute um ${time}`
 | 
							return `heute um ${time}`
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const date = format(zonedDateTime, "eeee dd.MM", { locale: de })
 | 
						const date = format(zonedDateTime, "eeee dd.MM.", { locale: de })
 | 
				
			||||||
	return `am ${date} um ${time}`
 | 
						return `am ${date} um ${time}`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										20
									
								
								server/helper/messageIdentifiers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								server/helper/messageIdentifiers.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					import { Message } from "discord.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// branded types to differentiate objects of identical Type but different contents
 | 
				
			||||||
 | 
					export type VoteEndMessage = Message<true> & { readonly __brand: 'voteend' }
 | 
				
			||||||
 | 
					export type AnnouncementMessage = Message<true> & { readonly __brand: 'announcement' }
 | 
				
			||||||
 | 
					export type VoteMessage = Message<true> & { readonly __brand: 'vote' }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type KnownDiscordMessage = VoteMessage | VoteEndMessage | AnnouncementMessage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function isVoteMessage(message: Message): message is VoteMessage {
 | 
				
			||||||
 | 
						return message.cleanContent.includes('[Abstimmung]')
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export function isInitialAnnouncement(message: Message): message is AnnouncementMessage {
 | 
				
			||||||
 | 
						return message.cleanContent.includes("[initial]")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export function isVoteEndedMessage(message: Message): message is VoteEndMessage {
 | 
				
			||||||
 | 
						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,5 +1,5 @@
 | 
				
			|||||||
import { Collection, GuildMember } from "discord.js"
 | 
					import { Collection, Guild, GuildMember, Role } from "discord.js"
 | 
				
			||||||
import { ChangedRoles, PermissionLevel } from "../interfaces"
 | 
					import { ChangedRoles, Maybe, PermissionLevel } from "../interfaces"
 | 
				
			||||||
import { logger } from "../logger"
 | 
					import { logger } from "../logger"
 | 
				
			||||||
import { config } from "../configuration"
 | 
					import { config } from "../configuration"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -16,6 +16,13 @@ export function filterRolesFromMemberUpdate(oldMember: GuildMember, newMember: G
 | 
				
			|||||||
	return { addedRoles, removedRoles }
 | 
						return { addedRoles, removedRoles }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function getMembersWithRoleFromGuild(roleId: string, guild: Guild): Promise<Collection<string, GuildMember>> {
 | 
				
			||||||
 | 
						const emptyResponse = new Collection<string, GuildMember>
 | 
				
			||||||
 | 
						const guildRole: Maybe<Role> = guild.roles.resolve(roleId)
 | 
				
			||||||
 | 
						if (!guildRole) return emptyResponse
 | 
				
			||||||
 | 
						return guildRole.members
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function getGuildSpecificTriggerRoleId(): Collection<string, PermissionLevel> {
 | 
					export function getGuildSpecificTriggerRoleId(): Collection<string, PermissionLevel> {
 | 
				
			||||||
	const outVal = new Collection<string, PermissionLevel>()
 | 
						const outVal = new Collection<string, PermissionLevel>()
 | 
				
			||||||
	outVal.set(config.bot.watcher_role, "VIEWER")
 | 
						outVal.set(config.bot.watcher_role, "VIEWER")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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()
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										361
									
								
								server/helper/vote.controller.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										361
									
								
								server/helper/vote.controller.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,361 @@
 | 
				
			|||||||
 | 
					import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildScheduledEventSetStatusArg, GuildScheduledEventStatus, Message, MessageCreateOptions, MessageReaction, TextChannel } from "discord.js"
 | 
				
			||||||
 | 
					import { ValidVoteEmotes, NONE_OF_THAT } from "../constants"
 | 
				
			||||||
 | 
					import { logger, newRequestId } from "../logger"
 | 
				
			||||||
 | 
					import { getMembersWithRoleFromGuild } from "./roleFilter"
 | 
				
			||||||
 | 
					import { config } from "../configuration"
 | 
				
			||||||
 | 
					import { VoteMessage, isVoteEndedMessage, isVoteMessage } from "./messageIdentifiers"
 | 
				
			||||||
 | 
					import { createDateStringFromEvent } from "./dateHelper"
 | 
				
			||||||
 | 
					import { Maybe, prepareVoteMessageInput } from "../interfaces"
 | 
				
			||||||
 | 
					import format from "date-fns/format"
 | 
				
			||||||
 | 
					import toDate from "date-fns/toDate"
 | 
				
			||||||
 | 
					import differenceInDays from "date-fns/differenceInDays"
 | 
				
			||||||
 | 
					import addDays from "date-fns/addDays"
 | 
				
			||||||
 | 
					import isAfter from "date-fns/isAfter"
 | 
				
			||||||
 | 
					import { ExtendedClient } from "../structures/client"
 | 
				
			||||||
 | 
					import { JellyfinHandler } from "../jellyfin/handler"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type Vote = {
 | 
				
			||||||
 | 
						emote: string, //todo habs nicht hinbekommen hier Emotes zu nutzen
 | 
				
			||||||
 | 
						count: number,
 | 
				
			||||||
 | 
						movie: string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export type VoteMessageInfo = {
 | 
				
			||||||
 | 
						votes: Vote[],
 | 
				
			||||||
 | 
						event: GuildScheduledEvent,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export default class VoteController {
 | 
				
			||||||
 | 
						private client: ExtendedClient
 | 
				
			||||||
 | 
						private yavinJellyfinHandler: JellyfinHandler
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public constructor(_client: ExtendedClient, _yavin: JellyfinHandler) {
 | 
				
			||||||
 | 
							this.client = _client
 | 
				
			||||||
 | 
							this.yavinJellyfinHandler = _yavin
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public async handleNoneOfThatVote(messageReaction: MessageReaction, reactedUponMessage: VoteMessage, requestId: string, guildId: string) {
 | 
				
			||||||
 | 
							if (!messageReaction.message.guild) return 'No guild'
 | 
				
			||||||
 | 
							const guild = messageReaction.message.guild
 | 
				
			||||||
 | 
							logger.debug(`${reactedUponMessage.id} is vote message`, { requestId, guildId })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const watcherRoleMember = await getMembersWithRoleFromGuild(config.bot.announcement_role, messageReaction.message.guild)
 | 
				
			||||||
 | 
							logger.info("ROLE MEMBERS " + JSON.stringify(watcherRoleMember), { requestId, guildId })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const watcherRoleMemberCount = watcherRoleMember.size
 | 
				
			||||||
 | 
							logger.info(`MEMBER COUNT: ${watcherRoleMemberCount}`, { requestId, guildId })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const noneOfThatReactions = reactedUponMessage.reactions.cache.get(NONE_OF_THAT)?.users.cache.filter(x => x.id !== this.client.user?.id).size ?? 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const memberThreshold = (watcherRoleMemberCount / 2)
 | 
				
			||||||
 | 
							logger.info(`Reroll ${noneOfThatReactions} > ${memberThreshold} ?`, { requestId, guildId })
 | 
				
			||||||
 | 
							if (noneOfThatReactions > memberThreshold)
 | 
				
			||||||
 | 
								logger.info(`No reroll`, { requestId, guildId })
 | 
				
			||||||
 | 
							else {
 | 
				
			||||||
 | 
								logger.info('Starting poll reroll', { requestId, guildId })
 | 
				
			||||||
 | 
								await this.handleReroll(reactedUponMessage, guild.id, requestId)
 | 
				
			||||||
 | 
								logger.info(`Finished handling NONE_OF_THAT vote`, { requestId, guildId })
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private async removeMessage(message: Message): Promise<Message<boolean>> {
 | 
				
			||||||
 | 
							if (message.pinned) {
 | 
				
			||||||
 | 
								await message.unpin()
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return await message.delete()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * 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
 | 
				
			||||||
 | 
								logger.info(`Fetching ${newMovieCount} from jellyfin`)
 | 
				
			||||||
 | 
								const newMovies: string[] = await this.yavinJellyfinHandler.getRandomMovieNames(newMovieCount, guildId, requestId)
 | 
				
			||||||
 | 
								// merge
 | 
				
			||||||
 | 
								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)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const announcementChannel = this.client.getAnnouncementChannelForGuild(guildId)
 | 
				
			||||||
 | 
							if (!announcementChannel) {
 | 
				
			||||||
 | 
								logger.error(`No announcementChannel found for ${guildId}, can't post poll`)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								logger.info(`Trying to remove old vote Message`, { requestId, guildId })
 | 
				
			||||||
 | 
								this.removeMessage(voteMessage)
 | 
				
			||||||
 | 
							} catch (err) {
 | 
				
			||||||
 | 
								// TODO: integrate failure DM to media Admin to inform about inability to delete old message
 | 
				
			||||||
 | 
								logger.error(`Error during removeMessage: ${err}`)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							const sentMessage = this.prepareAndSendVoteMessage({
 | 
				
			||||||
 | 
								event: voteInfo.event,
 | 
				
			||||||
 | 
								movies,
 | 
				
			||||||
 | 
								announcementChannel,
 | 
				
			||||||
 | 
								startDate: voteInfo.event.scheduledStartAt,
 | 
				
			||||||
 | 
								pinAfterSending: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
								guildId,
 | 
				
			||||||
 | 
								requestId)
 | 
				
			||||||
 | 
							logger.debug(`Sent reroll message: ${JSON.stringify(sentMessage)}`, { requestId, guildId })
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private async fetchEventByEventId(guild: Guild, eventId: string, requestId: string): Promise<Maybe<GuildScheduledEvent>> {
 | 
				
			||||||
 | 
							const guildEvent: GuildScheduledEvent = await guild.scheduledEvents.fetch(eventId)
 | 
				
			||||||
 | 
							if (!guildEvent) logger.error(`GuildScheduledEvent with id${eventId} could not be found`, { requestId, guildId: guild.id })
 | 
				
			||||||
 | 
							return guildEvent
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public async parseVoteInfoFromVoteMessage(message: VoteMessage, requestId: string): Promise<VoteMessageInfo> {
 | 
				
			||||||
 | 
							const lines = message.cleanContent.split('\n')
 | 
				
			||||||
 | 
							let parsedIds = this.parseGuildIdAndEventIdFromWholeMessage(message.cleanContent)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (!message.guild)
 | 
				
			||||||
 | 
								throw new Error(`Message ${message.id} not a guild message`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const event: Maybe<GuildScheduledEvent> = await this.fetchEventByEventId(message.guild, parsedIds.eventId, requestId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							let votes: Vote[] = []
 | 
				
			||||||
 | 
							for (const line of lines) {
 | 
				
			||||||
 | 
								if (line.slice(0, 5).includes(':')) {
 | 
				
			||||||
 | 
									const splitLine = line.split(":")
 | 
				
			||||||
 | 
									const [emoji, movie] = splitLine
 | 
				
			||||||
 | 
									const fetchedVoteFromMessage = message.reactions.cache.get(emoji)
 | 
				
			||||||
 | 
									if (fetchedVoteFromMessage) {
 | 
				
			||||||
 | 
										if (emoji === NONE_OF_THAT) {
 | 
				
			||||||
 | 
											votes.push({ movie: NONE_OF_THAT, emote: NONE_OF_THAT, count: fetchedVoteFromMessage.count })
 | 
				
			||||||
 | 
										} else
 | 
				
			||||||
 | 
											votes.push({ movie: movie.trim(), emote: emoji, count: fetchedVoteFromMessage.count })
 | 
				
			||||||
 | 
									} else {
 | 
				
			||||||
 | 
										logger.error(`No vote reaction found for movie, assuming 0`, requestId)
 | 
				
			||||||
 | 
										votes.push({ movie, emote: emoji, count: 0 })
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return <VoteMessageInfo>{ event, votes }
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						public parseEventDateFromMessage(message: string, guildId: string, requestId: string): Date {
 | 
				
			||||||
 | 
							logger.warn(`Falling back to RegEx parsing to get Event Date`, { guildId, requestId })
 | 
				
			||||||
 | 
							const datematcher = RegExp(/((?:0[1-9]|[12][0-9]|3[01])\.(?:0[1-9]|1[012])\.)(?:\ um\ )((?:(?:[01][0-9]|[2][0-3])\:[0-5][0-9])|(?:[2][4]\:00))!/i)
 | 
				
			||||||
 | 
							const result: RegExpMatchArray | null = message.match(datematcher)
 | 
				
			||||||
 | 
							const timeFromResult = result?.at(-1)
 | 
				
			||||||
 | 
							const dateFromResult = result?.at(1)?.concat(format(new Date(), 'yyyy')).concat(" " + timeFromResult) ?? ""
 | 
				
			||||||
 | 
							return new Date(dateFromResult)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						public parseGuildIdAndEventIdFromWholeMessage(message: string) {
 | 
				
			||||||
 | 
							const idmatch = RegExp(/(?:http|https):\/\/discord\.com\/events\/(\d*)\/(\d*)/)
 | 
				
			||||||
 | 
							const matches = message.match(idmatch)
 | 
				
			||||||
 | 
							if (matches && matches.length == 3)
 | 
				
			||||||
 | 
								return { guildId: matches[1], eventId: matches[2] }
 | 
				
			||||||
 | 
							throw Error(`Could not find eventId in Vote Message`)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public async prepareAndSendVoteMessage(inputInfo: prepareVoteMessageInput, guildId: string, requestId: string) {
 | 
				
			||||||
 | 
							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++) {
 | 
				
			||||||
 | 
								message = message.concat(ValidVoteEmotes[i]).concat(": ").concat(movies[i]).concat("\n")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							message = message.concat(NONE_OF_THAT).concat(": Wenn dir nichts davon gefällt.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return message
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// TODO: Refactor into separate message controller
 | 
				
			||||||
 | 
						public async sendVoteMessage(messageText: string, movieCount: number, announcementChannel: TextChannel) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const options: MessageCreateOptions = {
 | 
				
			||||||
 | 
								allowedMentions: { parse: ["roles"] },
 | 
				
			||||||
 | 
								content: messageText,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const sentMessage: Message<true> = await (await announcementChannel.fetch()).send(options)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for (let i = 0; i < movieCount; i++) {
 | 
				
			||||||
 | 
								sentMessage.react(ValidVoteEmotes[i])
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							sentMessage.react(NONE_OF_THAT)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return sentMessage
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public async closePoll(guild: Guild, requestId: string) {
 | 
				
			||||||
 | 
							const guildId = guild.id
 | 
				
			||||||
 | 
							logger.info("stopping poll", { guildId, requestId })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const announcementChannel: Maybe<TextChannel> = this.client.getAnnouncementChannelForGuild(guildId)
 | 
				
			||||||
 | 
							if (!announcementChannel) {
 | 
				
			||||||
 | 
								logger.error("Could not find the textchannel. Unable to close poll.", { guildId, requestId })
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const messages: Message<true>[] = (await announcementChannel.messages.fetch()) //todo: fetch only pinned messages
 | 
				
			||||||
 | 
								.map((value) => value)
 | 
				
			||||||
 | 
								.filter(message => !isVoteEndedMessage(message) && isVoteMessage(message))
 | 
				
			||||||
 | 
								.sort((a, b) => b.createdTimestamp - a.createdTimestamp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (!messages || messages.length <= 0) {
 | 
				
			||||||
 | 
								logger.info("Could not find any vote messages. Cancelling pollClose", { guildId, requestId })
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const lastMessage: Message<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(`Last message: ${JSON.stringify(lastMessage, null, 2)}`, { guildId, requestId })
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							else {
 | 
				
			||||||
 | 
								const votes = (await this.getVotesByEmote(lastMessage, guildId, requestId))
 | 
				
			||||||
 | 
									.sort((a, b) => b.count - a.count)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								logger.debug(`votes: ${JSON.stringify(votes, null, 2)}`, { guildId, requestId })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								logger.info("Deleting vote message")
 | 
				
			||||||
 | 
								lastMessage.unpin()
 | 
				
			||||||
 | 
								await lastMessage.delete()
 | 
				
			||||||
 | 
								const event = await this.getOpenPollEvent(guild, guild.id, requestId)
 | 
				
			||||||
 | 
								if (event && votes?.length > 0) {
 | 
				
			||||||
 | 
									this.updateOpenPollEventWithVoteResults(event, votes, guild, guildId, requestId)
 | 
				
			||||||
 | 
									this.sendVoteClosedMessage(event, votes[0].movie, guildId, requestId)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * gets votes for the movies without the NONE_OF_THAT votes
 | 
				
			||||||
 | 
						*/
 | 
				
			||||||
 | 
						public async getVotesByEmote(message: VoteMessage, guildId: string, requestId: string): Promise<Vote[]> {
 | 
				
			||||||
 | 
							const votes: Vote[] = []
 | 
				
			||||||
 | 
							logger.debug(`Number of items in emotes: ${Object.values(ValidVoteEmotes).length}`, { guildId, requestId })
 | 
				
			||||||
 | 
							for (let i = 0; i < Object.keys(ValidVoteEmotes).length / 2; i++) {
 | 
				
			||||||
 | 
								const emote = ValidVoteEmotes[i]
 | 
				
			||||||
 | 
								logger.debug(`Getting reaction for emote ${emote}`, { guildId, requestId })
 | 
				
			||||||
 | 
								const reaction = message.reactions.resolve(emote)
 | 
				
			||||||
 | 
								logger.debug(`Reaction for emote ${emote}: ${JSON.stringify(reaction, null, 2)}`, { guildId, requestId })
 | 
				
			||||||
 | 
								if (reaction) {
 | 
				
			||||||
 | 
									const vote: Vote = { emote: emote, count: reaction.count, movie: this.extractMovieFromMessageByEmote(message, emote) }
 | 
				
			||||||
 | 
									votes.push(vote)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return votes
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						public async getOpenPollEvent(guild: Guild, guildId: string, requestId: string): Promise<Maybe<GuildScheduledEvent>> {
 | 
				
			||||||
 | 
							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 an open vote event.", { guildId, requestId })
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return voteEvents[0]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						public async updateOpenPollEventWithVoteResults(voteEvent: GuildScheduledEvent, votes: Vote[], guild: Guild, guildId: string, requestId: string) {
 | 
				
			||||||
 | 
							logger.info(`Updating event with movie ${votes[0].movie}.`, { guildId, requestId })
 | 
				
			||||||
 | 
							const options: GuildScheduledEventEditOptions<GuildScheduledEventStatus.Scheduled, GuildScheduledEventSetStatusArg<GuildScheduledEventStatus.Scheduled>> = {
 | 
				
			||||||
 | 
								name: votes[0].movie,
 | 
				
			||||||
 | 
								description: `!wp\nNummer 2: ${votes[1].movie} mit ${votes[1].count - 1} Stimmen\nNummer 3: ${votes[2].movie} mit ${votes[2].count - 1} Stimmen`
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							logger.debug(`Updating event: ${JSON.stringify(voteEvent, null, 2)}`, { guildId, requestId })
 | 
				
			||||||
 | 
							logger.info("Updating event.", { guildId, requestId })
 | 
				
			||||||
 | 
							voteEvent.edit(options)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						public async sendVoteClosedMessage(event: GuildScheduledEvent, movie: string, guildId: string, requestId: string): Promise<Message<boolean>> {
 | 
				
			||||||
 | 
							const date = event.scheduledStartAt ? format(event.scheduledStartAt, "dd.MM.") : "Fehler, Event hatte kein Datum"
 | 
				
			||||||
 | 
							const time = event.scheduledStartAt ? format(event.scheduledStartAt, "HH:mm") : "Fehler, Event hatte keine Uhrzeit"
 | 
				
			||||||
 | 
							const body = `[Abstimmung beendet] für https://discord.com/events/${event.guildId}/${event.id}\n<@&${config.bot.announcement_role}> Wir gucken ${movie} am ${date} um ${time}`
 | 
				
			||||||
 | 
							const options: MessageCreateOptions = {
 | 
				
			||||||
 | 
								content: body,
 | 
				
			||||||
 | 
								allowedMentions: { parse: ["roles"] }
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							const announcementChannel = this.client.getAnnouncementChannelForGuild(guildId)
 | 
				
			||||||
 | 
							logger.info("Sending vote closed message.", { guildId, requestId })
 | 
				
			||||||
 | 
							if (!announcementChannel) {
 | 
				
			||||||
 | 
								const errorMessageText = "Could not find announcement channel. Please fix!"
 | 
				
			||||||
 | 
								logger.error(errorMessageText, { guildId, requestId })
 | 
				
			||||||
 | 
								throw errorMessageText
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return announcementChannel.send(options)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						private extractMovieFromMessageByEmote(voteMessage: VoteMessage, emote: string): string {
 | 
				
			||||||
 | 
							const lines = voteMessage.cleanContent.split("\n")
 | 
				
			||||||
 | 
							const emoteLines = lines.filter(line => line.includes(emote))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (!emoteLines) {
 | 
				
			||||||
 | 
								return ""
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							const movie = emoteLines[0].substring(emoteLines[0].indexOf(emote) + emote.length + 2) // plus colon and space
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return movie
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						public async checkForPollsToClose(guild: Guild): Promise<void> {
 | 
				
			||||||
 | 
							const requestId = newRequestId()
 | 
				
			||||||
 | 
							logger.info(`Automatic check for poll closing.`, { guildId: guild.id, requestId })
 | 
				
			||||||
 | 
							const events = (await guild.scheduledEvents.fetch()).filter(event => event.name.toLocaleLowerCase().includes("voting offen")).map(event => event)
 | 
				
			||||||
 | 
							if (events.length > 1) {
 | 
				
			||||||
 | 
								logger.error("Handling more than one Event is not implemented yet. Found more than one poll to close")
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							} else if (events.length == 0) {
 | 
				
			||||||
 | 
								logger.info("Could not find any events. Cancelling", { guildId: guild.id, requestId })
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const updatedEvent = events[0]             //add two hours because of different timezones in discord api and Date.now()
 | 
				
			||||||
 | 
							if (!updatedEvent.scheduledStartTimestamp) {
 | 
				
			||||||
 | 
								logger.error("Event does not have a scheduled start time. Cancelling", { guildId: guild.id, requestId })
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const createDate: Date = toDate(updatedEvent.createdTimestamp)
 | 
				
			||||||
 | 
							const eventDate: Date = toDate(updatedEvent.scheduledStartTimestamp)
 | 
				
			||||||
 | 
							const difference: number = differenceInDays(createDate, eventDate)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (difference <= 2) {
 | 
				
			||||||
 | 
								logger.info("Less than two days between event create and event start. Not closing poll.", { guildId: guild.id, requestId })
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const closePollDate: Date = addDays(eventDate, -2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (isAfter(Date.now(), closePollDate)) {
 | 
				
			||||||
 | 
								logger.info("Less than two days until event. Closing poll", { guildId: guild.id, requestId })
 | 
				
			||||||
 | 
								this.closePoll(guild, requestId)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								logger.info(`ScheduledStart: ${closePollDate}. Now: ${toDate(Date.now())}`, { guildId: guild.id, requestId })
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -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);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +1,12 @@
 | 
				
			|||||||
import { createLogger, format, transports } from "winston"
 | 
					import { createLogger, format, transports } from "winston"
 | 
				
			||||||
import { config } from "./configuration"
 | 
					import { config } from "./configuration"
 | 
				
			||||||
 | 
					import { v4 } from "uuid"
 | 
				
			||||||
 | 
					export function newRequestId() { return v4() }
 | 
				
			||||||
 | 
					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(
 | 
				
			||||||
@@ -13,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,13 +2,15 @@ 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";
 | 
				
			||||||
import { logger } from "../logger";
 | 
					import { logger } from "../logger";
 | 
				
			||||||
import { CommandType } from "../types/commandTypes";
 | 
					import { CommandType } from "../types/commandTypes";
 | 
				
			||||||
import { checkForPollsToClose } from "../commands/closepoll";
 | 
					import { isInitialAnnouncement } from "../helper/messageIdentifiers";
 | 
				
			||||||
 | 
					import VoteController from "../helper/vote.controller";
 | 
				
			||||||
 | 
					import { yavinJellyfinHandler } from "../..";
 | 
				
			||||||
 | 
					import RoleController from "../helper/role.controller";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -16,13 +18,15 @@ export class ExtendedClient extends Client {
 | 
				
			|||||||
	private eventFilePath = `${__dirname}/../events`
 | 
						private eventFilePath = `${__dirname}/../events`
 | 
				
			||||||
	private commandFilePath = `${__dirname}/../commands`
 | 
						private commandFilePath = `${__dirname}/../commands`
 | 
				
			||||||
	private jellyfin: JellyfinHandler
 | 
						private jellyfin: JellyfinHandler
 | 
				
			||||||
 | 
						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
 | 
				
			||||||
	private pollCloseBackgroundTasks: Collection<string, ScheduledTask> = new Collection()
 | 
						private pollCloseBackgroundTasks: Collection<string, ScheduledTask> = new Collection()
 | 
				
			||||||
	public constructor(jf: JellyfinHandler) {
 | 
						public constructor(jf: JellyfinHandler) {
 | 
				
			||||||
		const intents: IntentsBitField = new IntentsBitField()
 | 
							const intents: IntentsBitField = new IntentsBitField()
 | 
				
			||||||
		intents.add(IntentsBitField.Flags.GuildMembers, IntentsBitField.Flags.MessageContent, IntentsBitField.Flags.Guilds, IntentsBitField.Flags.DirectMessages, IntentsBitField.Flags.GuildScheduledEvents, IntentsBitField.Flags.GuildVoiceStates)
 | 
							intents.add(IntentsBitField.Flags.GuildMembers, IntentsBitField.Flags.MessageContent, IntentsBitField.Flags.Guilds, IntentsBitField.Flags.DirectMessages, IntentsBitField.Flags.GuildScheduledEvents, IntentsBitField.Flags.GuildMessageReactions, IntentsBitField.Flags.GuildVoiceStates)
 | 
				
			||||||
		const options: ClientOptions = { intents }
 | 
							const options: ClientOptions = { intents }
 | 
				
			||||||
		super(options)
 | 
							super(options)
 | 
				
			||||||
		this.jellyfin = jf
 | 
							this.jellyfin = jf
 | 
				
			||||||
@@ -74,6 +78,7 @@ export class ExtendedClient extends Client {
 | 
				
			|||||||
				this.registerCommands(slashCommands, guilds)
 | 
									this.registerCommands(slashCommands, guilds)
 | 
				
			||||||
				this.cacheUsers(guilds)
 | 
									this.cacheUsers(guilds)
 | 
				
			||||||
				await this.cacheAnnouncementServer(guilds)
 | 
									await this.cacheAnnouncementServer(guilds)
 | 
				
			||||||
 | 
									this.fetchAnnouncementChannelMessage(this.announcementChannels)
 | 
				
			||||||
				this.startAnnouncementRoleBackgroundTask(guilds)
 | 
									this.startAnnouncementRoleBackgroundTask(guilds)
 | 
				
			||||||
				this.startPollCloseBackgroundTasks()
 | 
									this.startPollCloseBackgroundTasks()
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
@@ -81,6 +86,21 @@ export class ExtendedClient extends Client {
 | 
				
			|||||||
			logger.info(`Error refreshing slash commands: ${error}`)
 | 
								logger.info(`Error refreshing slash commands: ${error}`)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
							* Fetches all messages from the provided channel collection.
 | 
				
			||||||
 | 
							* This is necessary for announcementChannels, because 'old' messages don't receive
 | 
				
			||||||
 | 
							* messageReactionAdd Events, only messages that were sent while the bot is online are tracked
 | 
				
			||||||
 | 
							* automatically.
 | 
				
			||||||
 | 
							* To prevent the need for a dedicated 'Collector' implementation which would listen on specific
 | 
				
			||||||
 | 
							* it's easiest to just fetch all messages from the backlog, which automatically makes the bot track them
 | 
				
			||||||
 | 
							* again.
 | 
				
			||||||
 | 
							* @param {Collection<string, TextChannel>} channels - All channels which should be fecthed for reactionTracking	
 | 
				
			||||||
 | 
							*/
 | 
				
			||||||
 | 
						private async fetchAnnouncementChannelMessage(channels: Collection<string, TextChannel>): Promise<void> {
 | 
				
			||||||
 | 
							channels.each(async ch => {
 | 
				
			||||||
 | 
								ch.messages.fetch()
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	private async cacheAnnouncementServer(guilds: Collection<Snowflake, Guild>) {
 | 
						private async cacheAnnouncementServer(guilds: Collection<Snowflake, Guild>) {
 | 
				
			||||||
		for (const guild of guilds.values()) {
 | 
							for (const guild of guilds.values()) {
 | 
				
			||||||
			const channels: TextChannel[] = <TextChannel[]>(await guild.channels.fetch())
 | 
								const channels: TextChannel[] = <TextChannel[]>(await guild.channels.fetch())
 | 
				
			||||||
@@ -136,7 +156,7 @@ export class ExtendedClient extends Client {
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
			this.announcementRoleHandlerTask.set(guild.id, schedule("*/10 * * * * *", async () => {
 | 
								this.announcementRoleHandlerTask.set(guild.id, schedule("*/10 * * * * *", async () => {
 | 
				
			||||||
				const requestId = uuid()
 | 
									const requestId = uuid()
 | 
				
			||||||
				const messages = (await textChannel.messages.fetchPinned()).filter(message => message.cleanContent.includes("[initial]"))
 | 
									const messages = (await textChannel.messages.fetchPinned()).filter(message => isInitialAnnouncement(message))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				if (messages.size > 1) {
 | 
									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 })
 | 
										logger.error("More than one pinned announcement Messages found. Unable to know which one people react to. Please fix!", { guildId: guild.id, requestId })
 | 
				
			||||||
@@ -153,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 })
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
@@ -175,7 +195,7 @@ export class ExtendedClient extends Client {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	private async startPollCloseBackgroundTasks() {
 | 
						private async startPollCloseBackgroundTasks() {
 | 
				
			||||||
		for (const guild of this.guilds.cache) {
 | 
							for (const guild of this.guilds.cache) {
 | 
				
			||||||
			this.pollCloseBackgroundTasks.set(guild[1].id, schedule("0 * * * * *", () => checkForPollsToClose(guild[1])))
 | 
								this.pollCloseBackgroundTasks.set(guild[1].id, schedule("0 * * * * *", () => this.voteController.checkForPollsToClose(guild[1])))
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										81
									
								
								tests/discord/noneofthat.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								tests/discord/noneofthat.test.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,81 @@
 | 
				
			|||||||
 | 
					import { Guild, GuildScheduledEvent, Message } from "discord.js"
 | 
				
			||||||
 | 
					import VoteController from "../../server/helper/vote.controller"
 | 
				
			||||||
 | 
					import { JellyfinHandler } from "../../server/jellyfin/handler"
 | 
				
			||||||
 | 
					import { ExtendedClient } from "../../server/structures/client"
 | 
				
			||||||
 | 
					import { Emoji, NONE_OF_THAT } from "../../server/constants"
 | 
				
			||||||
 | 
					import { isVoteMessage } from "../../server/helper/messageIdentifiers"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('vote controller - none_of_that functions', () => {
 | 
				
			||||||
 | 
						const testEventId = '1234321'
 | 
				
			||||||
 | 
						const testEventDate = new Date('2023-01-01')
 | 
				
			||||||
 | 
						const testGuildId = "888999888"
 | 
				
			||||||
 | 
						const testMovies = [
 | 
				
			||||||
 | 
							'Movie1',
 | 
				
			||||||
 | 
							'Movie2',
 | 
				
			||||||
 | 
							'Movie3',
 | 
				
			||||||
 | 
							'Movie4',
 | 
				
			||||||
 | 
							'Movie5',
 | 
				
			||||||
 | 
						]
 | 
				
			||||||
 | 
						const votesList = [
 | 
				
			||||||
 | 
							{ emote: Emoji.one, count: 1, movie: testMovies[0] },
 | 
				
			||||||
 | 
							{ emote: Emoji.two, count: 2, movie: testMovies[1] },
 | 
				
			||||||
 | 
							{ emote: Emoji.three, count: 3, movie: testMovies[2] },
 | 
				
			||||||
 | 
							{ emote: Emoji.four, count: 1, movie: testMovies[3] },
 | 
				
			||||||
 | 
							{ emote: Emoji.five, count: 1, movie: testMovies[4] },
 | 
				
			||||||
 | 
							{ emote: NONE_OF_THAT, count: 2, movie: NONE_OF_THAT },
 | 
				
			||||||
 | 
						]
 | 
				
			||||||
 | 
						const mockClient: ExtendedClient = <ExtendedClient><unknown>{
 | 
				
			||||||
 | 
							user: {
 | 
				
			||||||
 | 
								id: 'mockId'
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						const mockEvent: GuildScheduledEvent = <GuildScheduledEvent><unknown>{
 | 
				
			||||||
 | 
							scheduledStartAt: testEventDate,
 | 
				
			||||||
 | 
							id: testEventId,
 | 
				
			||||||
 | 
							guild: testGuildId
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						const mockJellyfinHandler: JellyfinHandler = <JellyfinHandler><unknown>{
 | 
				
			||||||
 | 
							getRandomMovieNames: jest.fn().mockReturnValue(["movie1"])
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						const votes = new VoteController(mockClient, mockJellyfinHandler)
 | 
				
			||||||
 | 
						const mockMessageContent = votes.createVoteMessageText(mockEvent, testMovies, testGuildId, "requestId")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						test('sendVoteClosedMessage', async () => {
 | 
				
			||||||
 | 
							mockClient.getAnnouncementChannelForGuild = jest.fn().mockReturnValue({
 | 
				
			||||||
 | 
								send: jest.fn().mockImplementation((options: any) => {
 | 
				
			||||||
 | 
									return new Promise((resolve) => {
 | 
				
			||||||
 | 
										resolve(options)
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							const scheduledEvent: GuildScheduledEvent = <GuildScheduledEvent>{
 | 
				
			||||||
 | 
								scheduledStartAt: testEventDate,
 | 
				
			||||||
 | 
								guildId: testGuildId,
 | 
				
			||||||
 | 
								id: testEventId
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const res = await votes.sendVoteClosedMessage(scheduledEvent, 'MovieNew', 'guild', 'request')
 | 
				
			||||||
 | 
							expect(res).toEqual({
 | 
				
			||||||
 | 
								allowedMentions: {
 | 
				
			||||||
 | 
									parse: ["roles"]
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								content: `[Abstimmung beendet] für https://discord.com/events/${testGuildId}/${testEventId}\n<@&WATCHPARTY_ANNOUNCEMENT_ROLE> Wir gucken MovieNew am 01.01. um 01:00`
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						test('getVotesByEmote', async () => {
 | 
				
			||||||
 | 
							const mockMessage: Message = <Message><unknown>{
 | 
				
			||||||
 | 
								cleanContent: mockMessageContent,
 | 
				
			||||||
 | 
								reactions: {
 | 
				
			||||||
 | 
									resolve: jest.fn().mockImplementation((input: any) => {
 | 
				
			||||||
 | 
										return votesList.find(e => e.emote === input)
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if (isVoteMessage(mockMessage)) {
 | 
				
			||||||
 | 
								const result = await votes.getVotesByEmote(mockMessage, 'guildId', 'requestId')
 | 
				
			||||||
 | 
								expect(result.length).toEqual(5)
 | 
				
			||||||
 | 
								expect(result).toEqual(votesList.filter(x => x.movie != NONE_OF_THAT))
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
							
								
								
									
										192
									
								
								tests/discord/votes.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										192
									
								
								tests/discord/votes.test.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,192 @@
 | 
				
			|||||||
 | 
					import { Emoji, NONE_OF_THAT } from "../../server/constants"
 | 
				
			||||||
 | 
					import VoteController, { VoteMessageInfo } from "../../server/helper/vote.controller"
 | 
				
			||||||
 | 
					import { JellyfinHandler } from "../../server/jellyfin/handler"
 | 
				
			||||||
 | 
					import { ExtendedClient } from "../../server/structures/client"
 | 
				
			||||||
 | 
					import { VoteMessage } from "../../server/helper/messageIdentifiers"
 | 
				
			||||||
 | 
					import { GuildScheduledEvent, MessageReaction } from "discord.js"
 | 
				
			||||||
 | 
					test('parse votes from vote message', async () => {
 | 
				
			||||||
 | 
						const testMovies = [
 | 
				
			||||||
 | 
							'Movie1',
 | 
				
			||||||
 | 
							'Movie2',
 | 
				
			||||||
 | 
							'Movie3',
 | 
				
			||||||
 | 
							'Movie4',
 | 
				
			||||||
 | 
							'Movie5',
 | 
				
			||||||
 | 
						]
 | 
				
			||||||
 | 
						const testEventId = '1234321'
 | 
				
			||||||
 | 
						const testEventDate = new Date('2023-01-01')
 | 
				
			||||||
 | 
						const testGuildId = "888999888"
 | 
				
			||||||
 | 
						const voteController: VoteController = new VoteController(<ExtendedClient>{}, <JellyfinHandler>{})
 | 
				
			||||||
 | 
						const mockEvent: GuildScheduledEvent = <GuildScheduledEvent><unknown>{
 | 
				
			||||||
 | 
							scheduledStartAt: testEventDate,
 | 
				
			||||||
 | 
							id: testEventId,
 | 
				
			||||||
 | 
							guild: testGuildId
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						const testMessage = voteController.createVoteMessageText(mockEvent, testMovies, testGuildId, "requestId")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const expectedResult: VoteMessageInfo = {
 | 
				
			||||||
 | 
							event: mockEvent,
 | 
				
			||||||
 | 
							votes: [
 | 
				
			||||||
 | 
								{ emote: Emoji.one, count: 1, movie: testMovies[0] },
 | 
				
			||||||
 | 
								{ emote: Emoji.two, count: 2, movie: testMovies[1] },
 | 
				
			||||||
 | 
								{ emote: Emoji.three, count: 3, movie: testMovies[2] },
 | 
				
			||||||
 | 
								{ emote: Emoji.four, count: 1, movie: testMovies[3] },
 | 
				
			||||||
 | 
								{ emote: Emoji.five, count: 1, movie: testMovies[4] },
 | 
				
			||||||
 | 
								{ emote: NONE_OF_THAT, count: 1, movie: NONE_OF_THAT },
 | 
				
			||||||
 | 
							]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const message: VoteMessage = <VoteMessage><unknown>{
 | 
				
			||||||
 | 
							cleanContent: testMessage,
 | 
				
			||||||
 | 
							guild: {
 | 
				
			||||||
 | 
								id: testGuildId,
 | 
				
			||||||
 | 
								scheduledEvents: {
 | 
				
			||||||
 | 
									fetch: jest.fn().mockImplementation((input: any) => {
 | 
				
			||||||
 | 
										if (input === testEventId)
 | 
				
			||||||
 | 
											return {
 | 
				
			||||||
 | 
												id: testEventId,
 | 
				
			||||||
 | 
												guild: testGuildId,
 | 
				
			||||||
 | 
												scheduledStartAt: testEventDate
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							reactions: {
 | 
				
			||||||
 | 
								cache: {
 | 
				
			||||||
 | 
									get: jest.fn().mockImplementation((input: any) => {
 | 
				
			||||||
 | 
										// Abusing duck typing
 | 
				
			||||||
 | 
										// Message Reaction has a method `count` and the expected votes
 | 
				
			||||||
 | 
										// have a field `count`
 | 
				
			||||||
 | 
										// this will evaluate to the same 'result'
 | 
				
			||||||
 | 
										return expectedResult.votes.find(e => e.emote === input)
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const result = await voteController.parseVoteInfoFromVoteMessage(message, 'requestId')
 | 
				
			||||||
 | 
						console.log(JSON.stringify(result))
 | 
				
			||||||
 | 
						expect(Array.isArray(result)).toBe(false)
 | 
				
			||||||
 | 
						expect(result.event.id).toEqual(testEventId)
 | 
				
			||||||
 | 
						expect(result.event.scheduledStartAt).toEqual(testEventDate)
 | 
				
			||||||
 | 
						expect(result.votes.length).toEqual(expectedResult.votes.length)
 | 
				
			||||||
 | 
						expect(result).toEqual(expectedResult)
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('parse votes from vote message', () => {
 | 
				
			||||||
 | 
						const testMovies = [
 | 
				
			||||||
 | 
							'Movie1',
 | 
				
			||||||
 | 
							'Movie2',
 | 
				
			||||||
 | 
							'Movie3',
 | 
				
			||||||
 | 
							'Movie4',
 | 
				
			||||||
 | 
							'Movie5',
 | 
				
			||||||
 | 
						]
 | 
				
			||||||
 | 
						const testEventId = '1234321'
 | 
				
			||||||
 | 
						const testEventDate = new Date('2023-01-01')
 | 
				
			||||||
 | 
						const testGuildId = "888999888"
 | 
				
			||||||
 | 
						const voteController: VoteController = new VoteController(<ExtendedClient>{}, <JellyfinHandler>{})
 | 
				
			||||||
 | 
						const mockEvent: GuildScheduledEvent = <GuildScheduledEvent><unknown>{
 | 
				
			||||||
 | 
							scheduledStartAt: testEventDate,
 | 
				
			||||||
 | 
							id: testEventId,
 | 
				
			||||||
 | 
							guild: testGuildId
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						const testMessage = voteController.createVoteMessageText(mockEvent, testMovies, testGuildId, "requestId")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const result = voteController.parseGuildIdAndEventIdFromWholeMessage(testMessage)
 | 
				
			||||||
 | 
						expect(result).toEqual({ guildId: testGuildId, eventId: testEventId })
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test.skip('handles complete none_of_that vote', () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const mockJellyfinHandler: JellyfinHandler = <JellyfinHandler><unknown>{
 | 
				
			||||||
 | 
							getRandomMovieNames: jest.fn().mockReturnValue(["movie1"])
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const testMovies = [
 | 
				
			||||||
 | 
							'Movie1',
 | 
				
			||||||
 | 
							'Movie2',
 | 
				
			||||||
 | 
							'Movie3',
 | 
				
			||||||
 | 
							'Movie4',
 | 
				
			||||||
 | 
							'Movie5',
 | 
				
			||||||
 | 
						]
 | 
				
			||||||
 | 
						const testEventId = '1234321'
 | 
				
			||||||
 | 
						const testEventDate = new Date('2023-01-01')
 | 
				
			||||||
 | 
						const testGuildId = "888999888"
 | 
				
			||||||
 | 
						const mockClient: ExtendedClient = <ExtendedClient><unknown>{
 | 
				
			||||||
 | 
							user: {
 | 
				
			||||||
 | 
								id: 'mockId'
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						const voteController = new VoteController(mockClient, mockJellyfinHandler)
 | 
				
			||||||
 | 
						const mockEvent: GuildScheduledEvent = <GuildScheduledEvent><unknown>{
 | 
				
			||||||
 | 
							scheduledStartAt: testEventDate,
 | 
				
			||||||
 | 
							id: testEventId,
 | 
				
			||||||
 | 
							guild: testGuildId
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						const mockMessageContent = voteController.createVoteMessageText(mockEvent, testMovies, testGuildId, "requestId")
 | 
				
			||||||
 | 
						const reactedUponMessage: VoteMessage = <VoteMessage><unknown>{
 | 
				
			||||||
 | 
							cleanContent: mockMessageContent,
 | 
				
			||||||
 | 
							guild: {
 | 
				
			||||||
 | 
								id: 'id',
 | 
				
			||||||
 | 
								roles: {
 | 
				
			||||||
 | 
									resolve: jest.fn().mockReturnValue({
 | 
				
			||||||
 | 
										members: [{}, {}, {}, {}, {}]//content does not matter
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								scheduledEvents: {
 | 
				
			||||||
 | 
									fetch: jest.fn().mockReturnValue([
 | 
				
			||||||
 | 
										{
 | 
				
			||||||
 | 
											name: 'voting offen'
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									])
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							unpin: jest.fn().mockImplementation(() => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
							delete: jest.fn().mockImplementation(() => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
							reactions: {
 | 
				
			||||||
 | 
								resolve: jest.fn().mockImplementation((input: any) => {
 | 
				
			||||||
 | 
									console.log(JSON.stringify(input))
 | 
				
			||||||
 | 
								}),
 | 
				
			||||||
 | 
								cache: {
 | 
				
			||||||
 | 
									get: jest.fn().mockReturnValue({
 | 
				
			||||||
 | 
										users: {
 | 
				
			||||||
 | 
											cache: [
 | 
				
			||||||
 | 
												{
 | 
				
			||||||
 | 
													id: "mockId"//to filter out
 | 
				
			||||||
 | 
												},
 | 
				
			||||||
 | 
												{
 | 
				
			||||||
 | 
													id: "userId1"
 | 
				
			||||||
 | 
												},
 | 
				
			||||||
 | 
												{
 | 
				
			||||||
 | 
													id: "userId2"
 | 
				
			||||||
 | 
												},
 | 
				
			||||||
 | 
												{
 | 
				
			||||||
 | 
													id: "userId3"
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
											]
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						const messageReaction: MessageReaction = <MessageReaction><unknown>{
 | 
				
			||||||
 | 
							message: reactedUponMessage
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						mockClient.getAnnouncementChannelForGuild = jest.fn().mockReturnValue({
 | 
				
			||||||
 | 
							messages: {
 | 
				
			||||||
 | 
								fetch: jest.fn().mockReturnValue([
 | 
				
			||||||
 | 
									reactedUponMessage
 | 
				
			||||||
 | 
								])
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const res = voteController.handleNoneOfThatVote(messageReaction, reactedUponMessage, 'requestId', 'guildId')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
@@ -1,4 +1,3 @@
 | 
				
			|||||||
import { GuildScheduledEvent } from "discord.js"
 | 
					 | 
				
			||||||
import { createDateStringFromEvent } from "../../server/helper/dateHelper"
 | 
					import { createDateStringFromEvent } from "../../server/helper/dateHelper"
 | 
				
			||||||
import MockDate from 'mockdate'
 | 
					import MockDate from 'mockdate'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -6,11 +5,11 @@ beforeAll(() => {
 | 
				
			|||||||
	MockDate.set('01-01-2023')
 | 
						MockDate.set('01-01-2023')
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getTestDate(date: string): GuildScheduledEvent {
 | 
					function getTestDate(date: string): Date {
 | 
				
			||||||
  return <GuildScheduledEvent>{ scheduledStartAt: new Date(date) }
 | 
						return new Date(date)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
test('createDateStringFromEvent - correct formatting', () => {
 | 
					test('createDateStringFromEvent - correct formatting', () => {
 | 
				
			||||||
	expect(createDateStringFromEvent(getTestDate('01-01-2023 12:30'), "")).toEqual('heute um 12:30')
 | 
						expect(createDateStringFromEvent(getTestDate('01-01-2023 12:30'), "")).toEqual('heute um 12:30')
 | 
				
			||||||
  expect(createDateStringFromEvent(getTestDate('01-02-2023 12:30'), "")).toEqual('am Montag 02.01 um 12:30')
 | 
						expect(createDateStringFromEvent(getTestDate('01-02-2023 12:30'), "")).toEqual('am Montag 02.01. um 12:30')
 | 
				
			||||||
  expect(createDateStringFromEvent(getTestDate('01-03-2023 12:30'), "")).toEqual('am Dienstag 03.01 um 12:30')
 | 
						expect(createDateStringFromEvent(getTestDate('01-03-2023 12:30'), "")).toEqual('am Dienstag 03.01. um 12:30')
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,61 +1,44 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
	"extends": "@tsconfig/recommended/tsconfig.json",
 | 
						"extends": "@tsconfig/recommended/tsconfig.json",
 | 
				
			||||||
	"exclude":["node_modules"],
 | 
						"exclude": [
 | 
				
			||||||
 | 
							"node_modules"
 | 
				
			||||||
 | 
						],
 | 
				
			||||||
	"compilerOptions": {
 | 
						"compilerOptions": {
 | 
				
			||||||
		/* Basic Options */
 | 
							/* Basic Options */
 | 
				
			||||||
		"target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
 | 
							"target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
 | 
				
			||||||
		"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
 | 
							"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
 | 
				
			||||||
		"resolveJsonModule": true,
 | 
							"resolveJsonModule": true,
 | 
				
			||||||
    // "lib": [],                             /* Specify library files to be included in the compilation. */
 | 
					 | 
				
			||||||
    // "allowJs": true,                       /* Allow javascript files to be compiled. */
 | 
					 | 
				
			||||||
    // "checkJs": true,                       /* Report errors in .js files. */
 | 
					 | 
				
			||||||
    // "jsx": "preserve",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
 | 
					 | 
				
			||||||
    // "declaration": true,                   /* Generates corresponding '.d.ts' file. */
 | 
					 | 
				
			||||||
    // "declarationMap": true,                /* Generates a sourcemap for each corresponding '.d.ts' file. */
 | 
					 | 
				
			||||||
    // "sourceMap": true,                     /* Generates corresponding '.map' file. */
 | 
					 | 
				
			||||||
    // "outFile": "./",                       /* Concatenate and emit output to single file. */
 | 
					 | 
				
			||||||
		"outDir": "./build" /* Redirect output structure to the directory. */,
 | 
							"outDir": "./build" /* Redirect output structure to the directory. */,
 | 
				
			||||||
		// "rootDir": "./",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
 | 
							// "rootDir": "./",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
 | 
				
			||||||
		// "composite": true,                     /* Enable project compilation */
 | 
							// "composite": true,                     /* Enable project compilation */
 | 
				
			||||||
    // "removeComments": true,                /* Do not emit comments to output. */
 | 
							"removeComments": true,                /* Do not emit comments to output. */
 | 
				
			||||||
    // "noEmit": true,                        /* Do not emit outputs. */
 | 
					 | 
				
			||||||
    // "importHelpers": true,                 /* Import emit helpers from 'tslib'. */
 | 
					 | 
				
			||||||
    // "downlevelIteration": true,            /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
 | 
					 | 
				
			||||||
    // "isolatedModules": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		/* Strict Type-Checking Options */
 | 
							/* Strict Type-Checking Options */
 | 
				
			||||||
		"strict": true /* Enable all strict type-checking options. */,
 | 
							"strict": true /* Enable all strict type-checking options. */,
 | 
				
			||||||
    // "noImplicitAny": true,                 /* Raise error on expressions and declarations with an implied 'any' type. */
 | 
							"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
 | 
				
			||||||
    // "strictNullChecks": true,              /* Enable strict null checks. */
 | 
							"strictNullChecks": true, /* Enable strict null checks. */
 | 
				
			||||||
    // "strictFunctionTypes": true,           /* Enable strict checking of function types. */
 | 
							"strictFunctionTypes": true, /* Enable strict checking of function types. */
 | 
				
			||||||
		// "strictBindCallApply": true,           /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
 | 
							// "strictBindCallApply": true,           /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
 | 
				
			||||||
		// "strictPropertyInitialization": true,  /* Enable strict checking of property initialization in classes. */
 | 
							// "strictPropertyInitialization": true,  /* Enable strict checking of property initialization in classes. */
 | 
				
			||||||
		// "noImplicitThis": true,                /* Raise error on 'this' expressions with an implied 'any' type. */
 | 
							// "noImplicitThis": true,                /* Raise error on 'this' expressions with an implied 'any' type. */
 | 
				
			||||||
    // "alwaysStrict": true,                  /* Parse in strict mode and emit "use strict" for each source file. */
 | 
							"alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
 | 
				
			||||||
 | 
							//"noUncheckedIndexedAccess": true,
 | 
				
			||||||
		/* Additional Checks */
 | 
							/* Additional Checks */
 | 
				
			||||||
		//"noUnusedLocals": true,                /* Report errors on unused locals. */
 | 
							//"noUnusedLocals": true,                /* Report errors on unused locals. */
 | 
				
			||||||
		// "noUnusedParameters": true,            /* Report errors on unused parameters. */
 | 
							// "noUnusedParameters": true,            /* Report errors on unused parameters. */
 | 
				
			||||||
		// "noImplicitReturns": true,             /* Report error when not all code paths in function return a value. */
 | 
							// "noImplicitReturns": true,             /* Report error when not all code paths in function return a value. */
 | 
				
			||||||
		// "noFallthroughCasesInSwitch": true,    /* Report errors for fallthrough cases in switch statement. */
 | 
							// "noFallthroughCasesInSwitch": true,    /* Report errors for fallthrough cases in switch statement. */
 | 
				
			||||||
 | 
					 | 
				
			||||||
		/* Module Resolution Options */
 | 
							/* Module Resolution Options */
 | 
				
			||||||
		"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
 | 
							"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
 | 
				
			||||||
		// "baseUrl": "./",                       /* Base directory to resolve non-absolute module names. */
 | 
							// "baseUrl": "./",                       /* Base directory to resolve non-absolute module names. */
 | 
				
			||||||
		// "paths": {},                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
 | 
							// "paths": {},                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
 | 
				
			||||||
		// "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
 | 
							// "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
 | 
				
			||||||
    // "typeRoots": [],                       /* List of folders to include type definitions from. */
 | 
					 | 
				
			||||||
    // "types": [],                           /* Type declaration files to be included in compilation. */
 | 
					 | 
				
			||||||
		"allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
 | 
							"allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
 | 
				
			||||||
		"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
 | 
							"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
 | 
				
			||||||
		// "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */
 | 
							// "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */
 | 
				
			||||||
 | 
					 | 
				
			||||||
		/* Source Map Options */
 | 
							/* Source Map Options */
 | 
				
			||||||
		// "sourceRoot": "",                      /* Specify the location where debugger should locate TypeScript files instead of source locations. */
 | 
							// "sourceRoot": "",                      /* Specify the location where debugger should locate TypeScript files instead of source locations. */
 | 
				
			||||||
		// "mapRoot": "",                         /* Specify the location where debugger should locate map files instead of generated locations. */
 | 
							// "mapRoot": "",                         /* Specify the location where debugger should locate map files instead of generated locations. */
 | 
				
			||||||
		"inlineSourceMap": true /* Emit a single file with source maps instead of having a separate file. */
 | 
							"inlineSourceMap": true /* Emit a single file with source maps instead of having a separate file. */
 | 
				
			||||||
    // "inlineSources": true,                 /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		/* Experimental Options */
 | 
							/* Experimental Options */
 | 
				
			||||||
		// "experimentalDecorators": true,        /* Enables experimental support for ES7 decorators. */
 | 
							// "experimentalDecorators": true,        /* Enables experimental support for ES7 decorators. */
 | 
				
			||||||
		// "emitDecoratorMetadata": true,         /* Enables experimental support for emitting type metadata for decorators. */
 | 
							// "emitDecoratorMetadata": true,         /* Enables experimental support for emitting type metadata for decorators. */
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user