feat/40-reroll-on-disinterest #54
@ -6,6 +6,7 @@ import { Maybe } from '../interfaces'
|
||||
import { logger } from '../logger'
|
||||
import { Command } from '../structures/command'
|
||||
import { RunOptions } from '../types/commandTypes'
|
||||
import { isInitialAnnouncement } from '../helper/messageIdentifiers'
|
||||
|
||||
export default new Command({
|
||||
name: 'announce',
|
||||
@ -61,7 +62,7 @@ async function sendInitialAnnouncement(guildId: string, requestId: string): Prom
|
||||
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(message => message.delete())
|
||||
|
||||
|
@ -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 { client } from '../..'
|
||||
import { config } from '../configuration'
|
||||
import { Emotes } from '../events/autoCreateVoteByWPEvent'
|
||||
import { Maybe } from '../interfaces'
|
||||
import { logger } from '../logger'
|
||||
import { Command } from '../structures/command'
|
||||
import { RunOptions } from '../types/commandTypes'
|
||||
@ -25,160 +20,6 @@ export default new Command({
|
||||
logger.info("Got command for closing poll!", { guildId, requestId })
|
||||
|
||||
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 { Command } from '../structures/command'
|
||||
import { RunOptions } from '../types/commandTypes'
|
||||
import { logger } from '../logger'
|
||||
export default new Command({
|
||||
name: 'echo',
|
||||
description: 'Echoes a text',
|
||||
@ -13,7 +14,7 @@ export default new Command({
|
||||
}
|
||||
],
|
||||
run: async (interaction: RunOptions) => {
|
||||
console.log('echo called')
|
||||
logger.info('echo called')
|
||||
interaction.interaction.reply(interaction.toString())
|
||||
}
|
||||
})
|
||||
|
@ -2,15 +2,16 @@ import { v4 as uuid } from 'uuid'
|
||||
import { jellyfinHandler } from "../.."
|
||||
import { Command } from '../structures/command'
|
||||
import { RunOptions } from '../types/commandTypes'
|
||||
import { logger } from '../logger'
|
||||
|
||||
export default new Command({
|
||||
name: 'passwort_reset',
|
||||
description: 'Ich vergebe dir ein neues Passwort und schicke es dir per DM zu. Kostet auch nix! Versprochen! 😉',
|
||||
options: [],
|
||||
run: async (interaction: RunOptions) => {
|
||||
console.log('PasswortReset called')
|
||||
logger.info('PasswortReset called')
|
||||
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())
|
||||
}
|
||||
})
|
||||
|
@ -30,6 +30,8 @@ export interface Config {
|
||||
yavin_jellyfin_url: string
|
||||
yavin_jellyfin_token: string
|
||||
yavin_jellyfin_collection_user: string
|
||||
random_movie_count: number
|
||||
reroll_retains_top_picks: boolean
|
||||
}
|
||||
}
|
||||
export const config: Config = {
|
||||
@ -59,16 +61,18 @@ export const config: Config = {
|
||||
client_id: process.env.CLIENT_ID ?? "",
|
||||
jellfin_token: process.env.JELLYFIN_TOKEN ?? "",
|
||||
jellyfin_url: process.env.JELLYFIN_URL ?? "",
|
||||
workaround_token: process.env.TOKEN ?? "",
|
||||
watcher_role: process.env.WATCHER_ROLE ?? "",
|
||||
jf_admin_role: process.env.ADMIN_ROLE ?? "",
|
||||
announcement_role: process.env.WATCHPARTY_ANNOUNCEMENT_ROLE ?? "",
|
||||
announcement_channel_id: process.env.CHANNEL_ID ?? "",
|
||||
workaround_token: process.env.TOKEN ?? "TOKEN",
|
||||
watcher_role: process.env.WATCHER_ROLE ?? "WATCHER_ROLE",
|
||||
jf_admin_role: process.env.ADMIN_ROLE ?? "ADMIN_ROLE",
|
||||
announcement_role: process.env.WATCHPARTY_ANNOUNCEMENT_ROLE ?? "ANNOUNCE_ROLE",
|
||||
announcement_channel_id: process.env.CHANNEL_ID ?? "ANNOUNCE_CHANNEL",
|
||||
jf_collection_id: process.env.JELLYFIN_COLLECTION_ID ?? "",
|
||||
yavin_collection_id: process.env.YAVIN_COLLECTION_ID ?? "",
|
||||
yavin_jellyfin_url: process.env.YAVIN_JELLYFIN_URL ?? "",
|
||||
yavin_jellyfin_token: process.env.YAVIN_TOKEN ?? "",
|
||||
yavin_jellyfin_collection_user: process.env.YAVIN_COLLECTION_USER ?? "",
|
||||
jf_user: process.env.JELLYFIN_USER ?? ""
|
||||
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
@ -0,0 +1,17 @@
|
||||
|
||||
export enum ValidVoteEmotes { "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟" }
|
||||
export const NONE_OF_THAT = "❌"
|
||||
magnetotail marked this conversation as resolved
|
||||
// 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
|
||||
}
|
||||
|
||||
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)
|
||||
} else {
|
||||
|
@ -1,20 +1,11 @@
|
||||
import { GuildScheduledEvent, Message, MessageCreateOptions, TextChannel } from "discord.js";
|
||||
import { ScheduledTask } from "node-cron";
|
||||
import { GuildScheduledEvent, TextChannel } from "discord.js";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { client, yavinJellyfinHandler } from "../..";
|
||||
import { config } from "../configuration";
|
||||
import { createDateStringFromEvent } from "../helper/dateHelper";
|
||||
import { Maybe } from "../interfaces";
|
||||
import { logger } from "../logger";
|
||||
|
||||
|
||||
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) {
|
||||
const requestId = uuid()
|
||||
|
||||
@ -33,31 +24,21 @@ export async function execute(event: GuildScheduledEvent) {
|
||||
return
|
||||
}
|
||||
logger.debug(`Found channel ${JSON.stringify(announcementChannel, null, 2)}`, { guildId: event.guildId, requestId })
|
||||
|
||||
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
|
||||
}
|
||||
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,
|
||||
magnetotail marked this conversation as resolved
Outdated
magnetotail
commented
Warum erst eine message erstellen lassen die dann vom gleichen controller in der nächsten Zeile verschickt wird? Fände besser dem Controller zu sagen "Schick ne Vote message, hier sind die Infos die du brauchst" Warum erst eine message erstellen lassen die dann vom gleichen controller in der nächsten Zeile verschickt wird? Fände besser dem Controller zu sagen "Schick ne Vote message, hier sind die Infos die du brauchst"
kenobi
commented
True. Habe das handling in den vote controller verlegt. True. Habe das handling in den vote controller verlegt.
Die Input Parameter sind dadurch allerdings lang genug geworden, dass ich ein separates Interface dafür erstellt habe.
4600820889d046972bb264912f0ad929c8950dac
|
||||
event,
|
||||
announcementChannel,
|
||||
pinAfterSending: true
|
||||
magnetotail marked this conversation as resolved
Outdated
magnetotail
commented
Das pinnen sollte glaube ich auch nicht das event übernehmen sondern der Controller Das pinnen sollte glaube ich auch nicht das event übernehmen sondern der Controller
magnetotail
commented
Evtl halt über nen Parameter bestimmen. Auf lange Sicht gehört das eigentliche verschicken der Message eh in einen eigenen controller aber der voteController kann die Daten für eine voteMessage entgegennehmen die aufbereiten und dann über den messageController die eigentliche message schicken lassen Evtl halt über nen Parameter bestimmen. Auf lange Sicht gehört das eigentliche verschicken der Message eh in einen eigenen controller aber der voteController kann die Daten für eine voteMessage entgegennehmen die aufbereiten und dann über den messageController die eigentliche message schicken lassen
kenobi
commented
4600820889d046972bb264912f0ad929c8950dac
kenobi
commented
gelöst durch das Handling in #54 (comment) gelöst durch das Handling in https://gitea.brudi.xyz/kenobi/jellyfin-discord-bot/pulls/54#issuecomment-526
Es gibt noch ein Auftreten vom 'separaten' Pinnen (vote.controller.ts:114). Dort ist ein bisschen mehr refactoring nötig um die Input Paramter zu füllen.
Dieser Kommentar sollte allerdings fertig behandelt sein.
|
||||
},
|
||||
event.guildId,
|
||||
requestId)
|
||||
|
||||
for (let i = 0; i < movies.length; i++) {
|
||||
message = message.concat(Emotes[i]).concat(": ").concat(movies[i]).concat("\n")
|
||||
}
|
||||
message = message.concat(NONE_OF_THAT).concat(": Wenn dir nichts davon gefällt.")
|
||||
|
||||
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
|
||||
logger.debug(JSON.stringify(sentMessage))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@ import { Collection, GuildScheduledEvent, GuildScheduledEventStatus, Message } f
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { client } from "../..";
|
||||
import { logger } from "../logger";
|
||||
import { isInitialAnnouncement } from "../helper/messageIdentifiers";
|
||||
|
||||
|
||||
export const name = 'guildScheduledEventUpdate'
|
||||
@ -25,7 +26,7 @@ export async function execute(oldEvent: GuildScheduledEvent, newEvent: GuildSche
|
||||
|
||||
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)
|
||||
logger.info(`Deleting ${announcementsWithoutEvent.length} announcements.`, { guildId, requestId })
|
||||
announcementsWithoutEvent.forEach(message => message.delete())
|
||||
|
46
server/events/handleMessageReactionAdd.ts
Normal file
@ -0,0 +1,46 @@
|
||||
|
||||
kenobi marked this conversation as resolved
magnetotail
commented
Rename file to something like "handleVoteReaction" to make it separate from future reactionadd handlers Rename file to something like "handleVoteReaction" to make it separate from future reactionadd handlers
kenobi
commented
Agreement to use handleMessageReactionAdd as a 'reaction pre processor' to hand off reaction emoji to other handling methods and keep the eventHandler itself generic. Agreement to use handleMessageReactionAdd as a 'reaction pre processor' to hand off reaction emoji to other handling methods and keep the eventHandler itself generic.
|
||||
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) {
|
||||
kenobi marked this conversation as resolved
Outdated
magnetotail
commented
need to return here need to return here
kenobi
commented
added added
|
||||
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.debug(`reactedUponMessage payload: ${JSON.stringify(reactedUponMessage)}`)
|
||||
|
||||
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)) {
|
||||
magnetotail marked this conversation as resolved
magnetotail
commented
? ?
magnetotail
commented
Replace this do something with a meaningful todo Replace this do something with a meaningful todo
kenobi
commented
removed removed
03b6a30ffa67afca9a4bc0563f7bf092bbcdd60b
|
||||
if (messageReaction.emoji.toString() === Emoji.ticket) {
|
||||
logger.error(`Got a role emoji. Not implemented yet. ${reactedUponMessage.id}`)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import { Message } from "discord.js"
|
||||
import { logger } from "../logger"
|
||||
|
||||
export const name = 'messageCreate'
|
||||
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 { GuildScheduledEvent } from "discord.js";
|
||||
import { logger } from "../logger";
|
||||
import de from "date-fns/locale/de";
|
||||
import { Maybe } from "../interfaces";
|
||||
|
||||
export function createDateStringFromEvent(event: GuildScheduledEvent, requestId: string, guildId?: string): string {
|
||||
if (!event.scheduledStartAt) {
|
||||
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"`
|
||||
}
|
||||
|
||||
const timeZone = 'Europe/Berlin'
|
||||
const zonedDateTime = utcToZonedTime(event.scheduledStartAt, timeZone)
|
||||
const zonedDateTime = utcToZonedTime(eventStartDate, timeZone)
|
||||
const time = format(zonedDateTime, "HH:mm", { locale: de })
|
||||
|
||||
if (isToday(zonedDateTime)) {
|
||||
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}`
|
||||
}
|
||||
|
20
server/helper/messageIdentifiers.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Message } from "discord.js";
|
||||
|
||||
|
||||
magnetotail marked this conversation as resolved
Outdated
magnetotail
commented
Would like an enum of message types and a method with input message and output array of message types for possible messages with multiple things. Also maybe remove message at start of function names to improve visibility by shorter function name. "isVoteMessage(message)" is precise and readable enough Would like an enum of message types and a method with input message and output array of message types for possible messages with multiple things.
Also maybe remove message at start of function names to improve visibility by shorter function name. "isVoteMessage(message)" is precise and readable enough
|
||||
// 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]")
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Collection, GuildMember } from "discord.js"
|
||||
import { ChangedRoles, PermissionLevel } from "../interfaces"
|
||||
import { Collection, Guild, GuildMember, Role } from "discord.js"
|
||||
import { ChangedRoles, Maybe, PermissionLevel } from "../interfaces"
|
||||
import { logger } from "../logger"
|
||||
import { config } from "../configuration"
|
||||
|
||||
@ -16,6 +16,13 @@ export function filterRolesFromMemberUpdate(oldMember: GuildMember, newMember: G
|
||||
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> {
|
||||
const outVal = new Collection<string, PermissionLevel>()
|
||||
outVal.set(config.bot.watcher_role, "VIEWER")
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { CustomError, errorCodes } from "../interfaces"
|
||||
import { logger } from "../logger"
|
||||
import { ExtendedClient } from "../structures/client"
|
||||
|
||||
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)
|
||||
const creator = await client.users.fetch(creatorId)
|
||||
console.log(`Creator ${JSON.stringify(creator)}`)
|
||||
logger.info(`Creator ${JSON.stringify(creator)}`)
|
||||
if (creator)
|
||||
if (!creator.dmChannel)
|
||||
await creator.createDM()
|
||||
|
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, voteMessageInputInformation as 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"
|
||||
kenobi marked this conversation as resolved
Outdated
magnetotail
commented
Move check to handleMessageReactionAdd Move check to handleMessageReactionAdd
kenobi
commented
moved moved
|
||||
import { ExtendedClient } from "../structures/client"
|
||||
import { JellyfinHandler } from "../jellyfin/handler"
|
||||
kenobi marked this conversation as resolved
Outdated
magnetotail
commented
duplicate check. already checked before call in handleMessageReactionAdd duplicate check. already checked before call in handleMessageReactionAdd
kenobi
commented
removed removed
|
||||
|
||||
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) {
|
||||
magnetotail marked this conversation as resolved
Outdated
magnetotail
commented
Parameter die eine Message sind heißen oft "message", manchmal aber auch "msg". sollte vereinheitlicht sein Parameter die eine Message sind heißen oft "message", manchmal aber auch "msg". sollte vereinheitlicht sein
kenobi
commented
66507cb08fa50ba3a7be28388c55b21227fb2261
kenobi
commented
Jetzt sollten alle Occurences beseitigt sein. fc64728a780f99b56aebff7f0a7c5d24a901d90d
Jetzt sollten alle Occurences beseitigt sein.
|
||||
await message.unpin()
|
||||
}
|
||||
return await message.delete()
|
||||
}
|
||||
|
||||
/**
|
||||
magnetotail marked this conversation as resolved
Outdated
magnetotail
commented
above WHAT threshold? What does it do?? above WHAT threshold? What does it do??
kenobi
commented
20da25f2bf9a473704f8b4660e5f05183679ba39
kenobi
commented
Mit einem Kommentar versehen und entsprechend deines vorschlags umbenannt Mit einem Kommentar versehen und entsprechend deines vorschlags umbenannt
|
||||
* returns true if a Vote object contains at least one vote
|
||||
magnetotail marked this conversation as resolved
Outdated
magnetotail
commented
threshold seems to be a magic number threshold seems to be a magic number
magnetotail
commented
Or rename method to "hasAtLeastOneVote" Or rename method to "hasAtLeastOneVote"
kenobi
commented
296a490e935cbdb79b70d73d2df9bc12a5774c53
kenobi
commented
done done
|
||||
* @param {Vote} vote
|
||||
*/
|
||||
private hasAtLeastOneVote(vote: Vote): boolean {
|
||||
// subtracting the bots initial vote
|
||||
const overOneVote = (vote.count - 1) >= 1
|
||||
magnetotail marked this conversation as resolved
Outdated
magnetotail
commented
comment is misleading, voteinfo is also used to get the eventid and eventdate in line 93 comment is misleading, voteinfo is also used to get the eventid and eventdate in line 93
kenobi
commented
119343c916b023a926e534575ae803cdec5b9594
|
||||
logger.debug(`${vote.movie} : ${vote.count} -> above: ${overOneVote}`)
|
||||
return overOneVote
|
||||
}
|
||||
|
||||
magnetotail marked this conversation as resolved
Outdated
magnetotail
commented
maybe extract this if-else to a method to keep code more compact maybe extract this if-else to a method to keep code more compact
kenobi
commented
7d794a8001a66d068f949c893d689a068c3caeed
done
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
magnetotail marked this conversation as resolved
Outdated
magnetotail
commented
is not a message, only messagetext is not a message, only messagetext
kenobi
commented
a455fd8ff7e6b8ffb032fb4aed9389da68ee513b
kenobi
commented
done done
|
||||
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
|
||||
}
|
||||
magnetotail marked this conversation as resolved
Outdated
magnetotail
commented
why not pin message in method above? why not pin message in method above?
kenobi
commented
7d794a8001a66d068f949c893d689a068c3caeed
should have been moved to prepareAndSendVoteMessage()
|
||||
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) {
|
||||
kenobi marked this conversation as resolved
Outdated
magnetotail
commented
wtf :D wtf :D
kenobi
commented
it's magic ✨ it's magic ✨
|
||||
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] }
|
||||
magnetotail marked this conversation as resolved
Outdated
magnetotail
commented
rename message to messageText rename message to messageText
kenobi
commented
done done
ca99987a20baeceda27cb5e206bff42a54f31b04
|
||||
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) {
|
||||
magnetotail marked this conversation as resolved
Outdated
magnetotail
commented
remove todo remove todo
kenobi
commented
removed removed
ca99987a20baeceda27cb5e206bff42a54f31b04
|
||||
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()
|
||||
magnetotail marked this conversation as resolved
Outdated
magnetotail
commented
rename to "getOpenVoteEvent", since other events get filtered out rename to "getOpenVoteEvent", since other events get filtered out
kenobi
commented
ca99987a20baeceda27cb5e206bff42a54f31b04
done
magnetotail
commented
renamed to getOpenPollEvent renamed to getOpenPollEvent
kenobi
commented
4e9fe587b0af1b91f049b820023bd0f7e6280517
renamed
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
magnetotail marked this conversation as resolved
magnetotail
commented
cancelling uptade log seems wrong here cancelling uptade log seems wrong here
kenobi
commented
6d40930dc126ba0581ffc5a0733caef93fd4cc60
done
|
||||
/**
|
||||
* 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[] = []
|
||||
magnetotail marked this conversation as resolved
magnetotail
commented
should be renamed so it's clear what the event gets updated with and what it looks like in the end and what kind of event gets updated (I guess open poll events) should be renamed so it's clear what the event gets updated with and what it looks like in the end and what kind of event gets updated (I guess open poll events)
kenobi
commented
ca99987a
done
|
||||
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
|
||||
magnetotail marked this conversation as resolved
magnetotail
commented
"Fehler, event hatte keine Uhrzeit" pls "Fehler, event hatte keine Uhrzeit" pls
kenobi
commented
ca99987a20baeceda27cb5e206bff42a54f31b04
done
|
||||
}
|
||||
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 })
|
||||
magnetotail marked this conversation as resolved
Outdated
magnetotail
commented
why plural? why plural?
kenobi
commented
Because this was unintentional :)
Because this was unintentional :)
ca99987a20baeceda27cb5e206bff42a54f31b04
fixed
|
||||
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 })
|
||||
magnetotail marked this conversation as resolved
Outdated
magnetotail
commented
rename parameter lastMessages to message. "last" seems to be very specific as a parameter, also false plural rename parameter lastMessages to message. "last" seems to be very specific as a parameter, also false plural
kenobi
commented
ca99987a20baeceda27cb5e206bff42a54f31b04
done
|
||||
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 { Role } from "discord.js"
|
||||
import { GuildScheduledEvent, Role, TextChannel } from "discord.js"
|
||||
|
||||
export type Maybe<T> = T | undefined | null
|
||||
export interface Player {
|
||||
@ -39,3 +39,10 @@ export interface JellyfinConfig {
|
||||
collectionUser: string
|
||||
}
|
||||
export type PermissionLevel = "VIEWER" | "ADMIN" | "TEMPORARY"
|
||||
export interface voteMessageInputInformation {
|
||||
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 {
|
||||
override name: "ResponseError" = "ResponseError";
|
||||
constructor(public response: Response, msg?: string) {
|
||||
super(msg);
|
||||
constructor(public response: Response, errorMessage?: string) {
|
||||
super(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
export class FetchError extends Error {
|
||||
override name: "FetchError" = "FetchError";
|
||||
constructor(public cause: Error, msg?: string) {
|
||||
super(msg);
|
||||
constructor(public cause: Error, errorMessage?: string) {
|
||||
super(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
export class RequiredError extends Error {
|
||||
override name: "RequiredError" = "RequiredError";
|
||||
constructor(public field: string, msg?: string) {
|
||||
super(msg);
|
||||
constructor(public field: string, errorMessage?: string) {
|
||||
super(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -8,7 +8,9 @@ import { Maybe } from "../interfaces";
|
||||
import { JellyfinHandler } from "../jellyfin/handler";
|
||||
import { logger } from "../logger";
|
||||
import { CommandType } from "../types/commandTypes";
|
||||
import { checkForPollsToClose } from "../commands/closepoll";
|
||||
import { isInitialAnnouncement } from "../helper/messageIdentifiers";
|
||||
import VoteController from "../helper/vote.controller";
|
||||
import { yavinJellyfinHandler } from "../..";
|
||||
|
||||
|
||||
|
||||
@ -16,13 +18,14 @@ export class ExtendedClient extends Client {
|
||||
private eventFilePath = `${__dirname}/../events`
|
||||
private commandFilePath = `${__dirname}/../commands`
|
||||
private jellyfin: JellyfinHandler
|
||||
public voteController: VoteController = new VoteController(this, yavinJellyfinHandler)
|
||||
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
|
||||
private pollCloseBackgroundTasks: Collection<string, ScheduledTask> = new Collection()
|
||||
public constructor(jf: JellyfinHandler) {
|
||||
const intents: IntentsBitField = new IntentsBitField()
|
||||
intents.add(IntentsBitField.Flags.GuildMembers, IntentsBitField.Flags.MessageContent, IntentsBitField.Flags.Guilds, IntentsBitField.Flags.DirectMessages, IntentsBitField.Flags.GuildScheduledEvents, IntentsBitField.Flags.GuildVoiceStates)
|
||||
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 }
|
||||
super(options)
|
||||
this.jellyfin = jf
|
||||
@ -74,6 +77,7 @@ export class ExtendedClient extends Client {
|
||||
this.registerCommands(slashCommands, guilds)
|
||||
this.cacheUsers(guilds)
|
||||
await this.cacheAnnouncementServer(guilds)
|
||||
this.fetchAnnouncementChannelMessage(this.announcementChannels)
|
||||
this.startAnnouncementRoleBackgroundTask(guilds)
|
||||
this.startPollCloseBackgroundTasks()
|
||||
})
|
||||
@ -81,6 +85,21 @@ export class ExtendedClient extends Client {
|
||||
logger.info(`Error refreshing slash commands: ${error}`)
|
||||
}
|
||||
}
|
||||
/**
|
||||
kenobi marked this conversation as resolved
Outdated
magnetotail
commented
why is this necessary? why is this necessary?
kenobi
commented
It's complicated. I put in some JSDoc comments:
It's complicated. I put in some JSDoc comments:
```ts
/**
* 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
*/
```
magnetotail
commented
Figured something like that. Thanks for the comment Figured something like that. Thanks for the comment
|
||||
* 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>) {
|
||||
for (const guild of guilds.values()) {
|
||||
const channels: TextChannel[] = <TextChannel[]>(await guild.channels.fetch())
|
||||
@ -136,7 +155,7 @@ export class ExtendedClient extends Client {
|
||||
}
|
||||
this.announcementRoleHandlerTask.set(guild.id, schedule("*/10 * * * * *", async () => {
|
||||
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) {
|
||||
logger.error("More than one pinned announcement Messages found. Unable to know which one people react to. Please fix!", { guildId: guild.id, requestId })
|
||||
@ -175,7 +194,7 @@ export class ExtendedClient extends Client {
|
||||
|
||||
private async startPollCloseBackgroundTasks() {
|
||||
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
@ -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
@ -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,16 +1,15 @@
|
||||
import { GuildScheduledEvent } from "discord.js"
|
||||
import { createDateStringFromEvent } from "../../server/helper/dateHelper"
|
||||
import MockDate from 'mockdate'
|
||||
|
||||
beforeAll(() => {
|
||||
MockDate.set('01-01-2023')
|
||||
MockDate.set('01-01-2023')
|
||||
})
|
||||
|
||||
function getTestDate(date: string): GuildScheduledEvent {
|
||||
return <GuildScheduledEvent>{ scheduledStartAt: new Date(date) }
|
||||
function getTestDate(date: string): Date {
|
||||
return new Date(date)
|
||||
}
|
||||
test('createDateStringFromEvent - correct formatting', () => {
|
||||
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-03-2023 12:30'), "")).toEqual('am Dienstag 03.01 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-03-2023 12:30'), "")).toEqual('am Dienstag 03.01. um 12:30')
|
||||
})
|
||||
|
Can this be integrated into Emoji? Maybe provide method to only get number emojis then? By definition "NONE_OF_THAT" is also an emoji
Emotes got renamed to validVoteEmotes
fb4ab59dc6
preliminary solution