Compare commits
74 Commits
690ba697b6
...
1bfcaa95f9
Author | SHA1 | Date | |
---|---|---|---|
1bfcaa95f9 | |||
fb4ab59dc6 | |||
6d40930dc1 | |||
4e9fe587b0 | |||
03b6a30ffa | |||
7d794a8001 | |||
8df180898e | |||
976175242b | |||
68546b0b50 | |||
1348abbd48 | |||
fce9091114 | |||
081f3c6201 | |||
ca99987a20 | |||
fc64728a78 | |||
20da25f2bf | |||
a455fd8ff7 | |||
119343c916 | |||
296a490e93 | |||
66507cb08f | |||
4600820889 | |||
4a3e8809be | |||
f705b97804 | |||
9cdc6e1934 | |||
c73cd20ccf | |||
e66aebc88c | |||
599243990e | |||
eef3a9c358 | |||
1e912b20ef | |||
ce4dc81f7d | |||
b76df79d2a | |||
4e563d57fd | |||
b6a1e06b03 | |||
2ebc7fbdbe | |||
8ff5aeff03 | |||
1101a84501 | |||
91ec2ece7e | |||
5e58765cf4 | |||
a2adef808f | |||
dc66c277b2 | |||
c022cc32d5 | |||
e763e76413 | |||
137d156981 | |||
fdfe7ce404 | |||
146848b759 | |||
e54f03292e | |||
fe45445811 | |||
8f02e11dba | |||
878c81bfa7 | |||
ca19168cf4 | |||
e8893646f0 | |||
e61b3a7b16 | |||
9383cee4a0 | |||
0748097a1f | |||
ffba737e5a | |||
4cd9c771f0 | |||
8c3cf7829b | |||
1a13638ed9 | |||
c351e27fdd | |||
6d3bea169e | |||
3f071c8a4e | |||
98d1ca73b5 | |||
ee742018e9 | |||
8ad651c753 | |||
a4a834ad27 | |||
e8dcfd8340 | |||
d9d1d74ef9 | |||
331ff89060 | |||
f6476c609b | |||
6220268b14 | |||
b6034d4fb7 | |||
ca0a9e3cb8 | |||
b8a32aab40 | |||
e3e755011d | |||
5a6c66cb3e |
@ -6,6 +6,7 @@ import { Maybe } from '../interfaces'
|
|||||||
import { logger } from '../logger'
|
import { logger } from '../logger'
|
||||||
import { Command } from '../structures/command'
|
import { Command } from '../structures/command'
|
||||||
import { RunOptions } from '../types/commandTypes'
|
import { RunOptions } from '../types/commandTypes'
|
||||||
|
import { isInitialAnnouncement } from '../helper/messageIdentifiers'
|
||||||
|
|
||||||
export default new Command({
|
export default new Command({
|
||||||
name: 'announce',
|
name: 'announce',
|
||||||
@ -61,7 +62,7 @@ async function sendInitialAnnouncement(guildId: string, requestId: string): Prom
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentPinnedAnnouncementMessages = (await announcementChannel.messages.fetchPinned()).filter(message => message.cleanContent.includes("[initial]"))
|
const currentPinnedAnnouncementMessages = (await announcementChannel.messages.fetchPinned()).filter(message => isInitialAnnouncement(message))
|
||||||
currentPinnedAnnouncementMessages.forEach(async (message) => await message.unpin())
|
currentPinnedAnnouncementMessages.forEach(async (message) => await message.unpin())
|
||||||
currentPinnedAnnouncementMessages.forEach(message => message.delete())
|
currentPinnedAnnouncementMessages.forEach(message => message.delete())
|
||||||
|
|
||||||
|
@ -1,10 +1,5 @@
|
|||||||
import { addDays, differenceInDays, format, isAfter, toDate } from 'date-fns'
|
|
||||||
import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildScheduledEventSetStatusArg, GuildScheduledEventStatus, Message, MessageCreateOptions, TextChannel } from 'discord.js'
|
|
||||||
import { v4 as uuid } from 'uuid'
|
import { v4 as uuid } from 'uuid'
|
||||||
import { client } from '../..'
|
import { client } from '../..'
|
||||||
import { config } from '../configuration'
|
|
||||||
import { Emotes } from '../events/autoCreateVoteByWPEvent'
|
|
||||||
import { Maybe } from '../interfaces'
|
|
||||||
import { logger } from '../logger'
|
import { logger } from '../logger'
|
||||||
import { Command } from '../structures/command'
|
import { Command } from '../structures/command'
|
||||||
import { RunOptions } from '../types/commandTypes'
|
import { RunOptions } from '../types/commandTypes'
|
||||||
@ -25,160 +20,6 @@ export default new Command({
|
|||||||
logger.info("Got command for closing poll!", { guildId, requestId })
|
logger.info("Got command for closing poll!", { guildId, requestId })
|
||||||
|
|
||||||
command.followUp("Alles klar, beende die Umfrage :)")
|
command.followUp("Alles klar, beende die Umfrage :)")
|
||||||
closePoll(command.guild, requestId)
|
client.voteController.closePoll(command.guild, requestId)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function closePoll(guild: Guild, requestId: string) {
|
|
||||||
const guildId = guild.id
|
|
||||||
logger.info("stopping poll", { guildId, requestId })
|
|
||||||
|
|
||||||
const announcementChannel: Maybe<TextChannel> = client.getAnnouncementChannelForGuild(guildId)
|
|
||||||
if (!announcementChannel) {
|
|
||||||
logger.error("Could not find the textchannel. Unable to close poll.", { guildId, requestId })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const messages: Message<true>[] = (await announcementChannel.messages.fetch()) //todo: fetch only pinned messages
|
|
||||||
.map((value) => value)
|
|
||||||
.filter(message => !message.cleanContent.includes("[Abstimmung beendet]") && message.cleanContent.includes("[Abstimmung]"))
|
|
||||||
.sort((a, b) => b.createdTimestamp - a.createdTimestamp)
|
|
||||||
|
|
||||||
if (!messages || messages.length <= 0) {
|
|
||||||
logger.info("Could not find any vote messages. Cancelling pollClose", { guildId, requestId })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastMessage: Message<true> = messages[0]
|
|
||||||
|
|
||||||
logger.debug(`Found messages: ${JSON.stringify(messages, null, 2)}`, { guildId, requestId })
|
|
||||||
|
|
||||||
logger.debug(`Last message: ${JSON.stringify(lastMessage, null, 2)}`, { guildId, requestId })
|
|
||||||
|
|
||||||
|
|
||||||
const votes = await (await getVotesByEmote(lastMessage, guildId, requestId))
|
|
||||||
.sort((a, b) => b.count - a.count)
|
|
||||||
|
|
||||||
logger.debug(`votes: ${JSON.stringify(votes, null, 2)}`, { guildId, requestId })
|
|
||||||
|
|
||||||
logger.info("Deleting vote message")
|
|
||||||
await lastMessage.delete()
|
|
||||||
const event = await getEvent(guild, guild.id, requestId)
|
|
||||||
if (event) {
|
|
||||||
updateEvent(event, votes, guild, guildId, requestId)
|
|
||||||
sendVoteClosedMessage(event, votes[0].movie, guildId, requestId)
|
|
||||||
}
|
|
||||||
|
|
||||||
//lastMessage.unpin() //todo: uncomment when bot has permission to pin/unpin
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sendVoteClosedMessage(event: GuildScheduledEvent, movie: string, guildId: string, requestId: string) {
|
|
||||||
const date = event.scheduledStartAt ? format(event.scheduledStartAt, "dd.MM") : "Fehler, event hatte kein Datum"
|
|
||||||
const time = event.scheduledStartAt ? format(event.scheduledStartAt, "HH:mm") : "Fehler, event hatte kein Datum"
|
|
||||||
const body = `[Abstimmung beendet] für https://discord.com/events/${event.guildId}/${event.id}\n<@&${config.bot.announcement_role}> Wir gucken ${movie} am ${date} um ${time}`
|
|
||||||
const options: MessageCreateOptions = {
|
|
||||||
content: body,
|
|
||||||
allowedMentions: { parse: ["roles"] }
|
|
||||||
}
|
|
||||||
const announcementChannel = client.getAnnouncementChannelForGuild(guildId)
|
|
||||||
logger.info("Sending vote closed message.", { guildId, requestId })
|
|
||||||
if (!announcementChannel) {
|
|
||||||
logger.error("Could not find announcement channel. Please fix!", { guildId, requestId })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
announcementChannel.send(options)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateEvent(voteEvent: GuildScheduledEvent, votes: Vote[], guild: Guild, guildId: string, requestId: string) {
|
|
||||||
logger.info(`Updating event with movie ${votes[0].movie}.`, { guildId, requestId })
|
|
||||||
const options: GuildScheduledEventEditOptions<GuildScheduledEventStatus.Scheduled, GuildScheduledEventSetStatusArg<GuildScheduledEventStatus.Scheduled>> = {
|
|
||||||
name: votes[0].movie,
|
|
||||||
description: `!wp\nNummer 2: ${votes[1].movie} mit ${votes[1].count - 1} Stimmen\nNummer 3: ${votes[2].movie} mit ${votes[2].count - 1} Stimmen`
|
|
||||||
}
|
|
||||||
logger.debug(`Updating event: ${JSON.stringify(voteEvent, null, 2)}`, { guildId, requestId })
|
|
||||||
logger.info("Updating event.", { guildId, requestId })
|
|
||||||
voteEvent.edit(options)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getEvent(guild: Guild, guildId: string, requestId: string): Promise<GuildScheduledEvent | null> {
|
|
||||||
const voteEvents = (await guild.scheduledEvents.fetch())
|
|
||||||
.map((value) => value)
|
|
||||||
.filter(event => event.name.toLowerCase().includes("voting offen"))
|
|
||||||
logger.debug(`Found events: ${JSON.stringify(voteEvents, null, 2)}`, { guildId, requestId })
|
|
||||||
|
|
||||||
if (!voteEvents || voteEvents.length <= 0) {
|
|
||||||
logger.error("Could not find vote event. Cancelling update!", { guildId, requestId })
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return voteEvents[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
type Vote = {
|
|
||||||
emote: string, //todo habs nicht hinbekommen hier Emotes zu nutzen
|
|
||||||
count: number,
|
|
||||||
movie: string
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getVotesByEmote(message: Message, guildId: string, requestId: string): Promise<Vote[]> {
|
|
||||||
const votes: Vote[] = []
|
|
||||||
logger.debug(`Number of items in emotes: ${Object.values(Emotes).length}`, { guildId, requestId })
|
|
||||||
for (let i = 0; i < Object.keys(Emotes).length / 2; i++) {
|
|
||||||
const emote = Emotes[i]
|
|
||||||
logger.debug(`Getting reaction for emote ${emote}`, { guildId, requestId })
|
|
||||||
const reaction = await message.reactions.resolve(emote)
|
|
||||||
logger.debug(`Reaction for emote ${emote}: ${JSON.stringify(reaction, null, 2)}`, { guildId, requestId })
|
|
||||||
if (reaction) {
|
|
||||||
const vote: Vote = { emote: emote, count: reaction.count, movie: extractMovieFromMessageByEmote(message, emote) }
|
|
||||||
votes.push(vote)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return votes
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractMovieFromMessageByEmote(message: Message, emote: string): string {
|
|
||||||
const lines = message.cleanContent.split("\n")
|
|
||||||
const emoteLines = lines.filter(line => line.includes(emote))
|
|
||||||
|
|
||||||
if (!emoteLines) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
const movie = emoteLines[0].substring(emoteLines[0].indexOf(emote) + emote.length + 2) // plus colon and space
|
|
||||||
|
|
||||||
return movie
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function checkForPollsToClose(guild: Guild): Promise<void> {
|
|
||||||
const requestId = uuid()
|
|
||||||
logger.info(`Automatic check for poll closing.`, { guildId: guild.id, requestId })
|
|
||||||
const events = (await guild.scheduledEvents.fetch()).filter(event => event.name.toLocaleLowerCase().includes("voting offen")).map(event => event)
|
|
||||||
if (events.length > 1) {
|
|
||||||
logger.error("Handling more than one Event is not implemented yet. Found more than one poll to close")
|
|
||||||
return
|
|
||||||
} else if (events.length == 0) {
|
|
||||||
logger.info("Could not find any events. Cancelling", { guildId: guild.id, requestId })
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedEvent = events[0] //add two hours because of different timezones in discord api and Date.now()
|
|
||||||
if (!updatedEvent.scheduledStartTimestamp) {
|
|
||||||
logger.error("Event does not have a scheduled start time. Cancelling", { guildId: guild.id, requestId })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const createDate: Date = toDate(updatedEvent.createdTimestamp)
|
|
||||||
const eventDate: Date = toDate(updatedEvent.scheduledStartTimestamp)
|
|
||||||
const difference: number = differenceInDays(createDate, eventDate)
|
|
||||||
|
|
||||||
if (difference <= 2) {
|
|
||||||
logger.info("Less than two days between event create and event start. Not closing poll.", { guildId: guild.id, requestId })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const closePollDate: Date = addDays(eventDate, -2)
|
|
||||||
|
|
||||||
if (isAfter(Date.now(), closePollDate)) {
|
|
||||||
logger.info("Less than two days until event. Closing poll", { guildId: guild.id, requestId })
|
|
||||||
closePoll(guild, requestId)
|
|
||||||
} else {
|
|
||||||
logger.info(`ScheduledStart: ${closePollDate}. Now: ${toDate(Date.now())}`, { guildId: guild.id, requestId })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { ApplicationCommandOptionType } from 'discord.js'
|
import { ApplicationCommandOptionType } from 'discord.js'
|
||||||
import { Command } from '../structures/command'
|
import { Command } from '../structures/command'
|
||||||
import { RunOptions } from '../types/commandTypes'
|
import { RunOptions } from '../types/commandTypes'
|
||||||
|
import { logger } from '../logger'
|
||||||
export default new Command({
|
export default new Command({
|
||||||
name: 'echo',
|
name: 'echo',
|
||||||
description: 'Echoes a text',
|
description: 'Echoes a text',
|
||||||
@ -13,7 +14,7 @@ export default new Command({
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
run: async (interaction: RunOptions) => {
|
run: async (interaction: RunOptions) => {
|
||||||
console.log('echo called')
|
logger.info('echo called')
|
||||||
interaction.interaction.reply(interaction.toString())
|
interaction.interaction.reply(interaction.toString())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -2,15 +2,16 @@ import { v4 as uuid } from 'uuid'
|
|||||||
import { jellyfinHandler } from "../.."
|
import { jellyfinHandler } from "../.."
|
||||||
import { Command } from '../structures/command'
|
import { Command } from '../structures/command'
|
||||||
import { RunOptions } from '../types/commandTypes'
|
import { RunOptions } from '../types/commandTypes'
|
||||||
|
import { logger } from '../logger'
|
||||||
|
|
||||||
export default new Command({
|
export default new Command({
|
||||||
name: 'passwort_reset',
|
name: 'passwort_reset',
|
||||||
description: 'Ich vergebe dir ein neues Passwort und schicke es dir per DM zu. Kostet auch nix! Versprochen! 😉',
|
description: 'Ich vergebe dir ein neues Passwort und schicke es dir per DM zu. Kostet auch nix! Versprochen! 😉',
|
||||||
options: [],
|
options: [],
|
||||||
run: async (interaction: RunOptions) => {
|
run: async (interaction: RunOptions) => {
|
||||||
console.log('PasswortReset called')
|
logger.info('PasswortReset called')
|
||||||
interaction.interaction.followUp('Yo, ich schick dir eins!')
|
interaction.interaction.followUp('Yo, ich schick dir eins!')
|
||||||
console.log(JSON.stringify(interaction.interaction.member, null, 2))
|
logger.info(JSON.stringify(interaction.interaction.member, null, 2))
|
||||||
jellyfinHandler.resetUserPasswort(interaction.interaction.member, uuid())
|
jellyfinHandler.resetUserPasswort(interaction.interaction.member, uuid())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -30,6 +30,8 @@ export interface Config {
|
|||||||
yavin_jellyfin_url: string
|
yavin_jellyfin_url: string
|
||||||
yavin_jellyfin_token: string
|
yavin_jellyfin_token: string
|
||||||
yavin_jellyfin_collection_user: string
|
yavin_jellyfin_collection_user: string
|
||||||
|
random_movie_count: number
|
||||||
|
reroll_retains_top_picks: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export const config: Config = {
|
export const config: Config = {
|
||||||
@ -59,16 +61,18 @@ export const config: Config = {
|
|||||||
client_id: process.env.CLIENT_ID ?? "",
|
client_id: process.env.CLIENT_ID ?? "",
|
||||||
jellfin_token: process.env.JELLYFIN_TOKEN ?? "",
|
jellfin_token: process.env.JELLYFIN_TOKEN ?? "",
|
||||||
jellyfin_url: process.env.JELLYFIN_URL ?? "",
|
jellyfin_url: process.env.JELLYFIN_URL ?? "",
|
||||||
workaround_token: process.env.TOKEN ?? "",
|
workaround_token: process.env.TOKEN ?? "TOKEN",
|
||||||
watcher_role: process.env.WATCHER_ROLE ?? "",
|
watcher_role: process.env.WATCHER_ROLE ?? "WATCHER_ROLE",
|
||||||
jf_admin_role: process.env.ADMIN_ROLE ?? "",
|
jf_admin_role: process.env.ADMIN_ROLE ?? "ADMIN_ROLE",
|
||||||
announcement_role: process.env.WATCHPARTY_ANNOUNCEMENT_ROLE ?? "",
|
announcement_role: process.env.WATCHPARTY_ANNOUNCEMENT_ROLE ?? "ANNOUNCE_ROLE",
|
||||||
announcement_channel_id: process.env.CHANNEL_ID ?? "",
|
announcement_channel_id: process.env.CHANNEL_ID ?? "ANNOUNCE_CHANNEL",
|
||||||
jf_collection_id: process.env.JELLYFIN_COLLECTION_ID ?? "",
|
jf_collection_id: process.env.JELLYFIN_COLLECTION_ID ?? "",
|
||||||
yavin_collection_id: process.env.YAVIN_COLLECTION_ID ?? "",
|
yavin_collection_id: process.env.YAVIN_COLLECTION_ID ?? "",
|
||||||
yavin_jellyfin_url: process.env.YAVIN_JELLYFIN_URL ?? "",
|
yavin_jellyfin_url: process.env.YAVIN_JELLYFIN_URL ?? "",
|
||||||
yavin_jellyfin_token: process.env.YAVIN_TOKEN ?? "",
|
yavin_jellyfin_token: process.env.YAVIN_TOKEN ?? "",
|
||||||
yavin_jellyfin_collection_user: process.env.YAVIN_COLLECTION_USER ?? "",
|
yavin_jellyfin_collection_user: process.env.YAVIN_COLLECTION_USER ?? "",
|
||||||
jf_user: process.env.JELLYFIN_USER ?? ""
|
jf_user: process.env.JELLYFIN_USER ?? "",
|
||||||
|
random_movie_count: parseInt(process.env.RANDOM_MOVIE_COUNT ?? "5") ?? 5,
|
||||||
|
reroll_retains_top_picks: process.env.REROLL_RETAIN === "true"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
17
server/constants.ts
Normal file
17
server/constants.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
export enum ValidVoteEmotes { "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟" }
|
||||||
|
export const NONE_OF_THAT = "❌"
|
||||||
|
// WIP
|
||||||
|
export const Emoji = {
|
||||||
|
"one": "\u0031\uFE0F\u20E3",
|
||||||
|
"two": "\u0032\uFE0F\u20E3",
|
||||||
|
"three": "\u0033\uFE0F\u20E3",
|
||||||
|
"four": "\u0034\uFE0F\u20E3",
|
||||||
|
"five": "\u0035\uFE0F\u20E3",
|
||||||
|
"six": "\u0036\uFE0F\u20E3",
|
||||||
|
"seven": "\u0037\uFE0F\u20E3",
|
||||||
|
"eight": "\u0038\uFE0F\u20E3",
|
||||||
|
"nine": "\u0039\uFE0F\u20E3",
|
||||||
|
"ten": "\uD83D\uDD1F",
|
||||||
|
"ticket": "🎫"
|
||||||
|
}
|
@ -32,7 +32,11 @@ export async function execute(event: GuildScheduledEvent) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = `[Watchparty] https://discord.com/events/${event.guildId}/${event.id} \nHey <@&${config.bot.announcement_role}>, wir gucken ${event.name} ${createDateStringFromEvent(event, guildId, requestId)}`
|
if (!event.scheduledStartAt) {
|
||||||
|
logger.error('Event has no start date, bailing out')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const message = `[Watchparty] https://discord.com/events/${event.guildId}/${event.id} \nHey <@&${config.bot.announcement_role}>, wir gucken ${event.name} ${createDateStringFromEvent(event.scheduledStartAt, guildId, requestId)}`
|
||||||
|
|
||||||
channel.send(message)
|
channel.send(message)
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,20 +1,11 @@
|
|||||||
import { GuildScheduledEvent, Message, MessageCreateOptions, TextChannel } from "discord.js";
|
import { GuildScheduledEvent, TextChannel } from "discord.js";
|
||||||
import { ScheduledTask } from "node-cron";
|
|
||||||
import { v4 as uuid } from "uuid";
|
import { v4 as uuid } from "uuid";
|
||||||
import { client, yavinJellyfinHandler } from "../..";
|
import { client, yavinJellyfinHandler } from "../..";
|
||||||
import { config } from "../configuration";
|
|
||||||
import { createDateStringFromEvent } from "../helper/dateHelper";
|
|
||||||
import { Maybe } from "../interfaces";
|
import { Maybe } from "../interfaces";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
|
|
||||||
|
|
||||||
export const name = 'guildScheduledEventCreate'
|
export const name = 'guildScheduledEventCreate'
|
||||||
|
|
||||||
export enum Emotes { "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟" }
|
|
||||||
export const NONE_OF_THAT = "❌"
|
|
||||||
|
|
||||||
export let task: ScheduledTask | undefined
|
|
||||||
|
|
||||||
export async function execute(event: GuildScheduledEvent) {
|
export async function execute(event: GuildScheduledEvent) {
|
||||||
const requestId = uuid()
|
const requestId = uuid()
|
||||||
|
|
||||||
@ -33,31 +24,21 @@ export async function execute(event: GuildScheduledEvent) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
logger.debug(`Found channel ${JSON.stringify(announcementChannel, null, 2)}`, { guildId: event.guildId, requestId })
|
logger.debug(`Found channel ${JSON.stringify(announcementChannel, null, 2)}`, { guildId: event.guildId, requestId })
|
||||||
|
|
||||||
if (!event.scheduledStartAt) {
|
if (!event.scheduledStartAt) {
|
||||||
logger.info("EVENT DOES NOT HAVE STARTDATE; CANCELLING", { guildId: event.guildId, requestId })
|
logger.info("Event does not have a start date, cancelling", { guildId: event.guildId, requestId })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let message = `[Abstimmung] für https://discord.com/events/${event.guildId}/${event.id}\n<@&${config.bot.announcement_role}> Es gibt eine neue Abstimmung für die nächste Watchparty ${createDateStringFromEvent(event, event.guildId, requestId)}! Stimme hierunter für den nächsten Film ab!\n`
|
const sentMessage = await client.voteController.prepareAndSendVoteMessage({
|
||||||
|
movies,
|
||||||
|
startDate: event.scheduledStartAt,
|
||||||
|
event,
|
||||||
|
announcementChannel,
|
||||||
|
pinAfterSending: true
|
||||||
|
},
|
||||||
|
event.guildId,
|
||||||
|
requestId)
|
||||||
|
|
||||||
for (let i = 0; i < movies.length; i++) {
|
logger.debug(JSON.stringify(sentMessage))
|
||||||
message = message.concat(Emotes[i]).concat(": ").concat(movies[i]).concat("\n")
|
|
||||||
}
|
|
||||||
message = message.concat(NONE_OF_THAT).concat(": Wenn dir nichts davon gefällt.")
|
|
||||||
|
|
||||||
const options: MessageCreateOptions = {
|
|
||||||
allowedMentions: { parse: ["roles"] },
|
|
||||||
content: message,
|
|
||||||
}
|
|
||||||
|
|
||||||
const sentMessage: Message<true> = await (await announcementChannel.fetch()).send(options)
|
|
||||||
|
|
||||||
for (let i = 0; i < movies.length; i++) {
|
|
||||||
sentMessage.react(Emotes[i])
|
|
||||||
}
|
|
||||||
sentMessage.react(NONE_OF_THAT)
|
|
||||||
|
|
||||||
// sentMessage.pin() //todo: uncomment when bot has permission to pin messages. Also update closepoll.ts to only fetch pinned messages
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import { Collection, GuildScheduledEvent, GuildScheduledEventStatus, Message } f
|
|||||||
import { v4 as uuid } from "uuid";
|
import { v4 as uuid } from "uuid";
|
||||||
import { client } from "../..";
|
import { client } from "../..";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
|
import { isInitialAnnouncement } from "../helper/messageIdentifiers";
|
||||||
|
|
||||||
|
|
||||||
export const name = 'guildScheduledEventUpdate'
|
export const name = 'guildScheduledEventUpdate'
|
||||||
@ -25,7 +26,7 @@ export async function execute(oldEvent: GuildScheduledEvent, newEvent: GuildSche
|
|||||||
|
|
||||||
const events = await newEvent.guild.scheduledEvents.fetch()
|
const events = await newEvent.guild.scheduledEvents.fetch()
|
||||||
|
|
||||||
const wpAnnouncements = (await announcementChannel.messages.fetch()).filter(message => !message.cleanContent.includes("[initial]"))
|
const wpAnnouncements = (await announcementChannel.messages.fetch()).filter(message => !isInitialAnnouncement(message))
|
||||||
const announcementsWithoutEvent = filterAnnouncementsByPendingWPs(wpAnnouncements, events)
|
const announcementsWithoutEvent = filterAnnouncementsByPendingWPs(wpAnnouncements, events)
|
||||||
logger.info(`Deleting ${announcementsWithoutEvent.length} announcements.`, { guildId, requestId })
|
logger.info(`Deleting ${announcementsWithoutEvent.length} announcements.`, { guildId, requestId })
|
||||||
announcementsWithoutEvent.forEach(message => message.delete())
|
announcementsWithoutEvent.forEach(message => message.delete())
|
||||||
|
46
server/events/handleMessageReactionAdd.ts
Normal file
46
server/events/handleMessageReactionAdd.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
|
||||||
|
import { Message, MessageReaction, User } from "discord.js";
|
||||||
|
import { logger, newRequestId, noGuildId } from "../logger";
|
||||||
|
import { Emoji, ValidVoteEmotes, NONE_OF_THAT } from "../constants";
|
||||||
|
import { client } from "../..";
|
||||||
|
import { isInitialAnnouncement, isVoteMessage } from "../helper/messageIdentifiers";
|
||||||
|
|
||||||
|
|
||||||
|
export const name = 'messageReactionAdd'
|
||||||
|
|
||||||
|
export async function execute(messageReaction: MessageReaction, user: User) {
|
||||||
|
if (user.id == client.user?.id) {
|
||||||
|
logger.info('Skipping bot reaction')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const requestId = newRequestId()
|
||||||
|
const guildId = messageReaction.message.inGuild() ? messageReaction.message.guildId : noGuildId
|
||||||
|
const reactedUponMessage: Message = messageReaction.message.partial ? await messageReaction.message.fetch() : messageReaction.message
|
||||||
|
if (!messageReaction.message.guild) {
|
||||||
|
logger.warn(`Received messageReactionAdd on non-guild message.`, { requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Got reaction on message`, { requestId, guildId })
|
||||||
|
//logger.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)) {
|
||||||
|
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 { Message } from "discord.js"
|
||||||
|
import { logger } from "../logger"
|
||||||
|
|
||||||
export const name = 'messageCreate'
|
export const name = 'messageCreate'
|
||||||
export function execute(message: Message) {
|
export function execute(message: Message) {
|
||||||
console.log(`${JSON.stringify(message)} has been created`)
|
logger.info(`${JSON.stringify(message)} has been created`)
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
import { format, isToday, toDate } from "date-fns";
|
import { format, isToday } from "date-fns";
|
||||||
import { utcToZonedTime } from "date-fns-tz"
|
import { utcToZonedTime } from "date-fns-tz"
|
||||||
import { GuildScheduledEvent } from "discord.js";
|
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
import de from "date-fns/locale/de";
|
import de from "date-fns/locale/de";
|
||||||
|
import { Maybe } from "../interfaces";
|
||||||
|
|
||||||
export function createDateStringFromEvent(event: GuildScheduledEvent, requestId: string, guildId?: string): string {
|
export function createDateStringFromEvent(eventStartDate: Maybe<Date>, requestId: string, guildId?: string): string {
|
||||||
if (!event.scheduledStartAt) {
|
if (!eventStartDate) {
|
||||||
logger.error("Event has no start. Cannot create dateString.", { guildId, requestId })
|
logger.error("Event has no start. Cannot create dateString.", { guildId, requestId })
|
||||||
return `"habe keinen Startzeitpunkt ermitteln können"`
|
return `"habe keinen Startzeitpunkt ermitteln können"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeZone = 'Europe/Berlin'
|
const timeZone = 'Europe/Berlin'
|
||||||
const zonedDateTime = utcToZonedTime(event.scheduledStartAt, timeZone)
|
const zonedDateTime = utcToZonedTime(eventStartDate, timeZone)
|
||||||
const time = format(zonedDateTime, "HH:mm", { locale: de })
|
const time = format(zonedDateTime, "HH:mm", { locale: de })
|
||||||
|
|
||||||
if (isToday(zonedDateTime)) {
|
if (isToday(zonedDateTime)) {
|
||||||
return `heute um ${time}`
|
return `heute um ${time}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const date = format(zonedDateTime, "eeee dd.MM", { locale: de })
|
const date = format(zonedDateTime, "eeee dd.MM.", { locale: de })
|
||||||
return `am ${date} um ${time}`
|
return `am ${date} um ${time}`
|
||||||
}
|
}
|
||||||
|
20
server/helper/messageIdentifiers.ts
Normal file
20
server/helper/messageIdentifiers.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Message } from "discord.js";
|
||||||
|
|
||||||
|
|
||||||
|
// branded types to differentiate objects of identical Type but different contents
|
||||||
|
export type VoteEndMessage = Message<true> & { readonly __brand: 'voteend' }
|
||||||
|
export type AnnouncementMessage = Message<true> & { readonly __brand: 'announcement' }
|
||||||
|
export type VoteMessage = Message<true> & { readonly __brand: 'vote' }
|
||||||
|
|
||||||
|
export type KnownDiscordMessage = VoteMessage | VoteEndMessage | AnnouncementMessage
|
||||||
|
|
||||||
|
export function isVoteMessage(message: Message): message is VoteMessage {
|
||||||
|
return message.cleanContent.includes('[Abstimmung]')
|
||||||
|
}
|
||||||
|
export function isInitialAnnouncement(message: Message): message is AnnouncementMessage {
|
||||||
|
return message.cleanContent.includes("[initial]")
|
||||||
|
}
|
||||||
|
export function isVoteEndedMessage(message: Message): message is VoteEndMessage {
|
||||||
|
return message.cleanContent.includes("[Abstimmung beendet]")
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
|||||||
import { Collection, GuildMember } from "discord.js"
|
import { Collection, Guild, GuildMember, Role } from "discord.js"
|
||||||
import { ChangedRoles, PermissionLevel } from "../interfaces"
|
import { ChangedRoles, Maybe, PermissionLevel } from "../interfaces"
|
||||||
import { logger } from "../logger"
|
import { logger } from "../logger"
|
||||||
import { config } from "../configuration"
|
import { config } from "../configuration"
|
||||||
|
|
||||||
@ -16,6 +16,13 @@ export function filterRolesFromMemberUpdate(oldMember: GuildMember, newMember: G
|
|||||||
return { addedRoles, removedRoles }
|
return { addedRoles, removedRoles }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getMembersWithRoleFromGuild(roleId: string, guild: Guild): Promise<Collection<string, GuildMember>> {
|
||||||
|
const emptyResponse = new Collection<string, GuildMember>
|
||||||
|
const guildRole: Maybe<Role> = guild.roles.resolve(roleId)
|
||||||
|
if (!guildRole) return emptyResponse
|
||||||
|
return guildRole.members
|
||||||
|
}
|
||||||
|
|
||||||
export function getGuildSpecificTriggerRoleId(): Collection<string, PermissionLevel> {
|
export function getGuildSpecificTriggerRoleId(): Collection<string, PermissionLevel> {
|
||||||
const outVal = new Collection<string, PermissionLevel>()
|
const outVal = new Collection<string, PermissionLevel>()
|
||||||
outVal.set(config.bot.watcher_role, "VIEWER")
|
outVal.set(config.bot.watcher_role, "VIEWER")
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { CustomError, errorCodes } from "../interfaces"
|
import { CustomError, errorCodes } from "../interfaces"
|
||||||
|
import { logger } from "../logger"
|
||||||
import { ExtendedClient } from "../structures/client"
|
import { ExtendedClient } from "../structures/client"
|
||||||
|
|
||||||
export async function sendFailureDM(creatorMessage: string, client: ExtendedClient, creatorId?: string): Promise<void> {
|
export async function sendFailureDM(creatorMessage: string, client: ExtendedClient, creatorId?: string): Promise<void> {
|
||||||
if (!creatorId) throw new CustomError('No creator ID present', errorCodes.no_creator_id)
|
if (!creatorId) throw new CustomError('No creator ID present', errorCodes.no_creator_id)
|
||||||
const creator = await client.users.fetch(creatorId)
|
const creator = await client.users.fetch(creatorId)
|
||||||
console.log(`Creator ${JSON.stringify(creator)}`)
|
logger.info(`Creator ${JSON.stringify(creator)}`)
|
||||||
if (creator)
|
if (creator)
|
||||||
if (!creator.dmChannel)
|
if (!creator.dmChannel)
|
||||||
await creator.createDM()
|
await creator.createDM()
|
||||||
|
361
server/helper/vote.controller.ts
Normal file
361
server/helper/vote.controller.ts
Normal file
@ -0,0 +1,361 @@
|
|||||||
|
import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildScheduledEventSetStatusArg, GuildScheduledEventStatus, Message, MessageCreateOptions, MessageReaction, TextChannel } from "discord.js"
|
||||||
|
import { ValidVoteEmotes, NONE_OF_THAT } from "../constants"
|
||||||
|
import { logger, newRequestId } from "../logger"
|
||||||
|
import { getMembersWithRoleFromGuild } from "./roleFilter"
|
||||||
|
import { config } from "../configuration"
|
||||||
|
import { VoteMessage, isVoteEndedMessage, isVoteMessage } from "./messageIdentifiers"
|
||||||
|
import { createDateStringFromEvent } from "./dateHelper"
|
||||||
|
import { Maybe, 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"
|
||||||
|
import { ExtendedClient } from "../structures/client"
|
||||||
|
import { JellyfinHandler } from "../jellyfin/handler"
|
||||||
|
|
||||||
|
export type Vote = {
|
||||||
|
emote: string, //todo habs nicht hinbekommen hier Emotes zu nutzen
|
||||||
|
count: number,
|
||||||
|
movie: string
|
||||||
|
}
|
||||||
|
export type VoteMessageInfo = {
|
||||||
|
votes: Vote[],
|
||||||
|
event: GuildScheduledEvent,
|
||||||
|
}
|
||||||
|
export default class VoteController {
|
||||||
|
private client: ExtendedClient
|
||||||
|
private yavinJellyfinHandler: JellyfinHandler
|
||||||
|
|
||||||
|
public constructor(_client: ExtendedClient, _yavin: JellyfinHandler) {
|
||||||
|
this.client = _client
|
||||||
|
this.yavinJellyfinHandler = _yavin
|
||||||
|
}
|
||||||
|
|
||||||
|
public async handleNoneOfThatVote(messageReaction: MessageReaction, reactedUponMessage: VoteMessage, requestId: string, guildId: string) {
|
||||||
|
if (!messageReaction.message.guild) return 'No guild'
|
||||||
|
const guild = messageReaction.message.guild
|
||||||
|
logger.debug(`${reactedUponMessage.id} is vote message`, { requestId, guildId })
|
||||||
|
|
||||||
|
const watcherRoleMember = await getMembersWithRoleFromGuild(config.bot.announcement_role, messageReaction.message.guild)
|
||||||
|
logger.info("ROLE MEMBERS " + JSON.stringify(watcherRoleMember), { requestId, guildId })
|
||||||
|
|
||||||
|
const watcherRoleMemberCount = watcherRoleMember.size
|
||||||
|
logger.info(`MEMBER COUNT: ${watcherRoleMemberCount}`, { requestId, guildId })
|
||||||
|
|
||||||
|
const noneOfThatReactions = reactedUponMessage.reactions.cache.get(NONE_OF_THAT)?.users.cache.filter(x => x.id !== this.client.user?.id).size ?? 0
|
||||||
|
|
||||||
|
const memberThreshold = (watcherRoleMemberCount / 2)
|
||||||
|
logger.info(`Reroll ${noneOfThatReactions} > ${memberThreshold} ?`, { requestId, guildId })
|
||||||
|
if (noneOfThatReactions > memberThreshold)
|
||||||
|
logger.info(`No reroll`, { requestId, guildId })
|
||||||
|
else {
|
||||||
|
logger.info('Starting poll reroll', { requestId, guildId })
|
||||||
|
await this.handleReroll(reactedUponMessage, guild.id, requestId)
|
||||||
|
logger.info(`Finished handling NONE_OF_THAT vote`, { requestId, guildId })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async removeMessage(message: Message): Promise<Message<boolean>> {
|
||||||
|
if (message.pinned) {
|
||||||
|
await message.unpin()
|
||||||
|
}
|
||||||
|
return await message.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns true if a Vote object contains at least one vote
|
||||||
|
* @param {Vote} vote
|
||||||
|
*/
|
||||||
|
private hasAtLeastOneVote(vote: Vote): boolean {
|
||||||
|
// subtracting the bots initial vote
|
||||||
|
const overOneVote = (vote.count - 1) >= 1
|
||||||
|
logger.debug(`${vote.movie} : ${vote.count} -> above: ${overOneVote}`)
|
||||||
|
return overOneVote
|
||||||
|
}
|
||||||
|
|
||||||
|
public async generateRerollMovieList(voteInfo: VoteMessageInfo, guildId: string, requestId: string) {
|
||||||
|
if (config.bot.reroll_retains_top_picks) {
|
||||||
|
const votedOnMovies = voteInfo.votes.filter(this.hasAtLeastOneVote).filter(x => x.emote !== NONE_OF_THAT)
|
||||||
|
logger.info(`Found ${votedOnMovies.length} with votes`, { requestId, guildId })
|
||||||
|
const newMovieCount: number = config.bot.random_movie_count - votedOnMovies.length
|
||||||
|
logger.info(`Fetching ${newMovieCount} from jellyfin`)
|
||||||
|
const newMovies: string[] = await this.yavinJellyfinHandler.getRandomMovieNames(newMovieCount, guildId, requestId)
|
||||||
|
// merge
|
||||||
|
return newMovies.concat(votedOnMovies.map(x => x.movie))
|
||||||
|
} else {
|
||||||
|
// get movies from jellyfin to fill the remaining slots
|
||||||
|
const newMovieCount: number = config.bot.random_movie_count
|
||||||
|
logger.info(`Fetching ${newMovieCount} from jellyfin`)
|
||||||
|
return await this.yavinJellyfinHandler.getRandomMovieNames(newMovieCount, guildId, requestId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async handleReroll(voteMessage: VoteMessage, guildId: string, requestId: string) {
|
||||||
|
// get the movies currently being voted on, their votes, the eventId and its date
|
||||||
|
const voteInfo: VoteMessageInfo = await this.parseVoteInfoFromVoteMessage(voteMessage, requestId)
|
||||||
|
if (!voteInfo.event.scheduledStartAt) {
|
||||||
|
logger.info("Event does not have a start date, cancelling", { guildId: voteInfo.event.guildId, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let movies: string[] = await this.generateRerollMovieList(voteInfo, guildId, requestId)
|
||||||
|
|
||||||
|
const announcementChannel = this.client.getAnnouncementChannelForGuild(guildId)
|
||||||
|
if (!announcementChannel) {
|
||||||
|
logger.error(`No announcementChannel found for ${guildId}, can't post poll`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
logger.info(`Trying to remove old vote Message`, { requestId, guildId })
|
||||||
|
this.removeMessage(voteMessage)
|
||||||
|
} catch (err) {
|
||||||
|
// TODO: integrate failure DM to media Admin to inform about inability to delete old message
|
||||||
|
logger.error(`Error during removeMessage: ${err}`)
|
||||||
|
}
|
||||||
|
const sentMessage = this.prepareAndSendVoteMessage({
|
||||||
|
event: voteInfo.event,
|
||||||
|
movies,
|
||||||
|
announcementChannel,
|
||||||
|
startDate: voteInfo.event.scheduledStartAt,
|
||||||
|
pinAfterSending: true
|
||||||
|
},
|
||||||
|
guildId,
|
||||||
|
requestId)
|
||||||
|
logger.debug(`Sent reroll message: ${JSON.stringify(sentMessage)}`, { requestId, guildId })
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchEventByEventId(guild: Guild, eventId: string, requestId: string): Promise<Maybe<GuildScheduledEvent>> {
|
||||||
|
const guildEvent: GuildScheduledEvent = await guild.scheduledEvents.fetch(eventId)
|
||||||
|
if (!guildEvent) logger.error(`GuildScheduledEvent with id${eventId} could not be found`, { requestId, guildId: guild.id })
|
||||||
|
return guildEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
public async parseVoteInfoFromVoteMessage(message: VoteMessage, requestId: string): Promise<VoteMessageInfo> {
|
||||||
|
const lines = message.cleanContent.split('\n')
|
||||||
|
let parsedIds = this.parseGuildIdAndEventIdFromWholeMessage(message.cleanContent)
|
||||||
|
|
||||||
|
if (!message.guild)
|
||||||
|
throw new Error(`Message ${message.id} not a guild message`)
|
||||||
|
|
||||||
|
const event: Maybe<GuildScheduledEvent> = await this.fetchEventByEventId(message.guild, parsedIds.eventId, requestId)
|
||||||
|
|
||||||
|
let votes: Vote[] = []
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.slice(0, 5).includes(':')) {
|
||||||
|
const splitLine = line.split(":")
|
||||||
|
const [emoji, movie] = splitLine
|
||||||
|
const fetchedVoteFromMessage = message.reactions.cache.get(emoji)
|
||||||
|
if (fetchedVoteFromMessage) {
|
||||||
|
if (emoji === NONE_OF_THAT) {
|
||||||
|
votes.push({ movie: NONE_OF_THAT, emote: NONE_OF_THAT, count: fetchedVoteFromMessage.count })
|
||||||
|
} else
|
||||||
|
votes.push({ movie: movie.trim(), emote: emoji, count: fetchedVoteFromMessage.count })
|
||||||
|
} else {
|
||||||
|
logger.error(`No vote reaction found for movie, assuming 0`, requestId)
|
||||||
|
votes.push({ movie, emote: emoji, count: 0 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return <VoteMessageInfo>{ event, votes }
|
||||||
|
}
|
||||||
|
public parseEventDateFromMessage(message: string, guildId: string, requestId: string): Date {
|
||||||
|
logger.warn(`Falling back to RegEx parsing to get Event Date`, { guildId, requestId })
|
||||||
|
const datematcher = RegExp(/((?:0[1-9]|[12][0-9]|3[01])\.(?:0[1-9]|1[012])\.)(?:\ um\ )((?:(?:[01][0-9]|[2][0-3])\:[0-5][0-9])|(?:[2][4]\:00))!/i)
|
||||||
|
const result: RegExpMatchArray | null = message.match(datematcher)
|
||||||
|
const timeFromResult = result?.at(-1)
|
||||||
|
const dateFromResult = result?.at(1)?.concat(format(new Date(), 'yyyy')).concat(" " + timeFromResult) ?? ""
|
||||||
|
return new Date(dateFromResult)
|
||||||
|
}
|
||||||
|
public parseGuildIdAndEventIdFromWholeMessage(message: string) {
|
||||||
|
const idmatch = RegExp(/(?:http|https):\/\/discord\.com\/events\/(\d*)\/(\d*)/)
|
||||||
|
const matches = message.match(idmatch)
|
||||||
|
if (matches && matches.length == 3)
|
||||||
|
return { guildId: matches[1], eventId: matches[2] }
|
||||||
|
throw Error(`Could not find eventId in Vote Message`)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async prepareAndSendVoteMessage(inputInfo: prepareVoteMessageInput, guildId: string, requestId: string) {
|
||||||
|
const messageText = this.createVoteMessageText(inputInfo.event, inputInfo.movies, guildId, requestId)
|
||||||
|
const sentMessage = await this.sendVoteMessage(messageText, inputInfo.movies.length, inputInfo.announcementChannel)
|
||||||
|
if (inputInfo.pinAfterSending)
|
||||||
|
sentMessage.pin()
|
||||||
|
return sentMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
public createVoteMessageText(event: GuildScheduledEvent, movies: string[], guildId: string, requestId: string): string {
|
||||||
|
let message = `[Abstimmung] für https://discord.com/events/${guildId}/${event.id} \n<@&${config.bot.announcement_role}> Es gibt eine neue Abstimmung für die nächste Watchparty ${createDateStringFromEvent(event.scheduledStartAt, guildId, requestId)}! Stimme hierunter für den nächsten Film ab!\n`
|
||||||
|
|
||||||
|
for (let i = 0; i < movies.length; i++) {
|
||||||
|
message = message.concat(ValidVoteEmotes[i]).concat(": ").concat(movies[i]).concat("\n")
|
||||||
|
}
|
||||||
|
message = message.concat(NONE_OF_THAT).concat(": Wenn dir nichts davon gefällt.")
|
||||||
|
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Refactor into separate message controller
|
||||||
|
public async sendVoteMessage(messageText: string, movieCount: number, announcementChannel: TextChannel) {
|
||||||
|
|
||||||
|
const options: MessageCreateOptions = {
|
||||||
|
allowedMentions: { parse: ["roles"] },
|
||||||
|
content: messageText,
|
||||||
|
}
|
||||||
|
|
||||||
|
const sentMessage: Message<true> = await (await announcementChannel.fetch()).send(options)
|
||||||
|
|
||||||
|
for (let i = 0; i < movieCount; i++) {
|
||||||
|
sentMessage.react(ValidVoteEmotes[i])
|
||||||
|
}
|
||||||
|
sentMessage.react(NONE_OF_THAT)
|
||||||
|
|
||||||
|
return sentMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
public async closePoll(guild: Guild, requestId: string) {
|
||||||
|
const guildId = guild.id
|
||||||
|
logger.info("stopping poll", { guildId, requestId })
|
||||||
|
|
||||||
|
const announcementChannel: Maybe<TextChannel> = this.client.getAnnouncementChannelForGuild(guildId)
|
||||||
|
if (!announcementChannel) {
|
||||||
|
logger.error("Could not find the textchannel. Unable to close poll.", { guildId, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages: Message<true>[] = (await announcementChannel.messages.fetch()) //todo: fetch only pinned messages
|
||||||
|
.map((value) => value)
|
||||||
|
.filter(message => !isVoteEndedMessage(message) && isVoteMessage(message))
|
||||||
|
.sort((a, b) => b.createdTimestamp - a.createdTimestamp)
|
||||||
|
|
||||||
|
if (!messages || messages.length <= 0) {
|
||||||
|
logger.info("Could not find any vote messages. Cancelling pollClose", { guildId, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastMessage: Message<true> = messages[0]
|
||||||
|
|
||||||
|
if (!isVoteMessage(lastMessage)) {
|
||||||
|
logger.error(`Found message that is not a vote message, can't proceed`, { guildId, requestId })
|
||||||
|
logger.debug(`Found messages: ${JSON.stringify(messages, null, 2)}`, { guildId, requestId })
|
||||||
|
logger.debug(`Last message: ${JSON.stringify(lastMessage, null, 2)}`, { guildId, requestId })
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const votes = (await this.getVotesByEmote(lastMessage, guildId, requestId))
|
||||||
|
.sort((a, b) => b.count - a.count)
|
||||||
|
|
||||||
|
logger.debug(`votes: ${JSON.stringify(votes, null, 2)}`, { guildId, requestId })
|
||||||
|
|
||||||
|
logger.info("Deleting vote message")
|
||||||
|
lastMessage.unpin()
|
||||||
|
await lastMessage.delete()
|
||||||
|
const event = await this.getOpenPollEvent(guild, guild.id, requestId)
|
||||||
|
if (event && votes?.length > 0) {
|
||||||
|
this.updateOpenPollEventWithVoteResults(event, votes, guild, guildId, requestId)
|
||||||
|
this.sendVoteClosedMessage(event, votes[0].movie, guildId, requestId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* gets votes for the movies without the NONE_OF_THAT votes
|
||||||
|
*/
|
||||||
|
public async getVotesByEmote(message: VoteMessage, guildId: string, requestId: string): Promise<Vote[]> {
|
||||||
|
const votes: Vote[] = []
|
||||||
|
logger.debug(`Number of items in emotes: ${Object.values(ValidVoteEmotes).length}`, { guildId, requestId })
|
||||||
|
for (let i = 0; i < Object.keys(ValidVoteEmotes).length / 2; i++) {
|
||||||
|
const emote = ValidVoteEmotes[i]
|
||||||
|
logger.debug(`Getting reaction for emote ${emote}`, { guildId, requestId })
|
||||||
|
const reaction = message.reactions.resolve(emote)
|
||||||
|
logger.debug(`Reaction for emote ${emote}: ${JSON.stringify(reaction, null, 2)}`, { guildId, requestId })
|
||||||
|
if (reaction) {
|
||||||
|
const vote: Vote = { emote: emote, count: reaction.count, movie: this.extractMovieFromMessageByEmote(message, emote) }
|
||||||
|
votes.push(vote)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return votes
|
||||||
|
}
|
||||||
|
public async getOpenPollEvent(guild: Guild, guildId: string, requestId: string): Promise<Maybe<GuildScheduledEvent>> {
|
||||||
|
const voteEvents = (await guild.scheduledEvents.fetch())
|
||||||
|
.map((value) => value)
|
||||||
|
.filter(event => event.name.toLowerCase().includes("voting offen"))
|
||||||
|
logger.debug(`Found events: ${JSON.stringify(voteEvents, null, 2)}`, { guildId, requestId })
|
||||||
|
|
||||||
|
if (!voteEvents || voteEvents.length <= 0) {
|
||||||
|
logger.error("Could not find an open vote event.", { guildId, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return voteEvents[0]
|
||||||
|
}
|
||||||
|
public async updateOpenPollEventWithVoteResults(voteEvent: GuildScheduledEvent, votes: Vote[], guild: Guild, guildId: string, requestId: string) {
|
||||||
|
logger.info(`Updating event with movie ${votes[0].movie}.`, { guildId, requestId })
|
||||||
|
const options: GuildScheduledEventEditOptions<GuildScheduledEventStatus.Scheduled, GuildScheduledEventSetStatusArg<GuildScheduledEventStatus.Scheduled>> = {
|
||||||
|
name: votes[0].movie,
|
||||||
|
description: `!wp\nNummer 2: ${votes[1].movie} mit ${votes[1].count - 1} Stimmen\nNummer 3: ${votes[2].movie} mit ${votes[2].count - 1} Stimmen`
|
||||||
|
}
|
||||||
|
logger.debug(`Updating event: ${JSON.stringify(voteEvent, null, 2)}`, { guildId, requestId })
|
||||||
|
logger.info("Updating event.", { guildId, requestId })
|
||||||
|
voteEvent.edit(options)
|
||||||
|
}
|
||||||
|
public async sendVoteClosedMessage(event: GuildScheduledEvent, movie: string, guildId: string, requestId: string): Promise<Message<boolean>> {
|
||||||
|
const date = event.scheduledStartAt ? format(event.scheduledStartAt, "dd.MM.") : "Fehler, Event hatte kein Datum"
|
||||||
|
const time = event.scheduledStartAt ? format(event.scheduledStartAt, "HH:mm") : "Fehler, Event hatte keine Uhrzeit"
|
||||||
|
const body = `[Abstimmung beendet] für https://discord.com/events/${event.guildId}/${event.id}\n<@&${config.bot.announcement_role}> Wir gucken ${movie} am ${date} um ${time}`
|
||||||
|
const options: MessageCreateOptions = {
|
||||||
|
content: body,
|
||||||
|
allowedMentions: { parse: ["roles"] }
|
||||||
|
}
|
||||||
|
const announcementChannel = this.client.getAnnouncementChannelForGuild(guildId)
|
||||||
|
logger.info("Sending vote closed message.", { guildId, requestId })
|
||||||
|
if (!announcementChannel) {
|
||||||
|
const errorMessageText = "Could not find announcement channel. Please fix!"
|
||||||
|
logger.error(errorMessageText, { guildId, requestId })
|
||||||
|
throw errorMessageText
|
||||||
|
}
|
||||||
|
return announcementChannel.send(options)
|
||||||
|
}
|
||||||
|
private extractMovieFromMessageByEmote(voteMessage: VoteMessage, emote: string): string {
|
||||||
|
const lines = voteMessage.cleanContent.split("\n")
|
||||||
|
const emoteLines = lines.filter(line => line.includes(emote))
|
||||||
|
|
||||||
|
if (!emoteLines) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
const movie = emoteLines[0].substring(emoteLines[0].indexOf(emote) + emote.length + 2) // plus colon and space
|
||||||
|
|
||||||
|
return movie
|
||||||
|
}
|
||||||
|
public async checkForPollsToClose(guild: Guild): Promise<void> {
|
||||||
|
const requestId = newRequestId()
|
||||||
|
logger.info(`Automatic check for poll closing.`, { guildId: guild.id, requestId })
|
||||||
|
const events = (await guild.scheduledEvents.fetch()).filter(event => event.name.toLocaleLowerCase().includes("voting offen")).map(event => event)
|
||||||
|
if (events.length > 1) {
|
||||||
|
logger.error("Handling more than one Event is not implemented yet. Found more than one poll to close")
|
||||||
|
return
|
||||||
|
} else if (events.length == 0) {
|
||||||
|
logger.info("Could not find any events. Cancelling", { guildId: guild.id, requestId })
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedEvent = events[0] //add two hours because of different timezones in discord api and Date.now()
|
||||||
|
if (!updatedEvent.scheduledStartTimestamp) {
|
||||||
|
logger.error("Event does not have a scheduled start time. Cancelling", { guildId: guild.id, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const createDate: Date = toDate(updatedEvent.createdTimestamp)
|
||||||
|
const eventDate: Date = toDate(updatedEvent.scheduledStartTimestamp)
|
||||||
|
const difference: number = differenceInDays(createDate, eventDate)
|
||||||
|
|
||||||
|
if (difference <= 2) {
|
||||||
|
logger.info("Less than two days between event create and event start. Not closing poll.", { guildId: guild.id, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const closePollDate: Date = addDays(eventDate, -2)
|
||||||
|
|
||||||
|
if (isAfter(Date.now(), closePollDate)) {
|
||||||
|
logger.info("Less than two days until event. Closing poll", { guildId: guild.id, requestId })
|
||||||
|
this.closePoll(guild, requestId)
|
||||||
|
} else {
|
||||||
|
logger.info(`ScheduledStart: ${closePollDate}. Now: ${toDate(Date.now())}`, { guildId: guild.id, requestId })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import { Collection } from "@discordjs/collection"
|
import { Collection } from "@discordjs/collection"
|
||||||
import { Role } from "discord.js"
|
import { GuildScheduledEvent, Role, TextChannel } from "discord.js"
|
||||||
|
|
||||||
export type Maybe<T> = T | undefined | null
|
export type Maybe<T> = T | undefined | null
|
||||||
export interface Player {
|
export interface Player {
|
||||||
@ -39,3 +39,10 @@ export interface JellyfinConfig {
|
|||||||
collectionUser: string
|
collectionUser: string
|
||||||
}
|
}
|
||||||
export type PermissionLevel = "VIEWER" | "ADMIN" | "TEMPORARY"
|
export type PermissionLevel = "VIEWER" | "ADMIN" | "TEMPORARY"
|
||||||
|
export interface 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 {
|
export class ResponseError extends Error {
|
||||||
override name: "ResponseError" = "ResponseError";
|
override name: "ResponseError" = "ResponseError";
|
||||||
constructor(public response: Response, msg?: string) {
|
constructor(public response: Response, errorMessage?: string) {
|
||||||
super(msg);
|
super(errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FetchError extends Error {
|
export class FetchError extends Error {
|
||||||
override name: "FetchError" = "FetchError";
|
override name: "FetchError" = "FetchError";
|
||||||
constructor(public cause: Error, msg?: string) {
|
constructor(public cause: Error, errorMessage?: string) {
|
||||||
super(msg);
|
super(errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RequiredError extends Error {
|
export class RequiredError extends Error {
|
||||||
override name: "RequiredError" = "RequiredError";
|
override name: "RequiredError" = "RequiredError";
|
||||||
constructor(public field: string, msg?: string) {
|
constructor(public field: string, errorMessage?: string) {
|
||||||
super(msg);
|
super(errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ export const noGuildId = 'NoGuildId'
|
|||||||
|
|
||||||
|
|
||||||
const printFn = format.printf(({ guildId, level, message, errorCode, requestId, timestamp: logTimestamp }: { [k: string]: string }) => {
|
const printFn = format.printf(({ guildId, level, message, errorCode, requestId, timestamp: logTimestamp }: { [k: string]: string }) => {
|
||||||
return `[${guildId ?? ''}][${level}][${logTimestamp}][${errorCode ?? ''}][${requestId ?? ''}]:${message}`
|
return `[${guildId ?? ''}][${level.padStart(5, " ")}][${logTimestamp}][${errorCode ?? ''}][${requestId ?? ''}]:${message}`
|
||||||
})
|
})
|
||||||
|
|
||||||
const logFormat = format.combine(
|
const logFormat = format.combine(
|
||||||
|
@ -8,7 +8,9 @@ import { Maybe } from "../interfaces";
|
|||||||
import { JellyfinHandler } from "../jellyfin/handler";
|
import { JellyfinHandler } from "../jellyfin/handler";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
import { CommandType } from "../types/commandTypes";
|
import { CommandType } from "../types/commandTypes";
|
||||||
import { checkForPollsToClose } from "../commands/closepoll";
|
import { isInitialAnnouncement } from "../helper/messageIdentifiers";
|
||||||
|
import VoteController from "../helper/vote.controller";
|
||||||
|
import { yavinJellyfinHandler } from "../..";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -16,13 +18,14 @@ export class ExtendedClient extends Client {
|
|||||||
private eventFilePath = `${__dirname}/../events`
|
private eventFilePath = `${__dirname}/../events`
|
||||||
private commandFilePath = `${__dirname}/../commands`
|
private commandFilePath = `${__dirname}/../commands`
|
||||||
private jellyfin: JellyfinHandler
|
private jellyfin: JellyfinHandler
|
||||||
|
public voteController: VoteController = new VoteController(this, yavinJellyfinHandler)
|
||||||
public commands: Collection<string, CommandType> = new Collection()
|
public commands: Collection<string, CommandType> = new Collection()
|
||||||
private announcementChannels: Collection<string, TextChannel> = new Collection() //guildId to TextChannel
|
private announcementChannels: Collection<string, TextChannel> = new Collection() //guildId to TextChannel
|
||||||
private announcementRoleHandlerTask: Collection<string, ScheduledTask> = new Collection() //one task per guild
|
private announcementRoleHandlerTask: Collection<string, ScheduledTask> = new Collection() //one task per guild
|
||||||
private pollCloseBackgroundTasks: Collection<string, ScheduledTask> = new Collection()
|
private pollCloseBackgroundTasks: Collection<string, ScheduledTask> = new Collection()
|
||||||
public constructor(jf: JellyfinHandler) {
|
public constructor(jf: JellyfinHandler) {
|
||||||
const intents: IntentsBitField = new IntentsBitField()
|
const intents: IntentsBitField = new IntentsBitField()
|
||||||
intents.add(IntentsBitField.Flags.GuildMembers, IntentsBitField.Flags.MessageContent, IntentsBitField.Flags.Guilds, IntentsBitField.Flags.DirectMessages, IntentsBitField.Flags.GuildScheduledEvents, IntentsBitField.Flags.GuildVoiceStates)
|
intents.add(IntentsBitField.Flags.GuildMembers, IntentsBitField.Flags.MessageContent, IntentsBitField.Flags.Guilds, IntentsBitField.Flags.DirectMessages, IntentsBitField.Flags.GuildScheduledEvents, IntentsBitField.Flags.GuildMessageReactions, IntentsBitField.Flags.GuildVoiceStates)
|
||||||
const options: ClientOptions = { intents }
|
const options: ClientOptions = { intents }
|
||||||
super(options)
|
super(options)
|
||||||
this.jellyfin = jf
|
this.jellyfin = jf
|
||||||
@ -74,6 +77,7 @@ export class ExtendedClient extends Client {
|
|||||||
this.registerCommands(slashCommands, guilds)
|
this.registerCommands(slashCommands, guilds)
|
||||||
this.cacheUsers(guilds)
|
this.cacheUsers(guilds)
|
||||||
await this.cacheAnnouncementServer(guilds)
|
await this.cacheAnnouncementServer(guilds)
|
||||||
|
this.fetchAnnouncementChannelMessage(this.announcementChannels)
|
||||||
this.startAnnouncementRoleBackgroundTask(guilds)
|
this.startAnnouncementRoleBackgroundTask(guilds)
|
||||||
this.startPollCloseBackgroundTasks()
|
this.startPollCloseBackgroundTasks()
|
||||||
})
|
})
|
||||||
@ -81,6 +85,21 @@ export class ExtendedClient extends Client {
|
|||||||
logger.info(`Error refreshing slash commands: ${error}`)
|
logger.info(`Error refreshing slash commands: ${error}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Fetches all messages from the provided channel collection.
|
||||||
|
* This is necessary for announcementChannels, because 'old' messages don't receive
|
||||||
|
* messageReactionAdd Events, only messages that were sent while the bot is online are tracked
|
||||||
|
* automatically.
|
||||||
|
* To prevent the need for a dedicated 'Collector' implementation which would listen on specific
|
||||||
|
* it's easiest to just fetch all messages from the backlog, which automatically makes the bot track them
|
||||||
|
* again.
|
||||||
|
* @param {Collection<string, TextChannel>} channels - All channels which should be fecthed for reactionTracking
|
||||||
|
*/
|
||||||
|
private async fetchAnnouncementChannelMessage(channels: Collection<string, TextChannel>): Promise<void> {
|
||||||
|
channels.each(async ch => {
|
||||||
|
ch.messages.fetch()
|
||||||
|
})
|
||||||
|
}
|
||||||
private async cacheAnnouncementServer(guilds: Collection<Snowflake, Guild>) {
|
private async cacheAnnouncementServer(guilds: Collection<Snowflake, Guild>) {
|
||||||
for (const guild of guilds.values()) {
|
for (const guild of guilds.values()) {
|
||||||
const channels: TextChannel[] = <TextChannel[]>(await guild.channels.fetch())
|
const channels: TextChannel[] = <TextChannel[]>(await guild.channels.fetch())
|
||||||
@ -136,7 +155,7 @@ export class ExtendedClient extends Client {
|
|||||||
}
|
}
|
||||||
this.announcementRoleHandlerTask.set(guild.id, schedule("*/10 * * * * *", async () => {
|
this.announcementRoleHandlerTask.set(guild.id, schedule("*/10 * * * * *", async () => {
|
||||||
const requestId = uuid()
|
const requestId = uuid()
|
||||||
const messages = (await textChannel.messages.fetchPinned()).filter(message => message.cleanContent.includes("[initial]"))
|
const messages = (await textChannel.messages.fetchPinned()).filter(message => isInitialAnnouncement(message))
|
||||||
|
|
||||||
if (messages.size > 1) {
|
if (messages.size > 1) {
|
||||||
logger.error("More than one pinned announcement Messages found. Unable to know which one people react to. Please fix!", { guildId: guild.id, requestId })
|
logger.error("More than one pinned announcement Messages found. Unable to know which one people react to. Please fix!", { guildId: guild.id, requestId })
|
||||||
@ -175,7 +194,7 @@ export class ExtendedClient extends Client {
|
|||||||
|
|
||||||
private async startPollCloseBackgroundTasks() {
|
private async startPollCloseBackgroundTasks() {
|
||||||
for (const guild of this.guilds.cache) {
|
for (const guild of this.guilds.cache) {
|
||||||
this.pollCloseBackgroundTasks.set(guild[1].id, schedule("0 * * * * *", () => checkForPollsToClose(guild[1])))
|
this.pollCloseBackgroundTasks.set(guild[1].id, schedule("0 * * * * *", () => this.voteController.checkForPollsToClose(guild[1])))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
81
tests/discord/noneofthat.test.ts
Normal file
81
tests/discord/noneofthat.test.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { Guild, GuildScheduledEvent, Message } from "discord.js"
|
||||||
|
import VoteController from "../../server/helper/vote.controller"
|
||||||
|
import { JellyfinHandler } from "../../server/jellyfin/handler"
|
||||||
|
import { ExtendedClient } from "../../server/structures/client"
|
||||||
|
import { Emoji, NONE_OF_THAT } from "../../server/constants"
|
||||||
|
import { isVoteMessage } from "../../server/helper/messageIdentifiers"
|
||||||
|
|
||||||
|
describe('vote controller - none_of_that functions', () => {
|
||||||
|
const testEventId = '1234321'
|
||||||
|
const testEventDate = new Date('2023-01-01')
|
||||||
|
const testGuildId = "888999888"
|
||||||
|
const testMovies = [
|
||||||
|
'Movie1',
|
||||||
|
'Movie2',
|
||||||
|
'Movie3',
|
||||||
|
'Movie4',
|
||||||
|
'Movie5',
|
||||||
|
]
|
||||||
|
const votesList = [
|
||||||
|
{ emote: Emoji.one, count: 1, movie: testMovies[0] },
|
||||||
|
{ emote: Emoji.two, count: 2, movie: testMovies[1] },
|
||||||
|
{ emote: Emoji.three, count: 3, movie: testMovies[2] },
|
||||||
|
{ emote: Emoji.four, count: 1, movie: testMovies[3] },
|
||||||
|
{ emote: Emoji.five, count: 1, movie: testMovies[4] },
|
||||||
|
{ emote: NONE_OF_THAT, count: 2, movie: NONE_OF_THAT },
|
||||||
|
]
|
||||||
|
const mockClient: ExtendedClient = <ExtendedClient><unknown>{
|
||||||
|
user: {
|
||||||
|
id: 'mockId'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const mockEvent: GuildScheduledEvent = <GuildScheduledEvent><unknown>{
|
||||||
|
scheduledStartAt: testEventDate,
|
||||||
|
id: testEventId,
|
||||||
|
guild: testGuildId
|
||||||
|
}
|
||||||
|
const mockJellyfinHandler: JellyfinHandler = <JellyfinHandler><unknown>{
|
||||||
|
getRandomMovieNames: jest.fn().mockReturnValue(["movie1"])
|
||||||
|
}
|
||||||
|
const votes = new VoteController(mockClient, mockJellyfinHandler)
|
||||||
|
const mockMessageContent = votes.createVoteMessageText(mockEvent, testMovies, testGuildId, "requestId")
|
||||||
|
|
||||||
|
test('sendVoteClosedMessage', async () => {
|
||||||
|
mockClient.getAnnouncementChannelForGuild = jest.fn().mockReturnValue({
|
||||||
|
send: jest.fn().mockImplementation((options: any) => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
resolve(options)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const scheduledEvent: GuildScheduledEvent = <GuildScheduledEvent>{
|
||||||
|
scheduledStartAt: testEventDate,
|
||||||
|
guildId: testGuildId,
|
||||||
|
id: testEventId
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await votes.sendVoteClosedMessage(scheduledEvent, 'MovieNew', 'guild', 'request')
|
||||||
|
expect(res).toEqual({
|
||||||
|
allowedMentions: {
|
||||||
|
parse: ["roles"]
|
||||||
|
},
|
||||||
|
content: `[Abstimmung beendet] für https://discord.com/events/${testGuildId}/${testEventId}\n<@&WATCHPARTY_ANNOUNCEMENT_ROLE> Wir gucken MovieNew am 01.01. um 01:00`
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('getVotesByEmote', async () => {
|
||||||
|
const mockMessage: Message = <Message><unknown>{
|
||||||
|
cleanContent: mockMessageContent,
|
||||||
|
reactions: {
|
||||||
|
resolve: jest.fn().mockImplementation((input: any) => {
|
||||||
|
return votesList.find(e => e.emote === input)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isVoteMessage(mockMessage)) {
|
||||||
|
const result = await votes.getVotesByEmote(mockMessage, 'guildId', 'requestId')
|
||||||
|
expect(result.length).toEqual(5)
|
||||||
|
expect(result).toEqual(votesList.filter(x => x.movie != NONE_OF_THAT))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
192
tests/discord/votes.test.ts
Normal file
192
tests/discord/votes.test.ts
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
import { Emoji, NONE_OF_THAT } from "../../server/constants"
|
||||||
|
import VoteController, { VoteMessageInfo } from "../../server/helper/vote.controller"
|
||||||
|
import { JellyfinHandler } from "../../server/jellyfin/handler"
|
||||||
|
import { ExtendedClient } from "../../server/structures/client"
|
||||||
|
import { VoteMessage } from "../../server/helper/messageIdentifiers"
|
||||||
|
import { GuildScheduledEvent, MessageReaction } from "discord.js"
|
||||||
|
test('parse votes from vote message', async () => {
|
||||||
|
const testMovies = [
|
||||||
|
'Movie1',
|
||||||
|
'Movie2',
|
||||||
|
'Movie3',
|
||||||
|
'Movie4',
|
||||||
|
'Movie5',
|
||||||
|
]
|
||||||
|
const testEventId = '1234321'
|
||||||
|
const testEventDate = new Date('2023-01-01')
|
||||||
|
const testGuildId = "888999888"
|
||||||
|
const voteController: VoteController = new VoteController(<ExtendedClient>{}, <JellyfinHandler>{})
|
||||||
|
const mockEvent: GuildScheduledEvent = <GuildScheduledEvent><unknown>{
|
||||||
|
scheduledStartAt: testEventDate,
|
||||||
|
id: testEventId,
|
||||||
|
guild: testGuildId
|
||||||
|
}
|
||||||
|
const testMessage = voteController.createVoteMessageText(mockEvent, testMovies, testGuildId, "requestId")
|
||||||
|
|
||||||
|
|
||||||
|
const expectedResult: VoteMessageInfo = {
|
||||||
|
event: mockEvent,
|
||||||
|
votes: [
|
||||||
|
{ emote: Emoji.one, count: 1, movie: testMovies[0] },
|
||||||
|
{ emote: Emoji.two, count: 2, movie: testMovies[1] },
|
||||||
|
{ emote: Emoji.three, count: 3, movie: testMovies[2] },
|
||||||
|
{ emote: Emoji.four, count: 1, movie: testMovies[3] },
|
||||||
|
{ emote: Emoji.five, count: 1, movie: testMovies[4] },
|
||||||
|
{ emote: NONE_OF_THAT, count: 1, movie: NONE_OF_THAT },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const message: VoteMessage = <VoteMessage><unknown>{
|
||||||
|
cleanContent: testMessage,
|
||||||
|
guild: {
|
||||||
|
id: testGuildId,
|
||||||
|
scheduledEvents: {
|
||||||
|
fetch: jest.fn().mockImplementation((input: any) => {
|
||||||
|
if (input === testEventId)
|
||||||
|
return {
|
||||||
|
id: testEventId,
|
||||||
|
guild: testGuildId,
|
||||||
|
scheduledStartAt: testEventDate
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reactions: {
|
||||||
|
cache: {
|
||||||
|
get: jest.fn().mockImplementation((input: any) => {
|
||||||
|
// Abusing duck typing
|
||||||
|
// Message Reaction has a method `count` and the expected votes
|
||||||
|
// have a field `count`
|
||||||
|
// this will evaluate to the same 'result'
|
||||||
|
return expectedResult.votes.find(e => e.emote === input)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await voteController.parseVoteInfoFromVoteMessage(message, 'requestId')
|
||||||
|
console.log(JSON.stringify(result))
|
||||||
|
expect(Array.isArray(result)).toBe(false)
|
||||||
|
expect(result.event.id).toEqual(testEventId)
|
||||||
|
expect(result.event.scheduledStartAt).toEqual(testEventDate)
|
||||||
|
expect(result.votes.length).toEqual(expectedResult.votes.length)
|
||||||
|
expect(result).toEqual(expectedResult)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('parse votes from vote message', () => {
|
||||||
|
const testMovies = [
|
||||||
|
'Movie1',
|
||||||
|
'Movie2',
|
||||||
|
'Movie3',
|
||||||
|
'Movie4',
|
||||||
|
'Movie5',
|
||||||
|
]
|
||||||
|
const testEventId = '1234321'
|
||||||
|
const testEventDate = new Date('2023-01-01')
|
||||||
|
const testGuildId = "888999888"
|
||||||
|
const voteController: VoteController = new VoteController(<ExtendedClient>{}, <JellyfinHandler>{})
|
||||||
|
const mockEvent: GuildScheduledEvent = <GuildScheduledEvent><unknown>{
|
||||||
|
scheduledStartAt: testEventDate,
|
||||||
|
id: testEventId,
|
||||||
|
guild: testGuildId
|
||||||
|
}
|
||||||
|
const testMessage = voteController.createVoteMessageText(mockEvent, testMovies, testGuildId, "requestId")
|
||||||
|
|
||||||
|
const result = voteController.parseGuildIdAndEventIdFromWholeMessage(testMessage)
|
||||||
|
expect(result).toEqual({ guildId: testGuildId, eventId: testEventId })
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
test.skip('handles complete none_of_that vote', () => {
|
||||||
|
|
||||||
|
const mockJellyfinHandler: JellyfinHandler = <JellyfinHandler><unknown>{
|
||||||
|
getRandomMovieNames: jest.fn().mockReturnValue(["movie1"])
|
||||||
|
}
|
||||||
|
|
||||||
|
const testMovies = [
|
||||||
|
'Movie1',
|
||||||
|
'Movie2',
|
||||||
|
'Movie3',
|
||||||
|
'Movie4',
|
||||||
|
'Movie5',
|
||||||
|
]
|
||||||
|
const testEventId = '1234321'
|
||||||
|
const testEventDate = new Date('2023-01-01')
|
||||||
|
const testGuildId = "888999888"
|
||||||
|
const mockClient: ExtendedClient = <ExtendedClient><unknown>{
|
||||||
|
user: {
|
||||||
|
id: 'mockId'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const voteController = new VoteController(mockClient, mockJellyfinHandler)
|
||||||
|
const mockEvent: GuildScheduledEvent = <GuildScheduledEvent><unknown>{
|
||||||
|
scheduledStartAt: testEventDate,
|
||||||
|
id: testEventId,
|
||||||
|
guild: testGuildId
|
||||||
|
}
|
||||||
|
const mockMessageContent = voteController.createVoteMessageText(mockEvent, testMovies, testGuildId, "requestId")
|
||||||
|
const reactedUponMessage: VoteMessage = <VoteMessage><unknown>{
|
||||||
|
cleanContent: mockMessageContent,
|
||||||
|
guild: {
|
||||||
|
id: 'id',
|
||||||
|
roles: {
|
||||||
|
resolve: jest.fn().mockReturnValue({
|
||||||
|
members: [{}, {}, {}, {}, {}]//content does not matter
|
||||||
|
})
|
||||||
|
},
|
||||||
|
scheduledEvents: {
|
||||||
|
fetch: jest.fn().mockReturnValue([
|
||||||
|
{
|
||||||
|
name: 'voting offen'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
unpin: jest.fn().mockImplementation(() => {
|
||||||
|
|
||||||
|
}),
|
||||||
|
delete: jest.fn().mockImplementation(() => {
|
||||||
|
|
||||||
|
}),
|
||||||
|
reactions: {
|
||||||
|
resolve: jest.fn().mockImplementation((input: any) => {
|
||||||
|
console.log(JSON.stringify(input))
|
||||||
|
}),
|
||||||
|
cache: {
|
||||||
|
get: jest.fn().mockReturnValue({
|
||||||
|
users: {
|
||||||
|
cache: [
|
||||||
|
{
|
||||||
|
id: "mockId"//to filter out
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "userId1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "userId2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "userId3"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const messageReaction: MessageReaction = <MessageReaction><unknown>{
|
||||||
|
message: reactedUponMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
mockClient.getAnnouncementChannelForGuild = jest.fn().mockReturnValue({
|
||||||
|
messages: {
|
||||||
|
fetch: jest.fn().mockReturnValue([
|
||||||
|
reactedUponMessage
|
||||||
|
])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = voteController.handleNoneOfThatVote(messageReaction, reactedUponMessage, 'requestId', 'guildId')
|
||||||
|
|
||||||
|
|
||||||
|
})
|
@ -1,4 +1,3 @@
|
|||||||
import { GuildScheduledEvent } from "discord.js"
|
|
||||||
import { createDateStringFromEvent } from "../../server/helper/dateHelper"
|
import { createDateStringFromEvent } from "../../server/helper/dateHelper"
|
||||||
import MockDate from 'mockdate'
|
import MockDate from 'mockdate'
|
||||||
|
|
||||||
@ -6,11 +5,11 @@ beforeAll(() => {
|
|||||||
MockDate.set('01-01-2023')
|
MockDate.set('01-01-2023')
|
||||||
})
|
})
|
||||||
|
|
||||||
function getTestDate(date: string): GuildScheduledEvent {
|
function getTestDate(date: string): Date {
|
||||||
return <GuildScheduledEvent>{ scheduledStartAt: new Date(date) }
|
return new Date(date)
|
||||||
}
|
}
|
||||||
test('createDateStringFromEvent - correct formatting', () => {
|
test('createDateStringFromEvent - correct formatting', () => {
|
||||||
expect(createDateStringFromEvent(getTestDate('01-01-2023 12:30'), "")).toEqual('heute um 12:30')
|
expect(createDateStringFromEvent(getTestDate('01-01-2023 12:30'), "")).toEqual('heute um 12:30')
|
||||||
expect(createDateStringFromEvent(getTestDate('01-02-2023 12:30'), "")).toEqual('am Montag 02.01 um 12:30')
|
expect(createDateStringFromEvent(getTestDate('01-02-2023 12:30'), "")).toEqual('am Montag 02.01. um 12:30')
|
||||||
expect(createDateStringFromEvent(getTestDate('01-03-2023 12:30'), "")).toEqual('am Dienstag 03.01 um 12:30')
|
expect(createDateStringFromEvent(getTestDate('01-03-2023 12:30'), "")).toEqual('am Dienstag 03.01. um 12:30')
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user