25 Commits

Author SHA1 Message Date
0cb19ba8f1 use bash scripts for docker stage
All checks were successful
Build a docker image for node-jellyfin-role-ot / build-docker-image (push) Successful in 11s
2023-06-14 22:27:18 +02:00
5dcf766593 dockerbuild and dockerpush scripts 2023-06-14 22:27:05 +02:00
808bdd033e string it
All checks were successful
Build a docker image for node-jellyfin-role-ot / build-docker-image (push) Successful in 9s
2023-06-14 22:20:06 +02:00
33f031d333 simplify push
All checks were successful
Build a docker image for node-jellyfin-role-ot / build-docker-image (push) Successful in 9s
2023-06-14 22:14:46 +02:00
40d9523e21 doing it manually 2023-06-14 22:14:19 +02:00
26e74a62c1 add branch 2023-06-14 21:54:22 +02:00
c0f91aad79 add secrets to 'on' 2023-06-14 21:42:16 +02:00
79ffde5f34 check env step
Some checks failed
Build a docker image for node-jellyfin-role-ot / build-docker-image (push) Failing after 23s
2023-06-14 21:30:23 +02:00
911b9e4884 adjust pipeline to be personalized
Some checks failed
Build a docker image for node-jellyfin-role-ot / build-docker-image (push) Failing after 17s
2023-06-14 21:27:16 +02:00
31a9e0eb28 adjust url
Some checks failed
Build a docker image for node-jellyfin-role-ot / build-docker-image (push) Failing after 22s
2023-06-14 19:44:50 +02:00
bcf788293e hardcoded docker tags
Some checks failed
Build a docker image for node-jellyfin-role-ot / build-docker-image (push) Failing after 28s
2023-06-14 19:27:28 +02:00
934b6dfead remove custom script
Some checks failed
Build a docker image for node-jellyfin-role-ot / build-docker-image (push) Failing after 15s
2023-06-14 19:21:22 +02:00
cd0c8c0017 latest tag by default
Some checks failed
Build a docker image for node-jellyfin-role-ot / build-docker-image (push) Failing after 15s
2023-06-14 19:18:46 +02:00
83f803d0e7 remove metadata action
Some checks failed
Build a docker image for node-jellyfin-role-ot / build-docker-image (push) Failing after 20s
2023-06-14 19:16:23 +02:00
2cb652aee6 add metadata and registry push
Some checks failed
Build a docker image for node-jellyfin-role-ot / build-docker-image (push) Failing after 9s
2023-06-14 19:11:05 +02:00
034d14eb15 use a container with docker
All checks were successful
Build a docker image for node-jellyfin-role-ot / build-docker-image (push) Successful in 2m3s
2023-06-14 18:59:07 +02:00
c8bfc47ddf add docker login
Some checks failed
Build a docker image for node-jellyfin-role-ot / build-docker-image (push) Failing after 14s
2023-06-13 23:47:22 +02:00
b67982ed38 ls
Some checks failed
Build a docker image for node-jellyfin-role-ot / build-docker-image (push) Failing after 9s
2023-06-13 23:29:29 +02:00
e3144fc402 add checkout option 2023-06-13 23:29:03 +02:00
1970f4b0cb adjust docker build stage
Some checks failed
Build a docker image for node-jellyfin-role-ot / build-docker-image (push) Failing after 34s
2023-06-13 23:24:56 +02:00
8ac4f568a0 add on push
Some checks failed
Build a docker image for node-jellyfin-role-ot / build-docker-image (push) Failing after 41s
2023-06-13 21:51:26 +02:00
09f4efc96c change os 2023-06-13 21:50:21 +02:00
6e0c3b8ef6 add gitea cicd 2023-06-13 21:49:04 +02:00
8ee36f7510 add woodpecker integration 2023-06-13 20:31:33 +02:00
1593e126eb add docker build files 2023-06-13 20:31:19 +02:00
20 changed files with 127 additions and 329 deletions

View File

@ -1,10 +1,12 @@
name: Build a docker image for node-jellyfin-role-bot
name: Build a docker image for node-jellyfin-role-ot
run-name: ${{ gitea.actor }} is building an image
on: [push]
env:
REGISTRY: gitea.brudi.xyz
IMAGE_NAME: ${{ gitea.repository }}
USER: ${{ gitea.actor }}
jobs:
build-docker-image:
runs-on: ubuntu-latest
@ -12,12 +14,15 @@ jobs:
permissions:
contents: read
packages: write
steps:
- name: Check environment
run: echo "Registry ${{ env.Registry }} Image ${{ env.IMAGE_NAME }} User ${{ env.USER }}"
- name: Checkout repository
uses: actions/checkout@v3
- 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: ./dockerbuild.sh "${{ env.REGISTRY }}/${{ env.USER }}/${{ IMAGE_NAME }}:latest"
- name: Push Container
run: docker push "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest"
run: docker push "${{ env.REGISTRY }}/${{ env.USER }}/${{ IMAGE_NAME }}:latest"

7
.woodpecker.yml Normal file
View File

@ -0,0 +1,7 @@
pipeline:
docker:
image: plugins/docker
settings:
registry: registry.brudi.xyz
repo: registry.brudi.xyz/kenobi/node-jellyfin-role-bot
tags: latest

2
dockerbuild.sh Executable file
View File

@ -0,0 +1,2 @@
#!/bin/bash
docker build --tag $1 .

2
dockerpush.sh Normal file
View File

@ -0,0 +1,2 @@
#!/bin/bash
docker push $1

View File

@ -5,8 +5,7 @@ import { JellyfinHandler } from "./server/jellyfin/handler"
import { attachedImages } from "./server/assets/attachments"
const requestId = 'startup'
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 jellyfinHandler = new JellyfinHandler(config)
export const client = new ExtendedClient(jellyfinHandler)

4
package-lock.json generated
View File

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

View File

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

View File

@ -1,121 +0,0 @@
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,13 +1,11 @@
import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildScheduledEventSetStatusArg, GuildScheduledEventStatus, Message, MessageCreateOptions, TextChannel } from 'discord.js'
import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildScheduledEventSetStatusArg, GuildScheduledEventStatus, Message, MessageEditOptions, TextChannel } from 'discord.js'
import { v4 as uuid } from 'uuid'
import { client } from '../..'
import { config } from '../configuration'
import { Emotes } from '../events/guildScheduledEventCreate'
import { logger } from '../logger'
import { Command } from '../structures/command'
import { RunOptions } from '../types/commandTypes'
import { format } from 'date-fns'
import { Maybe } from '../interfaces'
import { client } from '../..'
export default new Command({
name: 'closepoll',
@ -16,14 +14,14 @@ 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", { requestId })
logger.error("No guild found in interaction. Cancelling closing request", { guildId, 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)
}
@ -33,14 +31,10 @@ 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 announcementChannel: TextChannel = client.getAnnouncementChannelForGuild(guildId)
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)
@ -55,42 +49,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)
.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)
}
updateEvent(votes, guild!, guildId, requestId)
updateMessage(votes[0].movie, lastMessage, 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] <@&${config.bot.announcement_role}> Wir gucken ${movie} am ${date} um ${time}`
const options: MessageCreateOptions = {
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 = {
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)
logger.info("Updating message.", { guildId, requestId })
message.edit(options)
}
async function updateEvent(voteEvent: GuildScheduledEvent, votes: Vote[], guild: Guild, guildId: string, requestId: string) {
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
}
const voteEvent: GuildScheduledEvent<GuildScheduledEventStatus> = voteEvents[0]
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`
@ -100,19 +94,6 @@ async function updateEvent(voteEvent: GuildScheduledEvent, votes: Vote[], guild:
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,
@ -128,14 +109,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) }
const vote: Vote = { emote: emote, count: reaction.count, movie: extractMovieFromMessageByEmote(message, emote, guildId, requestId) }
votes.push(vote)
}
}
return votes
}
function extractMovieFromMessageByEmote(message: Message, emote: string): string {
function extractMovieFromMessageByEmote(message: Message, emote: string, guildId: string, requestId: string): string {
const lines = message.cleanContent.split("\n")
const emoteLines = lines.filter(line => line.includes(emote))

View File

@ -1,7 +1,8 @@
import { v4 as uuid } from 'uuid'
import { jellyfinHandler } from "../.."
import { ApplicationCommandOptionType, BurstHandlerMajorIdKey } from 'discord.js'
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,4 +1,5 @@
import dotenv from "dotenv"
import { AddListingProviderRequestToJSON } from "./jellyfin"
dotenv.config()
interface options {
@ -22,14 +23,8 @@ 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 = {
@ -62,13 +57,7 @@ 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 ?? "",
yavin_collection_id: process.env.YAVIN_COLLECTION_ID ?? "",
yavin_jellyfin_url: process.env.YAVIN_JELLYFIN_URL ?? "",
yavin_jellyfin_token: process.env.YAVIN_TOKEN ?? "",
yavin_jellyfin_collection_user: process.env.YAVIN_COLLECTION_USER ?? "",
jf_user: process.env.JELLYFIN_USER ?? ""
jf_collection_id: process.env.JELLYFIN_COLLECTION_ID ?? ""
}
}

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()
const triggerRoleIds: Collection<string, PermissionLevel> = getGuildSpecificTriggerRoleId(oldMember.guild.id)
triggerRoleIds.forEach((level, key) => {
const addedRoleMatches = changedRoles.addedRoles.find(aRole => aRole.id === key)

View File

@ -1,13 +1,12 @@
import { addDays, format, isAfter } from "date-fns";
import toDate from "date-fns/fp/toDate";
import { GuildScheduledEvent, Message, MessageCreateOptions, TextChannel } from "discord.js";
import { GuildScheduledEvent, Message, TextChannel } from "discord.js";
import { ScheduledTask, schedule } from "node-cron";
import { v4 as uuid } from "uuid";
import { client, yavinJellyfinHandler } from "../..";
import { client, jellyfinHandler } from "../..";
import { closePoll } from "../commands/closepoll";
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'
@ -24,36 +23,21 @@ 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 yavinJellyfinHandler.getRandomMovies(5, event.guildId, requestId)
const movies = await jellyfinHandler.getRandomMovies(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 })
const announcementChannel: Maybe<TextChannel> = client.getAnnouncementChannelForGuild(event.guildId)
if(!announcementChannel) {
logger.error("Could not find announcement channel. Aborting", { guildId: event.guildId, requestId })
return
}
const announcementChannel: TextChannel = client.getAnnouncementChannelForGuild(event.guildId)
logger.debug(`Found channel ${JSON.stringify(announcementChannel, null, 2)}`, { guildId: event.guildId, requestId })
if(!event.scheduledStartAt) {
logger.info("EVENT DOES NOT HAVE STARTDATE; CANCELLING", {guildId: event.guildId, requestId})
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`
let message = "[Abstimmung]\nEs gibt eine neue Abstimmung für die nächste Watchparty! 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 ?? "Film hatte keinen Namen :(").concat("\n")
message = message.concat(Emotes[i]).concat(": ").concat(movies[i].name!).concat("\n")
}
const options: MessageCreateOptions = {
allowedMentions: { parse: ["roles"]},
content: message
}
const sentMessage: Message<true> = await (await announcementChannel.fetch()).send(options)
const sentMessage: Message<true> = await (await announcementChannel.fetch()).send(message)
for (let i = 0; i < movies.length; i++) {
sentMessage.react(Emotes[i])
@ -77,7 +61,7 @@ async function checkForPollsToClose(event: GuildScheduledEvent): Promise<void> {
//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)
.map((value, _) => value)
if (!events || events.length <= 0) {
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 { v4 as uuid } from "uuid";
import { client, jellyfinHandler } from "../..";
import { jellyfinHandler } from "../..";
import { getGuildSpecificTriggerRoleId } from "../helper/roleFilter";
import { logger } from "../logger";
@ -10,34 +10,23 @@ 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 })
if (!newEvent.guild) {
logger.error("Event has no guild, aborting.", { guildId: newEvent.guildId, requestId })
return
}
logger.debug(`Got scheduledEvent update. New Event: ${JSON.stringify(newEvent, null, 2)}`,{guildId: newEvent.guildId, requestId})
if (newEvent.description?.toLowerCase().includes("!wp") && [GuildScheduledEventStatus.Active, GuildScheduledEventStatus.Completed].includes(newEvent.status)) {
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 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 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.`))
})

4
server/events/ready.ts Normal file
View File

@ -0,0 +1,4 @@
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) => key.description?.toLowerCase().includes("!wp") && key.isActive())
.map((key) => key)
.filter((key, value) => key.description?.toLowerCase().includes("!wp") && key.isActive())
.map((key, value) => 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(): Collection<string, PermissionLevel> {
export function getGuildSpecificTriggerRoleId(guildId: string): 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,10 +32,4 @@ 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,7 +1,8 @@
import { GuildMember } from "discord.js";
import { JellyfinConfig, Maybe, PermissionLevel } from "../interfaces";
import { Config } from "../configuration";
import { Maybe, PermissionLevel } from "../interfaces";
import { logger } from "../logger";
import { CreateUserByNameOperationRequest, DeleteUserRequest, GetItemsRequest, ItemsApi, SystemApi, UpdateUserPasswordOperationRequest, UpdateUserPolicyOperationRequest, UserApi } from "./apis";
import { CreateUserByNameOperationRequest, DeleteUserRequest, GetItemsRequest, GetMovieRemoteSearchResultsOperationRequest, ItemLookupApi, ItemsApi, LibraryApi, SystemApi, UpdateUserPasswordOperationRequest, UpdateUserPolicyOperationRequest, UserApi } from "./apis";
import { BaseItemDto, UpdateUserPasswordRequest } from "./models";
import { UserDto } from "./models/UserDto";
import { Configuration, ConfigurationParameters } from "./runtime";
@ -14,27 +15,34 @@ export class JellyfinHandler {
private moviesApi: ItemsApi
private token: string
private authHeader: { headers: { 'X-Emby-Authorization': string } }
private config: JellyfinConfig
private config: Config
private serverName = "";
constructor(_config: JellyfinConfig, _userApi?: UserApi, _systemApi?: SystemApi, _itemsApi?: ItemsApi) {
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) {
this.config = _config
this.token = this.config.jellyfinToken
this.token = this.config.bot.jellfin_token
this.authHeader = {
headers: {
"X-Emby-Authorization": this.config.jellyfinToken
"X-Emby-Authorization": this.config.bot.workaround_token
}
}
const userApiConfigurationParams: ConfigurationParameters = {
basePath: this.config.jellyfinUrl,
basePath: this.config.bot.jellyfin_url,
headers: this.authHeader.headers
}
const systemApiConfigurationParams: ConfigurationParameters = {
basePath: this.config.jellyfinUrl,
basePath: this.config.bot.jellyfin_url,
headers: this.authHeader.headers
}
const libraryApiConfigurationParams: ConfigurationParameters = {
basePath: this.config.jellyfinUrl,
basePath: this.config.bot.jellyfin_url,
headers: this.authHeader.headers
}
@ -48,6 +56,10 @@ 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)
}
@ -220,9 +232,11 @@ 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: this.config.collectionUser,
parentId: this.config.movieCollectionId // collection ID for all movies
userId: liloJfUser?.id,
parentId: this.config.bot.jf_collection_id // 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 })
@ -239,7 +253,7 @@ export class JellyfinHandler {
}
const movies: BaseItemDto[] = []
for (let i = 0; i < count; i++) {
const index = Math.floor(Math.random() * allMovies.length)
const index = Math.random() * allMovies.length
movies.push(...allMovies.splice(index, 1)) // maybe out of bounds? ?
}

View File

@ -1,13 +1,9 @@
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 { Maybe } from "../interfaces";
import { JellyfinHandler } from "../jellyfin/handler";
import { logger } from "../logger";
import { ApplicationCommandDataResolvable, Client, ClientOptions, Collection, GatewayIntentBits, Guild, IntentsBitField, Snowflake, TextChannel } from "discord.js";
import { CommandType } from "../types/commandTypes";
import fs from 'fs'
import { config } from "../configuration";
import { logger } from "../logger";
import { JellyfinHandler } from "../jellyfin/handler";
@ -17,7 +13,6 @@ export class ExtendedClient extends Client {
private jellyfin: JellyfinHandler
public commands: Collection<string, CommandType> = new Collection()
private announcementChannels: Collection<string, TextChannel> = new Collection //guildId to TextChannel
private announcementRoleHandlerTask: Collection<string, ScheduledTask> = new Collection //one task per guild
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)
@ -33,6 +28,7 @@ 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}`)
@ -64,15 +60,14 @@ export class ExtendedClient extends Client {
this.commands.set(command.name, command)
slashCommands.push(command)
}
this.on("ready", async (client: Client) => {
this.on("ready", (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)
await this.cacheAnnouncementServer(guilds)
this.startAnnouncementRoleBackgroundTask(guilds)
this.cacheAnnouncementServer(guilds)
})
} catch (error) {
logger.info(`Error refreshing slash commands: ${error}`)
@ -81,8 +76,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}`)
@ -92,8 +87,8 @@ export class ExtendedClient extends Client {
this.announcementChannels.set(guild.id, channels[0])
}
}
public getAnnouncementChannelForGuild(guildId: string): Maybe<TextChannel> {
return this.announcementChannels.get(guildId)
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 async cacheUsers(guilds: Collection<Snowflake, Guild>) {
guilds.forEach((guild: Guild, id: Snowflake) => {
@ -122,51 +117,4 @@ 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()
}
}