2023-07-13 22:47:28 +02:00
import { Guild , GuildScheduledEvent , GuildScheduledEventEditOptions , GuildScheduledEventSetStatusArg , GuildScheduledEventStatus , Message , MessageCreateOptions , MessageReaction , PartialMessage , TextChannel , User } from "discord.js"
import { Emoji , Emotes , NONE_OF_THAT } from "../constants"
2023-07-05 22:55:24 +02:00
import { logger , newRequestId } from "../logger"
2023-06-26 23:47:43 +02:00
import { getMembersWithRoleFromGuild } from "./roleFilter"
import { config } from "../configuration"
2023-07-05 22:55:24 +02:00
import { VoteMessage , isVoteEndedMessage , isVoteMessage } from "./messageIdentifiers"
import { createDateStringFromEvent } from "./dateHelper"
import { Maybe } 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"
2023-07-13 22:47:28 +02:00
import { ExtendedClient } from "../structures/client"
import { JellyfinHandler } from "../jellyfin/handler"
2023-06-26 23:47:43 +02:00
2023-07-05 22:55:24 +02:00
export type Vote = {
emote : string , //todo habs nicht hinbekommen hier Emotes zu nutzen
count : number ,
movie : string
}
2023-07-13 22:47:28 +02:00
export type VoteMessageInfo = {
votes : Vote [ ] ,
eventId : string ,
eventDate : Date
}
2023-06-26 23:47:43 +02:00
export default class VoteController {
2023-07-13 22:47:28 +02:00
private client : ExtendedClient
private yavinJellyfinHandler : JellyfinHandler
public constructor ( _client : ExtendedClient , _yavin : JellyfinHandler ) {
this . client = _client
this . yavinJellyfinHandler = _yavin
}
2023-06-26 23:47:43 +02:00
2023-07-05 22:55:24 +02:00
public async handleNoneOfThatVote ( messageReaction : MessageReaction , user : User , reactedUponMessage : VoteMessage , requestId : string , guildId : string ) {
2023-06-26 23:47:43 +02:00
if ( ! messageReaction . message . guild ) return 'No guild'
2023-06-27 20:22:44 +02:00
logger . debug ( ` ${ reactedUponMessage . id } is vote message ` , { requestId , guildId } )
2023-07-05 22:55:24 +02:00
2023-06-27 20:22:44 +02:00
const watcherRoleMember = await getMembersWithRoleFromGuild ( config . bot . announcement_role , messageReaction . message . guild )
logger . info ( "ROLE MEMBERS " + JSON . stringify ( watcherRoleMember ) , { requestId , guildId } )
2023-07-05 22:55:24 +02:00
2023-06-27 20:22:44 +02:00
const watcherRoleMemberCount = watcherRoleMember . size
logger . info ( ` MEMBER COUNT: ${ watcherRoleMemberCount } ` , { requestId , guildId } )
2023-07-05 22:55:24 +02:00
2023-07-13 22:47:28 +02:00
const noneOfThatReactions = messageReaction . message . reactions . cache . get ( NONE_OF_THAT ) ? . users . cache . filter ( x = > x . id !== this . client . user ? . id ) . size ? ? 0
2023-06-26 23:47:43 +02:00
2023-06-27 20:22:44 +02:00
const memberThreshold = ( watcherRoleMemberCount / 2 )
logger . info ( ` Reroll ${ noneOfThatReactions } > ${ memberThreshold } ? ` , { requestId , guildId } )
if ( noneOfThatReactions > memberThreshold ) {
logger . info ( 'Starting poll reroll' , { requestId , guildId } )
messageReaction . message . edit ( ( messageReaction . message . content ? ? "" ) . concat ( '\nDiese Abstimmung muss wiederholt werden.' ) )
2023-07-05 23:22:01 +02:00
// get movies that _had_ votes
2023-07-13 22:47:28 +02:00
//const oldMovieNames: Vote[] = this.parseVotesFromVoteMessage(messageReaction.message, requestId)
2023-07-05 23:22:01 +02:00
const eventId = this . parseEventIdFromMessage ( messageReaction . message , requestId )
2023-07-13 22:47:28 +02:00
const eventStartDate : Date = this . fetchEventStartDateByEventId ( eventId , requestId ) //TODO
2023-07-05 23:22:01 +02:00
//
// get movies from jellyfin to fill the remaining slots
2023-07-13 22:47:28 +02:00
const newMovieCount = config . bot . random_movie_count //- oldMovieNames.length
const newMovies = await this . yavinJellyfinHandler . getRandomMovieNames ( newMovieCount , guildId , requestId )
2023-07-05 23:22:01 +02:00
// merge
2023-07-13 22:47:28 +02:00
const movies = newMovies
2023-07-05 23:22:01 +02:00
// create new message
await this . closePoll ( messageReaction . message . guild , requestId )
const message = this . createVoteMessageText ( eventId , eventStartDate , movies , guildId , requestId )
2023-07-13 22:47:28 +02:00
const announcementChannel = this . client . getAnnouncementChannelForGuild ( guildId )
2023-07-05 23:22:01 +02:00
if ( ! announcementChannel ) {
logger . error ( ` No announcementChannel found for ${ guildId } , can't post poll ` )
return
}
const sentMessage = await this . sendVoteMessage ( message , movies . length , announcementChannel )
sentMessage . pin ( )
2023-06-26 23:47:43 +02:00
}
2023-06-27 20:22:44 +02:00
logger . info ( ` No reroll ` , { requestId , guildId } )
2023-06-26 23:47:43 +02:00
}
2023-07-17 21:30:02 +02:00
parseEventIdFromMessage ( message : Message < boolean > | PartialMessage , requestId : string ) : string {
throw new Error ( "Method not implemented." )
}
2023-07-05 23:22:01 +02:00
private fetchEventStartDateByEventId ( eventId : string , requestId : string ) : Date {
throw new Error ( "Method not implemented." )
}
2023-07-13 22:47:28 +02:00
public parseVotesFromVoteMessage ( message : VoteMessage , requestId : string ) : VoteMessageInfo {
const lines = message . cleanContent . split ( '\n' )
let eventId = ""
let eventDate : Date = new Date ( )
let votes : Vote [ ] = [ ]
for ( const line of lines ) {
if ( line . includes ( 'https://discord.com/events' ) ) {
const urlMatcher = RegExp ( /(http|https|ftp):\/\/(\S*)/ig )
const result = line . match ( urlMatcher )
if ( ! result ) throw Error ( 'No event url found in Message' )
eventId = result ? . [ 0 ] . split ( '/' ) . at ( - 1 ) ? ? ""
} else if ( ! line . slice ( 0 , 5 ) . includes ( ':' ) ) {
2023-07-17 21:30:02 +02:00
eventDate = this . parseEventDateFromLine ( line )
2023-07-13 22:47:28 +02:00
} else if ( line . slice ( 0 , 5 ) . includes ( ':' ) ) {
const splitLine = line . split ( ":" )
const [ emoji , movie ] = splitLine
const fetchedVoteFromMessage = message . reactions . cache . get ( emoji )
if ( fetchedVoteFromMessage ) {
2023-07-17 21:30:02 +02:00
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 } )
2023-07-13 22:47:28 +02:00
} else {
logger . error ( ` No vote reaction found for movie, assuming 0 ` , requestId )
votes . push ( { movie , emote : emoji , count : 0 } )
}
}
}
return < VoteMessageInfo > { eventId , eventDate , votes }
2023-07-05 23:22:01 +02:00
}
2023-07-17 21:30:02 +02:00
public parseEventDateFromLine ( line : string ) : Date {
const datematcher = RegExp ( /((0[1-9]|[12][0-9]|3[01])\.(0[1-9]|1[012]))(\ um\ )(([012][0-9]:[0-5][0-9]))/i )
const result : RegExpMatchArray | null = line . match ( datematcher )
const timeFromResult = result ? . at ( - 1 )
const dateFromResult = result ? . at ( 1 ) ? . concat ( format ( new Date ( ) , '.yyyy' ) ) . concat ( " " + timeFromResult ) ? ? ""
return new Date ( dateFromResult )
2023-07-05 23:22:01 +02:00
}
2023-07-05 22:55:24 +02:00
2023-07-05 23:22:01 +02:00
public createVoteMessageText ( eventId : string , eventStartDate : Date , movies : string [ ] , guildId : string , requestId : string ) : string {
2023-07-05 22:55:24 +02:00
2023-07-13 22:47:28 +02:00
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 `
2023-07-05 22:55:24 +02:00
for ( let i = 0 ; i < movies . length ; i ++ ) {
message = message . concat ( Emotes [ i ] ) . concat ( ": " ) . concat ( movies [ i ] ) . concat ( "\n" )
}
message = message . concat ( NONE_OF_THAT ) . concat ( ": Wenn dir nichts davon gefällt." )
2023-07-05 23:22:01 +02:00
return message
}
public async sendVoteMessage ( message : string , movieCount : number , announcementChannel : TextChannel ) {
2023-07-05 22:55:24 +02:00
const options : MessageCreateOptions = {
allowedMentions : { parse : [ "roles" ] } ,
content : message ,
}
const sentMessage : Message < true > = await ( await announcementChannel . fetch ( ) ) . send ( options )
2023-07-05 23:22:01 +02:00
for ( let i = 0 ; i < movieCount ; i ++ ) {
2023-07-05 22:55:24 +02:00
sentMessage . react ( Emotes [ 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 } )
2023-07-13 22:47:28 +02:00
const announcementChannel : Maybe < TextChannel > = this . client . getAnnouncementChannelForGuild ( guildId )
2023-07-05 22:55:24 +02:00
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 ]
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 . getEvent ( guild , guild . id , requestId )
if ( event ) {
this . updateEvent ( event , votes , guild , guildId , requestId )
this . sendVoteClosedMessage ( event , votes [ 0 ] . movie , guildId , requestId )
}
lastMessage . unpin ( ) //todo: uncomment when bot has permission to pin/unpin
}
public async 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 = 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 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 ]
}
public async 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 )
}
public async 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" ] }
}
2023-07-13 22:47:28 +02:00
const announcementChannel = this . client . getAnnouncementChannelForGuild ( guildId )
2023-07-05 22:55:24 +02:00
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 )
}
private 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
}
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 } )
}
}
2023-06-26 23:47:43 +02:00
}