39 Commits

Author SHA1 Message Date
d61457cb5f 1.0.2
Some checks failed
Build a docker image for node-jellyfin-role-bot / build-docker-image (push) Failing after 10s
2023-06-22 23:05:47 +02:00
8d0dda0fa9 Merge pull request 'fix/ci-tag' (#47) from fix/ci-tag into master
Reviewed-on: #47
2023-06-22 18:38:34 +02:00
777ae330ad remove master branch restriction on docker-build
All checks were successful
Compile the repository / compile (pull_request) Successful in 10s
since the pipeline is only ever called on a tag, which will by convention only be committed to the master branch, we can be reasonably sure
that we are on the correct branch.
Tags are independent on branches which makes it impossible to check for tag AND master branch ref at the same time.
2023-06-19 23:34:25 +02:00
111ccaa880 simplify the test compile
All checks were successful
Compile the repository / compile (pull_request) Successful in 11s
2023-06-19 23:28:39 +02:00
c00453d3d3 push all tags of an image
All checks were successful
Compile the repository / compile (pull_request) Successful in 11s
2023-06-19 23:26:56 +02:00
8a7973a2e3 compile only on PR 2023-06-19 23:26:41 +02:00
0b67b126dd 1.0.1
All checks were successful
Compile the repository / compile (push) Successful in 9s
Build a docker image for node-jellyfin-role-bot / build-docker-image (push) Has been skipped
2023-06-19 23:19:40 +02:00
6d5725be90 Merge pull request 'fix/movie_names_and_background_task' (#46) from fix/movie_names_and_background_task into master
All checks were successful
Compile the repository / compile (push) Successful in 11s
Reviewed-on: #46
2023-06-19 11:29:04 +02:00
59f5b34e5a Merge branch 'master' into fix/movie_names_and_background_task
All checks were successful
Compile the repository / compile (push) Successful in 1m31s
2023-06-19 11:19:17 +02:00
670a64af22 Check if less than 2 days between create and start for deciding if to close
All checks were successful
Build a docker image for node-jellyfin-role-bot / build-docker-image (push) Successful in 1m9s
2023-06-17 13:18:52 +02:00
4cc332820f prevent poll close if event is less than 24h old
All checks were successful
Build a docker image for node-jellyfin-role-bot / build-docker-image (push) Successful in 1m15s
2023-06-17 13:03:48 +02:00
f5928049ea Merge pull request 'fix and refactor mitgucken guide' (#44) from fix/guide into master
All checks were successful
Compile the repository / compile (push) Successful in 9s
Reviewed-on: #44
2023-06-17 12:17:29 +02:00
99905f98d0 Merge pull request 'fix/ci-cd-push' (#45) from fix/ci-cd-push into master
All checks were successful
Compile the repository / compile (push) Successful in 10s
Reviewed-on: #45
2023-06-17 12:16:13 +02:00
07849d331a move scheduling of pollclose task to startup
All checks were successful
Build a docker image for node-jellyfin-role-bot / build-docker-image (push) Successful in 1m37s
Also moved check function to closepoll.ts
2023-06-17 12:00:14 +02:00
1e6a75687a add package version extraction to docker push job
All checks were successful
Compile the repository / compile (push) Successful in 9s
2023-06-17 01:31:03 +02:00
2c09033c3f add new tag restriction to docker push job
All checks were successful
Compile the repository / compile (push) Successful in 9s
2023-06-17 01:27:49 +02:00
ce4441cee3 add restriction to master branch on docker push job
All checks were successful
Compile the repository / compile (push) Successful in 9s
Build a docker image for node-jellyfin-role-bot / build-docker-image (push) Has been skipped
2023-06-17 01:22:53 +02:00
7c8072b295 add separate compile step
should be run on every push to a branch to check for compilability
2023-06-17 01:20:54 +02:00
7899aac5ce adjust server connection message
All checks were successful
Build a docker image for node-jellyfin-role-bot / build-docker-image (push) Successful in 1m22s
remove extraneous repetition of the server address
2023-06-17 01:20:01 +02:00
26c2d91252 fix and refactor mitgucken guide
All checks were successful
Build a docker image for node-jellyfin-role-bot / build-docker-image (push) Successful in 1m16s
- reorder account acquisition in front of login
- rebox explainrole function
- adjust external reference to function
2023-06-17 01:13:33 +02:00
d6300e8bec improve movie name resolving
All checks were successful
Build a docker image for node-jellyfin-role-bot / build-docker-image (push) Successful in 1m55s
When creating the poll the bot will now only request movie names instead of movies
This improves the errorhandling because the movie names cannot be null
Also the movieName function filters empty movienames and will guarantee the requested number of names
2023-06-16 20:15:36 +02:00
f78e4c3e3e 1.0.0
All checks were successful
Build a docker image for node-jellyfin-role-bot / build-docker-image (push) Successful in 8s
2023-06-15 23:30:13 +02:00
a1e1fca650 Merge pull request 'Use yavin to fetch the random movies for votes' (#26) from feat/yavin_movie_select into master
All checks were successful
Build a docker image for node-jellyfin-role-bot / build-docker-image (push) Successful in 12s
Reviewed-on: #26
2023-06-15 23:00:12 +02:00
2fae61fc1f change announcement message to specify MOVIE watchparties
All checks were successful
Build a docker image for node-jellyfin-role-bot / build-docker-image (push) Successful in 1m16s
2023-06-15 22:33:42 +02:00
71ffc6ba50 use yavin to get random movies 2023-06-15 22:33:22 +02:00
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
20 changed files with 419 additions and 160 deletions

View File

@ -0,0 +1,17 @@
name: Compile the repository
on: [pull_request]
env:
REGISTRY: gitea.brudi.xyz
IMAGE_NAME: ${{ gitea.repository }}
USER: ${{ gitea.actor }}
jobs:
compile:
runs-on: ubuntu-latest
container: catthehacker/ubuntu:act-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Build Container
run: docker build .

View File

@ -1,6 +1,9 @@
name: Build a docker image for node-jellyfin-role-bot
run-name: ${{ gitea.actor }} is building an image
on: [push]
on:
push:
tags:
- '*'
env:
REGISTRY: gitea.brudi.xyz
IMAGE_NAME: ${{ gitea.repository }}
@ -8,6 +11,7 @@ env:
jobs:
build-docker-image:
runs-on: ubuntu-latest
#if: gitea.ref == 'refs/heads/master'
container: catthehacker/ubuntu:act-latest
permissions:
contents: read
@ -15,9 +19,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Get Package Version
run: VERSION = node -p "require('./package.json').version"
- name: Log in to the Container registry
run: docker login -u ${{ env.USER }} -p ${{ secrets.TOKEN }} ${{ env.REGISTRY }}
- name: Build Container
run: docker build -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" .
run: docker build -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}".
- name: Push Container
run: docker push "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest"
run: docker push --all-tags "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest"

View File

@ -5,7 +5,8 @@ import { JellyfinHandler } from "./server/jellyfin/handler"
import { attachedImages } from "./server/assets/attachments"
const requestId = 'startup'
export const jellyfinHandler = new JellyfinHandler(config)
export const jellyfinHandler = new JellyfinHandler({jellyfinToken: config.bot.workaround_token, jellyfinUrl: config.bot.jellyfin_url, movieCollectionId: config.bot.jf_collection_id, collectionUser: config.bot.jf_user})
export const yavinJellyfinHandler = new JellyfinHandler({jellyfinToken: config.bot.yavin_jellyfin_token, jellyfinUrl: config.bot.yavin_jellyfin_url, movieCollectionId: config.bot.yavin_collection_id, collectionUser: config.bot.yavin_jellyfin_collection_user})
export const client = new ExtendedClient(jellyfinHandler)

4
package-lock.json generated
View File

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

View File

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

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 in denen wir zusammen Filme gucken! 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 { addDays, differenceInDays, format, isAfter, toDate } from 'date-fns'
import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildScheduledEventSetStatusArg, GuildScheduledEventStatus, Message, MessageCreateOptions, TextChannel } from 'discord.js'
import { v4 as uuid } from 'uuid'
import { client } from '../..'
import { config } from '../configuration'
import { Emotes } from '../events/guildScheduledEventCreate'
import { Maybe } from '../interfaces'
import { logger } from '../logger'
import { Command } from '../structures/command'
import { RunOptions } from '../types/commandTypes'
import { client } from '../..'
export default new Command({
name: 'closepoll',
@ -14,13 +16,13 @@ export default new Command({
run: async (interaction: RunOptions) => {
const command = interaction.interaction
const requestId = uuid()
const guildId = command.guildId!
logger.info("Got command for closing poll!", { guildId, requestId })
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 :(")
return
}
const guildId = command.guildId
logger.info("Got command for closing poll!", { guildId, requestId })
command.followUp("Alles klar, beende die Umfrage :)")
closePoll(command.guild, requestId)
@ -31,10 +33,14 @@ export async function closePoll(guild: Guild, requestId: string) {
const guildId = guild.id
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
.map((value, _) => value)
.map((value) => value)
.filter(message => !message.cleanContent.includes("[Abstimmung beendet]") && message.cleanContent.includes("[Abstimmung]"))
.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 })
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 })
updateEvent(votes, guild!, guildId, requestId)
updateMessage(votes[0].movie, lastMessage, 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 updateMessage(movie: string, message: Message, guildId: string, requestId: string) {
const body = `[Abstimmung beendet] Gewonnen hat: ${movie}`
.concat(message.cleanContent.substring("[Abstimmung]".length))
const options: MessageEditOptions = {
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] <@&${config.bot.announcement_role}> Wir gucken ${movie} am ${date} um ${time}`
const options: MessageCreateOptions = {
content: body,
allowedMentions: { parse: ["roles"] }
}
logger.info("Updating message.", { guildId, requestId })
message.edit(options)
}
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 })
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)
}
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>> = {
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`
@ -94,6 +100,19 @@ async function updateEvent(votes: Vote[], guild: Guild, guildId: string, request
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,
@ -109,14 +128,14 @@ async function getVotesByEmote(message: Message, guildId: string, requestId: str
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, guildId, requestId) }
const vote: Vote = { emote: emote, count: reaction.count, movie: extractMovieFromMessageByEmote(message, emote) }
votes.push(vote)
}
}
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 emoteLines = lines.filter(line => line.includes(emote))
@ -126,4 +145,40 @@ function extractMovieFromMessageByEmote(message: Message, emote: string, guildId
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 })
}
}

View File

@ -5,7 +5,7 @@ import { accountChoice, joingroup, leavegroup, loginScreen, overview, resume, se
import { logger } from '../logger'
import { Command } from '../structures/command'
import { RunOptions } from '../types/commandTypes'
import { configureServer, explainRoles, installation, loginInfo, useSyncgroup } from './mitgucken'
import { configureServer, explainRole, installation, loginInfo, useSyncgroup } from './mitgucken'
export default new Command({
name: 'guides',
@ -70,7 +70,7 @@ export default new Command({
userDMChannel.send({ embeds: useSyncgroup(), files: [overview, joingroup, resume, leavegroup] })
} else if (guideSelection.customId === 'explainRoles') {
const userDMChannel = await guideSelection.user.createDM()
userDMChannel.send(explainRoles())
userDMChannel.send({ embeds: explainRole() })
}
guideSelection.update({ content: "Hab ich dir per DM geschickt :)", components: [] })

View File

@ -16,13 +16,9 @@ export default new Command({
const embedList: APIEmbed[] = []
embedList.push(...installation())
embedList.push(...configureServer())
embedList.push(...explainRole())
embedList.push(...loginInfo())
embedList.push(...useSyncgroup())
embedList.push({
color,
title: "Wie du an einen Account kommst",
description: explainRoles()
})
//logger.info(`Trying to use ${splashScreen.name}`, { requestId, guildId: interaction.interaction.guild?.id })
logger.info(`Sending guide to ${interaction.interaction.user.id}`, { requestId, guildId: interaction.interaction.guild?.id })
@ -32,6 +28,13 @@ export default new Command({
})
export function explainRole(): APIEmbed[] {
return [{
color,
title: "Wie du an einen Account kommst",
description: roleExplanation
}]
}
export function installation(): APIEmbed[] {
const embedList: APIEmbed[] = []
// DownloadLink and installation
@ -67,7 +70,7 @@ export function configureServer(): APIEmbed[] {
title: "Server Verbindung",
description: "Stelle eine Verbindung zum Hartzarett Jellyfin Server her",
fields: [
{ name: "Server Adresse", value: "https://media.hartzarett.ruhr" }
{ name: "Server Adresse", value: "`https://media.hartzarett.ruhr`" }
],
image: {
url: 'attachment://server_verbindung.png'
@ -91,7 +94,7 @@ export function loginInfo(): APIEmbed[] {
embedList.push({
color,
title: "Login",
description: "Melde dich mit dem Usernamen und Passwort an, welches dir von mir zugeschickt wird. Falls du ein neues brauchst führe einmal /reset_passwort aus :)",
description: "Melde dich mit dem Usernamen und Passwort an, welches dir von mir zugeschickt wird. Falls du ein neues brauchst führe einmal `/passwort_reset` aus :)",
image: {
url: 'attachment://login_screen.png'
}
@ -139,8 +142,7 @@ export function useSyncgroup(): APIEmbed[] {
return embedList
}
export function explainRoles(): string {
return `Mit einer Rolle kann dafür gesorgt werden, dass du einen dauerhaften Account auf dem Mediaserver hast. Wende dich bei Bedarf an Samantha oder Markus.\n
const roleExplanation = `Mit einer Rolle kann dafür gesorgt werden, dass du einen dauerhaften Account auf dem Mediaserver hast. Wende dich bei Bedarf an Samantha oder Markus.\n
Für eine watchparty bekommst du allerdings automatisch einen Account. Hierfür melde einfach Interesse an dem Event an. Wenn du für das Event Interesse angemeldet hast bekommst du automatisch beim Start des Events einen Benutzernamen und das dazugehörige Passwort zugesendet.\n
Hast du kein Interesse angemeldet bekommst du automatisch einen Nutzernamen und Passwort zugeschickt wenn du den Channel betrittst in dem das Event stattfindet.`
}

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 { RunOptions } from '../types/commandTypes'
import { jellyfinHandler } from "../.."
import { v4 as uuid } from 'uuid'
export default new Command({
name: 'passwort_reset',

View File

@ -1,5 +1,4 @@
import dotenv from "dotenv"
import { AddListingProviderRequestToJSON } from "./jellyfin"
dotenv.config()
interface options {
@ -23,8 +22,14 @@ export interface Config {
workaround_token: string
watcher_role: string
jf_admin_role: string
announcement_role: string
announcement_channel_id: string
jf_collection_id: string
jf_user: string
yavin_collection_id: string
yavin_jellyfin_url: string
yavin_jellyfin_token: string
yavin_jellyfin_collection_user: string
}
}
export const config: Config = {
@ -57,7 +62,13 @@ export const config: Config = {
workaround_token: process.env.TOKEN ?? "",
watcher_role: process.env.WATCHER_ROLE ?? "",
jf_admin_role: process.env.ADMIN_ROLE ?? "",
announcement_role: process.env.WATCHPARTY_ANNOUNCEMENT_ROLE ?? "",
announcement_channel_id: process.env.CHANNEL_ID ?? "",
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_jellyfin_url: process.env.YAVIN_JELLYFIN_URL ?? "",
yavin_jellyfin_token: process.env.YAVIN_TOKEN ?? "",
yavin_jellyfin_collection_user: process.env.YAVIN_COLLECTION_USER ?? "",
jf_user: process.env.JELLYFIN_USER ?? ""
}
}

View File

@ -9,7 +9,7 @@ export async function execute(oldMember: GuildMember, newMember: GuildMember) {
try {
const requestId = uuid()
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) => {
const addedRoleMatches = changedRoles.addedRoles.find(aRole => aRole.id === key)

View File

@ -1,12 +1,11 @@
import { GuildScheduledEvent, Message, TextChannel } from "discord.js";
import { ScheduledTask, schedule } from "node-cron";
import { format } from "date-fns";
import { GuildScheduledEvent, Message, MessageCreateOptions, TextChannel } from "discord.js";
import { ScheduledTask } from "node-cron";
import { v4 as uuid } from "uuid";
import { client, jellyfinHandler } from "../..";
import { closePoll } from "../commands/closepoll";
import { client, yavinJellyfinHandler } from "../..";
import { config } from "../configuration";
import { Maybe } from "../interfaces";
import { logger } from "../logger";
import toDate from "date-fns/fp/toDate";
import { addDays, isAfter, isBefore } from "date-fns";
export const name = 'guildScheduledEventCreate'
@ -23,66 +22,42 @@ export async function execute(event: GuildScheduledEvent) {
logger.info("Event was a placeholder event to start a new watchparty and voting. Creating vote!", { guildId: event.guildId, requestId })
logger.debug("Renaming event", { guildId: event.guildId, requestId })
event.edit({ name: "Watchparty - Voting offen" })
const movies = await jellyfinHandler.getRandomMovies(5, event.guildId, requestId)
const movies = await yavinJellyfinHandler.getRandomMovieNames(5, 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)}`, { 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 })
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++) {
message = message.concat(Emotes[i]).concat(": ").concat(movies[i].name!).concat("\n")
message = message.concat(Emotes[i]).concat(": ").concat(movies[i]).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++) {
sentMessage.react(Emotes[i])
}
if (!task) {
task = schedule("0 * * * * *", () => checkForPollsToClose(event))
}
// sentMessage.pin() //todo: uncomment when bot has permission to pin messages. Also update closepoll.ts to only fetch pinned messages
}
}
async function checkForPollsToClose(event: GuildScheduledEvent): Promise<void> {
const requestId = uuid()
logger.info(`Automatic check for poll closing.`, { guildId: event.guildId, requestId })
if (!event.guild) {
logger.error("No guild in event. Cancelling.", { guildId: event.guildId, requestId })
return
}
//refetch event in case the time changed or the poll is already closed
const events = (await event.guild.scheduledEvents.fetch())
.filter(event => event.name.toLowerCase().includes("voting offen"))
.map((value, _) => value)
if (!events || events.length <= 0) {
logger.info("Did not find any events. Cancelling", { guildId: event.guildId, requestId })
return
} else if (events.length > 1) {
logger.error(`More than one event found. Don't know which one is the right one :( Events: ${JSON.stringify(events, null, 2)}`, { guildId: event.guildId, requestId })
return
}
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: event.guildId, requestId })
return
}
const eventDate: Date = toDate(updatedEvent.scheduledStartTimestamp)
const closePollDate: Date = addDays(eventDate, -2)
if (isAfter(Date.now(), closePollDate)) {
logger.info("Less than two days until event. Closing poll", { guildId: event.guildId, requestId })
closePoll(event.guild, requestId)
} else {
logger.info(`ScheduledStart: ${closePollDate}. Now: ${toDate(Date.now())}`, { guildId: event.guildId, requestId })
}
}

View File

@ -1,6 +1,6 @@
import { GuildMember, GuildScheduledEvent, GuildScheduledEventStatus } from "discord.js";
import { v4 as uuid } from "uuid";
import { jellyfinHandler } from "../..";
import { client, jellyfinHandler } from "../..";
import { getGuildSpecificTriggerRoleId } from "../helper/roleFilter";
import { logger } from "../logger";
@ -10,23 +10,35 @@ export const name = 'guildScheduledEventUpdate'
export async function execute(oldEvent: GuildScheduledEvent, newEvent: GuildScheduledEvent) {
try {
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)) {
const roles = getGuildSpecificTriggerRoleId(newEvent.guildId).map((key, value)=> value)
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 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 channelMembers = newEvent.channel?.members.filter(member => !member.roles.cache.hasAny(...roles)).map((value) => value)
const allMembers = eventMembers.concat(channelMembers ?? [])
const members: GuildMember[] = []
for(const member of allMembers){
if(!members.find(x => x.id == member.id))
for (const member of allMembers) {
if (!members.find(x => x.id == member.id))
members.push(member)
}
if (newEvent.status === GuildScheduledEventStatus.Active)
createJFUsers(members, newEvent.name, requestId)
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 => {
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())
.filter((key, value) => key.description?.toLowerCase().includes("!wp") && key.isActive())
.map((key, value) => key)
.filter((key) => key.description?.toLowerCase().includes("!wp") && key.isActive())
.map((key) => key)
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 }
}
export function getGuildSpecificTriggerRoleId(guildId: string): Collection<string, PermissionLevel> {
export function getGuildSpecificTriggerRoleId(): Collection<string, PermissionLevel> {
const outVal = new Collection<string, PermissionLevel>()
outVal.set(config.bot.watcher_role, "VIEWER")
outVal.set(config.bot.jf_admin_role, "ADMIN")

View File

@ -32,4 +32,10 @@ export interface ChangedRoles {
addedRoles: Collection<string, Role>
removedRoles: Collection<string, Role>
}
export interface JellyfinConfig {
jellyfinUrl: string,
jellyfinToken: string,
movieCollectionId: string,
collectionUser: string
}
export type PermissionLevel = "VIEWER" | "ADMIN" | "TEMPORARY"

View File

@ -1,8 +1,7 @@
import { GuildMember } from "discord.js";
import { Config } from "../configuration";
import { Maybe, PermissionLevel } from "../interfaces";
import { JellyfinConfig, Maybe, PermissionLevel } from "../interfaces";
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 { UserDto } from "./models/UserDto";
import { Configuration, ConfigurationParameters } from "./runtime";
@ -15,34 +14,27 @@ export class JellyfinHandler {
private moviesApi: ItemsApi
private token: string
private authHeader: { headers: { 'X-Emby-Authorization': string } }
private config: Config
private config: JellyfinConfig
private serverName = "";
public async ServerName(): Promise<string> {
if (this.serverName === "") {
const info = await this.systemApi.getSystemInfo(this.authHeader)
this.serverName = info.serverName ?? this.config.bot.jellyfin_url
}
return this.serverName
}
constructor(_config: Config, _userApi?: UserApi, _systemApi?: SystemApi, _itemsApi?: ItemsApi) {
constructor(_config: JellyfinConfig, _userApi?: UserApi, _systemApi?: SystemApi, _itemsApi?: ItemsApi) {
this.config = _config
this.token = this.config.bot.jellfin_token
this.token = this.config.jellyfinToken
this.authHeader = {
headers: {
"X-Emby-Authorization": this.config.bot.workaround_token
"X-Emby-Authorization": this.config.jellyfinToken
}
}
const userApiConfigurationParams: ConfigurationParameters = {
basePath: this.config.bot.jellyfin_url,
basePath: this.config.jellyfinUrl,
headers: this.authHeader.headers
}
const systemApiConfigurationParams: ConfigurationParameters = {
basePath: this.config.bot.jellyfin_url,
basePath: this.config.jellyfinUrl,
headers: this.authHeader.headers
}
const libraryApiConfigurationParams: ConfigurationParameters = {
basePath: this.config.bot.jellyfin_url,
basePath: this.config.jellyfinUrl,
headers: this.authHeader.headers
}
@ -56,10 +48,6 @@ export class JellyfinHandler {
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 {
return (Math.random() * 10000 + 10000).toFixed(0)
}
@ -232,11 +220,9 @@ export class JellyfinHandler {
public async getAllMovies(guildId: string, requestId: string): Promise<BaseItemDto[]> {
logger.info("requesting all movies from jellyfin", { guildId, requestId })
const liloJfUser = await this.getUser(<GuildMember>{ guild: { id: guildId }, displayName: "lilo" }, requestId)
const searchParams: GetItemsRequest = {
userId: liloJfUser?.id,
parentId: this.config.bot.jf_collection_id // collection ID for all movies
userId: this.config.collectionUser,
parentId: this.config.movieCollectionId // collection ID for all movies
}
const movies = (await (this.moviesApi.getItems(searchParams))).items?.filter(item => !item.isFolder)
// logger.debug(JSON.stringify(movies, null, 2), { guildId: guildId, requestId })
@ -253,13 +239,24 @@ export class JellyfinHandler {
}
const movies: BaseItemDto[] = []
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? ?
}
return movies
}
public async getRandomMovieNames(count: number, guildId: string, requestId: string): Promise<string[]> {
logger.info(`${count} random movie names requested`, { guildId, requestId })
let movieCount = 0
let movieNames: string[]
do {
movieNames = (await this.getRandomMovies(count, guildId, requestId)).filter(movie => movie.name && movie.name.length > 0).map(movie => <string> movie.name)
movieCount = movieNames.length
} while (movieCount < count)
return movieNames
}
}
export enum UserUpsertResult { enabled, created }

View File

@ -1,9 +1,14 @@
import { ApplicationCommandDataResolvable, Client, ClientOptions, Collection, GatewayIntentBits, Guild, IntentsBitField, Snowflake, TextChannel } from "discord.js";
import { CommandType } from "../types/commandTypes";
import fs from 'fs'
import { ApplicationCommandDataResolvable, Client, ClientOptions, Collection, Guild, IntentsBitField, Snowflake, TextChannel } from "discord.js";
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 { logger } from "../logger";
import { Maybe } from "../interfaces";
import { JellyfinHandler } from "../jellyfin/handler";
import { logger } from "../logger";
import { CommandType } from "../types/commandTypes";
import { checkForPollsToClose } from "../commands/closepoll";
@ -12,7 +17,9 @@ export class ExtendedClient extends Client {
private commandFilePath = `${__dirname}/../commands`
private jellyfin: JellyfinHandler
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 pollCloseBackgroundTasks: Collection<string, ScheduledTask> = new Collection()
public constructor(jf: JellyfinHandler) {
const intents: IntentsBitField = new IntentsBitField()
intents.add(IntentsBitField.Flags.GuildMembers, IntentsBitField.Flags.MessageContent, IntentsBitField.Flags.Guilds, IntentsBitField.Flags.DirectMessages, IntentsBitField.Flags.GuildScheduledEvents, IntentsBitField.Flags.GuildVoiceStates)
@ -28,7 +35,6 @@ export class ExtendedClient extends Client {
Promise.all(promises).then(() => {
this.login(config.bot.token)
})
logger.info(`Connected with ${await this.jellyfin.ServerName()}`)
}
private async importFile(filepath: string): Promise<any> {
logger.debug(`Importing ${filepath}`)
@ -60,14 +66,16 @@ export class ExtendedClient extends Client {
this.commands.set(command.name, command)
slashCommands.push(command)
}
this.on("ready", (client: Client) => {
this.on("ready", async (client: Client) => {
//logger.info(`Ready processing ${JSON.stringify(client)}`)
logger.info(`SlashCommands: ${JSON.stringify(slashCommands)}`)
const guilds = client.guilds.cache
this.registerCommands(slashCommands, guilds)
this.cacheUsers(guilds)
this.cacheAnnouncementServer(guilds)
await this.cacheAnnouncementServer(guilds)
this.startAnnouncementRoleBackgroundTask(guilds)
this.startPollCloseBackgroundTasks()
})
} catch (error) {
logger.info(`Error refreshing slash commands: ${error}`)
@ -76,8 +84,8 @@ export class ExtendedClient extends Client {
private async cacheAnnouncementServer(guilds: Collection<Snowflake, Guild>) {
for (const guild of guilds.values()) {
const channels: TextChannel[] = <TextChannel[]>(await guild.channels.fetch())
?.filter(channel => channel!.id === config.bot.announcement_channel_id)
.map((value, _) => value)
?.filter(channel => channel?.id === config.bot.announcement_channel_id)
.map((value) => value)
if (!channels || channels.length != 1) {
logger.error(`Could not find announcement channel for guild ${guild.name} with guildId ${guild.id}. Found ${channels}`)
@ -87,8 +95,8 @@ export class ExtendedClient extends Client {
this.announcementChannels.set(guild.id, channels[0])
}
}
public getAnnouncementChannelForGuild(guildId: string): 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)
public getAnnouncementChannelForGuild(guildId: string): Maybe<TextChannel> {
return this.announcementChannels.get(guildId)
}
public async cacheUsers(guilds: Collection<Snowflake, Guild>) {
guilds.forEach((guild: Guild, id: Snowflake) => {
@ -117,4 +125,57 @@ export class ExtendedClient extends Client {
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()
}
private async startPollCloseBackgroundTasks() {
for(const guild of this.guilds.cache) {
this.pollCloseBackgroundTasks.set(guild[1].id, schedule("0 * * * * *", () => checkForPollsToClose(guild[1])))
}
}
}