29 Commits

Author SHA1 Message Date
a60fc2db7e remove unnecessary maybe type union
All checks were successful
Compile the repository / compile (pull_request) Successful in 17s
Run unit tests / test (pull_request) Successful in 15s
2023-11-25 20:35:15 +01:00
a50ac1716f make return value for role non optional, instead throws on error
All checks were successful
Compile the repository / compile (pull_request) Successful in 22s
Run unit tests / test (pull_request) Successful in 19s
2023-11-25 20:05:37 +01:00
ef39c6315d fix interface name and import
All checks were successful
Compile the repository / compile (pull_request) Successful in 19s
Run unit tests / test (pull_request) Successful in 18s
2023-11-25 19:51:02 +01:00
1f372b0aac fix naming for reaction 2023-11-25 19:50:54 +01:00
d1aacbb3d3 fix incorrect parameter passing
All checks were successful
Compile the repository / compile (pull_request) Successful in 16s
Run unit tests / test (pull_request) Successful in 15s
2023-11-22 19:56:20 +01:00
1ae8278fb8 remove unused client
Some checks failed
Compile the repository / compile (pull_request) Failing after 18s
Run unit tests / test (pull_request) Successful in 16s
2023-11-22 19:51:35 +01:00
417b24d408 add function to fetch announcement role id
All checks were successful
Compile the repository / compile (pull_request) Successful in 16s
Run unit tests / test (pull_request) Successful in 14s
intended flow: use id fetch function to get role for current guild, use role id to fetch role from discord role cache,
use role in intended use case
currently the role id is hardcoded, needs to be read from some sort of cache which persists the ids in config file
needs to be differentiated by guild id and role type
could be the target of a user configurable frontend in the future
2023-11-21 23:33:25 +01:00
88061c361c rename announcement role assign function
All checks were successful
Compile the repository / compile (pull_request) Successful in 16s
Run unit tests / test (pull_request) Successful in 15s
2023-11-21 22:42:50 +01:00
f83f54749d move manage announce roles to role controller
All checks were successful
Run unit tests / test (pull_request) Successful in 16s
Compile the repository / compile (pull_request) Successful in 17s
2023-11-21 22:42:00 +01:00
90b0b07080 extend Maybe type to also include void 2023-11-21 22:40:53 +01:00
6d0eaed426 fix incorrect method call
All checks were successful
Compile the repository / compile (pull_request) Successful in 17s
Run unit tests / test (pull_request) Successful in 13s
2023-11-20 00:31:16 +01:00
8f320cee5c implement very basic functionality of role add/rm
Some checks failed
Compile the repository / compile (pull_request) Failing after 16s
Run unit tests / test (pull_request) Successful in 14s
2023-11-20 00:27:23 +01:00
016bb243cc add messageReactionRemove
All checks were successful
Compile the repository / compile (pull_request) Successful in 21s
Run unit tests / test (pull_request) Successful in 1m33s
2023-11-20 00:17:28 +01:00
2c8cd96ac7 call vote controller in messageReactionAdd 2023-11-20 00:17:19 +01:00
ba4aefed8e add a minimal vote controller 2023-11-20 00:17:02 +01:00
8efae12907 add hook 2023-11-20 00:03:45 +01:00
fec0bc31f1 1.1.4
All checks were successful
Build a docker image for node-jellyfin-role-bot / build-docker-image (push) Successful in 56s
2023-11-19 20:25:32 +01:00
1bfcaa95f9 Merge pull request 'feat/40-reroll-on-disinterest' (#54) from feat/40-reroll-on-disinterest into master
Reviewed-on: #54
2023-11-19 20:24:35 +01:00
fb4ab59dc6 rename emotes to validvoteemotes
All checks were successful
Run unit tests / test (pull_request) Successful in 14s
Compile the repository / compile (pull_request) Successful in 16s
2023-11-19 20:22:14 +01:00
6d40930dc1 fix incorrect log regarding update cancellation, fixes return type of function to use Maybe
All checks were successful
Compile the repository / compile (pull_request) Successful in 17s
Run unit tests / test (pull_request) Successful in 14s
2023-11-19 20:17:51 +01:00
4e9fe587b0 rename to getOpenPollEvent
All checks were successful
Compile the repository / compile (pull_request) Successful in 17s
Run unit tests / test (pull_request) Successful in 13s
2023-11-19 20:13:49 +01:00
03b6a30ffa remove unnecessary if
All checks were successful
Compile the repository / compile (pull_request) Successful in 16s
Run unit tests / test (pull_request) Successful in 18s
2023-11-19 20:11:03 +01:00
7d794a8001 refactor voteInfo to include event instead of eventid and startDate
All checks were successful
Run unit tests / test (pull_request) Successful in 16s
Compile the repository / compile (pull_request) Successful in 18s
2023-11-19 20:04:30 +01:00
8df180898e pad logging level to always be 5 characters 2023-11-19 20:04:06 +01:00
976175242b reorder close poll and use message identifier
All checks were successful
Compile the repository / compile (pull_request) Successful in 28s
Run unit tests / test (pull_request) Successful in 16s
2023-11-19 18:56:39 +01:00
68546b0b50 adjust message identifier in test 2023-11-19 18:56:08 +01:00
1348abbd48 make message identifiers actually work properly with LSP 2023-11-19 18:55:51 +01:00
fce9091114 rename message type union to better reflect its intention 2023-11-19 18:24:33 +01:00
081f3c6201 fix incorrect branded type 2023-11-19 18:24:13 +01:00
15 changed files with 239 additions and 148 deletions

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "node-jellyfin-discord-bot",
"version": "1.1.3",
"version": "1.1.4",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "node-jellyfin-discord-bot",
"version": "1.1.3",
"version": "1.1.4",
"license": "MIT",
"dependencies": {
"@discordjs/rest": "^1.7.0",

View File

@ -1,6 +1,6 @@
{
"name": "node-jellyfin-discord-bot",
"version": "1.1.3",
"version": "1.1.4",
"description": "A discord bot to sync jellyfin accounts with discord roles",
"main": "index.js",
"license": "MIT",

View File

@ -82,41 +82,5 @@ Für eine Erklärung wie das alles funktioniert mach einfach /mitgucken für ein
}
export async function manageAnnouncementRoles(guild: Guild, reaction: MessageReaction, requestId: string) {
const guildId = guild.id
logger.info("Managing roles", { guildId, requestId })
const announcementRole: Role | undefined = (await guild.roles.fetch()).find(role => role.id === config.bot.announcement_role)
if (!announcementRole) {
logger.error(`Could not find announcement role! Aborting! Was looking for role with id: ${config.bot.announcement_role}`, { guildId, requestId })
return
}
const usersWhoWantRole: User[] = (await reaction.users.fetch()).filter(user => !user.bot).map(user => user)
const allUsers = (await guild.members.fetch())
const usersWhoHaveRole: GuildMember[] = allUsers
.filter(member => member.roles.cache
.find(role => role.id === config.bot.announcement_role) !== undefined)
.map(member => member)
const usersWhoNeedRoleRevoked: GuildMember[] = usersWhoHaveRole
.filter(userWhoHas => !usersWhoWantRole.map(wanter => wanter.id).includes(userWhoHas.id))
const usersWhoDontHaveRole: GuildMember[] = allUsers
.filter(member => member.roles.cache
.find(role => role.id === config.bot.announcement_role) === undefined)
.map(member => member)
const usersWhoNeedRole: GuildMember[] = usersWhoDontHaveRole
.filter(userWhoNeeds => usersWhoWantRole.map(wanter => wanter.id).includes(userWhoNeeds.id))
logger.debug(`Theses users will get the role removed: ${JSON.stringify(usersWhoNeedRoleRevoked)}`, { guildId, requestId })
logger.debug(`Theses users will get the role added: ${JSON.stringify(usersWhoNeedRole)}`, { guildId, requestId })
usersWhoNeedRoleRevoked.forEach(user => user.roles.remove(announcementRole))
usersWhoNeedRole.forEach(user => user.roles.add(announcementRole))
}

View File

@ -1,6 +1,7 @@
export enum Emotes { "1⃣", "2⃣", "3⃣", "4⃣", "5⃣", "6⃣", "7⃣", "8⃣", "9⃣", "🔟" }
export enum ValidVoteEmotes { "1⃣", "2⃣", "3⃣", "4⃣", "5⃣", "6⃣", "7⃣", "8⃣", "9⃣", "🔟" }
export const NONE_OF_THAT = "❌"
// WIP
export const Emoji = {
"one": "\u0031\uFE0F\u20E3",
"two": "\u0032\uFE0F\u20E3",

View File

@ -1,7 +1,7 @@
import { Message, MessageReaction, User } from "discord.js";
import { logger, newRequestId, noGuildId } from "../logger";
import { Emoji, Emotes, NONE_OF_THAT } from "../constants";
import { Emoji, ValidVoteEmotes, NONE_OF_THAT } from "../constants";
import { client } from "../..";
import { isInitialAnnouncement, isVoteMessage } from "../helper/messageIdentifiers";
@ -22,11 +22,10 @@ export async function execute(messageReaction: MessageReaction, user: User) {
}
logger.info(`Got reaction on message`, { requestId, guildId })
//logger.debug(`reactedUponMessage payload: ${JSON.stringify(reactedUponMessage)}`)
logger.info(`emoji: ${messageReaction.emoji.toString()}`)
if (!Object.values(Emotes).includes(messageReaction.emoji.toString()) && messageReaction.emoji.toString() !== NONE_OF_THAT) {
if (!Object.values(ValidVoteEmotes).includes(messageReaction.emoji.toString()) && messageReaction.emoji.toString() !== NONE_OF_THAT) {
logger.info(`${messageReaction.emoji.toString()} currently not handled`)
return
}
@ -36,13 +35,11 @@ export async function execute(messageReaction: MessageReaction, user: User) {
logger.info(`Reaction is NONE_OF_THAT on a vote message. Handling`, { requestId, guildId })
return client.voteController.handleNoneOfThatVote(messageReaction, reactedUponMessage, requestId, guildId)
}
if (messageReaction.emoji.toString() === Emoji.one) {
// do something
}
}
else if (isInitialAnnouncement(reactedUponMessage)) {
if (messageReaction.emoji.toString() === Emoji.ticket) {
logger.error(`Got a role emoji. Not implemented yet. ${reactedUponMessage.id}`)
logger.error(`Got a role emoji. ${reactedUponMessage.id}`)
return client.roleController.addMediaRoleToUser(user, messageReaction.message.guild, requestId)
}
return
}

View 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)
}
}
}

View File

@ -1,10 +1,10 @@
import { format, isToday } from "date-fns";
import { utcToZonedTime } from "date-fns-tz"
import { GuildScheduledEvent } from "discord.js";
import { logger } from "../logger";
import de from "date-fns/locale/de";
import { Maybe } from "../interfaces";
export function createDateStringFromEvent(eventStartDate:Date, requestId: string, guildId?: string): string {
export function createDateStringFromEvent(eventStartDate: Maybe<Date>, requestId: string, guildId?: string): string {
if (!eventStartDate) {
logger.error("Event has no start. Cannot create dateString.", { guildId, requestId })
return `"habe keinen Startzeitpunkt ermitteln können"`

View File

@ -2,11 +2,11 @@ import { Message } from "discord.js";
// branded types to differentiate objects of identical Type but different contents
export type VoteEndMessage = Message & { readonly __brand: 'vote' }
export type AnnouncementMessage = Message & { readonly __brand: 'announcement' }
export type VoteMessage = Message & { readonly __brand: 'voteend' }
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 DiscordMessage = VoteMessage | VoteEndMessage | AnnouncementMessage
export type KnownDiscordMessage = VoteMessage | VoteEndMessage | AnnouncementMessage
export function isVoteMessage(message: Message): message is VoteMessage {
return message.cleanContent.includes('[Abstimmung]')

View 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))
}
}

View File

@ -1,11 +1,11 @@
import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildScheduledEventSetStatusArg, GuildScheduledEventStatus, Message, MessageCreateOptions, MessageReaction, TextChannel } from "discord.js"
import { Emotes, NONE_OF_THAT } from "../constants"
import { ValidVoteEmotes, NONE_OF_THAT } from "../constants"
import { logger, newRequestId } from "../logger"
import { getMembersWithRoleFromGuild } from "./roleFilter"
import { config } from "../configuration"
import { VoteMessage, isVoteEndedMessage, isVoteMessage } from "./messageIdentifiers"
import { createDateStringFromEvent } from "./dateHelper"
import { Maybe, voteMessageInputInformation as prepareVoteMessageInput } from "../interfaces"
import { Maybe, prepareVoteMessageInput } from "../interfaces"
import format from "date-fns/format"
import toDate from "date-fns/toDate"
import differenceInDays from "date-fns/differenceInDays"
@ -21,8 +21,7 @@ export type Vote = {
}
export type VoteMessageInfo = {
votes: Vote[],
eventId: string,
eventDate: Date
event: GuildScheduledEvent,
}
export default class VoteController {
private client: ExtendedClient
@ -74,11 +73,8 @@ export default class VoteController {
logger.debug(`${vote.movie} : ${vote.count} -> above: ${overOneVote}`)
return overOneVote
}
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)
let movies: string[] = Array()
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 })
@ -86,40 +82,53 @@ export default class VoteController {
logger.info(`Fetching ${newMovieCount} from jellyfin`)
const newMovies: string[] = await this.yavinJellyfinHandler.getRandomMovieNames(newMovieCount, guildId, requestId)
// merge
movies = newMovies.concat(votedOnMovies.map(x => x.movie))
return newMovies.concat(votedOnMovies.map(x => x.movie))
} else {
// get movies from jellyfin to fill the remaining slots
const newMovieCount: number = config.bot.random_movie_count
logger.info(`Fetching ${newMovieCount} from jellyfin`)
movies = await this.yavinJellyfinHandler.getRandomMovieNames(newMovieCount, guildId, requestId)
return await this.yavinJellyfinHandler.getRandomMovieNames(newMovieCount, guildId, requestId)
}
// create new message
}
public async handleReroll(voteMessage: VoteMessage, guildId: string, requestId: string) {
// get the movies currently being voted on, their votes, the eventId and its date
const voteInfo: VoteMessageInfo = await this.parseVoteInfoFromVoteMessage(voteMessage, requestId)
if (!voteInfo.event.scheduledStartAt) {
logger.info("Event does not have a start date, cancelling", { guildId: voteInfo.event.guildId, requestId })
return
}
let movies: string[] = await this.generateRerollMovieList(voteInfo, guildId, requestId)
logger.info(`Creating new poll message with new movies: ${movies}`, { requestId, guildId })
const messageText = this.createVoteMessageText(voteInfo.eventId, voteInfo.eventDate, movies, 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 = await this.sendVoteMessage(messageText, movies.length, announcementChannel)
sentMessage.pin()
logger.info(`Sent and pinned new poll message`, { requestId, guildId })
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 fetchEventStartDateByEventId(guild: Guild, eventId: string, requestId: string): Promise<Maybe<Date>> {
private async fetchEventByEventId(guild: Guild, eventId: string, requestId: string): Promise<Maybe<GuildScheduledEvent>> {
const guildEvent: GuildScheduledEvent = await guild.scheduledEvents.fetch(eventId)
if (!guildEvent) logger.error(`GuildScheduledEvent with id${eventId} could not be found`, { requestId, guildId: guild.id })
if (guildEvent.scheduledStartAt)
return guildEvent.scheduledStartAt
return guildEvent
}
public async parseVoteInfoFromVoteMessage(message: VoteMessage, requestId: string): Promise<VoteMessageInfo> {
@ -129,8 +138,7 @@ export default class VoteController {
if (!message.guild)
throw new Error(`Message ${message.id} not a guild message`)
let eventStartDate: Maybe<Date> = await this.fetchEventStartDateByEventId(message.guild, parsedIds.eventId, requestId)
if (!eventStartDate) eventStartDate = this.parseEventDateFromMessage(message.cleanContent, message.guild.id, requestId)
const event: Maybe<GuildScheduledEvent> = await this.fetchEventByEventId(message.guild, parsedIds.eventId, requestId)
let votes: Vote[] = []
for (const line of lines) {
@ -149,7 +157,7 @@ export default class VoteController {
}
}
}
return <VoteMessageInfo>{ eventId: parsedIds.eventId, eventDate: eventStartDate, votes }
return <VoteMessageInfo>{ event, votes }
}
public parseEventDateFromMessage(message: string, guildId: string, requestId: string): Date {
logger.warn(`Falling back to RegEx parsing to get Event Date`, { guildId, requestId })
@ -168,24 +176,25 @@ export default class VoteController {
}
public async prepareAndSendVoteMessage(inputInfo: prepareVoteMessageInput, guildId: string, requestId: string) {
const messageText = this.createVoteMessageText(inputInfo.event.id, inputInfo.startDate, inputInfo.movies, guildId, requestId)
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(eventId: string, eventStartDate: Date, movies: string[], guildId: string, requestId: string): string {
let message = `[Abstimmung] für https://discord.com/events/${guildId}/${eventId} \n<@&${config.bot.announcement_role}> Es gibt eine neue Abstimmung für die nächste Watchparty ${createDateStringFromEvent(eventStartDate, guildId, requestId)}! Stimme hierunter für den nächsten Film ab!\n`
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(Emotes[i]).concat(": ").concat(movies[i]).concat("\n")
message = message.concat(ValidVoteEmotes[i]).concat(": ").concat(movies[i]).concat("\n")
}
message = message.concat(NONE_OF_THAT).concat(": Wenn dir nichts davon gefällt.")
return message
}
// TODO: Refactor into separate message controller
public async sendVoteMessage(messageText: string, movieCount: number, announcementChannel: TextChannel) {
const options: MessageCreateOptions = {
@ -196,7 +205,7 @@ export default class VoteController {
const sentMessage: Message<true> = await (await announcementChannel.fetch()).send(options)
for (let i = 0; i < movieCount; i++) {
sentMessage.react(Emotes[i])
sentMessage.react(ValidVoteEmotes[i])
}
sentMessage.react(NONE_OF_THAT)
@ -225,33 +234,35 @@ export default class VoteController {
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 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")
await lastMessage.delete()
const event = await this.getOpenEvent(guild, guild.id, requestId)
if (event && votes?.length > 0) {
this.updateOpenPollEventWithVoteResults(event, votes, guild, guildId, requestId)
this.sendVoteClosedMessage(event, votes[0].movie, guildId, requestId)
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)
}
}
lastMessage.unpin()
}
/**
* gets votes for the movies without the NONE_OF_THAT votes
*/
public async getVotesByEmote(message: Message, guildId: string, requestId: string): Promise<Vote[]> {
public async getVotesByEmote(message: VoteMessage, guildId: string, requestId: string): Promise<Vote[]> {
const votes: Vote[] = []
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(`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 })
@ -262,15 +273,15 @@ export default class VoteController {
}
return votes
}
public async getOpenEvent(guild: Guild, guildId: string, requestId: string): Promise<GuildScheduledEvent | null> {
public async getOpenPollEvent(guild: Guild, guildId: string, requestId: string): Promise<Maybe<GuildScheduledEvent>> {
const voteEvents = (await guild.scheduledEvents.fetch())
.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
logger.error("Could not find an open vote event.", { guildId, requestId })
return
}
return voteEvents[0]
}

View File

@ -39,7 +39,7 @@ export interface JellyfinConfig {
collectionUser: string
}
export type PermissionLevel = "VIEWER" | "ADMIN" | "TEMPORARY"
export interface voteMessageInputInformation {
export interface prepareVoteMessageInput {
movies: string[],
startDate: Date,
event: GuildScheduledEvent,

View File

@ -6,7 +6,7 @@ export const noGuildId = 'NoGuildId'
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(

View File

@ -2,7 +2,6 @@ import { ApplicationCommandDataResolvable, Client, ClientOptions, Collection, Gu
import fs from 'fs';
import { ScheduledTask, schedule } from "node-cron";
import { v4 as uuid } from 'uuid';
import { manageAnnouncementRoles } from "../commands/announce";
import { config } from "../configuration";
import { Maybe } from "../interfaces";
import { JellyfinHandler } from "../jellyfin/handler";
@ -11,6 +10,7 @@ import { CommandType } from "../types/commandTypes";
import { isInitialAnnouncement } from "../helper/messageIdentifiers";
import VoteController from "../helper/vote.controller";
import { yavinJellyfinHandler } from "../..";
import RoleController from "../helper/role.controller";
@ -19,6 +19,7 @@ export class ExtendedClient extends Client {
private commandFilePath = `${__dirname}/../commands`
private jellyfin: JellyfinHandler
public voteController: VoteController = new VoteController(this, yavinJellyfinHandler)
public roleController: RoleController = new RoleController()
public commands: Collection<string, CommandType> = new Collection()
private announcementChannels: Collection<string, TextChannel> = new Collection() //guildId to TextChannel
private announcementRoleHandlerTask: Collection<string, ScheduledTask> = new Collection() //one task per guild
@ -172,10 +173,10 @@ export class ExtendedClient extends Client {
}
//logger.debug(`Message: ${JSON.stringify(message, null, 2)}`, { guildId: guild.id, requestId })
const reactions = message.reactions.resolve("🎫")
const ticketReaction = message.reactions.resolve("🎫")
//logger.debug(`reactions: ${JSON.stringify(reactions, null, 2)}`, { guildId: guild.id, requestId })
if (reactions) {
manageAnnouncementRoles(message.guild, reactions, requestId)
if (ticketReaction) {
this.roleController.assignAnnouncementRolesFromReaction(message.guild, ticketReaction, requestId)
} else {
logger.error("Did not get reactions! Aborting!", { guildId: guild.id, requestId })
}

View File

@ -3,6 +3,7 @@ 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'
@ -28,11 +29,16 @@ describe('vote controller - none_of_that functions', () => {
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(testEventId, testEventDate, testMovies, testGuildId, "requestId")
const mockMessageContent = votes.createVoteMessageText(mockEvent, testMovies, testGuildId, "requestId")
test('sendVoteClosedMessage', async () => {
mockClient.getAnnouncementChannelForGuild = jest.fn().mockReturnValue({
@ -56,29 +62,6 @@ describe('vote controller - none_of_that functions', () => {
content: `[Abstimmung beendet] für https://discord.com/events/${testGuildId}/${testEventId}\n<@&WATCHPARTY_ANNOUNCEMENT_ROLE> Wir gucken MovieNew am 01.01. um 01:00`
})
})
// test('checkForPollsToClose', async () => {
//
// const testGuild: Guild = <Guild><unknown>{
// scheduledEvents: {
// fetch: jest.fn().mockImplementation(() => {
// return new Promise(resolve => {
// resolve([
// { name: "Event Name" },
// { name: "Event: VOTING OFFEN", scheduledStartTimestamp: "" },
// { name: "another voting" },
// ]
// )
// })
// })
// }
// }
//
// const result = await votes.checkForPollsToClose(testGuild)
//
//
//
//
// })
test('getVotesByEmote', async () => {
const mockMessage: Message = <Message><unknown>{
@ -89,8 +72,10 @@ describe('vote controller - none_of_that functions', () => {
})
}
}
const result = await votes.getVotesByEmote(mockMessage, 'guildId', 'requestId')
expect(result.length).toEqual(5)
expect(result).toEqual(votesList.filter(x => x.movie != NONE_OF_THAT))
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))
}
})
})

View File

@ -1,9 +1,9 @@
import { Emoji, NONE_OF_THAT } from "../../server/constants"
import VoteController, { Vote, VoteMessageInfo } from "../../server/helper/vote.controller"
import VoteController, { VoteMessageInfo } from "../../server/helper/vote.controller"
import { JellyfinHandler } from "../../server/jellyfin/handler"
import { ExtendedClient } from "../../server/structures/client"
import { VoteMessage } from "../../server/helper/messageIdentifiers"
import { Message, MessageReaction } from "discord.js"
import { GuildScheduledEvent, MessageReaction } from "discord.js"
test('parse votes from vote message', async () => {
const testMovies = [
'Movie1',
@ -16,12 +16,16 @@ test('parse votes from vote message', async () => {
const testEventDate = new Date('2023-01-01')
const testGuildId = "888999888"
const voteController: VoteController = new VoteController(<ExtendedClient>{}, <JellyfinHandler>{})
const testMessage = voteController.createVoteMessageText(testEventId, testEventDate, testMovies, testGuildId, "requestId")
const mockEvent: GuildScheduledEvent = <GuildScheduledEvent><unknown>{
scheduledStartAt: testEventDate,
id: testEventId,
guild: testGuildId
}
const testMessage = voteController.createVoteMessageText(mockEvent, testMovies, testGuildId, "requestId")
const expectedResult: VoteMessageInfo = {
eventId: testEventId,
eventDate: testEventDate,
event: mockEvent,
votes: [
{ emote: Emoji.one, count: 1, movie: testMovies[0] },
{ emote: Emoji.two, count: 2, movie: testMovies[1] },
@ -40,6 +44,8 @@ test('parse votes from vote message', async () => {
fetch: jest.fn().mockImplementation((input: any) => {
if (input === testEventId)
return {
id: testEventId,
guild: testGuildId,
scheduledStartAt: testEventDate
}
})
@ -61,8 +67,8 @@ test('parse votes from vote message', async () => {
const result = await voteController.parseVoteInfoFromVoteMessage(message, 'requestId')
console.log(JSON.stringify(result))
expect(Array.isArray(result)).toBe(false)
expect(result.eventId).toEqual(testEventId)
expect(result.eventDate).toEqual(testEventDate)
expect(result.event.id).toEqual(testEventId)
expect(result.event.scheduledStartAt).toEqual(testEventDate)
expect(result.votes.length).toEqual(expectedResult.votes.length)
expect(result).toEqual(expectedResult)
})
@ -79,7 +85,12 @@ test('parse votes from vote message', () => {
const testEventDate = new Date('2023-01-01')
const testGuildId = "888999888"
const voteController: VoteController = new VoteController(<ExtendedClient>{}, <JellyfinHandler>{})
const testMessage = voteController.createVoteMessageText(testEventId, testEventDate, testMovies, testGuildId, "requestId")
const mockEvent: GuildScheduledEvent = <GuildScheduledEvent><unknown>{
scheduledStartAt: testEventDate,
id: testEventId,
guild: testGuildId
}
const testMessage = voteController.createVoteMessageText(mockEvent, testMovies, testGuildId, "requestId")
const result = voteController.parseGuildIdAndEventIdFromWholeMessage(testMessage)
expect(result).toEqual({ guildId: testGuildId, eventId: testEventId })
@ -108,7 +119,12 @@ test.skip('handles complete none_of_that vote', () => {
}
}
const voteController = new VoteController(mockClient, mockJellyfinHandler)
const mockMessageContent = voteController.createVoteMessageText(testEventId, testEventDate, testMovies, testGuildId, "requestId")
const mockEvent: GuildScheduledEvent = <GuildScheduledEvent><unknown>{
scheduledStartAt: testEventDate,
id: testEventId,
guild: testGuildId
}
const mockMessageContent = voteController.createVoteMessageText(mockEvent, testMovies, testGuildId, "requestId")
const reactedUponMessage: VoteMessage = <VoteMessage><unknown>{
cleanContent: mockMessageContent,
guild: {