Compare commits
61 Commits
d3fff38e36
...
v1.0.1
Author | SHA1 | Date | |
---|---|---|---|
0b67b126dd | |||
6d5725be90 | |||
59f5b34e5a | |||
670a64af22 | |||
4cc332820f | |||
f5928049ea | |||
99905f98d0 | |||
07849d331a | |||
1e6a75687a | |||
2c09033c3f | |||
ce4441cee3 | |||
7c8072b295 | |||
7899aac5ce | |||
26c2d91252 | |||
d6300e8bec | |||
f78e4c3e3e | |||
a1e1fca650 | |||
2fae61fc1f | |||
71ffc6ba50 | |||
8caf80f54e | |||
1ccb1a7cae | |||
d22e38efbf | |||
68662e72ad | |||
5b99c843b4 | |||
251756c622 | |||
9420eb4366 | |||
220f9dc8ef | |||
198a25d145 | |||
baefcf9bb9 | |||
a5eab2f7be | |||
e774474a55 | |||
24754decf4 | |||
a2c55ad676 | |||
e50cb10c5b | |||
acc38fdcb0 | |||
fdc0fc47b5 | |||
f3669ec34f | |||
c0369fcb49 | |||
40d220ed7b | |||
117ff23a0c | |||
550aa53188 | |||
1ee55f995c | |||
0d5c3d30a9 | |||
c2d8838cf8 | |||
c8fa89ae63 | |||
2707f7d73b | |||
2c5bf1272e | |||
1e1ab93667 | |||
8f1c6e10fa | |||
c0b24ee3ad | |||
3e152864fd | |||
24c120a890 | |||
8ae5fd2c1b | |||
0aef525994 | |||
0ba867b23a | |||
3294d9ca77 | |||
e234e9f68f | |||
84a0d7bbe1 | |||
efd4d69c74 | |||
64c5874249 | |||
892562cd0b |
17
.gitea/workflows/compile.yaml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
name: Compile the repository
|
||||||
|
on: [push]
|
||||||
|
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 -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" .
|
29
.gitea/workflows/docker-build.yaml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
name: Build a docker image for node-jellyfin-role-bot
|
||||||
|
run-name: ${{ gitea.actor }} is building an image
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
env:
|
||||||
|
REGISTRY: gitea.brudi.xyz
|
||||||
|
IMAGE_NAME: ${{ gitea.repository }}
|
||||||
|
USER: ${{ gitea.actor }}
|
||||||
|
jobs:
|
||||||
|
build-docker-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: gitea.ref == 'refs/heads/master'
|
||||||
|
container: catthehacker/ubuntu:act-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
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" -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}".
|
||||||
|
- name: Push Container
|
||||||
|
run: docker push "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest"
|
11
Dockerfile
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
FROM node:alpine as Build
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY [ "package-lock.json", "package.json", "index.ts", "tsconfig.json", "./" ]
|
||||||
|
COPY server ./server
|
||||||
|
|
||||||
|
RUN npm ci --omit=dev
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
CMD ["npm","run","start"]
|
6
index.ts
@ -2,12 +2,16 @@ import { ExtendedClient } from "./server/structures/client"
|
|||||||
import { config } from "./server/configuration"
|
import { config } from "./server/configuration"
|
||||||
import { logger } from "./server/logger"
|
import { logger } from "./server/logger"
|
||||||
import { JellyfinHandler } from "./server/jellyfin/handler"
|
import { JellyfinHandler } from "./server/jellyfin/handler"
|
||||||
|
import { attachedImages } from "./server/assets/attachments"
|
||||||
const requestId = 'startup'
|
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)
|
export const client = new ExtendedClient(jellyfinHandler)
|
||||||
|
|
||||||
|
export const attachmentImages = attachedImages
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
try {
|
try {
|
||||||
const users = await jellyfinHandler.getCurrentUsers("", requestId)
|
const users = await jellyfinHandler.getCurrentUsers("", requestId)
|
||||||
|
50
package-lock.json
generated
@ -1,17 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "node-jellyfin-discord-bot",
|
"name": "node-jellyfin-discord-bot",
|
||||||
"version": "0.0.1",
|
"version": "1.0.1",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "node-jellyfin-discord-bot",
|
"name": "node-jellyfin-discord-bot",
|
||||||
"version": "0.0.1",
|
"version": "1.0.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordjs/rest": "^1.7.0",
|
"@discordjs/rest": "^1.7.0",
|
||||||
"@tsconfig/recommended": "^1.0.2",
|
"@tsconfig/recommended": "^1.0.2",
|
||||||
"@types/node": "^18.15.11",
|
"@types/node": "^18.15.11",
|
||||||
|
"@types/node-cron": "^3.0.7",
|
||||||
"@types/request": "^2.48.8",
|
"@types/request": "^2.48.8",
|
||||||
"@types/uuid": "^9.0.1",
|
"@types/uuid": "^9.0.1",
|
||||||
"axios": "^1.3.5",
|
"axios": "^1.3.5",
|
||||||
@ -20,6 +21,7 @@
|
|||||||
"discord.js": "^14.9.0",
|
"discord.js": "^14.9.0",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"jellyfin-apiclient": "^1.10.0",
|
"jellyfin-apiclient": "^1.10.0",
|
||||||
|
"node-cron": "^3.0.2",
|
||||||
"sqlite3": "^5.1.6",
|
"sqlite3": "^5.1.6",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^5.0.4",
|
"typescript": "^5.0.4",
|
||||||
@ -1585,6 +1587,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
|
||||||
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q=="
|
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/node-cron": {
|
||||||
|
"version": "3.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.7.tgz",
|
||||||
|
"integrity": "sha512-9PuLtBboc/+JJ7FshmJWv769gDonTpItN0Ol5TMwclpSQNjVyB2SRxSKBcTtbSysSL5R7Oea06kTTFNciCoYwA=="
|
||||||
|
},
|
||||||
"node_modules/@types/prettier": {
|
"node_modules/@types/prettier": {
|
||||||
"version": "2.7.2",
|
"version": "2.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz",
|
||||||
@ -5004,6 +5011,25 @@
|
|||||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz",
|
||||||
"integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ=="
|
"integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/node-cron": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-iP8l0yGlNpE0e6q1o185yOApANRe47UPbLf4YxfbiNHt/RU5eBcGB/e0oudruheSf+LQeDMezqC5BVAb5wwRcQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"uuid": "8.3.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-cron/node_modules/uuid": {
|
||||||
|
"version": "8.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||||
|
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-fetch": {
|
"node_modules/node-fetch": {
|
||||||
"version": "2.6.9",
|
"version": "2.6.9",
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz",
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz",
|
||||||
@ -8124,6 +8150,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.11.tgz",
|
||||||
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q=="
|
"integrity": "sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q=="
|
||||||
},
|
},
|
||||||
|
"@types/node-cron": {
|
||||||
|
"version": "3.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.7.tgz",
|
||||||
|
"integrity": "sha512-9PuLtBboc/+JJ7FshmJWv769gDonTpItN0Ol5TMwclpSQNjVyB2SRxSKBcTtbSysSL5R7Oea06kTTFNciCoYwA=="
|
||||||
|
},
|
||||||
"@types/prettier": {
|
"@types/prettier": {
|
||||||
"version": "2.7.2",
|
"version": "2.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz",
|
||||||
@ -10702,6 +10733,21 @@
|
|||||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz",
|
||||||
"integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ=="
|
"integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ=="
|
||||||
},
|
},
|
||||||
|
"node-cron": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-iP8l0yGlNpE0e6q1o185yOApANRe47UPbLf4YxfbiNHt/RU5eBcGB/e0oudruheSf+LQeDMezqC5BVAb5wwRcQ==",
|
||||||
|
"requires": {
|
||||||
|
"uuid": "8.3.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"uuid": {
|
||||||
|
"version": "8.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||||
|
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node-fetch": {
|
"node-fetch": {
|
||||||
"version": "2.6.9",
|
"version": "2.6.9",
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz",
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "node-jellyfin-discord-bot",
|
"name": "node-jellyfin-discord-bot",
|
||||||
"version": "0.0.1",
|
"version": "1.0.1",
|
||||||
"description": "A discord bot to sync jellyfin accounts with discord roles",
|
"description": "A discord bot to sync jellyfin accounts with discord roles",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -8,6 +8,7 @@
|
|||||||
"@discordjs/rest": "^1.7.0",
|
"@discordjs/rest": "^1.7.0",
|
||||||
"@tsconfig/recommended": "^1.0.2",
|
"@tsconfig/recommended": "^1.0.2",
|
||||||
"@types/node": "^18.15.11",
|
"@types/node": "^18.15.11",
|
||||||
|
"@types/node-cron": "^3.0.7",
|
||||||
"@types/request": "^2.48.8",
|
"@types/request": "^2.48.8",
|
||||||
"@types/uuid": "^9.0.1",
|
"@types/uuid": "^9.0.1",
|
||||||
"axios": "^1.3.5",
|
"axios": "^1.3.5",
|
||||||
@ -16,6 +17,7 @@
|
|||||||
"discord.js": "^14.9.0",
|
"discord.js": "^14.9.0",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"jellyfin-apiclient": "^1.10.0",
|
"jellyfin-apiclient": "^1.10.0",
|
||||||
|
"node-cron": "^3.0.2",
|
||||||
"sqlite3": "^5.1.6",
|
"sqlite3": "^5.1.6",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "^5.0.4",
|
"typescript": "^5.0.4",
|
||||||
|
12
server/assets/attachments.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { AttachmentBuilder } from "discord.js"
|
||||||
|
|
||||||
|
export const splashScreen = new AttachmentBuilder('./server/assets/images/set_splashscreen.png')
|
||||||
|
export const startScreen = new AttachmentBuilder('./server/assets/images/start_screen.png')
|
||||||
|
export const serverConnection = new AttachmentBuilder('./server/assets/images/server_verbindung.png')
|
||||||
|
export const accountChoice = new AttachmentBuilder('./server/assets/images/auswahl_anmeldung.png')
|
||||||
|
export const loginScreen = new AttachmentBuilder('./server/assets/images/login_screen.png')
|
||||||
|
export const overview = new AttachmentBuilder('./server/assets/images/jellyfin_ubersicht.png')
|
||||||
|
export const joingroup = new AttachmentBuilder('./server/assets/images/gruppe_beitreten.png')
|
||||||
|
export const resume = new AttachmentBuilder('./server/assets/images/wiedergabe_fortsetzen.png')
|
||||||
|
export const leavegroup = new AttachmentBuilder('./server/assets/images/gruppe_verlassen.png')
|
||||||
|
export const attachedImages = [splashScreen, startScreen, serverConnection, accountChoice, loginScreen, overview, resume, leavegroup, joingroup]
|
BIN
server/assets/images/auswahl_anmeldung.png
Normal file
After Width: | Height: | Size: 67 KiB |
BIN
server/assets/images/gruppe_beitreten.png
Normal file
After Width: | Height: | Size: 424 KiB |
BIN
server/assets/images/gruppe_verlassen.png
Normal file
After Width: | Height: | Size: 892 KiB |
BIN
server/assets/images/jellyfin_ubersicht.png
Normal file
After Width: | Height: | Size: 495 KiB |
BIN
server/assets/images/login_screen.png
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
server/assets/images/server_verbindung.png
Normal file
After Width: | Height: | Size: 53 KiB |
BIN
server/assets/images/set_splashscreen.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
server/assets/images/start_screen.png
Normal file
After Width: | Height: | Size: 84 KiB |
BIN
server/assets/images/wiedergabe_fortsetzen.png
Normal file
After Width: | Height: | Size: 436 KiB |
121
server/commands/announce.ts
Normal 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))
|
||||||
|
}
|
||||||
|
|
184
server/commands/closepoll.ts
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
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'
|
||||||
|
|
||||||
|
export default new Command({
|
||||||
|
name: 'closepoll',
|
||||||
|
description: 'Aktuelle Umfrage für nächste Watchparty beenden und Gewinner in Event eintragen.',
|
||||||
|
options: [],
|
||||||
|
run: async (interaction: RunOptions) => {
|
||||||
|
const command = interaction.interaction
|
||||||
|
const requestId = uuid()
|
||||||
|
if (!command.guild) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export async function closePoll(guild: Guild, requestId: string) {
|
||||||
|
const guildId = guild.id
|
||||||
|
logger.info("stopping poll", { guildId, requestId })
|
||||||
|
|
||||||
|
const announcementChannel: Maybe<TextChannel> = client.getAnnouncementChannelForGuild(guildId)
|
||||||
|
if (!announcementChannel) {
|
||||||
|
logger.error("Could not find the textchannel. Unable to close poll.", { guildId, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages: Message<true>[] = (await announcementChannel.messages.fetch()) //todo: fetch only pinned messages
|
||||||
|
.map((value) => value)
|
||||||
|
.filter(message => !message.cleanContent.includes("[Abstimmung beendet]") && message.cleanContent.includes("[Abstimmung]"))
|
||||||
|
.sort((a, b) => b.createdTimestamp - a.createdTimestamp)
|
||||||
|
|
||||||
|
if (!messages || messages.length <= 0) {
|
||||||
|
logger.info("Could not find any vote messages. Cancelling pollClose", { guildId, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastMessage: Message<true> = messages[0]
|
||||||
|
|
||||||
|
logger.debug(`Found messages: ${JSON.stringify(messages, null, 2)}`, { guildId, requestId })
|
||||||
|
|
||||||
|
logger.debug(`Last message: ${JSON.stringify(lastMessage, null, 2)}`, { guildId, requestId })
|
||||||
|
|
||||||
|
|
||||||
|
const votes = await (await getVotesByEmote(lastMessage, guildId, requestId))
|
||||||
|
.sort((a, b) => b.count - a.count)
|
||||||
|
|
||||||
|
logger.debug(`votes: ${JSON.stringify(votes, null, 2)}`, { guildId, requestId })
|
||||||
|
|
||||||
|
logger.info("Deleting vote message")
|
||||||
|
await lastMessage.delete()
|
||||||
|
const event = await getEvent(guild, guild.id, requestId)
|
||||||
|
if (event) {
|
||||||
|
updateEvent(event, votes, guild, guildId, requestId)
|
||||||
|
sendVoteClosedMessage(event, votes[0].movie, guildId, requestId)
|
||||||
|
}
|
||||||
|
|
||||||
|
//lastMessage.unpin() //todo: uncomment when bot has permission to pin/unpin
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendVoteClosedMessage(event: GuildScheduledEvent, movie: string, guildId: string, requestId: string) {
|
||||||
|
const date = event.scheduledStartAt ? format(event.scheduledStartAt, "dd.MM") : "Fehler, event hatte kein Datum"
|
||||||
|
const time = event.scheduledStartAt ? format(event.scheduledStartAt, "HH:mm") : "Fehler, event hatte kein Datum"
|
||||||
|
const body = `[Abstimmung beendet] <@&${config.bot.announcement_role}> Wir gucken ${movie} am ${date} um ${time}`
|
||||||
|
const options: MessageCreateOptions = {
|
||||||
|
content: body,
|
||||||
|
allowedMentions: { parse: ["roles"] }
|
||||||
|
}
|
||||||
|
const announcementChannel = client.getAnnouncementChannelForGuild(guildId)
|
||||||
|
logger.info("Sending vote closed message.", { guildId, requestId })
|
||||||
|
if (!announcementChannel) {
|
||||||
|
logger.error("Could not find announcement channel. Please fix!", { guildId, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
announcementChannel.send(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateEvent(voteEvent: GuildScheduledEvent, votes: Vote[], guild: Guild, guildId: string, requestId: string) {
|
||||||
|
logger.info(`Updating event with movie ${votes[0].movie}.`, { guildId, requestId })
|
||||||
|
const options: GuildScheduledEventEditOptions<GuildScheduledEventStatus.Scheduled, GuildScheduledEventSetStatusArg<GuildScheduledEventStatus.Scheduled>> = {
|
||||||
|
name: votes[0].movie,
|
||||||
|
description: `!wp\nNummer 2: ${votes[1].movie} mit ${votes[1].count - 1} Stimmen\nNummer 3: ${votes[2].movie} mit ${votes[2].count - 1} Stimmen`
|
||||||
|
}
|
||||||
|
logger.debug(`Updating event: ${JSON.stringify(voteEvent, null, 2)}`, { guildId, requestId })
|
||||||
|
logger.info("Updating event.", { guildId, requestId })
|
||||||
|
voteEvent.edit(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getEvent(guild: Guild, guildId: string, requestId: string): Promise<GuildScheduledEvent | null> {
|
||||||
|
const voteEvents = (await guild.scheduledEvents.fetch())
|
||||||
|
.map((value) => value)
|
||||||
|
.filter(event => event.name.toLowerCase().includes("voting offen"))
|
||||||
|
logger.debug(`Found events: ${JSON.stringify(voteEvents, null, 2)}`, { guildId, requestId })
|
||||||
|
|
||||||
|
if (!voteEvents || voteEvents.length <= 0) {
|
||||||
|
logger.error("Could not find vote event. Cancelling update!", { guildId, requestId })
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return voteEvents[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
type Vote = {
|
||||||
|
emote: string, //todo habs nicht hinbekommen hier Emotes zu nutzen
|
||||||
|
count: number,
|
||||||
|
movie: string
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getVotesByEmote(message: Message, guildId: string, requestId: string): Promise<Vote[]> {
|
||||||
|
const votes: Vote[] = []
|
||||||
|
logger.debug(`Number of items in emotes: ${Object.values(Emotes).length}`, { guildId, requestId })
|
||||||
|
for (let i = 0; i < Object.keys(Emotes).length / 2; i++) {
|
||||||
|
const emote = Emotes[i]
|
||||||
|
logger.debug(`Getting reaction for emote ${emote}`, { guildId, requestId })
|
||||||
|
const reaction = await message.reactions.resolve(emote)
|
||||||
|
logger.debug(`Reaction for emote ${emote}: ${JSON.stringify(reaction, null, 2)}`, { guildId, requestId })
|
||||||
|
if (reaction) {
|
||||||
|
const vote: Vote = { emote: emote, count: reaction.count, movie: extractMovieFromMessageByEmote(message, emote) }
|
||||||
|
votes.push(vote)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return votes
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractMovieFromMessageByEmote(message: Message, emote: string): string {
|
||||||
|
const lines = message.cleanContent.split("\n")
|
||||||
|
const emoteLines = lines.filter(line => line.includes(emote))
|
||||||
|
|
||||||
|
if (!emoteLines) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
const movie = emoteLines[0].substring(emoteLines[0].indexOf(emote) + emote.length + 2) // plus colon and space
|
||||||
|
|
||||||
|
return movie
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkForPollsToClose(guild: Guild): Promise<void> {
|
||||||
|
const requestId = uuid()
|
||||||
|
logger.info(`Automatic check for poll closing.`, { guildId: guild.id, requestId })
|
||||||
|
const events = (await guild.scheduledEvents.fetch()).filter(event => event.name.toLocaleLowerCase().includes("voting offen")).map(event => event)
|
||||||
|
if (events.length > 1) {
|
||||||
|
logger.error("Handling more than one Event is not implemented yet. Found more than one poll to close")
|
||||||
|
return
|
||||||
|
} else if (events.length == 0) {
|
||||||
|
logger.info("Could not find any events. Cancelling", { guildId: guild.id, requestId })
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedEvent = events[0] //add two hours because of different timezones in discord api and Date.now()
|
||||||
|
if (!updatedEvent.scheduledStartTimestamp) {
|
||||||
|
logger.error("Event does not have a scheduled start time. Cancelling", { guildId: guild.id, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const createDate: Date = toDate(updatedEvent.createdTimestamp)
|
||||||
|
const eventDate: Date = toDate(updatedEvent.scheduledStartTimestamp)
|
||||||
|
const difference: number = differenceInDays(createDate, eventDate)
|
||||||
|
|
||||||
|
if (difference <= 2) {
|
||||||
|
logger.info("Less than two days between event create and event start. Not closing poll.", { guildId: guild.id, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const closePollDate: Date = addDays(eventDate, -2)
|
||||||
|
|
||||||
|
if (isAfter(Date.now(), closePollDate)) {
|
||||||
|
logger.info("Less than two days until event. Closing poll", { guildId: guild.id, requestId })
|
||||||
|
closePoll(guild, requestId)
|
||||||
|
} else {
|
||||||
|
logger.info(`ScheduledStart: ${closePollDate}. Now: ${toDate(Date.now())}`, { guildId: guild.id, requestId })
|
||||||
|
}
|
||||||
|
}
|
83
server/commands/guides.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
|
||||||
|
import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js'
|
||||||
|
import { v4 as uuid } from 'uuid'
|
||||||
|
import { accountChoice, joingroup, leavegroup, loginScreen, overview, resume, serverConnection, splashScreen, startScreen } from '../assets/attachments'
|
||||||
|
import { logger } from '../logger'
|
||||||
|
import { Command } from '../structures/command'
|
||||||
|
import { RunOptions } from '../types/commandTypes'
|
||||||
|
import { configureServer, explainRole, installation, loginInfo, useSyncgroup } from './mitgucken'
|
||||||
|
|
||||||
|
export default new Command({
|
||||||
|
name: 'guides',
|
||||||
|
description: 'Bekomme eine Auswahl von Guides per DM',
|
||||||
|
options: [],
|
||||||
|
run: async (interaction: RunOptions) => {
|
||||||
|
const requestId = uuid()
|
||||||
|
const guildId = interaction.interaction.guild?.id
|
||||||
|
logger.info(`Starting guides interaction for user ${interaction.interaction.user.id}`, { requestId, guildId })
|
||||||
|
|
||||||
|
const mediaPlayerGuideButton = new ButtonBuilder()
|
||||||
|
.setCustomId('jfInstallation')
|
||||||
|
.setLabel('Media Player Installation')
|
||||||
|
.setStyle(ButtonStyle.Primary)
|
||||||
|
|
||||||
|
const accountSetupGuideButton = new ButtonBuilder()
|
||||||
|
.setCustomId('configureServer')
|
||||||
|
.setLabel('Server einstellen')
|
||||||
|
.setStyle(ButtonStyle.Primary)
|
||||||
|
|
||||||
|
const loginGuideButton = new ButtonBuilder()
|
||||||
|
.setCustomId('login')
|
||||||
|
.setLabel('Einloggen')
|
||||||
|
.setStyle(ButtonStyle.Primary)
|
||||||
|
|
||||||
|
const useSyncGroupGuideButton = new ButtonBuilder()
|
||||||
|
.setCustomId('useSyncGroup')
|
||||||
|
.setLabel('Watch Parties nutzen')
|
||||||
|
.setStyle(ButtonStyle.Primary)
|
||||||
|
|
||||||
|
const roleExplanationButton = new ButtonBuilder()
|
||||||
|
.setCustomId('explainRoles')
|
||||||
|
.setLabel('Wie bekomme ich Zugang')
|
||||||
|
.setStyle(ButtonStyle.Primary)
|
||||||
|
|
||||||
|
|
||||||
|
const row = new ActionRowBuilder<ButtonBuilder>()
|
||||||
|
.addComponents(mediaPlayerGuideButton, accountSetupGuideButton, loginGuideButton, useSyncGroupGuideButton, roleExplanationButton)
|
||||||
|
|
||||||
|
|
||||||
|
//const userDMchannel = await interaction.interaction.user.createDM()
|
||||||
|
const response = await interaction.interaction.followUp({
|
||||||
|
content: `Hier ist eine Auswahl von Guides.`,
|
||||||
|
components: [row]
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
const guideSelection = await response.awaitMessageComponent({ time: 60_000 })
|
||||||
|
|
||||||
|
if (guideSelection.customId === 'jfInstallation') {
|
||||||
|
const userDMChannel = await guideSelection.user.createDM()
|
||||||
|
userDMChannel.send({ embeds: installation(), files: [splashScreen] })
|
||||||
|
} else if (guideSelection.customId === 'configureServer') {
|
||||||
|
const userDMChannel = await guideSelection.user.createDM()
|
||||||
|
userDMChannel.send({ embeds: configureServer(), files: [startScreen, serverConnection] })
|
||||||
|
} else if (guideSelection.customId === 'login') {
|
||||||
|
const userDMChannel = await guideSelection.user.createDM()
|
||||||
|
userDMChannel.send({ embeds: loginInfo(), files: [accountChoice, loginScreen] })
|
||||||
|
} else if (guideSelection.customId === 'useSyncGroup') {
|
||||||
|
const userDMChannel = await guideSelection.user.createDM()
|
||||||
|
userDMChannel.send({ embeds: useSyncgroup(), files: [overview, joingroup, resume, leavegroup] })
|
||||||
|
} else if (guideSelection.customId === 'explainRoles') {
|
||||||
|
const userDMChannel = await guideSelection.user.createDM()
|
||||||
|
userDMChannel.send({ embeds: explainRole() })
|
||||||
|
}
|
||||||
|
|
||||||
|
guideSelection.update({ content: "Hab ich dir per DM geschickt :)", components: [] })
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
await interaction.interaction.editReply({ content: 'Das dauert mir zu lange, frag mich nochmal wenn du nen Guide brauchst', components: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
148
server/commands/mitgucken.ts
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import { Command } from '../structures/command'
|
||||||
|
import { RunOptions } from '../types/commandTypes'
|
||||||
|
import { APIEmbed } from 'discord.js'
|
||||||
|
import { v4 as uuid } from 'uuid'
|
||||||
|
import { logger } from '../logger'
|
||||||
|
import { attachmentImages } from '../..'
|
||||||
|
|
||||||
|
const color = 0x0099FF
|
||||||
|
export default new Command({
|
||||||
|
name: 'mitgucken',
|
||||||
|
description: 'Erfahre wie die Verbindung mit Jellyfin funktioniert und eine WatchTogether Gruppe funktioniert.',
|
||||||
|
options: [],
|
||||||
|
run: async (interaction: RunOptions) => {
|
||||||
|
const requestId = uuid()
|
||||||
|
interaction.interaction.followUp('Ich schicke dir einen Guide per DM!')
|
||||||
|
const embedList: APIEmbed[] = []
|
||||||
|
embedList.push(...installation())
|
||||||
|
embedList.push(...configureServer())
|
||||||
|
embedList.push(...explainRole())
|
||||||
|
embedList.push(...loginInfo())
|
||||||
|
embedList.push(...useSyncgroup())
|
||||||
|
|
||||||
|
//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 })
|
||||||
|
const userDMchannel = await interaction.interaction.user.createDM()
|
||||||
|
userDMchannel.send({ embeds: embedList, files: attachmentImages })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
embedList.push({
|
||||||
|
color,
|
||||||
|
title: 'Jellyfin Media Player Installation',
|
||||||
|
description: 'Du kannst den Jellyfin Media Player von github herunterladen.\n Der Mediaplayer muss genutzt werden, da ein Schauen direkt über das Webinterface den Server zum Schmelzen bringt.\nFühre die Datei aus und installiere den Jellyfin Media Player an den Ort deiner Wahl.',
|
||||||
|
fields: [
|
||||||
|
{ name: "Windows", value: "https://github.com/jellyfin/jellyfin-media-player/releases/download/v1.9.1/JellyfinMediaPlayer-1.9.1-windows-x64.exe" },
|
||||||
|
{ name: "Mac", value: "https://github.com/jellyfin/jellyfin-media-player/releases/download/v1.9.1/JellyfinMediaPlayer-1.9.1-macos-notarized.dmg" }
|
||||||
|
],
|
||||||
|
image: {
|
||||||
|
url: 'attachment://set_splashscreen.png'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return embedList
|
||||||
|
}
|
||||||
|
|
||||||
|
export function configureServer(): APIEmbed[] {
|
||||||
|
const embedList: APIEmbed[] = []
|
||||||
|
// Login
|
||||||
|
embedList.push({
|
||||||
|
color,
|
||||||
|
title: "Server Auswahl",
|
||||||
|
description: "Die Jellyfin App kann sich mit mehreren Servern verbinden.\n Hattest du noch nie eine Server Verbindung wähle hier 'Server hinzufügen'.",
|
||||||
|
image: {
|
||||||
|
url: 'attachment://start_screen.png'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Server Address
|
||||||
|
embedList.push({
|
||||||
|
color,
|
||||||
|
title: "Server Verbindung",
|
||||||
|
description: "Stelle eine Verbindung zum Hartzarett Jellyfin Server her",
|
||||||
|
fields: [
|
||||||
|
{ name: "Server Adresse", value: "`https://media.hartzarett.ruhr`" }
|
||||||
|
],
|
||||||
|
image: {
|
||||||
|
url: 'attachment://server_verbindung.png'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return embedList
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loginInfo(): APIEmbed[] {
|
||||||
|
const embedList: APIEmbed[] = []
|
||||||
|
// Account choice
|
||||||
|
embedList.push({
|
||||||
|
color,
|
||||||
|
title: "Account Auswahl",
|
||||||
|
description: "In der Regel sind die Accounts aus Datenschutzgründen versteckt.\nWähle 'Manuelle Anmeldung' aus",
|
||||||
|
image: {
|
||||||
|
url: 'attachment://auswahl_anmeldung.png'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// password screen
|
||||||
|
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 `/passwort_reset` aus :)",
|
||||||
|
image: {
|
||||||
|
url: 'attachment://login_screen.png'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return embedList
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSyncgroup(): APIEmbed[] {
|
||||||
|
const embedList: APIEmbed[] = []
|
||||||
|
embedList.push({
|
||||||
|
color,
|
||||||
|
title: "SyncPlay Menü",
|
||||||
|
image: {
|
||||||
|
url: 'attachment://jellyfin_ubersicht.png'
|
||||||
|
},
|
||||||
|
description: "Im Hauptbildschirm findest du die 'SyncPlay' Einstellungen oben rechts.",
|
||||||
|
})
|
||||||
|
// join group
|
||||||
|
embedList.push({
|
||||||
|
color,
|
||||||
|
title: "Gruppe beitreten",
|
||||||
|
image: {
|
||||||
|
url: 'attachment://gruppe_beitreten.png'
|
||||||
|
},
|
||||||
|
description: "Suche dir aus dem Dropdown die SyncPlay Gruppe aus, die zu deinem Event gehört.",
|
||||||
|
})
|
||||||
|
// leave group
|
||||||
|
embedList.push({
|
||||||
|
color,
|
||||||
|
title: "Gruppe verlassen",
|
||||||
|
image: {
|
||||||
|
url: 'attachment://gruppe_verlassen.png'
|
||||||
|
},
|
||||||
|
description: "Wenn du die Watchparty verlassen möchtest, kannst du das ebenfalls über das Menü oben rechts tun.",
|
||||||
|
})
|
||||||
|
//resume playback
|
||||||
|
embedList.push({
|
||||||
|
color,
|
||||||
|
title: "Wiedergabe fortsetzen",
|
||||||
|
image: {
|
||||||
|
url: 'attachment://wiedergabe_fortsetzen.png'
|
||||||
|
},
|
||||||
|
description: "Wenn du aus der Watchparty rausgeflogen bist, oder die Wiedergabe verlassen hast, kannst du über das Menü oben rechts auch wieder zurückkehren.",
|
||||||
|
})
|
||||||
|
return embedList
|
||||||
|
}
|
||||||
|
|
||||||
|
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.`
|
||||||
|
|
@ -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',
|
||||||
|
@ -20,6 +20,16 @@ export interface Config {
|
|||||||
jellyfin_url: string
|
jellyfin_url: string
|
||||||
port: number
|
port: number
|
||||||
workaround_token: string
|
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 = {
|
export const config: Config = {
|
||||||
@ -49,6 +59,16 @@ export const config: Config = {
|
|||||||
client_id: process.env.CLIENT_ID ?? "",
|
client_id: process.env.CLIENT_ID ?? "",
|
||||||
jellfin_token: process.env.JELLYFIN_TOKEN ?? "",
|
jellfin_token: process.env.JELLYFIN_TOKEN ?? "",
|
||||||
jellyfin_url: process.env.JELLYFIN_URL ?? "",
|
jellyfin_url: process.env.JELLYFIN_URL ?? "",
|
||||||
workaround_token: process.env.TOKEN ?? ""
|
workaround_token: process.env.TOKEN ?? "",
|
||||||
|
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 ?? ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Collection, GuildMember } from "discord.js"
|
import { Collection, GuildMember } from "discord.js"
|
||||||
import { filterRolesFromMemberUpdate } from "../helper/roleFilter"
|
import { filterRolesFromMemberUpdate, getGuildSpecificTriggerRoleId } from "../helper/roleFilter"
|
||||||
import { ChangedRoles, PermissionLevel } from "../interfaces"
|
import { ChangedRoles, PermissionLevel } from "../interfaces"
|
||||||
import { jellyfinHandler } from "../.."
|
import { jellyfinHandler } from "../.."
|
||||||
import { v4 as uuid } from "uuid"
|
import { v4 as uuid } from "uuid"
|
||||||
@ -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)
|
||||||
@ -18,7 +18,7 @@ export async function execute(oldMember: GuildMember, newMember: GuildMember) {
|
|||||||
}
|
}
|
||||||
const removedRoleMatches = changedRoles.removedRoles.find(rRole => rRole.id === key)
|
const removedRoleMatches = changedRoles.removedRoles.find(rRole => rRole.id === key)
|
||||||
if (removedRoleMatches) {
|
if (removedRoleMatches) {
|
||||||
jellyfinHandler.removeUser(newMember, requestId)
|
jellyfinHandler.removeUser(newMember, level, requestId)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -26,10 +26,5 @@ export async function execute(oldMember: GuildMember, newMember: GuildMember) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGuildSpecificTriggerRoleId(id: string): Collection<string, PermissionLevel> {
|
|
||||||
const outVal = new Collection<string, PermissionLevel>()
|
|
||||||
outVal.set('1096819983889215659', "VIEWER")
|
|
||||||
outVal.set('1097990848613986526', "ADMIN")
|
|
||||||
return outVal
|
|
||||||
}
|
|
||||||
|
|
||||||
|
63
server/events/guildScheduledEventCreate.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
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, yavinJellyfinHandler } from "../..";
|
||||||
|
import { config } from "../configuration";
|
||||||
|
import { Maybe } from "../interfaces";
|
||||||
|
import { logger } from "../logger";
|
||||||
|
|
||||||
|
|
||||||
|
export const name = 'guildScheduledEventCreate'
|
||||||
|
|
||||||
|
export enum Emotes { "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟" }
|
||||||
|
|
||||||
|
export let task: ScheduledTask | undefined
|
||||||
|
|
||||||
|
export async function execute(event: GuildScheduledEvent) {
|
||||||
|
const requestId = uuid()
|
||||||
|
logger.debug(`New event created: ${JSON.stringify(event, null, 2)}`, { guildId: event.guildId, requestId })
|
||||||
|
|
||||||
|
if (event.name.toLowerCase().includes("!nextwp")) {
|
||||||
|
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.getRandomMovieNames(5, event.guildId, requestId)
|
||||||
|
|
||||||
|
logger.info(`Got ${movies.length} random movies. Creating voting`, { guildId: event.guildId, requestId })
|
||||||
|
logger.debug(`Movies: ${JSON.stringify(movies)}`, { 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
|
||||||
|
}
|
||||||
|
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`
|
||||||
|
|
||||||
|
for (let i = 0; i < movies.length; i++) {
|
||||||
|
message = message.concat(Emotes[i]).concat(": ").concat(movies[i]).concat("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: MessageCreateOptions = {
|
||||||
|
allowedMentions: { parse: ["roles"]},
|
||||||
|
content: message
|
||||||
|
}
|
||||||
|
|
||||||
|
const sentMessage: Message<true> = await (await announcementChannel.fetch()).send(options)
|
||||||
|
|
||||||
|
for (let i = 0; i < movies.length; i++) {
|
||||||
|
sentMessage.react(Emotes[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
// sentMessage.pin() //todo: uncomment when bot has permission to pin messages. Also update closepoll.ts to only fetch pinned messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
64
server/events/guildScheduledEventUpdate.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { GuildMember, GuildScheduledEvent, GuildScheduledEventStatus } from "discord.js";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
|
import { client, jellyfinHandler } from "../..";
|
||||||
|
import { getGuildSpecificTriggerRoleId } from "../helper/roleFilter";
|
||||||
|
import { logger } from "../logger";
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 allMembers = eventMembers.concat(channelMembers ?? [])
|
||||||
|
|
||||||
|
const members: GuildMember[] = []
|
||||||
|
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.`))
|
||||||
|
})
|
||||||
|
deleteJFUsers(newEvent.guildId, requestId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createJFUsers(members: GuildMember[], movieName: string, requestId?: string) {
|
||||||
|
logger.info(`Creating users for: \n ${JSON.stringify(members, null, 2)}`)
|
||||||
|
members.forEach(member => {
|
||||||
|
member.createDM().then(channel => channel.send(`Hey! Du hast dich für die Watchparty von ${movieName} angemeldet! Es geht gleich los!`))
|
||||||
|
jellyfinHandler.upsertUser(member, "TEMPORARY", requestId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteJFUsers(guildId: string, requestId?: string) {
|
||||||
|
logger.info(`Watchparty ended, deleting tmp users`, { guildId, requestId })
|
||||||
|
jellyfinHandler.purge(guildId, requestId)
|
||||||
|
}
|
@ -8,7 +8,7 @@ export async function execute(interaction: ExtendedInteraction) {
|
|||||||
//console.dir(interaction, { depth: null })
|
//console.dir(interaction, { depth: null })
|
||||||
if (interaction.isCommand()) {
|
if (interaction.isCommand()) {
|
||||||
logger.info(`Interaction is a command.`, { guildId: interaction.guild?.id })
|
logger.info(`Interaction is a command.`, { guildId: interaction.guild?.id })
|
||||||
await interaction.deferReply()
|
await interaction.deferReply({ ephemeral: true })
|
||||||
const command = client.commands.get(interaction.commandName)
|
const command = client.commands.get(interaction.commandName)
|
||||||
if (!command)
|
if (!command)
|
||||||
return interaction.followUp('Invalid command')
|
return interaction.followUp('Invalid command')
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
export const name = 'ready'
|
|
||||||
export function execute(client: any) {
|
|
||||||
//console.log(`Processing ready: ${JSON.stringify(client)} has been created.`)
|
|
||||||
}
|
|
59
server/events/voiceStateUpdate.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { VoiceState } from "discord.js";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
|
import { jellyfinHandler } from "../..";
|
||||||
|
import { UserUpsertResult } from "../jellyfin/handler";
|
||||||
|
import { logger } from "../logger";
|
||||||
|
|
||||||
|
|
||||||
|
export const name = 'voiceStateUpdate'
|
||||||
|
|
||||||
|
export async function execute(oldState: VoiceState, newState: VoiceState) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.info(JSON.stringify(newState, null, 2))
|
||||||
|
//ignore events like mute/unmute
|
||||||
|
if(newState.channel?.id === oldState.channel?.id) {
|
||||||
|
logger.info("Not handling VoiceState event because channelid of old and new was the same (i.e. mute/unmute event)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const scheduledEvents = (await newState.guild.scheduledEvents.fetch())
|
||||||
|
.filter((key) => key.description?.toLowerCase().includes("!wp") && key.isActive())
|
||||||
|
.map((key) => key)
|
||||||
|
|
||||||
|
const scheduledEventUsers = (await Promise.all(scheduledEvents.map(event => event.fetchSubscribers({withMember: true}))))
|
||||||
|
|
||||||
|
//Dont handle users, that are already subscribed to the event. We only want to handle unsubscribed users here
|
||||||
|
let userFound = false;
|
||||||
|
scheduledEventUsers.forEach(collection => {
|
||||||
|
collection.each(key => {
|
||||||
|
logger.info(JSON.stringify(key, null, 2))
|
||||||
|
if(key.member.user.id === newState.member?.user.id)
|
||||||
|
userFound = true;
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if(userFound) {
|
||||||
|
logger.info(`Not handling VoiceState event because user was already subscribed and got an account from there. User: ${JSON.stringify(newState.member, null, 2)}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (scheduledEvents.find(event => event.channelId === newState.channelId)) {
|
||||||
|
if(newState.member){
|
||||||
|
logger.info("YO! Da ist jemand dem Channel mit dem Event beigetreten, ich kümmer mich mal um nen Account!")
|
||||||
|
const result = await jellyfinHandler.upsertUser(newState.member, "TEMPORARY", uuid())
|
||||||
|
if (result === UserUpsertResult.created) {
|
||||||
|
newState.member.createDM().then(channel => channel.send(`Hey! Du bist unserer Watchparty beigetreten, ich hab dir gerade die Zugangsdaten für den Mediaserver geschickt!`))
|
||||||
|
} else {
|
||||||
|
newState.member.createDM().then(channel => channel.send(`Hey! Du bist unserer Watchparty beigetreten aber du hast bereits einen Account. Falls du ein neues Passwort brauchst nutze /reset_passwort!`))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.error("WTF? Expected Member?? When doing things")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.info("VoiceState channelId was not the id of any channel with events")
|
||||||
|
}
|
||||||
|
}catch(error){
|
||||||
|
logger.error(error)
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import { GuildMember } from "discord.js"
|
import { Collection, GuildMember } from "discord.js"
|
||||||
import { ChangedRoles } from "../interfaces"
|
import { ChangedRoles, PermissionLevel } from "../interfaces"
|
||||||
import { logger } from "../logger"
|
import { logger } from "../logger"
|
||||||
|
import { config } from "../configuration"
|
||||||
|
|
||||||
export function filterRolesFromMemberUpdate(oldMember: GuildMember, newMember: GuildMember): ChangedRoles {
|
export function filterRolesFromMemberUpdate(oldMember: GuildMember, newMember: GuildMember): ChangedRoles {
|
||||||
|
|
||||||
@ -14,3 +15,10 @@ export function filterRolesFromMemberUpdate(oldMember: GuildMember, newMember: G
|
|||||||
|
|
||||||
return { addedRoles, removedRoles }
|
return { addedRoles, removedRoles }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
return outVal
|
||||||
|
}
|
||||||
|
@ -32,4 +32,10 @@ export interface ChangedRoles {
|
|||||||
addedRoles: Collection<string, Role>
|
addedRoles: Collection<string, Role>
|
||||||
removedRoles: Collection<string, Role>
|
removedRoles: Collection<string, Role>
|
||||||
}
|
}
|
||||||
export type PermissionLevel = "VIEWER" | "ADMIN"
|
export interface JellyfinConfig {
|
||||||
|
jellyfinUrl: string,
|
||||||
|
jellyfinToken: string,
|
||||||
|
movieCollectionId: string,
|
||||||
|
collectionUser: string
|
||||||
|
}
|
||||||
|
export type PermissionLevel = "VIEWER" | "ADMIN" | "TEMPORARY"
|
||||||
|
@ -1,65 +1,59 @@
|
|||||||
import { GuildMember } from "discord.js";
|
import { GuildMember } from "discord.js";
|
||||||
import { CreateUserByNameOperationRequest, SystemApi, UpdateUserPasswordOperationRequest, UpdateUserPolicyOperationRequest, UserApi } from "./apis";
|
import { JellyfinConfig, Maybe, PermissionLevel } from "../interfaces";
|
||||||
|
import { logger } from "../logger";
|
||||||
|
import { CreateUserByNameOperationRequest, DeleteUserRequest, GetItemsRequest, ItemsApi, SystemApi, UpdateUserPasswordOperationRequest, UpdateUserPolicyOperationRequest, UserApi } from "./apis";
|
||||||
|
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";
|
||||||
import { CreateUserByNameRequest, UpdateUserEasyPasswordRequest, UpdateUserPasswordRequest, UpdateUserPolicyRequest } from "./models";
|
|
||||||
import { Config } from "../configuration";
|
|
||||||
import { logger } from "../logger";
|
|
||||||
import { Maybe } from "../interfaces";
|
|
||||||
import { v4 as uuid } from "uuid";
|
|
||||||
|
|
||||||
|
|
||||||
export class JellyfinHandler {
|
export class JellyfinHandler {
|
||||||
|
|
||||||
private userApi: UserApi
|
private userApi: UserApi
|
||||||
private systemApi: SystemApi
|
private systemApi: SystemApi
|
||||||
|
private moviesApi: ItemsApi
|
||||||
private token: string
|
private token: string
|
||||||
private authHeader: { headers: { 'X-Emby-Authorization': string } }
|
private authHeader: { headers: { 'X-Emby-Authorization': string } }
|
||||||
private config: Config
|
private config: JellyfinConfig
|
||||||
private serverName = "";
|
private serverName = "";
|
||||||
|
|
||||||
public async ServerName(): Promise<string> {
|
constructor(_config: JellyfinConfig, _userApi?: UserApi, _systemApi?: SystemApi, _itemsApi?: ItemsApi) {
|
||||||
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) {
|
|
||||||
this.config = _config
|
this.config = _config
|
||||||
this.token = this.config.bot.jellfin_token
|
this.token = this.config.jellyfinToken
|
||||||
this.authHeader = {
|
this.authHeader = {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Emby-Authorization": this.config.bot.workaround_token
|
"X-Emby-Authorization": this.config.jellyfinToken
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const userApiConfigurationParams: ConfigurationParameters = {
|
const userApiConfigurationParams: ConfigurationParameters = {
|
||||||
basePath: this.config.bot.jellyfin_url,
|
basePath: this.config.jellyfinUrl,
|
||||||
headers: this.authHeader.headers
|
headers: this.authHeader.headers
|
||||||
}
|
}
|
||||||
const systemApiConfigurationParams: ConfigurationParameters = {
|
const systemApiConfigurationParams: ConfigurationParameters = {
|
||||||
basePath: this.config.bot.jellyfin_url,
|
basePath: this.config.jellyfinUrl,
|
||||||
headers: this.authHeader.headers
|
headers: this.authHeader.headers
|
||||||
}
|
}
|
||||||
|
const libraryApiConfigurationParams: ConfigurationParameters = {
|
||||||
|
basePath: this.config.jellyfinUrl,
|
||||||
|
headers: this.authHeader.headers
|
||||||
|
}
|
||||||
|
|
||||||
this.userApi = _userApi ?? new UserApi(new Configuration(userApiConfigurationParams))
|
this.userApi = _userApi ?? new UserApi(new Configuration(userApiConfigurationParams))
|
||||||
this.systemApi = _systemApi ?? new SystemApi(new Configuration(systemApiConfigurationParams))
|
this.systemApi = _systemApi ?? new SystemApi(new Configuration(systemApiConfigurationParams))
|
||||||
|
this.moviesApi = _itemsApi ?? new ItemsApi(new Configuration(libraryApiConfigurationParams))
|
||||||
logger.info(`Initialized Jellyfin handler`, { requestId: 'Init' })
|
logger.info(`Initialized Jellyfin handler`, { requestId: 'Init' })
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateJFUserName(discordUser: GuildMember): string {
|
private generateJFUserName(discordUser: GuildMember, level: PermissionLevel): string {
|
||||||
return discordUser.displayName
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createUserAccountForDiscordUser(discordUser: GuildMember, guildId?: string, requestId?: string): Promise<UserDto> {
|
public async createUserAccountForDiscordUser(discordUser: GuildMember, level: PermissionLevel, guildId?: string, requestId?: string): Promise<UserDto> {
|
||||||
const newUserName = this.generateJFUserName(discordUser)
|
const newUserName = this.generateJFUserName(discordUser, level)
|
||||||
logger.info(`New Username for ${discordUser.displayName}: ${newUserName}`, { guildId, requestId })
|
logger.info(`New Username for ${discordUser.displayName}: ${newUserName}`, { guildId, requestId })
|
||||||
const req: CreateUserByNameOperationRequest = {
|
const req: CreateUserByNameOperationRequest = {
|
||||||
createUserByNameRequest: {
|
createUserByNameRequest: {
|
||||||
@ -67,7 +61,7 @@ export class JellyfinHandler {
|
|||||||
password: this.generatePasswordForUser(),
|
password: this.generatePasswordForUser(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.debug(JSON.stringify(req))
|
logger.debug(JSON.stringify(req), { requestId, guildId })
|
||||||
const createResult = await this.userApi.createUserByName(req)
|
const createResult = await this.userApi.createUserByName(req)
|
||||||
if (createResult) {
|
if (createResult) {
|
||||||
(await discordUser.createDM()).send(`Ich hab dir mal nen Account angelegt :)\nDein Username ist ${createResult.name}, dein Password ist "${req.createUserByNameRequest.password}"!`)
|
(await discordUser.createDM()).send(`Ich hab dir mal nen Account angelegt :)\nDein Username ist ${createResult.name}, dein Password ist "${req.createUserByNameRequest.password}"!`)
|
||||||
@ -78,7 +72,7 @@ export class JellyfinHandler {
|
|||||||
|
|
||||||
public async isUserAlreadyPresent(discordUser: GuildMember, requestId?: string): Promise<boolean> {
|
public async isUserAlreadyPresent(discordUser: GuildMember, requestId?: string): Promise<boolean> {
|
||||||
const jfuser = await this.getUser(discordUser, requestId)
|
const jfuser = await this.getUser(discordUser, requestId)
|
||||||
logger.debug(`Presence for DiscordUser ${discordUser.id}:${jfuser !== undefined}`)
|
logger.debug(`Presence for DiscordUser ${discordUser.id}:${jfuser !== undefined}`, { guildId: discordUser.guild.id, requestId })
|
||||||
return jfuser !== undefined
|
return jfuser !== undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,30 +88,50 @@ export class JellyfinHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getUser(discordUser: GuildMember, requestId?: string): Promise<Maybe<UserDto>> {
|
public async getUser(discordUser: GuildMember, requestId?: string): Promise<Maybe<UserDto>> {
|
||||||
logger.info(`Getting user for discord member ${discordUser.displayName}`, {requestId})
|
logger.info(`Getting user for discord member ${discordUser.displayName}`, { requestId, guildId: discordUser.guild.id })
|
||||||
const jfUsernameFromDiscordUsername = this.generateJFUserName(discordUser)
|
|
||||||
const jfUsers = await this.getCurrentUsers(discordUser.guild.id, requestId)
|
const jfUsers = await this.getCurrentUsers(discordUser.guild.id, requestId)
|
||||||
const foundUser = jfUsers.find(x => x.name === jfUsernameFromDiscordUsername)
|
const foundUser = jfUsers.find(x => x.name?.includes(discordUser.displayName))
|
||||||
return foundUser
|
return foundUser
|
||||||
}
|
}
|
||||||
|
|
||||||
public async removeUser(newMember: GuildMember, requestId?: string) {
|
public async removeUser(newMember: GuildMember, level: PermissionLevel, requestId?: string) {
|
||||||
logger.error(`Trying to remove user ${newMember.displayName}, but method is not implemented`, {requestId})
|
logger.info(`${level == "TEMPORARY" ? "Deleting" : "Disabling"} user ${newMember.displayName}, but method is not implemented`, { requestId, guildId: newMember.guild.id })
|
||||||
const jfuser = await this.getUser(newMember, requestId)
|
const jfuser = await this.getUser(newMember, requestId)
|
||||||
if (jfuser) {
|
if (jfuser && jfuser.id) {
|
||||||
await this.disableUser(jfuser, newMember.guild.id, requestId)
|
if (level === "TEMPORARY") {
|
||||||
|
const r: DeleteUserRequest = {
|
||||||
|
userId: jfuser.id
|
||||||
|
}
|
||||||
|
this.userApi.deleteUser(r)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
await this.disableUser(jfuser, newMember.guild.id, requestId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async purge(guildId: string, requestId?: string) {
|
||||||
|
logger.info("Deleting tmp users", { requestId, guildId })
|
||||||
|
const users = (await this.userApi.getUsers()).filter(user => user.name?.endsWith("_tmp"))
|
||||||
|
|
||||||
|
users.forEach(user => {
|
||||||
|
if (user.id) {
|
||||||
|
const r: DeleteUserRequest = {
|
||||||
|
userId: user.id
|
||||||
|
}
|
||||||
|
this.userApi.deleteUser(r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
public async resetUserPasswort(member: GuildMember, requestId?: string) {
|
public async resetUserPasswort(member: GuildMember, requestId?: string) {
|
||||||
logger.info(`Resetting password for user ${member.displayName}`, {requestId})
|
logger.info(`Resetting password for user ${member.displayName}`, { requestId, guildId: member.guild.id })
|
||||||
const jfUser = await this.getUser(member, requestId)
|
const jfUser = await this.getUser(member, requestId)
|
||||||
if (jfUser && jfUser.id) {
|
if (jfUser && jfUser.id) {
|
||||||
|
|
||||||
// const reset: UpdateUserPasswordRequest = {
|
// const reset: UpdateUserPasswordRequest = {
|
||||||
// resetPassword: true
|
// resetPassword: true
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// const shit: UpdateUserPasswordOperationRequest = {
|
// const shit: UpdateUserPasswordOperationRequest = {
|
||||||
// updateUserPasswordRequest: reset,
|
// updateUserPasswordRequest: reset,
|
||||||
// userId: jfUser.id
|
// userId: jfUser.id
|
||||||
@ -127,7 +141,7 @@ export class JellyfinHandler {
|
|||||||
|
|
||||||
// logger.info("Resetting password", {requestId})
|
// logger.info("Resetting password", {requestId})
|
||||||
// await this.userApi.updateUserPassword(shit);
|
// await this.userApi.updateUserPassword(shit);
|
||||||
|
|
||||||
const password = this.generatePasswordForUser()
|
const password = this.generatePasswordForUser()
|
||||||
const passwordRequest: UpdateUserPasswordRequest = {
|
const passwordRequest: UpdateUserPasswordRequest = {
|
||||||
// resetPassword: true,
|
// resetPassword: true,
|
||||||
@ -140,7 +154,7 @@ export class JellyfinHandler {
|
|||||||
userId: jfUser.id
|
userId: jfUser.id
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("Setting new password", {requestId})
|
logger.info("Setting new password", { requestId, guildId: member.guild.id })
|
||||||
await this.userApi.updateUserPassword(passwordOperationRequest);
|
await this.userApi.updateUserPassword(passwordOperationRequest);
|
||||||
|
|
||||||
|
|
||||||
@ -155,7 +169,7 @@ export class JellyfinHandler {
|
|||||||
public async disableUser(user: UserDto, guildId?: string, requestId?: string): Promise<void> {
|
public async disableUser(user: UserDto, guildId?: string, requestId?: string): Promise<void> {
|
||||||
if (user.id) {
|
if (user.id) {
|
||||||
const jfUser = await this.getUser(<GuildMember>{ displayName: user.name, guild: { id: guildId } }, requestId)
|
const jfUser = await this.getUser(<GuildMember>{ displayName: user.name, guild: { id: guildId } }, requestId)
|
||||||
logger.info(`Trying to disable user: ${user.name}|${user.id}|${JSON.stringify(jfUser, null, 2)}`)
|
logger.info(`Trying to disable user: ${user.name}|${user.id}|${JSON.stringify(jfUser, null, 2)}`, { guildId, requestId })
|
||||||
const r: UpdateUserPolicyOperationRequest = {
|
const r: UpdateUserPolicyOperationRequest = {
|
||||||
userId: user.id ?? "",
|
userId: user.id ?? "",
|
||||||
updateUserPolicyRequest: {
|
updateUserPolicyRequest: {
|
||||||
@ -164,7 +178,7 @@ export class JellyfinHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
await this.userApi.updateUserPolicy(r)
|
await this.userApi.updateUserPolicy(r)
|
||||||
logger.info(`Succeeded with disabling user: ${user.name}`)
|
logger.info(`Succeeded with disabling user: ${user.name}`, { guildId, requestId })
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
logger.error(`Can not disable user ${JSON.stringify(user)}, has no id?!`, { requestId, guildId })
|
logger.error(`Can not disable user ${JSON.stringify(user)}, has no id?!`, { requestId, guildId })
|
||||||
@ -174,7 +188,7 @@ export class JellyfinHandler {
|
|||||||
public async enableUser(user: UserDto, guildId: string, requestId?: string): Promise<void> {
|
public async enableUser(user: UserDto, guildId: string, requestId?: string): Promise<void> {
|
||||||
if (user.id) {
|
if (user.id) {
|
||||||
const jfUser = await this.getUser(<GuildMember>{ displayName: user.name, guild: { id: guildId } }, requestId)
|
const jfUser = await this.getUser(<GuildMember>{ displayName: user.name, guild: { id: guildId } }, requestId)
|
||||||
logger.info(`Trying to enable user: ${user.name}|${user.id}|${JSON.stringify(jfUser, null, 2)}`)
|
logger.info(`Trying to enable user: ${user.name}|${user.id}|${JSON.stringify(jfUser, null, 2)}`, { guildId, requestId })
|
||||||
const r: UpdateUserPolicyOperationRequest = {
|
const r: UpdateUserPolicyOperationRequest = {
|
||||||
userId: user.id ?? "",
|
userId: user.id ?? "",
|
||||||
updateUserPolicyRequest: {
|
updateUserPolicyRequest: {
|
||||||
@ -183,22 +197,67 @@ export class JellyfinHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
await this.userApi.updateUserPolicy(r)
|
await this.userApi.updateUserPolicy(r)
|
||||||
logger.info(`Succeeded with enabling user: ${user.name}`)
|
logger.info(`Succeeded with enabling user: ${user.name}`, { guildId, requestId })
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
logger.error(`Can not enable user ${JSON.stringify(user)}, has no id?!`, { requestId, guildId })
|
logger.error(`Can not enable user ${JSON.stringify(user)}, has no id?!`, { requestId, guildId })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async upsertUser(newMember: GuildMember, level: string, requestId?: string) {
|
public async upsertUser(newMember: GuildMember, level: PermissionLevel, requestId?: string): Promise<UserUpsertResult> {
|
||||||
logger.error(`Trying to upsert user ${newMember.displayName}, with permissionLevel ${level}`)
|
logger.info(`Trying to upsert user ${newMember.displayName}, with permissionLevel ${level}`, { guildId: newMember.guild.id, requestId })
|
||||||
const jfuser = await this.getUser(newMember, requestId)
|
const jfuser = await this.getUser(newMember, requestId)
|
||||||
if (jfuser) {
|
if (jfuser && !jfuser.policy?.isDisabled) {
|
||||||
logger.info(`User with name ${newMember.displayName} is already present`)
|
logger.info(`User with name ${newMember.displayName} is already present`, { guildId: newMember.guild.id, requestId })
|
||||||
await this.enableUser(jfuser, newMember.guild.id, requestId)
|
await this.enableUser(jfuser, newMember.guild.id, requestId)
|
||||||
|
return UserUpsertResult.enabled
|
||||||
} else {
|
} else {
|
||||||
this.createUserAccountForDiscordUser(newMember, newMember.guild.id, requestId)
|
this.createUserAccountForDiscordUser(newMember, level, newMember.guild.id, requestId)
|
||||||
|
return UserUpsertResult.created
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public async getAllMovies(guildId: string, requestId: string): Promise<BaseItemDto[]> {
|
||||||
|
logger.info("requesting all movies from jellyfin", { guildId, requestId })
|
||||||
|
|
||||||
|
const searchParams: GetItemsRequest = {
|
||||||
|
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 })
|
||||||
|
logger.info(`Found ${movies?.length} movies in total`, { guildId, requestId })
|
||||||
|
return movies ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getRandomMovies(count: number, guildId: string, requestId: string): Promise<BaseItemDto[]> {
|
||||||
|
logger.info(`${count} random movies requested.`, { guildId, requestId })
|
||||||
|
const allMovies = await this.getAllMovies(guildId, requestId)
|
||||||
|
if (count >= allMovies.length) {
|
||||||
|
logger.info(`${count} random movies requested but found only ${allMovies.length}. Returning all Movies.`, { guildId, requestId })
|
||||||
|
return allMovies
|
||||||
|
}
|
||||||
|
const movies: BaseItemDto[] = []
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
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 }
|
||||||
|
|
||||||
|
@ -1,18 +1,28 @@
|
|||||||
import { ApplicationCommandDataResolvable, Client, ClientOptions, Collection, GatewayIntentBits, Guild, IntentsBitField, Snowflake } 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";
|
||||||
|
import { checkForPollsToClose } from "../commands/closepoll";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export class ExtendedClient extends Client {
|
export class ExtendedClient extends Client {
|
||||||
private eventFilePath = `${__dirname}/../events`
|
private eventFilePath = `${__dirname}/../events`
|
||||||
private commandFilePath = `${__dirname}/../commands`
|
private commandFilePath = `${__dirname}/../commands`
|
||||||
private jellyfin: JellyfinHandler
|
private jellyfin: JellyfinHandler
|
||||||
public commands: Collection<string, CommandType> = new Collection()
|
public commands: Collection<string, CommandType> = new Collection()
|
||||||
|
private announcementChannels: Collection<string, TextChannel> = new Collection() //guildId to TextChannel
|
||||||
|
private announcementRoleHandlerTask: Collection<string, ScheduledTask> = new Collection() //one task per guild
|
||||||
|
private pollCloseBackgroundTasks: Collection<string, ScheduledTask> = new Collection()
|
||||||
public constructor(jf: JellyfinHandler) {
|
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)
|
intents.add(IntentsBitField.Flags.GuildMembers, IntentsBitField.Flags.MessageContent, IntentsBitField.Flags.Guilds, IntentsBitField.Flags.DirectMessages, IntentsBitField.Flags.GuildScheduledEvents, IntentsBitField.Flags.GuildVoiceStates)
|
||||||
const options: ClientOptions = { intents }
|
const options: ClientOptions = { intents }
|
||||||
super(options)
|
super(options)
|
||||||
this.jellyfin = jf
|
this.jellyfin = jf
|
||||||
@ -25,7 +35,6 @@ export class ExtendedClient extends Client {
|
|||||||
Promise.all(promises).then(() => {
|
Promise.all(promises).then(() => {
|
||||||
this.login(config.bot.token)
|
this.login(config.bot.token)
|
||||||
})
|
})
|
||||||
logger.info(`Connected with ${await this.jellyfin.ServerName()}`)
|
|
||||||
}
|
}
|
||||||
private async importFile(filepath: string): Promise<any> {
|
private async importFile(filepath: string): Promise<any> {
|
||||||
logger.debug(`Importing ${filepath}`)
|
logger.debug(`Importing ${filepath}`)
|
||||||
@ -57,24 +66,44 @@ 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)
|
||||||
|
await this.cacheAnnouncementServer(guilds)
|
||||||
|
this.startAnnouncementRoleBackgroundTask(guilds)
|
||||||
|
this.startPollCloseBackgroundTasks()
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.info(`Error refreshing slash commands: ${error}`)
|
logger.info(`Error refreshing slash commands: ${error}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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)
|
||||||
|
|
||||||
|
if (!channels || channels.length != 1) {
|
||||||
|
logger.error(`Could not find announcement channel for guild ${guild.name} with guildId ${guild.id}. Found ${channels}`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
logger.info(`Fetched announcement channel: ${JSON.stringify(channels[0])}`)
|
||||||
|
this.announcementChannels.set(guild.id, channels[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public getAnnouncementChannelForGuild(guildId: string): Maybe<TextChannel> {
|
||||||
|
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) => {
|
||||||
logger.info(`Fetching members for ${guild.name}|${id}`)
|
logger.info(`Fetching members for ${guild.name}|${id}`)
|
||||||
guild.members.fetch()
|
guild.members.fetch()
|
||||||
logger.info(`Fetched: ${guild.memberCount} members`)
|
logger.info(`Fetched: ${guild.memberCount} members`)
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
public async registerEventCallback() {
|
public async registerEventCallback() {
|
||||||
try {
|
try {
|
||||||
@ -96,4 +125,57 @@ 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
private async startPollCloseBackgroundTasks() {
|
||||||
|
for(const guild of this.guilds.cache) {
|
||||||
|
this.pollCloseBackgroundTasks.set(guild[1].id, schedule("0 * * * * *", () => checkForPollsToClose(guild[1])))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|