Compare commits

...

14 Commits

Author SHA1 Message Date
8caf80f54e Merge pull request 'announcements' (#18) from feat/announce into master
All checks were successful
Build a docker image for node-jellyfin-role-bot / build-docker-image (push) Successful in 10s
Reviewed-on: #18
2023-06-15 22:05:20 +02:00
1ccb1a7cae Merge branch 'feat/announce' of ssh://gitea.brudi.xyz:222/kenobi/jellyfin-discord-bot into feat/announce
All checks were successful
Build a docker image for node-jellyfin-role-bot / build-docker-image (push) Successful in 1m26s
2023-06-15 22:02:49 +02:00
d22e38efbf fix build 2023-06-15 22:02:37 +02:00
68662e72ad Merge branch 'master' into feat/announce
Some checks failed
Build a docker image for node-jellyfin-role-bot / build-docker-image (push) Failing after 1m1s
2023-06-15 21:59:25 +02:00
5b99c843b4 Fix PR and linting issues 2023-06-15 21:56:15 +02:00
9420eb4366 Change announcements
All announcements but initial will be deleted upon event end.
Vote announcement will be deleted upon vote end
Vote and vote end announcement now contain date and time
2023-06-14 22:24:39 +02:00
220f9dc8ef undo fetching role to get roleID
We already have the role id?!?!?
2023-06-14 19:45:33 +02:00
198a25d145 ping watch role when voting starts and closes 2023-06-13 23:15:03 +02:00
baefcf9bb9 add options for announcements 2023-06-13 21:12:32 +02:00
a5eab2f7be fix bug that reactions are not loaded after restart
the message needed to be fetched again. Probably something with caches..
2023-06-13 20:13:13 +02:00
e774474a55 Put role handling in background task scheduled at startup 2023-06-13 18:58:41 +02:00
24754decf4 implement announcement role management by reaction 2023-06-13 18:18:26 +02:00
a2c55ad676 restrict announcements to admins 2023-06-12 22:34:39 +02:00
e50cb10c5b implement slash command for announcement 2023-06-12 22:21:10 +02:00
12 changed files with 291 additions and 79 deletions

121
server/commands/announce.ts Normal file
View File

@ -0,0 +1,121 @@
import { ApplicationCommandOptionType, Guild, GuildMember, Message, MessageCreateOptions, MessageReaction, Role, TextChannel, User } from 'discord.js'
import { v4 as uuid } from 'uuid'
import { client } from '../..'
import { config } from '../configuration'
import { Maybe } from '../interfaces'
import { logger } from '../logger'
import { Command } from '../structures/command'
import { RunOptions } from '../types/commandTypes'
export default new Command({
name: 'announce',
description: 'Neues announcement im announcement Channel an alle senden.',
options: [{
name: "typ",
type: ApplicationCommandOptionType.String,
description:"Was für ein announcement?",
choices: [{name: "initial", value:"initial"},{name: "votepls", value:"votepls"},{name: "cancel", value:"cancel"}],
required: true
}],
run: async (interaction: RunOptions) => {
const command = interaction.interaction
const requestId = uuid()
if(!command.guildId) {
logger.error("COMMAND DOES NOT HAVE A GUILD ID; CANCELLING!!!", {requestId})
return
}
const guildId = command.guildId
const announcementType = command.options.data.find(option => option.name.includes("typ"))
logger.info(`Got command for announcing ${announcementType?.value}!`, { guildId, requestId })
if(!announcementType) {
logger.error("Did not get an announcement type!", { guildId, requestId })
return
}
if (!isAdmin(command.member)) {
logger.info(`Announcement was requested by ${command.member.displayName} but they are not an admin! Not sending announcement.`, { guildId, requestId })
return
} else {
logger.info(`User ${command.member.displayName} seems to be admin`)
}
if((<string>announcementType.value).includes("initial")) {
sendInitialAnnouncement(guildId, requestId)
command.followUp("Ist rausgeschickt!")
} else {
command.followUp(`${announcementType.value} ist aktuell noch nicht implementiert`)
}
}
})
function isAdmin(member: GuildMember): boolean {
return member.roles.cache.find((role) => role.id === config.bot.jf_admin_role) !== undefined
}
async function sendInitialAnnouncement(guildId: string, requestId: string): Promise<void> {
logger.info("Sending initial announcement")
const announcementChannel: Maybe<TextChannel> = client.getAnnouncementChannelForGuild(guildId)
if(!announcementChannel) {
logger.error("Could not find announcement channel. Aborting", { guildId, requestId })
return
}
const currentPinnedAnnouncementMessages = (await announcementChannel.messages.fetchPinned()).filter(message => message.cleanContent.includes("[initial]"))
currentPinnedAnnouncementMessages.forEach(async (message) => await message.unpin())
currentPinnedAnnouncementMessages.forEach(message => message.delete())
const body = `[initial] Hey! @everyone! Hier ist der Watchparty Bot vom Hartzarett.
Wir machen in Zukunft regelmäßig Watchparties! Falls du mitmachen möchtest, reagiere einfach auf diesen Post mit 🎫, dann bekommst du automatisch eine Rolle zugewiesen und wirst benachrichtigt sobald es in der Zukunft weitere Watchparties und Filme zum abstimmen gibt.
Für eine Erklärung wie das alles funktioniert mach einfach /mitgucken für eine lange Erklärung am Stück oder /guides wenn du auswählen möchtest wozu du Infos bekommst.`
const options: MessageCreateOptions = {
allowedMentions: { parse: ['everyone'] },
content: body
}
const message: Message<true> = await announcementChannel.send(options)
await message.react("🎫")
await message.pin()
}
export async function manageAnnouncementRoles(guild: Guild, reaction: MessageReaction, requestId: string) {
const guildId = guild.id
logger.info("Managing roles", { guildId, requestId })
const announcementRole: Role | undefined = (await guild.roles.fetch()).find(role => role.id === config.bot.announcement_role)
if (!announcementRole) {
logger.error(`Could not find announcement role! Aborting! Was looking for role with id: ${config.bot.announcement_role}`, { guildId, requestId })
return
}
const usersWhoWantRole: User[] = (await reaction.users.fetch()).filter(user => !user.bot).map(user => user)
const allUsers = (await guild.members.fetch())
const usersWhoHaveRole: GuildMember[] = allUsers
.filter(member=> member.roles.cache
.find(role => role.id === config.bot.announcement_role) !== undefined)
.map(member => member)
const usersWhoNeedRoleRevoked: GuildMember[] = usersWhoHaveRole
.filter(userWhoHas => !usersWhoWantRole.map(wanter => wanter.id).includes(userWhoHas.id))
const usersWhoDontHaveRole: GuildMember[] = allUsers
.filter(member => member.roles.cache
.find(role=> role.id === config.bot.announcement_role) === undefined)
.map(member => member)
const usersWhoNeedRole: GuildMember[] = usersWhoDontHaveRole
.filter(userWhoNeeds => usersWhoWantRole.map(wanter => wanter.id).includes(userWhoNeeds.id))
logger.debug(`Theses users will get the role removed: ${JSON.stringify(usersWhoNeedRoleRevoked)}`, {guildId, requestId})
logger.debug(`Theses users will get the role added: ${JSON.stringify(usersWhoNeedRole)}`, {guildId, requestId})
usersWhoNeedRoleRevoked.forEach(user => user.roles.remove(announcementRole))
usersWhoNeedRole.forEach(user => user.roles.add(announcementRole))
}

View File

@ -1,11 +1,13 @@
import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildScheduledEventSetStatusArg, GuildScheduledEventStatus, Message, MessageEditOptions, TextChannel } from 'discord.js' 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 { config } from '../configuration' import { config } from '../configuration'
import { Emotes } from '../events/guildScheduledEventCreate' import { Emotes } from '../events/guildScheduledEventCreate'
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 { client } from '../..' import { format } from 'date-fns'
import { Maybe } from '../interfaces'
export default new Command({ export default new Command({
name: 'closepoll', name: 'closepoll',
@ -14,13 +16,13 @@ export default new Command({
run: async (interaction: RunOptions) => { run: async (interaction: RunOptions) => {
const command = interaction.interaction const command = interaction.interaction
const requestId = uuid() const requestId = uuid()
const guildId = command.guildId!
logger.info("Got command for closing poll!", { guildId, requestId })
if (!command.guild) { if (!command.guild) {
logger.error("No guild found in interaction. Cancelling closing request", { guildId, requestId }) logger.error("No guild found in interaction. Cancelling closing request", { requestId })
command.followUp("Es gab leider ein Problem. Ich konnte deine Anfrage nicht bearbeiten :(") command.followUp("Es gab leider ein Problem. Ich konnte deine Anfrage nicht bearbeiten :(")
return return
} }
const guildId = command.guildId
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) closePoll(command.guild, requestId)
@ -31,10 +33,14 @@ export async function closePoll(guild: Guild, requestId: string) {
const guildId = guild.id const guildId = guild.id
logger.info("stopping poll", { guildId, requestId }) logger.info("stopping poll", { guildId, requestId })
const announcementChannel: TextChannel = client.getAnnouncementChannelForGuild(guildId) 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 const messages: Message<true>[] = (await announcementChannel.messages.fetch()) //todo: fetch only pinned messages
.map((value, _) => value) .map((value) => value)
.filter(message => !message.cleanContent.includes("[Abstimmung beendet]") && message.cleanContent.includes("[Abstimmung]")) .filter(message => !message.cleanContent.includes("[Abstimmung beendet]") && message.cleanContent.includes("[Abstimmung]"))
.sort((a, b) => b.createdTimestamp - a.createdTimestamp) .sort((a, b) => b.createdTimestamp - a.createdTimestamp)
@ -49,42 +55,42 @@ export async function closePoll(guild: Guild, requestId: string) {
logger.debug(`Last message: ${JSON.stringify(lastMessage, null, 2)}`, { guildId, requestId }) logger.debug(`Last message: ${JSON.stringify(lastMessage, null, 2)}`, { guildId, requestId })
const votes = await (await getVotesByEmote(lastMessage, guildId, requestId)) const votes = await (await getVotesByEmote(lastMessage, guildId, requestId))
.sort((a, b) => b.count - a.count) .sort((a, b) => b.count - a.count)
logger.debug(`votes: ${JSON.stringify(votes, null, 2)}`, { guildId, requestId }) logger.debug(`votes: ${JSON.stringify(votes, null, 2)}`, { guildId, requestId })
updateEvent(votes, guild!, guildId, requestId) logger.info("Deleting vote message")
updateMessage(votes[0].movie, lastMessage, guildId, requestId) 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 //lastMessage.unpin() //todo: uncomment when bot has permission to pin/unpin
} }
async function updateMessage(movie: string, message: Message, guildId: string, requestId: string) { async function sendVoteClosedMessage(event: GuildScheduledEvent, movie: string, guildId: string, requestId: string) {
const body = `[Abstimmung beendet] Gewonnen hat: ${movie}` const date = event.scheduledStartAt ? format(event.scheduledStartAt, "dd.MM") : "Fehler, event hatte kein Datum"
.concat(message.cleanContent.substring("[Abstimmung]".length)) const time = event.scheduledStartAt ? format(event.scheduledStartAt, "HH:mm") : "Fehler, event hatte kein Datum"
const body = `[Abstimmung beendet] <@&${config.bot.announcement_role}> Wir gucken ${movie} am ${date} um ${time}`
const options: MessageEditOptions = { const options: MessageCreateOptions = {
content: body, content: body,
allowedMentions: { parse: ["roles"] }
} }
logger.info("Updating message.", { guildId, requestId }) const announcementChannel = client.getAnnouncementChannelForGuild(guildId)
message.edit(options) logger.info("Sending vote closed message.", { guildId, requestId })
if(!announcementChannel) {
} logger.error("Could not find announcement channel. Please fix!", { guildId, requestId })
async function updateEvent(votes: Vote[], guild: Guild, guildId: string, requestId: string) {
logger.info(`Updating event with movie ${votes[0].movie}.`, { guildId, requestId })
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 return
} }
announcementChannel.send(options)
}
const voteEvent: GuildScheduledEvent<GuildScheduledEventStatus> = voteEvents[0] 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>> = { const options: GuildScheduledEventEditOptions<GuildScheduledEventStatus.Scheduled, GuildScheduledEventSetStatusArg<GuildScheduledEventStatus.Scheduled>> = {
name: votes[0].movie, 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` description: `!wp\nNummer 2: ${votes[1].movie} mit ${votes[1].count - 1} Stimmen\nNummer 3: ${votes[2].movie} mit ${votes[2].count - 1} Stimmen`
@ -94,6 +100,19 @@ async function updateEvent(votes: Vote[], guild: Guild, guildId: string, request
voteEvent.edit(options) 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 = { type Vote = {
emote: string, //todo habs nicht hinbekommen hier Emotes zu nutzen emote: string, //todo habs nicht hinbekommen hier Emotes zu nutzen
count: number, count: number,
@ -109,14 +128,14 @@ async function getVotesByEmote(message: Message, guildId: string, requestId: str
const reaction = await message.reactions.resolve(emote) const reaction = await message.reactions.resolve(emote)
logger.debug(`Reaction for emote ${emote}: ${JSON.stringify(reaction, null, 2)}`, { guildId, requestId }) logger.debug(`Reaction for emote ${emote}: ${JSON.stringify(reaction, null, 2)}`, { guildId, requestId })
if (reaction) { if (reaction) {
const vote: Vote = { emote: emote, count: reaction.count, movie: extractMovieFromMessageByEmote(message, emote, guildId, requestId) } const vote: Vote = { emote: emote, count: reaction.count, movie: extractMovieFromMessageByEmote(message, emote) }
votes.push(vote) votes.push(vote)
} }
} }
return votes return votes
} }
function extractMovieFromMessageByEmote(message: Message, emote: string, guildId: string, requestId: string): string { function extractMovieFromMessageByEmote(message: Message, emote: string): string {
const lines = message.cleanContent.split("\n") const lines = message.cleanContent.split("\n")
const emoteLines = lines.filter(line => line.includes(emote)) const emoteLines = lines.filter(line => line.includes(emote))

View File

@ -1,8 +1,7 @@
import { ApplicationCommandOptionType, BurstHandlerMajorIdKey } from 'discord.js' import { v4 as uuid } from 'uuid'
import { jellyfinHandler } from "../.."
import { Command } from '../structures/command' import { Command } from '../structures/command'
import { RunOptions } from '../types/commandTypes' import { RunOptions } from '../types/commandTypes'
import { jellyfinHandler } from "../.."
import { v4 as uuid } from 'uuid'
export default new Command({ export default new Command({
name: 'passwort_reset', name: 'passwort_reset',

View File

@ -1,5 +1,4 @@
import dotenv from "dotenv" import dotenv from "dotenv"
import { AddListingProviderRequestToJSON } from "./jellyfin"
dotenv.config() dotenv.config()
interface options { interface options {
@ -23,6 +22,7 @@ export interface Config {
workaround_token: string workaround_token: string
watcher_role: string watcher_role: string
jf_admin_role: string jf_admin_role: string
announcement_role: string
announcement_channel_id: string announcement_channel_id: string
jf_collection_id: string jf_collection_id: string
} }
@ -57,6 +57,7 @@ export const config: Config = {
workaround_token: process.env.TOKEN ?? "", workaround_token: process.env.TOKEN ?? "",
watcher_role: process.env.WATCHER_ROLE ?? "", watcher_role: process.env.WATCHER_ROLE ?? "",
jf_admin_role: process.env.ADMIN_ROLE ?? "", jf_admin_role: process.env.ADMIN_ROLE ?? "",
announcement_role: process.env.WATCHPARTY_ANNOUNCEMENT_ROLE ?? "",
announcement_channel_id: process.env.CHANNEL_ID ?? "", announcement_channel_id: process.env.CHANNEL_ID ?? "",
jf_collection_id: process.env.JELLYFIN_COLLECTION_ID ?? "" jf_collection_id: process.env.JELLYFIN_COLLECTION_ID ?? ""
} }

View File

@ -9,7 +9,7 @@ export async function execute(oldMember: GuildMember, newMember: GuildMember) {
try { try {
const requestId = uuid() const requestId = uuid()
const changedRoles: ChangedRoles = filterRolesFromMemberUpdate(oldMember, newMember) const changedRoles: ChangedRoles = filterRolesFromMemberUpdate(oldMember, newMember)
const triggerRoleIds: Collection<string, PermissionLevel> = getGuildSpecificTriggerRoleId(oldMember.guild.id) const triggerRoleIds: Collection<string, PermissionLevel> = getGuildSpecificTriggerRoleId()
triggerRoleIds.forEach((level, key) => { triggerRoleIds.forEach((level, key) => {
const addedRoleMatches = changedRoles.addedRoles.find(aRole => aRole.id === key) const addedRoleMatches = changedRoles.addedRoles.find(aRole => aRole.id === key)

View File

@ -1,12 +1,13 @@
import { GuildScheduledEvent, Message, TextChannel } from "discord.js"; import { addDays, format, isAfter } from "date-fns";
import toDate from "date-fns/fp/toDate";
import { GuildScheduledEvent, Message, MessageCreateOptions, TextChannel } from "discord.js";
import { ScheduledTask, schedule } from "node-cron"; import { ScheduledTask, schedule } from "node-cron";
import { v4 as uuid } from "uuid"; import { v4 as uuid } from "uuid";
import { client, jellyfinHandler } from "../.."; import { client, jellyfinHandler } from "../..";
import { closePoll } from "../commands/closepoll"; import { closePoll } from "../commands/closepoll";
import { config } from "../configuration"; import { config } from "../configuration";
import { logger } from "../logger"; import { logger } from "../logger";
import toDate from "date-fns/fp/toDate"; import { Maybe } from "../interfaces";
import { addDays, isAfter, isBefore } from "date-fns";
export const name = 'guildScheduledEventCreate' export const name = 'guildScheduledEventCreate'
@ -28,16 +29,31 @@ export async function execute(event: GuildScheduledEvent) {
logger.info(`Got ${movies.length} random movies. Creating voting`, { guildId: event.guildId, requestId }) logger.info(`Got ${movies.length} random movies. Creating voting`, { guildId: event.guildId, requestId })
logger.debug(`Movies: ${JSON.stringify(movies.map(movie => movie.name))}`, { guildId: event.guildId, requestId }) logger.debug(`Movies: ${JSON.stringify(movies.map(movie => movie.name))}`, { guildId: event.guildId, requestId })
const announcementChannel: TextChannel = client.getAnnouncementChannelForGuild(event.guildId) const announcementChannel: Maybe<TextChannel> = client.getAnnouncementChannelForGuild(event.guildId)
if(!announcementChannel) {
logger.error("Could not find announcement channel. Aborting", { guildId: event.guildId, requestId })
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 })
let message = "[Abstimmung]\nEs gibt eine neue Abstimmung für die nächste Watchparty! Stimme hierunter für den nächsten Film ab!\n" if(!event.scheduledStartAt) {
logger.info("EVENT DOES NOT HAVE STARTDATE; CANCELLING", {guildId: event.guildId, requestId})
return
}
const date = format(event.scheduledStartAt, "dd.MM")
const time = format(event.scheduledStartAt, "HH:mm")
let message = `[Abstimmung]\n<@&${config.bot.announcement_role}> Es gibt eine neue Abstimmung für die nächste Watchparty am ${date} um ${time}}! Stimme hierunter für den nächsten Film ab!\n`
for (let i = 0; i < movies.length; i++) { for (let i = 0; i < movies.length; i++) {
message = message.concat(Emotes[i]).concat(": ").concat(movies[i].name!).concat("\n") message = message.concat(Emotes[i]).concat(": ").concat(movies[i].name ?? "Film hatte keinen Namen :(").concat("\n")
} }
const sentMessage: Message<true> = await (await announcementChannel.fetch()).send(message) 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++) { for (let i = 0; i < movies.length; i++) {
sentMessage.react(Emotes[i]) sentMessage.react(Emotes[i])
@ -61,7 +77,7 @@ async function checkForPollsToClose(event: GuildScheduledEvent): Promise<void> {
//refetch event in case the time changed or the poll is already closed //refetch event in case the time changed or the poll is already closed
const events = (await event.guild.scheduledEvents.fetch()) const events = (await event.guild.scheduledEvents.fetch())
.filter(event => event.name.toLowerCase().includes("voting offen")) .filter(event => event.name.toLowerCase().includes("voting offen"))
.map((value, _) => value) .map((value) => value)
if (!events || events.length <= 0) { if (!events || events.length <= 0) {
logger.info("Did not find any events. Cancelling", { guildId: event.guildId, requestId }) logger.info("Did not find any events. Cancelling", { guildId: event.guildId, requestId })

View File

@ -1,6 +1,6 @@
import { GuildMember, GuildScheduledEvent, GuildScheduledEventStatus } from "discord.js"; import { GuildMember, GuildScheduledEvent, GuildScheduledEventStatus } from "discord.js";
import { v4 as uuid } from "uuid"; import { v4 as uuid } from "uuid";
import { jellyfinHandler } from "../.."; import { client, jellyfinHandler } from "../..";
import { getGuildSpecificTriggerRoleId } from "../helper/roleFilter"; import { getGuildSpecificTriggerRoleId } from "../helper/roleFilter";
import { logger } from "../logger"; import { logger } from "../logger";
@ -10,23 +10,34 @@ export const name = 'guildScheduledEventUpdate'
export async function execute(oldEvent: GuildScheduledEvent, newEvent: GuildScheduledEvent) { export async function execute(oldEvent: GuildScheduledEvent, newEvent: GuildScheduledEvent) {
try { try {
const requestId = uuid() const requestId = uuid()
logger.debug(`Got scheduledEvent update. New Event: ${JSON.stringify(newEvent, null, 2)}`,{guildId: newEvent.guildId, requestId}) logger.debug(`Got scheduledEvent update. New Event: ${JSON.stringify(newEvent, null, 2)}`, { guildId: newEvent.guildId, requestId })
if (!newEvent.guild) {
logger.error("Event has no guild, aborting.", { guildId: newEvent.guildId, requestId })
return
}
if (newEvent.description?.toLowerCase().includes("!wp") && [GuildScheduledEventStatus.Active, GuildScheduledEventStatus.Completed].includes(newEvent.status)) { if (newEvent.description?.toLowerCase().includes("!wp") && [GuildScheduledEventStatus.Active, GuildScheduledEventStatus.Completed].includes(newEvent.status)) {
const roles = getGuildSpecificTriggerRoleId(newEvent.guildId).map((key, value)=> value) const roles = getGuildSpecificTriggerRoleId().map((key, value) => value)
const eventMembers = (await newEvent.fetchSubscribers({ withMember: true })).filter(member => !member.member.roles.cache.hasAny(...roles)).map((value, _) => value.member) const eventMembers = (await newEvent.fetchSubscribers({ withMember: true })).filter(member => !member.member.roles.cache.hasAny(...roles)).map((value) => value.member)
const channelMembers = newEvent.channel?.members.filter(member => !member.roles.cache.hasAny(...roles)).map((value, _) => value ) const channelMembers = newEvent.channel?.members.filter(member => !member.roles.cache.hasAny(...roles)).map((value) => value)
const allMembers = eventMembers.concat(channelMembers ?? []) const allMembers = eventMembers.concat(channelMembers ?? [])
const members: GuildMember[] = [] const members: GuildMember[] = []
for(const member of allMembers){ for (const member of allMembers) {
if(!members.find(x => x.id == member.id)) if (!members.find(x => x.id == member.id))
members.push(member) members.push(member)
} }
if (newEvent.status === GuildScheduledEventStatus.Active) if (newEvent.status === GuildScheduledEventStatus.Active)
createJFUsers(members, newEvent.name, requestId) createJFUsers(members, newEvent.name, requestId)
else { else {
const announcementChannel = await client.getAnnouncementChannelForGuild(newEvent.guild.id)
if(!announcementChannel) {
logger.error("Could not find announcement channel. Aborting", { guildId: newEvent.guild.id, requestId })
return
}
const announcements = (await announcementChannel.messages.fetch()).filter(message => !message.pinned)
announcements.forEach(message => message.delete())
members.forEach(member => { members.forEach(member => {
member.createDM().then(channel => channel.send(`Die Watchparty ist vorbei, dein Account wurde wieder gelöscht. Wenn du einen permanenten Account haben möchtest, melde dich bei Samantha oder Marukus.`)) member.createDM().then(channel => channel.send(`Die Watchparty ist vorbei, dein Account wurde wieder gelöscht. Wenn du einen permanenten Account haben möchtest, melde dich bei Samantha oder Marukus.`))
}) })

View File

@ -1,4 +0,0 @@
export const name = 'ready'
export function execute(client: any) {
//console.log(`Processing ready: ${JSON.stringify(client)} has been created.`)
}

View File

@ -18,8 +18,8 @@ export async function execute(oldState: VoiceState, newState: VoiceState) {
} }
const scheduledEvents = (await newState.guild.scheduledEvents.fetch()) const scheduledEvents = (await newState.guild.scheduledEvents.fetch())
.filter((key, value) => key.description?.toLowerCase().includes("!wp") && key.isActive()) .filter((key) => key.description?.toLowerCase().includes("!wp") && key.isActive())
.map((key, value) => key) .map((key) => key)
const scheduledEventUsers = (await Promise.all(scheduledEvents.map(event => event.fetchSubscribers({withMember: true})))) const scheduledEventUsers = (await Promise.all(scheduledEvents.map(event => event.fetchSubscribers({withMember: true}))))

View File

@ -16,7 +16,7 @@ export function filterRolesFromMemberUpdate(oldMember: GuildMember, newMember: G
return { addedRoles, removedRoles } return { addedRoles, removedRoles }
} }
export function getGuildSpecificTriggerRoleId(guildId: string): 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")
outVal.set(config.bot.jf_admin_role, "ADMIN") outVal.set(config.bot.jf_admin_role, "ADMIN")

View File

@ -2,7 +2,7 @@ import { GuildMember } from "discord.js";
import { Config } from "../configuration"; import { Config } from "../configuration";
import { Maybe, PermissionLevel } from "../interfaces"; import { Maybe, PermissionLevel } from "../interfaces";
import { logger } from "../logger"; import { logger } from "../logger";
import { CreateUserByNameOperationRequest, DeleteUserRequest, GetItemsRequest, GetMovieRemoteSearchResultsOperationRequest, ItemLookupApi, ItemsApi, LibraryApi, SystemApi, UpdateUserPasswordOperationRequest, UpdateUserPolicyOperationRequest, UserApi } from "./apis"; import { CreateUserByNameOperationRequest, DeleteUserRequest, GetItemsRequest, ItemsApi, SystemApi, UpdateUserPasswordOperationRequest, UpdateUserPolicyOperationRequest, UserApi } from "./apis";
import { BaseItemDto, UpdateUserPasswordRequest } from "./models"; import { BaseItemDto, UpdateUserPasswordRequest } from "./models";
import { UserDto } from "./models/UserDto"; import { UserDto } from "./models/UserDto";
import { Configuration, ConfigurationParameters } from "./runtime"; import { Configuration, ConfigurationParameters } from "./runtime";
@ -56,10 +56,6 @@ export class JellyfinHandler {
return `${discordUser.displayName}${level == "TEMPORARY" ? "_tmp" : ""}` return `${discordUser.displayName}${level == "TEMPORARY" ? "_tmp" : ""}`
} }
public async addPermissionsToUserAccount(jfUserAccount: UserDto, guildId: string, requestId: string): Promise<UserDto> {
throw new Error("Method not implemented.");
}
private generatePasswordForUser(): string { private generatePasswordForUser(): string {
return (Math.random() * 10000 + 10000).toFixed(0) return (Math.random() * 10000 + 10000).toFixed(0)
} }
@ -253,7 +249,7 @@ export class JellyfinHandler {
} }
const movies: BaseItemDto[] = [] const movies: BaseItemDto[] = []
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
const index = Math.random() * allMovies.length const index = Math.floor(Math.random() * allMovies.length)
movies.push(...allMovies.splice(index, 1)) // maybe out of bounds? ? movies.push(...allMovies.splice(index, 1)) // maybe out of bounds? ?
} }

View File

@ -1,9 +1,13 @@
import { ApplicationCommandDataResolvable, Client, ClientOptions, Collection, GatewayIntentBits, Guild, IntentsBitField, Snowflake, TextChannel } from "discord.js"; import { ApplicationCommandDataResolvable, Client, ClientOptions, Collection, Guild, IntentsBitField, Snowflake, TextChannel } from "discord.js";
import { CommandType } from "../types/commandTypes"; import fs from 'fs';
import fs from 'fs' import { ScheduledTask, schedule } from "node-cron";
import { v4 as uuid } from 'uuid';
import { manageAnnouncementRoles } from "../commands/announce";
import { config } from "../configuration"; import { config } from "../configuration";
import { logger } from "../logger"; import { Maybe } from "../interfaces";
import { JellyfinHandler } from "../jellyfin/handler"; import { JellyfinHandler } from "../jellyfin/handler";
import { logger } from "../logger";
import { CommandType } from "../types/commandTypes";
@ -13,6 +17,7 @@ export class ExtendedClient extends Client {
private jellyfin: JellyfinHandler private jellyfin: JellyfinHandler
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
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.GuildVoiceStates)
@ -60,14 +65,15 @@ export class ExtendedClient extends Client {
this.commands.set(command.name, command) this.commands.set(command.name, command)
slashCommands.push(command) slashCommands.push(command)
} }
this.on("ready", (client: Client) => { this.on("ready", async (client: Client) => {
//logger.info(`Ready processing ${JSON.stringify(client)}`) //logger.info(`Ready processing ${JSON.stringify(client)}`)
logger.info(`SlashCommands: ${JSON.stringify(slashCommands)}`) logger.info(`SlashCommands: ${JSON.stringify(slashCommands)}`)
const guilds = client.guilds.cache const guilds = client.guilds.cache
this.registerCommands(slashCommands, guilds) this.registerCommands(slashCommands, guilds)
this.cacheUsers(guilds) this.cacheUsers(guilds)
this.cacheAnnouncementServer(guilds) await this.cacheAnnouncementServer(guilds)
this.startAnnouncementRoleBackgroundTask(guilds)
}) })
} catch (error) { } catch (error) {
logger.info(`Error refreshing slash commands: ${error}`) logger.info(`Error refreshing slash commands: ${error}`)
@ -76,8 +82,8 @@ export class ExtendedClient extends Client {
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())
?.filter(channel => channel!.id === config.bot.announcement_channel_id) ?.filter(channel => channel?.id === config.bot.announcement_channel_id)
.map((value, _) => value) .map((value) => value)
if (!channels || channels.length != 1) { if (!channels || channels.length != 1) {
logger.error(`Could not find announcement channel for guild ${guild.name} with guildId ${guild.id}. Found ${channels}`) logger.error(`Could not find announcement channel for guild ${guild.name} with guildId ${guild.id}. Found ${channels}`)
@ -87,8 +93,8 @@ export class ExtendedClient extends Client {
this.announcementChannels.set(guild.id, channels[0]) this.announcementChannels.set(guild.id, channels[0])
} }
} }
public getAnnouncementChannelForGuild(guildId: string): TextChannel { public getAnnouncementChannelForGuild(guildId: string): Maybe<TextChannel> {
return this.announcementChannels.get(guildId)! //we set the channel by ourselves only if we find one, I think this is sage (mark my words) return this.announcementChannels.get(guildId)
} }
public async cacheUsers(guilds: Collection<Snowflake, Guild>) { public async cacheUsers(guilds: Collection<Snowflake, Guild>) {
guilds.forEach((guild: Guild, id: Snowflake) => { guilds.forEach((guild: Guild, id: Snowflake) => {
@ -117,4 +123,51 @@ export class ExtendedClient extends Client {
logger.error(error) logger.error(error)
} }
} }
public async startAnnouncementRoleBackgroundTask(guilds: Collection<string, Guild>) {
for (const guild of guilds.values()) {
logger.info("Starting background task for announcement role", { guildId: guild.id })
const textChannel: Maybe<TextChannel> = this.getAnnouncementChannelForGuild(guild.id)
if(!textChannel) {
logger.error("Could not find announcement channel. Aborting", { guildId: guild.id })
return
}
this.announcementRoleHandlerTask.set(guild.id, schedule("*/10 * * * * *", async () => {
const requestId = uuid()
const messages = (await textChannel.messages.fetchPinned()).filter(message => message.cleanContent.includes("[initial]"))
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 })
return
} else if (messages.size == 0) {
logger.error("Could not find any pinned announcement messages. Unable to manage roles!", { guildId: guild.id, requestId })
return
}
const message = await messages.at(0)?.fetch()
if (!message) {
logger.error(`No pinned message found`, { guildId: guild.id, requestId })
return
}
//logger.debug(`Message: ${JSON.stringify(message, null, 2)}`, { guildId: guild.id, requestId })
const reactions = message.reactions.resolve("🎫")
//logger.debug(`reactions: ${JSON.stringify(reactions, null, 2)}`, { guildId: guild.id, requestId })
if (reactions) {
manageAnnouncementRoles(message.guild, reactions, requestId)
} else {
logger.error("Did not get reactions! Aborting!", { guildId: guild.id, requestId })
}
}))
}
}
public stopAnnouncementRoleBackgroundTask(guildId: string, requestId: string) {
const task: Maybe<ScheduledTask> = this.announcementRoleHandlerTask.get(guildId)
if (!task) {
logger.error(`No task found for guildID ${guildId}.`, { guildId, requestId })
return
}
task.stop()
}
} }