45 Commits

Author SHA1 Message Date
0d3c62c6ad Merge pull request 'feat/formatting' (#53) from feat/formatting into master
Reviewed-on: #53
2023-06-24 22:55:42 +02:00
5816db48e6 Merge branch 'master' into feat/formatting
All checks were successful
Compile the repository / compile (pull_request) Successful in 1m16s
Run unit tests / test (pull_request) Successful in 1m33s
2023-06-24 21:55:51 +02:00
66f843b399 format more files
All checks were successful
Compile the repository / compile (pull_request) Successful in 1m12s
2023-06-24 21:09:56 +02:00
d82a7cffd2 same config for all
All checks were successful
Compile the repository / compile (pull_request) Successful in 6s
2023-06-24 21:07:41 +02:00
8a06a661fa adjust some files to new formatting
All checks were successful
Compile the repository / compile (pull_request) Successful in 1m28s
2023-06-24 21:05:43 +02:00
4084f675cd change to tab indents 2023-06-24 21:05:33 +02:00
3bd26a9d6c Merge pull request 'feat/testing' (#52) from feat/testing into master
Reviewed-on: #52
2023-06-24 20:59:39 +02:00
e7b21fa658 apply editorconfig to ts files
All checks were successful
Compile the repository / compile (pull_request) Successful in 1m7s
2023-06-24 20:58:41 +02:00
2d32f9b680 format many files 2023-06-24 20:56:58 +02:00
5503aa8713 add editorconfig 2023-06-24 20:56:22 +02:00
25bb676fda readd compile
All checks were successful
Compile the repository / compile (pull_request) Successful in 10s
Run unit tests / test (pull_request) Successful in 8s
2023-06-24 20:37:59 +02:00
9f5abb8a90 Merge branch 'master' into feat/testing 2023-06-24 20:37:08 +02:00
0e67252976 Merge pull request 'use bash magic to get an env var from the package.json' (#51) from feat/build-versioned-image into master
Reviewed-on: #51
2023-06-24 20:35:23 +02:00
37b798818c handle timezone correctly in docker build
All checks were successful
Run unit tests / test (pull_request) Successful in 1m53s
Compile the repository / compile (pull_request) Successful in 1m10s
2023-06-24 20:31:37 +02:00
af414d0bad rename human facing name for test job
All checks were successful
Compile the repository / compile (pull_request) Successful in 7s
Run unit tests / test (pull_request) Successful in 7s
2023-06-24 20:17:04 +02:00
c32434a7eb rename test job
All checks were successful
Compile the repository / compile (pull_request) Successful in 9s
Compile the repository / test (pull_request) Successful in 9s
2023-06-24 20:16:03 +02:00
c133570d8c update other workflows to use staged builds
All checks were successful
Compile the repository / compile (pull_request) Successful in 8s
2023-06-24 20:11:39 +02:00
65cdee36e9 update testcase
All checks were successful
Compile the repository / compile (pull_request) Successful in 1m58s
2023-06-24 20:09:52 +02:00
6b0e84669a update dockerfile to support test stage 2023-06-24 20:09:09 +02:00
dd72f8e165 add automatic jest test in docker build to workflows 2023-06-24 20:09:00 +02:00
a6f19ccd2b add date test (WIP) 2023-06-24 19:56:49 +02:00
c39f9c6ee1 add first passing test 2023-06-24 19:56:30 +02:00
f41194ba71 add base jest setup 2023-06-24 19:11:12 +02:00
fa49dc0f76 use bash magic to get an env var from the package.json
All checks were successful
Compile the repository / compile (pull_request) Successful in 7s
this is shamelessly stolen from work
2023-06-24 02:14:53 +02:00
e52e845851 1.1.3
All checks were successful
Build a docker image for node-jellyfin-role-bot / build-docker-image (push) Successful in 1m59s
2023-06-23 23:46:52 +02:00
61544feaba Fix stupid timezone issues 2023-06-23 23:46:11 +02:00
1966640239 Merge branch 'master' of ssh://gitea.brudi.xyz:222/kenobi/jellyfin-discord-bot 2023-06-23 21:24:32 +02:00
fa9998e92c Unallow transcoding per default for new users 2023-06-23 21:23:54 +02:00
c1a449bafe 1.1.2
All checks were successful
Build a docker image for node-jellyfin-role-bot / build-docker-image (push) Successful in 1m50s
2023-06-23 19:46:20 +02:00
d5d82043f0 temporarily remove second tag on docker build 2023-06-23 19:46:06 +02:00
51ebf2e939 1.1.1
All checks were successful
Build a docker image for node-jellyfin-role-bot / build-docker-image (push) Successful in 12s
2023-06-23 19:44:58 +02:00
f314b2f355 maybe fix a docker build typo 2023-06-23 19:44:44 +02:00
a4d7c57d10 1.1.0
All checks were successful
Build a docker image for node-jellyfin-role-bot / build-docker-image (push) Successful in 14s
2023-06-23 19:33:31 +02:00
2802afa7d5 Merge pull request 'feat/#42_announce_manual_watchparty' (#50) from feat/#42_announce_manual_watchparty into master
Reviewed-on: #50
2023-06-23 19:31:57 +02:00
3a5ea5d4ff improve message clarity when no start date in event found
All checks were successful
Compile the repository / compile (pull_request) Successful in 4m8s
2023-06-23 19:30:17 +02:00
45d87275bf prevent announcement when description contains !private
All checks were successful
Compile the repository / compile (pull_request) Successful in 1m43s
2023-06-23 15:56:36 +02:00
31e440434e properly clean up wp announcements without event only 2023-06-23 15:51:48 +02:00
3d70b56eb7 Merge pull request 'feat/#43_append_event_invite' (#49) from feat/#43_append_event_invite into master
Reviewed-on: #49
2023-06-23 15:49:59 +02:00
3298c7a244 Merge pull request 'Create option "none of that" in voting' (#48) from feat/#39_none_of_that into master
Reviewed-on: #48
2023-06-23 15:49:43 +02:00
5b98c9bf2f Rename event files to specific case
We can add multiple eventhandlers per eventname. To avoid confusion and large files and to improve concise file names the event files were renamed
2023-06-23 14:37:41 +02:00
ee363e065c 1.0.4
All checks were successful
Build a docker image for node-jellyfin-role-bot / build-docker-image (push) Successful in 47s
2023-06-22 23:23:56 +02:00
9af847f234 Fix docker-build for good 2023-06-22 23:23:38 +02:00
9da8f47784 add event box to vote closed message
All checks were successful
Compile the repository / compile (pull_request) Successful in 1m18s
2023-06-22 19:38:59 +02:00
e8c58d5ff8 add event box to create message 2023-06-22 19:35:47 +02:00
8569a3e1e6 Create option "none of that" in voting
All checks were successful
Compile the repository / compile (pull_request) Successful in 1m19s
2023-06-22 18:53:29 +02:00
36 changed files with 1682 additions and 1413 deletions

7
.editorconfig Normal file
View File

@ -0,0 +1,7 @@
root = true
[*]
indent_style = tab
tab_width = 4
[*.ts]
indent_style = tab
tab_width = 4

View File

@ -14,4 +14,4 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Build Container - name: Build Container
run: docker build . run: docker build --target compile .

View File

@ -11,7 +11,6 @@ env:
jobs: jobs:
build-docker-image: build-docker-image:
runs-on: ubuntu-latest runs-on: ubuntu-latest
#if: gitea.ref == 'refs/heads/master'
container: catthehacker/ubuntu:act-latest container: catthehacker/ubuntu:act-latest
permissions: permissions:
contents: read contents: read
@ -22,6 +21,8 @@ jobs:
- name: Log in to the Container registry - name: Log in to the Container registry
run: docker login -u ${{ env.USER }} -p ${{ secrets.TOKEN }} ${{ env.REGISTRY }} run: docker login -u ${{ env.USER }} -p ${{ secrets.TOKEN }} ${{ env.REGISTRY }}
- name: Build Container - name: Build Container
run: docker build -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ node -p "require('./package.json').version" }}". run: docker build --target compile -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}" .
env:
version: $(cat package.json | awk 'match($0, /version/) {print $2}' | sed 's/[\",]//g') # extracts the version number from the package.json with bash magic
- name: Push Container - name: Push Container
run: docker push --all-tags "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" run: docker push --all-tags "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"

View File

@ -0,0 +1,18 @@
name: Run unit tests
on: [pull_request]
env:
REGISTRY: gitea.brudi.xyz
IMAGE_NAME: ${{ gitea.repository }}
USER: ${{ gitea.actor }}
jobs:
test:
runs-on: ubuntu-latest
container: catthehacker/ubuntu:act-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Run Tests
run: docker build --target test .

View File

@ -1,11 +1,21 @@
FROM node:alpine as Build FROM node:alpine as files
ENV NODE_ENV=production ENV TZ="Europe/Berlin"
WORKDIR /app WORKDIR /app
COPY [ "package-lock.json", "package.json", "index.ts", "tsconfig.json", "./" ] COPY [ "package-lock.json", "package.json", "index.ts", "tsconfig.json", "./" ]
COPY server ./server COPY server ./server
FROM files as proddependencies
ENV NODE_ENV=production
RUN npm ci --omit=dev RUN npm ci --omit=dev
FROM proddependencies as compile
RUN npm run build RUN npm run build
CMD ["npm","run","start"] CMD ["npm","run","start"]
FROM files as dependencies
RUN npm ci
FROM dependencies as test
COPY jest.config.js .
COPY tests ./tests
RUN npm run test

View File

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

18
jest.config.js Normal file
View File

@ -0,0 +1,18 @@
module.exports = {
'roots': [
'<rootDir>/tests',
'<rootDir>/server'
],
'transform': {
'^.+\\.tsx?$': 'ts-jest'
},
'testRegex': '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',
'moduleFileExtensions': [
'ts',
'tsx',
'js',
'jsx',
'json',
'node'
],
};

46
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "node-jellyfin-discord-bot", "name": "node-jellyfin-discord-bot",
"version": "1.0.3", "version": "1.1.3",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "node-jellyfin-discord-bot", "name": "node-jellyfin-discord-bot",
"version": "1.0.3", "version": "1.1.3",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@discordjs/rest": "^1.7.0", "@discordjs/rest": "^1.7.0",
@ -17,6 +17,7 @@
"@types/uuid": "^9.0.1", "@types/uuid": "^9.0.1",
"axios": "^1.3.5", "axios": "^1.3.5",
"date-fns": "^2.29.3", "date-fns": "^2.29.3",
"date-fns-tz": "^2.0.0",
"discord-api-types": "^0.37.38", "discord-api-types": "^0.37.38",
"discord.js": "^14.9.0", "discord.js": "^14.9.0",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
@ -29,12 +30,13 @@
"winston": "^3.8.2" "winston": "^3.8.2"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^29.5.0", "@types/jest": "^29.5.2",
"@typescript-eslint/eslint-plugin": "^5.58.0", "@typescript-eslint/eslint-plugin": "^5.58.0",
"@typescript-eslint/parser": "^5.58.0", "@typescript-eslint/parser": "^5.58.0",
"eslint": "^8.38.0", "eslint": "^8.38.0",
"jest": "^29.5.0", "jest": "^29.5.0",
"jest-cli": "^29.5.0", "jest-cli": "^29.5.0",
"mockdate": "^3.0.5",
"nodemon": "^2.0.22", "nodemon": "^2.0.22",
"rimraf": "^5.0.0", "rimraf": "^5.0.0",
"ts-jest": "^29.1.0" "ts-jest": "^29.1.0"
@ -1567,9 +1569,9 @@
} }
}, },
"node_modules/@types/jest": { "node_modules/@types/jest": {
"version": "29.5.0", "version": "29.5.2",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.0.tgz", "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.2.tgz",
"integrity": "sha512-3Emr5VOl/aoBwnWcH/EFQvlSAmjV+XtV9GGu5mwdYew5vhQh0IUZx/60x0TzHDu09Bi7HMx10t/namdJw5QIcg==", "integrity": "sha512-mSoZVJF5YzGVCk+FsDxzDuH7s+SCkzrgKZzf0Z0T2WudhBUPoF6ktoTPC4R0ZoCPCV5xUvuU6ias5NvxcBcMMg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"expect": "^29.0.0", "expect": "^29.0.0",
@ -2626,6 +2628,14 @@
"url": "https://opencollective.com/date-fns" "url": "https://opencollective.com/date-fns"
} }
}, },
"node_modules/date-fns-tz": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-2.0.0.tgz",
"integrity": "sha512-OAtcLdB9vxSXTWHdT8b398ARImVwQMyjfYGkKD2zaGpHseG2UPHbHjXELReErZFxWdSLph3c2zOaaTyHfOhERQ==",
"peerDependencies": {
"date-fns": ">=2.0.0"
}
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.3.4", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@ -4980,6 +4990,12 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/mockdate": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/mockdate/-/mockdate-3.0.5.tgz",
"integrity": "sha512-iniQP4rj1FhBdBYS/+eQv7j1tadJ9lJtdzgOpvsOHng/GbcDh2Fhdeq+ZRldrPYdXvCyfFUmFeEwEGXZB5I/AQ==",
"dev": true
},
"node_modules/ms": { "node_modules/ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@ -8130,9 +8146,9 @@
} }
}, },
"@types/jest": { "@types/jest": {
"version": "29.5.0", "version": "29.5.2",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.0.tgz", "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.2.tgz",
"integrity": "sha512-3Emr5VOl/aoBwnWcH/EFQvlSAmjV+XtV9GGu5mwdYew5vhQh0IUZx/60x0TzHDu09Bi7HMx10t/namdJw5QIcg==", "integrity": "sha512-mSoZVJF5YzGVCk+FsDxzDuH7s+SCkzrgKZzf0Z0T2WudhBUPoF6ktoTPC4R0ZoCPCV5xUvuU6ias5NvxcBcMMg==",
"dev": true, "dev": true,
"requires": { "requires": {
"expect": "^29.0.0", "expect": "^29.0.0",
@ -8905,6 +8921,12 @@
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
"integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==" "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA=="
}, },
"date-fns-tz": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-2.0.0.tgz",
"integrity": "sha512-OAtcLdB9vxSXTWHdT8b398ARImVwQMyjfYGkKD2zaGpHseG2UPHbHjXELReErZFxWdSLph3c2zOaaTyHfOhERQ==",
"requires": {}
},
"debug": { "debug": {
"version": "4.3.4", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@ -10705,6 +10727,12 @@
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
}, },
"mockdate": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/mockdate/-/mockdate-3.0.5.tgz",
"integrity": "sha512-iniQP4rj1FhBdBYS/+eQv7j1tadJ9lJtdzgOpvsOHng/GbcDh2Fhdeq+ZRldrPYdXvCyfFUmFeEwEGXZB5I/AQ==",
"dev": true
},
"ms": { "ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",

View File

@ -1,6 +1,6 @@
{ {
"name": "node-jellyfin-discord-bot", "name": "node-jellyfin-discord-bot",
"version": "1.0.3", "version": "1.1.3",
"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",
@ -13,6 +13,7 @@
"@types/uuid": "^9.0.1", "@types/uuid": "^9.0.1",
"axios": "^1.3.5", "axios": "^1.3.5",
"date-fns": "^2.29.3", "date-fns": "^2.29.3",
"date-fns-tz": "^2.0.0",
"discord-api-types": "^0.37.38", "discord-api-types": "^0.37.38",
"discord.js": "^14.9.0", "discord.js": "^14.9.0",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
@ -32,15 +33,18 @@
"debuggable": "node build/index.js --inspect-brk", "debuggable": "node build/index.js --inspect-brk",
"monitor": "nodemon build/index.js", "monitor": "nodemon build/index.js",
"lint": "eslint . --ext .ts", "lint": "eslint . --ext .ts",
"lint-fix": "eslint . --ext .ts --fix" "lint-fix": "eslint . --ext .ts --fix",
"test": "jest",
"test-watch": "jest --watch"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^29.5.0", "@types/jest": "^29.5.2",
"@typescript-eslint/eslint-plugin": "^5.58.0", "@typescript-eslint/eslint-plugin": "^5.58.0",
"@typescript-eslint/parser": "^5.58.0", "@typescript-eslint/parser": "^5.58.0",
"eslint": "^8.38.0", "eslint": "^8.38.0",
"jest": "^29.5.0", "jest": "^29.5.0",
"jest-cli": "^29.5.0", "jest-cli": "^29.5.0",
"mockdate": "^3.0.5",
"nodemon": "^2.0.22", "nodemon": "^2.0.22",
"rimraf": "^5.0.0", "rimraf": "^5.0.0",
"ts-jest": "^29.1.0" "ts-jest": "^29.1.0"

View File

@ -13,22 +13,22 @@ export default new Command({
options: [{ options: [{
name: "typ", name: "typ",
type: ApplicationCommandOptionType.String, type: ApplicationCommandOptionType.String,
description:"Was für ein announcement?", description: "Was für ein announcement?",
choices: [{name: "initial", value:"initial"},{name: "votepls", value:"votepls"},{name: "cancel", value:"cancel"}], choices: [{ name: "initial", value: "initial" }, { name: "votepls", value: "votepls" }, { name: "cancel", value: "cancel" }],
required: true required: true
}], }],
run: async (interaction: RunOptions) => { run: async (interaction: RunOptions) => {
const command = interaction.interaction const command = interaction.interaction
const requestId = uuid() const requestId = uuid()
if(!command.guildId) { if (!command.guildId) {
logger.error("COMMAND DOES NOT HAVE A GUILD ID; CANCELLING!!!", {requestId}) logger.error("COMMAND DOES NOT HAVE A GUILD ID; CANCELLING!!!", { requestId })
return return
} }
const guildId = command.guildId const guildId = command.guildId
const announcementType = command.options.data.find(option => option.name.includes("typ")) const announcementType = command.options.data.find(option => option.name.includes("typ"))
logger.info(`Got command for announcing ${announcementType?.value}!`, { guildId, requestId }) logger.info(`Got command for announcing ${announcementType?.value}!`, { guildId, requestId })
if(!announcementType) { if (!announcementType) {
logger.error("Did not get an announcement type!", { guildId, requestId }) logger.error("Did not get an announcement type!", { guildId, requestId })
return return
} }
@ -40,7 +40,7 @@ export default new Command({
logger.info(`User ${command.member.displayName} seems to be admin`) logger.info(`User ${command.member.displayName} seems to be admin`)
} }
if((<string>announcementType.value).includes("initial")) { if ((<string>announcementType.value).includes("initial")) {
sendInitialAnnouncement(guildId, requestId) sendInitialAnnouncement(guildId, requestId)
command.followUp("Ist rausgeschickt!") command.followUp("Ist rausgeschickt!")
} else { } else {
@ -56,7 +56,7 @@ function isAdmin(member: GuildMember): boolean {
async function sendInitialAnnouncement(guildId: string, requestId: string): Promise<void> { async function sendInitialAnnouncement(guildId: string, requestId: string): Promise<void> {
logger.info("Sending initial announcement") logger.info("Sending initial announcement")
const announcementChannel: Maybe<TextChannel> = client.getAnnouncementChannelForGuild(guildId) const announcementChannel: Maybe<TextChannel> = client.getAnnouncementChannelForGuild(guildId)
if(!announcementChannel) { if (!announcementChannel) {
logger.error("Could not find announcement channel. Aborting", { guildId, requestId }) logger.error("Could not find announcement channel. Aborting", { guildId, requestId })
return return
} }
@ -96,7 +96,7 @@ export async function manageAnnouncementRoles(guild: Guild, reaction: MessageRea
const allUsers = (await guild.members.fetch()) const allUsers = (await guild.members.fetch())
const usersWhoHaveRole: GuildMember[] = allUsers const usersWhoHaveRole: GuildMember[] = allUsers
.filter(member=> member.roles.cache .filter(member => member.roles.cache
.find(role => role.id === config.bot.announcement_role) !== undefined) .find(role => role.id === config.bot.announcement_role) !== undefined)
.map(member => member) .map(member => member)
@ -105,15 +105,15 @@ export async function manageAnnouncementRoles(guild: Guild, reaction: MessageRea
const usersWhoDontHaveRole: GuildMember[] = allUsers const usersWhoDontHaveRole: GuildMember[] = allUsers
.filter(member => member.roles.cache .filter(member => member.roles.cache
.find(role=> role.id === config.bot.announcement_role) === undefined) .find(role => role.id === config.bot.announcement_role) === undefined)
.map(member => member) .map(member => member)
const usersWhoNeedRole: GuildMember[] = usersWhoDontHaveRole const usersWhoNeedRole: GuildMember[] = usersWhoDontHaveRole
.filter(userWhoNeeds => usersWhoWantRole.map(wanter => wanter.id).includes(userWhoNeeds.id)) .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 removed: ${JSON.stringify(usersWhoNeedRoleRevoked)}`, { guildId, requestId })
logger.debug(`Theses users will get the role added: ${JSON.stringify(usersWhoNeedRole)}`, {guildId, requestId}) logger.debug(`Theses users will get the role added: ${JSON.stringify(usersWhoNeedRole)}`, { guildId, requestId })
usersWhoNeedRoleRevoked.forEach(user => user.roles.remove(announcementRole)) usersWhoNeedRoleRevoked.forEach(user => user.roles.remove(announcementRole))
usersWhoNeedRole.forEach(user => user.roles.add(announcementRole)) usersWhoNeedRole.forEach(user => user.roles.add(announcementRole))

View File

@ -3,7 +3,7 @@ import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildSchedu
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
import { client } from '../..' import { client } from '../..'
import { config } from '../configuration' import { config } from '../configuration'
import { Emotes } from '../events/guildScheduledEventCreate' import { Emotes } from '../events/autoCreateVoteByWPEvent'
import { Maybe } from '../interfaces' import { Maybe } from '../interfaces'
import { logger } from '../logger' import { logger } from '../logger'
import { Command } from '../structures/command' import { Command } from '../structures/command'
@ -75,7 +75,7 @@ export async function closePoll(guild: Guild, requestId: string) {
async function sendVoteClosedMessage(event: GuildScheduledEvent, movie: string, guildId: string, requestId: string) { 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 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 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 body = `[Abstimmung beendet] für https://discord.com/events/${event.guildId}/${event.id}\n<@&${config.bot.announcement_role}> Wir gucken ${movie} am ${date} um ${time}`
const options: MessageCreateOptions = { const options: MessageCreateOptions = {
content: body, content: body,
allowedMentions: { parse: ["roles"] } allowedMentions: { parse: ["roles"] }

View File

@ -0,0 +1,48 @@
import { GuildScheduledEvent, TextChannel } from "discord.js";
import { v4 as uuid } from "uuid";
import { client } from "../..";
import { config } from "../configuration";
import { createDateStringFromEvent } from "../helper/dateHelper";
import { Maybe } from "../interfaces";
import { logger } from "../logger";
export const name = 'guildScheduledEventCreate'
export async function execute(event: GuildScheduledEvent) {
const guildId = event.guildId
const requestId = uuid()
try {
if (!event.description) {
logger.debug("Got GuildScheduledEventCreate event. But has no description. Aborting.")
return
}
if (event.description.includes("!wp")) {
logger.info("Got manual create event of watchparty event!", { guildId, requestId })
if (event.description.includes("!private")) {
logger.info("Event description contains \"!private\". Won't announce.", { guildId, requestId })
return
}
const channel: Maybe<TextChannel> = client.getAnnouncementChannelForGuild(guildId)
if (!channel) {
logger.error("Could not obtain announcement channel. Aborting announcement.", { guildId, requestId })
return
}
const message = `[Watchparty] https://discord.com/events/${event.guildId}/${event.id} \nHey <@&${config.bot.announcement_role}>, wir gucken ${event.name} ${createDateStringFromEvent(event, guildId, requestId)}`
channel.send(message)
} else {
logger.debug("Got GuildScheduledEventCreate event but no !wp in description. Not creating manual wp announcement.", { guildId, requestId })
}
} catch (error) {
// sendFailureDM(error)
logger.error(<string>error, { guildId, requestId })
}
}

View File

@ -0,0 +1,63 @@
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 { createDateStringFromEvent } from "../helper/dateHelper";
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 const NONE_OF_THAT = "❌"
export let task: ScheduledTask | undefined
export async function execute(event: GuildScheduledEvent) {
const requestId = uuid()
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
}
let message = `[Abstimmung] für https://discord.com/events/${event.guildId}/${event.id}\n<@&${config.bot.announcement_role}> Es gibt eine neue Abstimmung für die nächste Watchparty ${createDateStringFromEvent(event, event.guildId, requestId)}! Stimme hierunter für den nächsten Film ab!\n`
for (let i = 0; i < movies.length; i++) {
message = message.concat(Emotes[i]).concat(": ").concat(movies[i]).concat("\n")
}
message = message.concat(NONE_OF_THAT).concat(": Wenn dir nichts davon gefällt.")
const options: MessageCreateOptions = {
allowedMentions: { parse: ["roles"] },
content: message,
}
const sentMessage: Message<true> = await (await announcementChannel.fetch()).send(options)
for (let i = 0; i < movies.length; i++) {
sentMessage.react(Emotes[i])
}
sentMessage.react(NONE_OF_THAT)
// sentMessage.pin() //todo: uncomment when bot has permission to pin messages. Also update closepoll.ts to only fetch pinned messages
}
}

View File

@ -0,0 +1,52 @@
import { Collection, GuildScheduledEvent, GuildScheduledEventStatus, Message } from "discord.js";
import { v4 as uuid } from "uuid";
import { client } from "../..";
import { logger } from "../logger";
export const name = 'guildScheduledEventUpdate'
export async function execute(oldEvent: GuildScheduledEvent, newEvent: GuildScheduledEvent) {
const requestId = uuid()
try {
if (!newEvent.guild) {
logger.error("Event has no guild, aborting.", { guildId: newEvent.guildId, requestId })
return
}
const guildId = newEvent.guildId
if (newEvent.description?.toLowerCase().includes("!wp") && newEvent.status === GuildScheduledEventStatus.Completed) {
logger.info("A watchparty ended. Cleaning up announcements!", { guildId, requestId })
const announcementChannel = client.getAnnouncementChannelForGuild(newEvent.guild.id)
if (!announcementChannel) {
logger.error("Could not find announcement channel. Aborting", { guildId: newEvent.guild.id, requestId })
return
}
const events = await newEvent.guild.scheduledEvents.fetch()
const wpAnnouncements = (await announcementChannel.messages.fetch()).filter(message => !message.cleanContent.includes("[initial]"))
const announcementsWithoutEvent = filterAnnouncementsByPendingWPs(wpAnnouncements, events)
logger.info(`Deleting ${announcementsWithoutEvent.length} announcements.`, { guildId, requestId })
announcementsWithoutEvent.forEach(message => message.delete())
}
} catch (error) {
logger.error(<string>error, { guildId: newEvent.guildId, requestId })
}
}
function filterAnnouncementsByPendingWPs(messages: Collection<string, Message<true>>, events: Collection<string, GuildScheduledEvent<GuildScheduledEventStatus>>): Message<true>[] {
const filteredMessages: Message<true>[] = []
for (const message of messages.values()) {
let foundEventForMessage = false
for (const event of events.values()) {
if (message.cleanContent.includes(event.id)) { //announcement always has eventid because of eventbox
foundEventForMessage = true
}
}
if (!foundEventForMessage) {
filteredMessages.push(message)
}
}
return filteredMessages
}

View File

@ -1,30 +0,0 @@
import { Collection, GuildMember } from "discord.js"
import { filterRolesFromMemberUpdate, getGuildSpecificTriggerRoleId } from "../helper/roleFilter"
import { ChangedRoles, PermissionLevel } from "../interfaces"
import { jellyfinHandler } from "../.."
import { v4 as uuid } from "uuid"
export const name = 'guildMemberUpdate'
export async function execute(oldMember: GuildMember, newMember: GuildMember) {
try {
const requestId = uuid()
const changedRoles: ChangedRoles = filterRolesFromMemberUpdate(oldMember, newMember)
const triggerRoleIds: Collection<string, PermissionLevel> = getGuildSpecificTriggerRoleId()
triggerRoleIds.forEach((level, key) => {
const addedRoleMatches = changedRoles.addedRoles.find(aRole => aRole.id === key)
if (addedRoleMatches) {
jellyfinHandler.upsertUser(newMember, level, requestId)
}
const removedRoleMatches = changedRoles.removedRoles.find(rRole => rRole.id === key)
if (removedRoleMatches) {
jellyfinHandler.removeUser(newMember, level, requestId)
}
})
} catch (error) {
console.error(error)
}
}

View File

@ -1,63 +0,0 @@
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
}
}

View File

@ -1,64 +0,0 @@
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)
}

View File

@ -0,0 +1,30 @@
import { Collection, GuildMember } from "discord.js"
import { filterRolesFromMemberUpdate, getGuildSpecificTriggerRoleId } from "../helper/roleFilter"
import { ChangedRoles, PermissionLevel } from "../interfaces"
import { jellyfinHandler } from "../.."
import { v4 as uuid } from "uuid"
export const name = 'guildMemberUpdate'
export async function execute(oldMember: GuildMember, newMember: GuildMember) {
try {
const requestId = uuid()
const changedRoles: ChangedRoles = filterRolesFromMemberUpdate(oldMember, newMember)
const triggerRoleIds: Collection<string, PermissionLevel> = getGuildSpecificTriggerRoleId()
triggerRoleIds.forEach((level, key) => {
const addedRoleMatches = changedRoles.addedRoles.find(aRole => aRole.id === key)
if (addedRoleMatches) {
jellyfinHandler.upsertUser(newMember, level, requestId)
}
const removedRoleMatches = changedRoles.removedRoles.find(rRole => rRole.id === key)
if (removedRoleMatches) {
jellyfinHandler.removeUser(newMember, level, requestId)
}
})
} catch (error) {
console.error(error)
}
}

View 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)
}
}

View File

@ -0,0 +1,58 @@
import { GuildMember, GuildScheduledEvent, GuildScheduledEventStatus } from "discord.js";
import { v4 as uuid } from "uuid";
import { 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 {
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)
}

View File

@ -1,59 +0,0 @@
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)
}
}

View File

@ -0,0 +1,23 @@
import { format, isToday, toDate } from "date-fns";
import { utcToZonedTime } from "date-fns-tz"
import { GuildScheduledEvent } from "discord.js";
import { logger } from "../logger";
import de from "date-fns/locale/de";
export function createDateStringFromEvent(event: GuildScheduledEvent, requestId: string, guildId?: string): string {
if (!event.scheduledStartAt) {
logger.error("Event has no start. Cannot create dateString.", { guildId, requestId })
return `"habe keinen Startzeitpunkt ermitteln können"`
}
const timeZone = 'Europe/Berlin'
const zonedDateTime = utcToZonedTime(event.scheduledStartAt, timeZone)
const time = format(zonedDateTime, "HH:mm", { locale: de })
if (isToday(zonedDateTime)) {
return `heute um ${time}`
}
const date = format(zonedDateTime, "eeee dd.MM", { locale: de })
return `am ${date} um ${time}`
}

View File

@ -2,7 +2,7 @@ import { GuildMember } from "discord.js";
import { JellyfinConfig, Maybe, PermissionLevel } from "../interfaces"; import { JellyfinConfig, Maybe, PermissionLevel } from "../interfaces";
import { logger } from "../logger"; import { logger } from "../logger";
import { CreateUserByNameOperationRequest, DeleteUserRequest, GetItemsRequest, ItemsApi, SystemApi, UpdateUserPasswordOperationRequest, UpdateUserPolicyOperationRequest, UserApi } from "./apis"; import { CreateUserByNameOperationRequest, DeleteUserRequest, GetItemsRequest, ItemsApi, SystemApi, UpdateUserPasswordOperationRequest, UpdateUserPolicyOperationRequest, UserApi } from "./apis";
import { BaseItemDto, UpdateUserPasswordRequest } from "./models"; import { BaseItemDto, UpdateUserPasswordRequest, UpdateUserPolicyRequest } from "./models";
import { UserDto } from "./models/UserDto"; import { UserDto } from "./models/UserDto";
import { Configuration, ConfigurationParameters } from "./runtime"; import { Configuration, ConfigurationParameters } from "./runtime";
@ -52,24 +52,46 @@ export class JellyfinHandler {
return (Math.random() * 10000 + 10000).toFixed(0) return (Math.random() * 10000 + 10000).toFixed(0)
} }
public async createUserAccountForDiscordUser(discordUser: GuildMember, level: PermissionLevel, guildId?: string, requestId?: string): Promise<UserDto> { public async createUserAccountForDiscordUser(discordUser: GuildMember, level: PermissionLevel, requestId: string, guildId?: string): Promise<UserDto> {
const newUserName = this.generateJFUserName(discordUser, level) 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: {
name: newUserName, name: newUserName,
password: this.generatePasswordForUser(), password: this.generatePasswordForUser()
} }
} }
logger.debug(JSON.stringify(req), { requestId, guildId }) 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) {
if (createResult.policy) {
this.setUserPermissions(createResult, requestId, guildId)
}
(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}"!`)
return createResult return createResult
} }
else throw new Error('Could not create User in Jellyfin') else throw new Error('Could not create User in Jellyfin')
} }
public async setUserPermissions(user: UserDto, requestId: string, guildId?: string) {
if (!user.policy || !user.id) {
logger.error(`Cannot update user policy. User ${user.name} has no policy to modify`, { guildId, requestId })
return
}
user.policy.enableVideoPlaybackTranscoding = false
const operation: UpdateUserPolicyRequest = {
...user.policy,
enableVideoPlaybackTranscoding: false
}
const request: UpdateUserPolicyOperationRequest = {
userId: user.id,
updateUserPolicyRequest: operation
}
this.userApi.updateUserPolicy(request)
}
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}`, { guildId: discordUser.guild.id, requestId }) logger.debug(`Presence for DiscordUser ${discordUser.id}:${jfuser !== undefined}`, { guildId: discordUser.guild.id, requestId })
@ -251,7 +273,7 @@ export class JellyfinHandler {
let movieCount = 0 let movieCount = 0
let movieNames: string[] let movieNames: string[]
do { do {
movieNames = (await this.getRandomMovies(count, guildId, requestId)).filter(movie => movie.name && movie.name.length > 0).map(movie => <string> movie.name) movieNames = (await this.getRandomMovies(count, guildId, requestId)).filter(movie => movie.name && movie.name.length > 0).map(movie => <string>movie.name)
movieCount = movieNames.length movieCount = movieNames.length
} while (movieCount < count) } while (movieCount < count)
return movieNames return movieNames

View File

@ -29,7 +29,7 @@ export interface ConfigurationParameters {
} }
export class Configuration { export class Configuration {
constructor(private configuration: ConfigurationParameters = {}) {} constructor(private configuration: ConfigurationParameters = {}) { }
set config(configuration: Configuration) { set config(configuration: Configuration) {
this.configuration = configuration; this.configuration = configuration;
@ -393,7 +393,7 @@ export interface ResponseTransformer<T> {
} }
export class JSONApiResponse<T> { export class JSONApiResponse<T> {
constructor(public raw: Response, private transformer: ResponseTransformer<T> = (jsonValue: any) => jsonValue) {} constructor(public raw: Response, private transformer: ResponseTransformer<T> = (jsonValue: any) => jsonValue) { }
async value(): Promise<T> { async value(): Promise<T> {
return this.transformer(await this.raw.json()); return this.transformer(await this.raw.json());
@ -401,7 +401,7 @@ export class JSONApiResponse<T> {
} }
export class VoidApiResponse { export class VoidApiResponse {
constructor(public raw: Response) {} constructor(public raw: Response) { }
async value(): Promise<void> { async value(): Promise<void> {
return undefined; return undefined;
@ -409,7 +409,7 @@ export class VoidApiResponse {
} }
export class BlobApiResponse { export class BlobApiResponse {
constructor(public raw: Response) {} constructor(public raw: Response) { }
async value(): Promise<Blob> { async value(): Promise<Blob> {
return await this.raw.blob(); return await this.raw.blob();
@ -417,7 +417,7 @@ export class BlobApiResponse {
} }
export class TextApiResponse { export class TextApiResponse {
constructor(public raw: Response) {} constructor(public raw: Response) { }
async value(): Promise<string> { async value(): Promise<string> {
return await this.raw.text(); return await this.raw.text();

View File

@ -130,7 +130,7 @@ export class ExtendedClient extends Client {
for (const guild of guilds.values()) { for (const guild of guilds.values()) {
logger.info("Starting background task for announcement role", { guildId: guild.id }) logger.info("Starting background task for announcement role", { guildId: guild.id })
const textChannel: Maybe<TextChannel> = this.getAnnouncementChannelForGuild(guild.id) const textChannel: Maybe<TextChannel> = this.getAnnouncementChannelForGuild(guild.id)
if(!textChannel) { if (!textChannel) {
logger.error("Could not find announcement channel. Aborting", { guildId: guild.id }) logger.error("Could not find announcement channel. Aborting", { guildId: guild.id })
return return
} }
@ -174,7 +174,7 @@ export class ExtendedClient extends Client {
} }
private async startPollCloseBackgroundTasks() { private async startPollCloseBackgroundTasks() {
for(const guild of this.guilds.cache) { for (const guild of this.guilds.cache) {
this.pollCloseBackgroundTasks.set(guild[1].id, schedule("0 * * * * *", () => checkForPollsToClose(guild[1]))) this.pollCloseBackgroundTasks.set(guild[1].id, schedule("0 * * * * *", () => checkForPollsToClose(guild[1])))
} }
} }

View File

@ -0,0 +1,16 @@
import { GuildScheduledEvent } from "discord.js"
import { createDateStringFromEvent } from "../../server/helper/dateHelper"
import MockDate from 'mockdate'
beforeAll(() => {
MockDate.set('01-01-2023')
})
function getTestDate(date: string): GuildScheduledEvent {
return <GuildScheduledEvent>{ scheduledStartAt: new Date(date) }
}
test('createDateStringFromEvent - correct formatting', () => {
expect(createDateStringFromEvent(getTestDate('01-01-2023 12:30'), "")).toEqual('heute um 12:30')
expect(createDateStringFromEvent(getTestDate('01-02-2023 12:30'), "")).toEqual('am Montag 02.01 um 12:30')
expect(createDateStringFromEvent(getTestDate('01-03-2023 12:30'), "")).toEqual('am Dienstag 03.01 um 12:30')
})

View File

@ -0,0 +1,28 @@
import { Collection, GuildMember, Role } from "discord.js"
import { filterRolesFromMemberUpdate } from "../../server/helper/roleFilter"
function buildFakeRole(id: string, name: string): Role {
return <Role>{ id, name }
}
test('filterRolesFromMemberUpdate', () => {
const oldMemberRoles: Collection<string, Role> = new Collection<string, Role>()
oldMemberRoles.set('1', buildFakeRole('01', 'Role01'))
oldMemberRoles.set('2', buildFakeRole('02', 'Role02'))
const newMemberRoles: Collection<string, Role> = new Collection<string, Role>()
newMemberRoles.set('1', buildFakeRole('01', 'Role01'))
newMemberRoles.set('2', buildFakeRole('02', 'Role02'))
newMemberRoles.set('3', buildFakeRole('03', 'Role03'))
const oldMember: GuildMember = <GuildMember>{ roles: { cache: oldMemberRoles }, guild: { id: "guildid" } }
const newMember: GuildMember = <GuildMember>{ roles: { cache: newMemberRoles }, guild: { id: "guildid" } }
const output = filterRolesFromMemberUpdate(oldMember, newMember)
const expectedAddedRoles: Collection<string, Role> = new Collection<string, Role>()
expectedAddedRoles.set('3', buildFakeRole('03', 'Role03'))
const expectedRemovedRoles: Collection<string, Role> = new Collection<string, Role>()
expect(output.addedRoles).toEqual(expectedAddedRoles)
expect(output.removedRoles).toEqual(expectedRemovedRoles)
})