Compare commits
166 Commits
v1.0.0
...
feat/20-re
Author | SHA1 | Date | |
---|---|---|---|
a60fc2db7e | |||
a50ac1716f | |||
ef39c6315d | |||
1f372b0aac | |||
d1aacbb3d3 | |||
1ae8278fb8 | |||
417b24d408 | |||
88061c361c | |||
f83f54749d | |||
90b0b07080 | |||
6d0eaed426 | |||
8f320cee5c | |||
016bb243cc | |||
2c8cd96ac7 | |||
ba4aefed8e | |||
8efae12907 | |||
fec0bc31f1 | |||
1bfcaa95f9 | |||
fb4ab59dc6 | |||
6d40930dc1 | |||
4e9fe587b0 | |||
03b6a30ffa | |||
7d794a8001 | |||
8df180898e | |||
976175242b | |||
68546b0b50 | |||
1348abbd48 | |||
fce9091114 | |||
081f3c6201 | |||
ca99987a20 | |||
fc64728a78 | |||
20da25f2bf | |||
a455fd8ff7 | |||
119343c916 | |||
296a490e93 | |||
66507cb08f | |||
4600820889 | |||
4a3e8809be | |||
690ba697b6 | |||
71343d6742 | |||
3f6e558d39 | |||
ca259c5f24 | |||
b1c581ca6e | |||
96189c2392 | |||
700353cff4 | |||
f705b97804 | |||
9cdc6e1934 | |||
c73cd20ccf | |||
e66aebc88c | |||
599243990e | |||
eef3a9c358 | |||
1e912b20ef | |||
ce4dc81f7d | |||
b76df79d2a | |||
4e563d57fd | |||
b6a1e06b03 | |||
2ebc7fbdbe | |||
8ff5aeff03 | |||
1101a84501 | |||
91ec2ece7e | |||
5e58765cf4 | |||
a2adef808f | |||
dc66c277b2 | |||
c022cc32d5 | |||
e763e76413 | |||
137d156981 | |||
fdfe7ce404 | |||
146848b759 | |||
e54f03292e | |||
fe45445811 | |||
8f02e11dba | |||
878c81bfa7 | |||
ca19168cf4 | |||
e8893646f0 | |||
e61b3a7b16 | |||
9383cee4a0 | |||
0748097a1f | |||
ffba737e5a | |||
4cd9c771f0 | |||
8c3cf7829b | |||
1a13638ed9 | |||
c351e27fdd | |||
6d3bea169e | |||
3f071c8a4e | |||
98d1ca73b5 | |||
ee742018e9 | |||
8ad651c753 | |||
a4a834ad27 | |||
e8dcfd8340 | |||
d9d1d74ef9 | |||
331ff89060 | |||
f6476c609b | |||
6220268b14 | |||
b6034d4fb7 | |||
ca0a9e3cb8 | |||
b8a32aab40 | |||
e3e755011d | |||
5a6c66cb3e | |||
0d3c62c6ad | |||
5816db48e6 | |||
66f843b399 | |||
d82a7cffd2 | |||
8a06a661fa | |||
4084f675cd | |||
3bd26a9d6c | |||
e7b21fa658 | |||
2d32f9b680 | |||
5503aa8713 | |||
25bb676fda | |||
9f5abb8a90 | |||
0e67252976 | |||
37b798818c | |||
af414d0bad | |||
c32434a7eb | |||
c133570d8c | |||
65cdee36e9 | |||
6b0e84669a | |||
dd72f8e165 | |||
a6f19ccd2b | |||
c39f9c6ee1 | |||
f41194ba71 | |||
fa49dc0f76 | |||
e52e845851 | |||
61544feaba | |||
1966640239 | |||
fa9998e92c | |||
c1a449bafe | |||
d5d82043f0 | |||
51ebf2e939 | |||
f314b2f355 | |||
a4d7c57d10 | |||
2802afa7d5 | |||
3a5ea5d4ff | |||
45d87275bf | |||
31e440434e | |||
3d70b56eb7 | |||
3298c7a244 | |||
5b98c9bf2f | |||
ee363e065c | |||
9af847f234 | |||
a18406e7e4 | |||
b9f65125dc | |||
d61457cb5f | |||
9da8f47784 | |||
e8c58d5ff8 | |||
8569a3e1e6 | |||
8d0dda0fa9 | |||
777ae330ad | |||
111ccaa880 | |||
c00453d3d3 | |||
8a7973a2e3 | |||
0b67b126dd | |||
6d5725be90 | |||
59f5b34e5a | |||
670a64af22 | |||
4cc332820f | |||
f5928049ea | |||
99905f98d0 | |||
07849d331a | |||
1e6a75687a | |||
2c09033c3f | |||
ce4441cee3 | |||
7c8072b295 | |||
7899aac5ce | |||
26c2d91252 | |||
d6300e8bec |
7
.editorconfig
Normal file
7
.editorconfig
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
root = true
|
||||||
|
[*]
|
||||||
|
indent_style = tab
|
||||||
|
tab_width = 4
|
||||||
|
[*.ts]
|
||||||
|
indent_style = tab
|
||||||
|
tab_width = 4
|
17
.gitea/workflows/compile.yaml
Normal file
17
.gitea/workflows/compile.yaml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
name: Compile the repository
|
||||||
|
on: [pull_request]
|
||||||
|
env:
|
||||||
|
REGISTRY: gitea.brudi.xyz
|
||||||
|
IMAGE_NAME: ${{ gitea.repository }}
|
||||||
|
USER: ${{ gitea.actor }}
|
||||||
|
jobs:
|
||||||
|
compile:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container: catthehacker/ubuntu:act-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Build Container
|
||||||
|
run: docker build --target compile .
|
@ -1,6 +1,9 @@
|
|||||||
name: Build a docker image for node-jellyfin-role-bot
|
name: Build a docker image for node-jellyfin-role-bot
|
||||||
run-name: ${{ gitea.actor }} is building an image
|
run-name: ${{ gitea.actor }} is building an image
|
||||||
on: [push]
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
env:
|
env:
|
||||||
REGISTRY: gitea.brudi.xyz
|
REGISTRY: gitea.brudi.xyz
|
||||||
IMAGE_NAME: ${{ gitea.repository }}
|
IMAGE_NAME: ${{ gitea.repository }}
|
||||||
@ -18,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" .
|
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 "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest"
|
run: docker push --all-tags "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
|
||||||
|
18
.gitea/workflows/test.yaml
Normal file
18
.gitea/workflows/test.yaml
Normal 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 .
|
||||||
|
|
19
Dockerfile
19
Dockerfile
@ -1,11 +1,22 @@
|
|||||||
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
|
|
||||||
|
|
||||||
|
FROM files as proddependencies
|
||||||
|
ENV NODE_ENV=production
|
||||||
RUN npm ci --omit=dev
|
RUN npm ci --omit=dev
|
||||||
|
|
||||||
|
FROM proddependencies as compile
|
||||||
|
COPY server ./server
|
||||||
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 server ./server
|
||||||
|
COPY jest.config.js .
|
||||||
|
COPY tests ./tests
|
||||||
|
RUN npm run test
|
||||||
|
19
jest.config.js
Normal file
19
jest.config.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
module.exports = {
|
||||||
|
'roots': [
|
||||||
|
'<rootDir>/tests',
|
||||||
|
'<rootDir>/server'
|
||||||
|
],
|
||||||
|
'transform': {
|
||||||
|
'^.+\\.tsx?$': 'ts-jest'
|
||||||
|
},
|
||||||
|
'testRegex': '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',
|
||||||
|
'setupFiles': ["<rootDir>/tests/testenv.js"],
|
||||||
|
'moduleFileExtensions': [
|
||||||
|
'ts',
|
||||||
|
'tsx',
|
||||||
|
'js',
|
||||||
|
'jsx',
|
||||||
|
'json',
|
||||||
|
'node'
|
||||||
|
],
|
||||||
|
};
|
46
package-lock.json
generated
46
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "node-jellyfin-discord-bot",
|
"name": "node-jellyfin-discord-bot",
|
||||||
"version": "1.0.0",
|
"version": "1.1.4",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "node-jellyfin-discord-bot",
|
"name": "node-jellyfin-discord-bot",
|
||||||
"version": "1.0.0",
|
"version": "1.1.4",
|
||||||
"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",
|
||||||
|
10
package.json
10
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "node-jellyfin-discord-bot",
|
"name": "node-jellyfin-discord-bot",
|
||||||
"version": "1.0.0",
|
"version": "1.1.4",
|
||||||
"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 --runInBand",
|
||||||
|
"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"
|
||||||
|
@ -6,6 +6,7 @@ import { Maybe } from '../interfaces'
|
|||||||
import { logger } from '../logger'
|
import { logger } from '../logger'
|
||||||
import { Command } from '../structures/command'
|
import { Command } from '../structures/command'
|
||||||
import { RunOptions } from '../types/commandTypes'
|
import { RunOptions } from '../types/commandTypes'
|
||||||
|
import { isInitialAnnouncement } from '../helper/messageIdentifiers'
|
||||||
|
|
||||||
export default new Command({
|
export default new Command({
|
||||||
name: 'announce',
|
name: 'announce',
|
||||||
@ -61,7 +62,7 @@ async function sendInitialAnnouncement(guildId: string, requestId: string): Prom
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentPinnedAnnouncementMessages = (await announcementChannel.messages.fetchPinned()).filter(message => message.cleanContent.includes("[initial]"))
|
const currentPinnedAnnouncementMessages = (await announcementChannel.messages.fetchPinned()).filter(message => isInitialAnnouncement(message))
|
||||||
currentPinnedAnnouncementMessages.forEach(async (message) => await message.unpin())
|
currentPinnedAnnouncementMessages.forEach(async (message) => await message.unpin())
|
||||||
currentPinnedAnnouncementMessages.forEach(message => message.delete())
|
currentPinnedAnnouncementMessages.forEach(message => message.delete())
|
||||||
|
|
||||||
@ -81,41 +82,5 @@ Für eine Erklärung wie das alles funktioniert mach einfach /mitgucken für ein
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@ -1,13 +1,8 @@
|
|||||||
import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildScheduledEventSetStatusArg, GuildScheduledEventStatus, Message, MessageCreateOptions, TextChannel } from 'discord.js'
|
|
||||||
import { v4 as uuid } from 'uuid'
|
import { v4 as uuid } from 'uuid'
|
||||||
import { client } from '../..'
|
import { client } from '../..'
|
||||||
import { config } from '../configuration'
|
|
||||||
import { Emotes } from '../events/guildScheduledEventCreate'
|
|
||||||
import { logger } from '../logger'
|
import { logger } from '../logger'
|
||||||
import { Command } from '../structures/command'
|
import { Command } from '../structures/command'
|
||||||
import { RunOptions } from '../types/commandTypes'
|
import { RunOptions } from '../types/commandTypes'
|
||||||
import { format } from 'date-fns'
|
|
||||||
import { Maybe } from '../interfaces'
|
|
||||||
|
|
||||||
export default new Command({
|
export default new Command({
|
||||||
name: 'closepoll',
|
name: 'closepoll',
|
||||||
@ -25,124 +20,6 @@ export default new Command({
|
|||||||
logger.info("Got command for closing poll!", { guildId, requestId })
|
logger.info("Got command for closing poll!", { guildId, requestId })
|
||||||
|
|
||||||
command.followUp("Alles klar, beende die Umfrage :)")
|
command.followUp("Alles klar, beende die Umfrage :)")
|
||||||
closePoll(command.guild, requestId)
|
client.voteController.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
|
|
||||||
}
|
|
@ -1,6 +1,7 @@
|
|||||||
import { ApplicationCommandOptionType } from 'discord.js'
|
import { ApplicationCommandOptionType } from 'discord.js'
|
||||||
import { Command } from '../structures/command'
|
import { Command } from '../structures/command'
|
||||||
import { RunOptions } from '../types/commandTypes'
|
import { RunOptions } from '../types/commandTypes'
|
||||||
|
import { logger } from '../logger'
|
||||||
export default new Command({
|
export default new Command({
|
||||||
name: 'echo',
|
name: 'echo',
|
||||||
description: 'Echoes a text',
|
description: 'Echoes a text',
|
||||||
@ -13,7 +14,7 @@ export default new Command({
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
run: async (interaction: RunOptions) => {
|
run: async (interaction: RunOptions) => {
|
||||||
console.log('echo called')
|
logger.info('echo called')
|
||||||
interaction.interaction.reply(interaction.toString())
|
interaction.interaction.reply(interaction.toString())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -5,7 +5,7 @@ import { accountChoice, joingroup, leavegroup, loginScreen, overview, resume, se
|
|||||||
import { logger } from '../logger'
|
import { logger } from '../logger'
|
||||||
import { Command } from '../structures/command'
|
import { Command } from '../structures/command'
|
||||||
import { RunOptions } from '../types/commandTypes'
|
import { RunOptions } from '../types/commandTypes'
|
||||||
import { configureServer, explainRoles, installation, loginInfo, useSyncgroup } from './mitgucken'
|
import { configureServer, explainRole, installation, loginInfo, useSyncgroup } from './mitgucken'
|
||||||
|
|
||||||
export default new Command({
|
export default new Command({
|
||||||
name: 'guides',
|
name: 'guides',
|
||||||
@ -70,7 +70,7 @@ export default new Command({
|
|||||||
userDMChannel.send({ embeds: useSyncgroup(), files: [overview, joingroup, resume, leavegroup] })
|
userDMChannel.send({ embeds: useSyncgroup(), files: [overview, joingroup, resume, leavegroup] })
|
||||||
} else if (guideSelection.customId === 'explainRoles') {
|
} else if (guideSelection.customId === 'explainRoles') {
|
||||||
const userDMChannel = await guideSelection.user.createDM()
|
const userDMChannel = await guideSelection.user.createDM()
|
||||||
userDMChannel.send(explainRoles())
|
userDMChannel.send({ embeds: explainRole() })
|
||||||
}
|
}
|
||||||
|
|
||||||
guideSelection.update({ content: "Hab ich dir per DM geschickt :)", components: [] })
|
guideSelection.update({ content: "Hab ich dir per DM geschickt :)", components: [] })
|
||||||
|
@ -16,13 +16,9 @@ export default new Command({
|
|||||||
const embedList: APIEmbed[] = []
|
const embedList: APIEmbed[] = []
|
||||||
embedList.push(...installation())
|
embedList.push(...installation())
|
||||||
embedList.push(...configureServer())
|
embedList.push(...configureServer())
|
||||||
|
embedList.push(...explainRole())
|
||||||
embedList.push(...loginInfo())
|
embedList.push(...loginInfo())
|
||||||
embedList.push(...useSyncgroup())
|
embedList.push(...useSyncgroup())
|
||||||
embedList.push({
|
|
||||||
color,
|
|
||||||
title: "Wie du an einen Account kommst",
|
|
||||||
description: explainRoles()
|
|
||||||
})
|
|
||||||
|
|
||||||
//logger.info(`Trying to use ${splashScreen.name}`, { requestId, guildId: interaction.interaction.guild?.id })
|
//logger.info(`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 })
|
logger.info(`Sending guide to ${interaction.interaction.user.id}`, { requestId, guildId: interaction.interaction.guild?.id })
|
||||||
@ -32,6 +28,13 @@ export default new Command({
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export function explainRole(): APIEmbed[] {
|
||||||
|
return [{
|
||||||
|
color,
|
||||||
|
title: "Wie du an einen Account kommst",
|
||||||
|
description: roleExplanation
|
||||||
|
}]
|
||||||
|
}
|
||||||
export function installation(): APIEmbed[] {
|
export function installation(): APIEmbed[] {
|
||||||
const embedList: APIEmbed[] = []
|
const embedList: APIEmbed[] = []
|
||||||
// DownloadLink and installation
|
// DownloadLink and installation
|
||||||
@ -67,7 +70,7 @@ export function configureServer(): APIEmbed[] {
|
|||||||
title: "Server Verbindung",
|
title: "Server Verbindung",
|
||||||
description: "Stelle eine Verbindung zum Hartzarett Jellyfin Server her",
|
description: "Stelle eine Verbindung zum Hartzarett Jellyfin Server her",
|
||||||
fields: [
|
fields: [
|
||||||
{ name: "Server Adresse", value: "https://media.hartzarett.ruhr" }
|
{ name: "Server Adresse", value: "`https://media.hartzarett.ruhr`" }
|
||||||
],
|
],
|
||||||
image: {
|
image: {
|
||||||
url: 'attachment://server_verbindung.png'
|
url: 'attachment://server_verbindung.png'
|
||||||
@ -91,7 +94,7 @@ export function loginInfo(): APIEmbed[] {
|
|||||||
embedList.push({
|
embedList.push({
|
||||||
color,
|
color,
|
||||||
title: "Login",
|
title: "Login",
|
||||||
description: "Melde dich mit dem Usernamen und Passwort an, welches dir von mir zugeschickt wird. Falls du ein neues brauchst führe einmal /reset_passwort aus :)",
|
description: "Melde dich mit dem Usernamen und Passwort an, welches dir von mir zugeschickt wird. Falls du ein neues brauchst führe einmal `/passwort_reset` aus :)",
|
||||||
image: {
|
image: {
|
||||||
url: 'attachment://login_screen.png'
|
url: 'attachment://login_screen.png'
|
||||||
}
|
}
|
||||||
@ -139,8 +142,7 @@ export function useSyncgroup(): APIEmbed[] {
|
|||||||
return embedList
|
return embedList
|
||||||
}
|
}
|
||||||
|
|
||||||
export function explainRoles(): string {
|
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
|
||||||
return `Mit einer Rolle kann dafür gesorgt werden, dass du einen dauerhaften Account auf dem Mediaserver hast. Wende dich bei Bedarf an Samantha oder Markus.\n
|
|
||||||
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
|
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.`
|
Hast du kein Interesse angemeldet bekommst du automatisch einen Nutzernamen und Passwort zugeschickt wenn du den Channel betrittst in dem das Event stattfindet.`
|
||||||
}
|
|
||||||
|
@ -2,15 +2,16 @@ import { v4 as uuid } from 'uuid'
|
|||||||
import { jellyfinHandler } from "../.."
|
import { jellyfinHandler } from "../.."
|
||||||
import { Command } from '../structures/command'
|
import { Command } from '../structures/command'
|
||||||
import { RunOptions } from '../types/commandTypes'
|
import { RunOptions } from '../types/commandTypes'
|
||||||
|
import { logger } from '../logger'
|
||||||
|
|
||||||
export default new Command({
|
export default new Command({
|
||||||
name: 'passwort_reset',
|
name: 'passwort_reset',
|
||||||
description: 'Ich vergebe dir ein neues Passwort und schicke es dir per DM zu. Kostet auch nix! Versprochen! 😉',
|
description: 'Ich vergebe dir ein neues Passwort und schicke es dir per DM zu. Kostet auch nix! Versprochen! 😉',
|
||||||
options: [],
|
options: [],
|
||||||
run: async (interaction: RunOptions) => {
|
run: async (interaction: RunOptions) => {
|
||||||
console.log('PasswortReset called')
|
logger.info('PasswortReset called')
|
||||||
interaction.interaction.followUp('Yo, ich schick dir eins!')
|
interaction.interaction.followUp('Yo, ich schick dir eins!')
|
||||||
console.log(JSON.stringify(interaction.interaction.member, null, 2))
|
logger.info(JSON.stringify(interaction.interaction.member, null, 2))
|
||||||
jellyfinHandler.resetUserPasswort(interaction.interaction.member, uuid())
|
jellyfinHandler.resetUserPasswort(interaction.interaction.member, uuid())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -30,6 +30,8 @@ export interface Config {
|
|||||||
yavin_jellyfin_url: string
|
yavin_jellyfin_url: string
|
||||||
yavin_jellyfin_token: string
|
yavin_jellyfin_token: string
|
||||||
yavin_jellyfin_collection_user: string
|
yavin_jellyfin_collection_user: string
|
||||||
|
random_movie_count: number
|
||||||
|
reroll_retains_top_picks: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export const config: Config = {
|
export const config: Config = {
|
||||||
@ -59,16 +61,18 @@ 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 ?? "TOKEN",
|
||||||
watcher_role: process.env.WATCHER_ROLE ?? "",
|
watcher_role: process.env.WATCHER_ROLE ?? "WATCHER_ROLE",
|
||||||
jf_admin_role: process.env.ADMIN_ROLE ?? "",
|
jf_admin_role: process.env.ADMIN_ROLE ?? "ADMIN_ROLE",
|
||||||
announcement_role: process.env.WATCHPARTY_ANNOUNCEMENT_ROLE ?? "",
|
announcement_role: process.env.WATCHPARTY_ANNOUNCEMENT_ROLE ?? "ANNOUNCE_ROLE",
|
||||||
announcement_channel_id: process.env.CHANNEL_ID ?? "",
|
announcement_channel_id: process.env.CHANNEL_ID ?? "ANNOUNCE_CHANNEL",
|
||||||
jf_collection_id: process.env.JELLYFIN_COLLECTION_ID ?? "",
|
jf_collection_id: process.env.JELLYFIN_COLLECTION_ID ?? "",
|
||||||
yavin_collection_id: process.env.YAVIN_COLLECTION_ID ?? "",
|
yavin_collection_id: process.env.YAVIN_COLLECTION_ID ?? "",
|
||||||
yavin_jellyfin_url: process.env.YAVIN_JELLYFIN_URL ?? "",
|
yavin_jellyfin_url: process.env.YAVIN_JELLYFIN_URL ?? "",
|
||||||
yavin_jellyfin_token: process.env.YAVIN_TOKEN ?? "",
|
yavin_jellyfin_token: process.env.YAVIN_TOKEN ?? "",
|
||||||
yavin_jellyfin_collection_user: process.env.YAVIN_COLLECTION_USER ?? "",
|
yavin_jellyfin_collection_user: process.env.YAVIN_COLLECTION_USER ?? "",
|
||||||
jf_user: process.env.JELLYFIN_USER ?? ""
|
jf_user: process.env.JELLYFIN_USER ?? "",
|
||||||
|
random_movie_count: parseInt(process.env.RANDOM_MOVIE_COUNT ?? "5") ?? 5,
|
||||||
|
reroll_retains_top_picks: process.env.REROLL_RETAIN === "true"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
17
server/constants.ts
Normal file
17
server/constants.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
export enum ValidVoteEmotes { "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟" }
|
||||||
|
export const NONE_OF_THAT = "❌"
|
||||||
|
// WIP
|
||||||
|
export const Emoji = {
|
||||||
|
"one": "\u0031\uFE0F\u20E3",
|
||||||
|
"two": "\u0032\uFE0F\u20E3",
|
||||||
|
"three": "\u0033\uFE0F\u20E3",
|
||||||
|
"four": "\u0034\uFE0F\u20E3",
|
||||||
|
"five": "\u0035\uFE0F\u20E3",
|
||||||
|
"six": "\u0036\uFE0F\u20E3",
|
||||||
|
"seven": "\u0037\uFE0F\u20E3",
|
||||||
|
"eight": "\u0038\uFE0F\u20E3",
|
||||||
|
"nine": "\u0039\uFE0F\u20E3",
|
||||||
|
"ten": "\uD83D\uDD1F",
|
||||||
|
"ticket": "🎫"
|
||||||
|
}
|
52
server/events/announceManualWatchparty.ts
Normal file
52
server/events/announceManualWatchparty.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!event.scheduledStartAt) {
|
||||||
|
logger.error('Event has no start date, bailing out')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const message = `[Watchparty] https://discord.com/events/${event.guildId}/${event.id} \nHey <@&${config.bot.announcement_role}>, wir gucken ${event.name} ${createDateStringFromEvent(event.scheduledStartAt, 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 })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
44
server/events/autoCreateVoteByWPEvent.ts
Normal file
44
server/events/autoCreateVoteByWPEvent.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { GuildScheduledEvent, TextChannel } from "discord.js";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
|
import { client, yavinJellyfinHandler } from "../..";
|
||||||
|
import { Maybe } from "../interfaces";
|
||||||
|
import { logger } from "../logger";
|
||||||
|
|
||||||
|
export const name = 'guildScheduledEventCreate'
|
||||||
|
|
||||||
|
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 a start date, cancelling", { guildId: event.guildId, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const sentMessage = await client.voteController.prepareAndSendVoteMessage({
|
||||||
|
movies,
|
||||||
|
startDate: event.scheduledStartAt,
|
||||||
|
event,
|
||||||
|
announcementChannel,
|
||||||
|
pinAfterSending: true
|
||||||
|
},
|
||||||
|
event.guildId,
|
||||||
|
requestId)
|
||||||
|
|
||||||
|
logger.debug(JSON.stringify(sentMessage))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
53
server/events/deleteAnnouncementsWhenWPEnds.ts
Normal file
53
server/events/deleteAnnouncementsWhenWPEnds.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { Collection, GuildScheduledEvent, GuildScheduledEventStatus, Message } from "discord.js";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
|
import { client } from "../..";
|
||||||
|
import { logger } from "../logger";
|
||||||
|
import { isInitialAnnouncement } from "../helper/messageIdentifiers";
|
||||||
|
|
||||||
|
|
||||||
|
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 => !isInitialAnnouncement(message))
|
||||||
|
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
|
||||||
|
}
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,104 +0,0 @@
|
|||||||
import { addDays, format, isAfter } from "date-fns";
|
|
||||||
import toDate from "date-fns/fp/toDate";
|
|
||||||
import { GuildScheduledEvent, Message, MessageCreateOptions, TextChannel } from "discord.js";
|
|
||||||
import { ScheduledTask, schedule } from "node-cron";
|
|
||||||
import { v4 as uuid } from "uuid";
|
|
||||||
import { client, yavinJellyfinHandler } from "../..";
|
|
||||||
import { closePoll } from "../commands/closepoll";
|
|
||||||
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.getRandomMovies(5, event.guildId, requestId)
|
|
||||||
|
|
||||||
logger.info(`Got ${movies.length} random movies. Creating voting`, { guildId: event.guildId, requestId })
|
|
||||||
logger.debug(`Movies: ${JSON.stringify(movies.map(movie => movie.name))}`, { guildId: event.guildId, requestId })
|
|
||||||
|
|
||||||
const announcementChannel: Maybe<TextChannel> = client.getAnnouncementChannelForGuild(event.guildId)
|
|
||||||
if(!announcementChannel) {
|
|
||||||
logger.error("Could not find announcement channel. Aborting", { guildId: event.guildId, requestId })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
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].name ?? "Film hatte keinen Namen :(").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])
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!task) {
|
|
||||||
task = schedule("0 * * * * *", () => checkForPollsToClose(event))
|
|
||||||
}
|
|
||||||
|
|
||||||
// sentMessage.pin() //todo: uncomment when bot has permission to pin messages. Also update closepoll.ts to only fetch pinned messages
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkForPollsToClose(event: GuildScheduledEvent): Promise<void> {
|
|
||||||
const requestId = uuid()
|
|
||||||
logger.info(`Automatic check for poll closing.`, { guildId: event.guildId, requestId })
|
|
||||||
if (!event.guild) {
|
|
||||||
logger.error("No guild in event. Cancelling.", { guildId: event.guildId, requestId })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
//refetch event in case the time changed or the poll is already closed
|
|
||||||
const events = (await event.guild.scheduledEvents.fetch())
|
|
||||||
.filter(event => event.name.toLowerCase().includes("voting offen"))
|
|
||||||
.map((value) => value)
|
|
||||||
|
|
||||||
if (!events || events.length <= 0) {
|
|
||||||
logger.info("Did not find any events. Cancelling", { guildId: event.guildId, requestId })
|
|
||||||
return
|
|
||||||
} else if (events.length > 1) {
|
|
||||||
logger.error(`More than one event found. Don't know which one is the right one :( Events: ${JSON.stringify(events, null, 2)}`, { guildId: event.guildId, requestId })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const updatedEvent = events[0] //add two hours because of different timezones in discord api and Date.now()
|
|
||||||
if (!updatedEvent.scheduledStartTimestamp) {
|
|
||||||
logger.error("Event does not have a scheduled start time. Cancelling", { guildId: event.guildId, requestId })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const eventDate: Date = toDate(updatedEvent.scheduledStartTimestamp)
|
|
||||||
const closePollDate: Date = addDays(eventDate, -2)
|
|
||||||
|
|
||||||
if (isAfter(Date.now(), closePollDate)) {
|
|
||||||
logger.info("Less than two days until event. Closing poll", { guildId: event.guildId, requestId })
|
|
||||||
closePoll(event.guild, requestId)
|
|
||||||
} else {
|
|
||||||
logger.info(`ScheduledStart: ${closePollDate}. Now: ${toDate(Date.now())}`, { guildId: event.guildId, requestId })
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,63 +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)
|
|
||||||
}
|
|
46
server/events/handleMessageReactionAdd.ts
Normal file
46
server/events/handleMessageReactionAdd.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
|
||||||
|
import { Message, MessageReaction, User } from "discord.js";
|
||||||
|
import { logger, newRequestId, noGuildId } from "../logger";
|
||||||
|
import { Emoji, ValidVoteEmotes, NONE_OF_THAT } from "../constants";
|
||||||
|
import { client } from "../..";
|
||||||
|
import { isInitialAnnouncement, isVoteMessage } from "../helper/messageIdentifiers";
|
||||||
|
|
||||||
|
|
||||||
|
export const name = 'messageReactionAdd'
|
||||||
|
|
||||||
|
export async function execute(messageReaction: MessageReaction, user: User) {
|
||||||
|
if (user.id == client.user?.id) {
|
||||||
|
logger.info('Skipping bot reaction')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const requestId = newRequestId()
|
||||||
|
const guildId = messageReaction.message.inGuild() ? messageReaction.message.guildId : noGuildId
|
||||||
|
const reactedUponMessage: Message = messageReaction.message.partial ? await messageReaction.message.fetch() : messageReaction.message
|
||||||
|
if (!messageReaction.message.guild) {
|
||||||
|
logger.warn(`Received messageReactionAdd on non-guild message.`, { requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Got reaction on message`, { requestId, guildId })
|
||||||
|
|
||||||
|
logger.info(`emoji: ${messageReaction.emoji.toString()}`)
|
||||||
|
|
||||||
|
if (!Object.values(ValidVoteEmotes).includes(messageReaction.emoji.toString()) && messageReaction.emoji.toString() !== NONE_OF_THAT) {
|
||||||
|
logger.info(`${messageReaction.emoji.toString()} currently not handled`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.info(`Found a match for ${messageReaction.emoji.toString()}`)
|
||||||
|
if (isVoteMessage(reactedUponMessage)) {
|
||||||
|
if (messageReaction.emoji.toString() === NONE_OF_THAT) {
|
||||||
|
logger.info(`Reaction is NONE_OF_THAT on a vote message. Handling`, { requestId, guildId })
|
||||||
|
return client.voteController.handleNoneOfThatVote(messageReaction, reactedUponMessage, requestId, guildId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (isInitialAnnouncement(reactedUponMessage)) {
|
||||||
|
if (messageReaction.emoji.toString() === Emoji.ticket) {
|
||||||
|
logger.error(`Got a role emoji. ${reactedUponMessage.id}`)
|
||||||
|
return client.roleController.addMediaRoleToUser(user, messageReaction.message.guild, requestId)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
33
server/events/handleMessageReactionRemove.ts
Normal file
33
server/events/handleMessageReactionRemove.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
|
||||||
|
import { Message, MessageReaction, User } from "discord.js";
|
||||||
|
import { logger, newRequestId, noGuildId } from "../logger";
|
||||||
|
import { Emoji } from "../constants";
|
||||||
|
import { client } from "../..";
|
||||||
|
import { isInitialAnnouncement } from "../helper/messageIdentifiers";
|
||||||
|
|
||||||
|
|
||||||
|
export const name = 'messageReactionRemove'
|
||||||
|
|
||||||
|
export async function execute(messageReaction: MessageReaction, user: User) {
|
||||||
|
if (user.id == client.user?.id) {
|
||||||
|
logger.info('Skipping bot reaction')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const requestId = newRequestId()
|
||||||
|
const guildId = messageReaction.message.inGuild() ? messageReaction.message.guildId : noGuildId
|
||||||
|
const reactedUponMessage: Message = messageReaction.message.partial ? await messageReaction.message.fetch() : messageReaction.message
|
||||||
|
if (!messageReaction.message.guild) {
|
||||||
|
logger.warn(`Received messageReactionRemove on non-guild message.`, { requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Got reaction on message`, { requestId, guildId })
|
||||||
|
|
||||||
|
logger.info(`emoji: ${messageReaction.emoji.toString()}`)
|
||||||
|
if (isInitialAnnouncement(reactedUponMessage)) {
|
||||||
|
if (messageReaction.emoji.toString() === Emoji.ticket) {
|
||||||
|
logger.info(`User: ${user.id}, ${user.username} has removed a ticket reaction. Starting role management`, { requestId, guildId })
|
||||||
|
return client.roleController.removeMediaRoleFromUser(user, messageReaction.message.guild, requestId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
server/events/handlePermJFAccountByRole.ts
Normal file
30
server/events/handlePermJFAccountByRole.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
59
server/events/handleTempJFUserByVoiceEvent.ts
Normal file
59
server/events/handleTempJFUserByVoiceEvent.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)
|
||||||
|
}
|
||||||
|
}
|
58
server/events/handleTempJFUsersByWPEvents.ts
Normal file
58
server/events/handleTempJFUsersByWPEvents.ts
Normal 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)
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import { Message } from "discord.js"
|
import { Message } from "discord.js"
|
||||||
|
import { logger } from "../logger"
|
||||||
|
|
||||||
export const name = 'messageCreate'
|
export const name = 'messageCreate'
|
||||||
export function execute(message: Message) {
|
export function execute(message: Message) {
|
||||||
console.log(`${JSON.stringify(message)} has been created`)
|
logger.info(`${JSON.stringify(message)} has been created`)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
23
server/helper/dateHelper.ts
Normal file
23
server/helper/dateHelper.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { format, isToday } from "date-fns";
|
||||||
|
import { utcToZonedTime } from "date-fns-tz"
|
||||||
|
import { logger } from "../logger";
|
||||||
|
import de from "date-fns/locale/de";
|
||||||
|
import { Maybe } from "../interfaces";
|
||||||
|
|
||||||
|
export function createDateStringFromEvent(eventStartDate: Maybe<Date>, requestId: string, guildId?: string): string {
|
||||||
|
if (!eventStartDate) {
|
||||||
|
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(eventStartDate, 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}`
|
||||||
|
}
|
20
server/helper/messageIdentifiers.ts
Normal file
20
server/helper/messageIdentifiers.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Message } from "discord.js";
|
||||||
|
|
||||||
|
|
||||||
|
// branded types to differentiate objects of identical Type but different contents
|
||||||
|
export type VoteEndMessage = Message<true> & { readonly __brand: 'voteend' }
|
||||||
|
export type AnnouncementMessage = Message<true> & { readonly __brand: 'announcement' }
|
||||||
|
export type VoteMessage = Message<true> & { readonly __brand: 'vote' }
|
||||||
|
|
||||||
|
export type KnownDiscordMessage = VoteMessage | VoteEndMessage | AnnouncementMessage
|
||||||
|
|
||||||
|
export function isVoteMessage(message: Message): message is VoteMessage {
|
||||||
|
return message.cleanContent.includes('[Abstimmung]')
|
||||||
|
}
|
||||||
|
export function isInitialAnnouncement(message: Message): message is AnnouncementMessage {
|
||||||
|
return message.cleanContent.includes("[initial]")
|
||||||
|
}
|
||||||
|
export function isVoteEndedMessage(message: Message): message is VoteEndMessage {
|
||||||
|
return message.cleanContent.includes("[Abstimmung beendet]")
|
||||||
|
}
|
||||||
|
|
83
server/helper/role.controller.ts
Normal file
83
server/helper/role.controller.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import { Guild, MessageReaction, Role, User } from "discord.js";
|
||||||
|
import { GuildMember } from "discord.js";
|
||||||
|
import { logger } from "../logger";
|
||||||
|
import { config } from "../configuration";
|
||||||
|
import { Maybe } from "../interfaces";
|
||||||
|
|
||||||
|
export default class RoleController {
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
private getAnnounceRoleIdForGuild(guildId: string): string {
|
||||||
|
const role = config.bot.announcement_role
|
||||||
|
if (!role) throw new Error(`No announcementRole defined for guild ${guildId}`)
|
||||||
|
return role
|
||||||
|
}
|
||||||
|
public async addRoleToUser(member: GuildMember, role: Role, guildId: string, requestId: string) {
|
||||||
|
logger.info(`Adding Role ${role.id} to user ${member.id}|${member.user.username}`, { requestId, guildId })
|
||||||
|
return await member.roles.add(role)
|
||||||
|
}
|
||||||
|
private async removeRoleFromUser(member: GuildMember, role: Role, guildId: string, requestId: string) {
|
||||||
|
logger.info(`Removing Role ${role.id} from user ${member.id}|${member.user.username}`, { requestId, guildId })
|
||||||
|
return await member.roles.remove(role)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async addMediaRoleToUser(user: User, guild: Guild, requestId: string) {
|
||||||
|
const roleToAdd = await this.getAnnouncementRoleForGuild(guild, requestId)
|
||||||
|
if (!roleToAdd) throw new Error(`No announcementRole found to add to user`)
|
||||||
|
const guildMember = await guild.members.fetch(user)
|
||||||
|
return this.addRoleToUser(guildMember, roleToAdd, guild.id, requestId)
|
||||||
|
}
|
||||||
|
public async removeMediaRoleFromUser(user: User, guild: Guild, requestId: string) {
|
||||||
|
const roleToRemove = await this.getAnnouncementRoleForGuild(guild, requestId)
|
||||||
|
if (!roleToRemove) throw new Error(`No announcementRole found to remove from user`)
|
||||||
|
const guildMember = await guild.members.fetch(user)
|
||||||
|
return this.removeRoleFromUser(guildMember, roleToRemove, guild.id, requestId)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getAnnouncementRoleForGuild(guild: Guild, requestId: string): Promise<Role> {
|
||||||
|
const mediaRole = this.getAnnounceRoleIdForGuild(guild.id)
|
||||||
|
const announcement_role = await guild.roles.fetch()
|
||||||
|
.then(fetchedRoles => fetchedRoles.find(role => role.id === mediaRole))
|
||||||
|
.catch(error => {
|
||||||
|
logger.error(`Could not find announcement_role with id ${config.bot.announcement_role}. Error: ${error}`, { requestId, guildId: guild.id })
|
||||||
|
throw error
|
||||||
|
})
|
||||||
|
if (!announcement_role) throw new Error(`Could not find announcement_role with id ${config.bot.announcement_role}.`)
|
||||||
|
return announcement_role
|
||||||
|
}
|
||||||
|
|
||||||
|
public async assignAnnouncementRolesFromReaction(guild: Guild, reaction: MessageReaction, requestId: string) {
|
||||||
|
const guildId = guild.id
|
||||||
|
logger.info("Managing roles", { guildId, requestId })
|
||||||
|
|
||||||
|
const announcementRole = await this.getAnnouncementRoleForGuild(guild, requestId)
|
||||||
|
|
||||||
|
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 === announcementRole.id) !== 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 === announcementRole.id) === 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 => this.removeRoleFromUser(user, announcementRole, guild.id, requestId))
|
||||||
|
usersWhoNeedRole.forEach(user => this.addRoleToUser(user, announcementRole, guild.id, requestId))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import { Collection, GuildMember } from "discord.js"
|
import { Collection, Guild, GuildMember, Role } from "discord.js"
|
||||||
import { ChangedRoles, PermissionLevel } from "../interfaces"
|
import { ChangedRoles, Maybe, PermissionLevel } from "../interfaces"
|
||||||
import { logger } from "../logger"
|
import { logger } from "../logger"
|
||||||
import { config } from "../configuration"
|
import { config } from "../configuration"
|
||||||
|
|
||||||
@ -16,6 +16,13 @@ export function filterRolesFromMemberUpdate(oldMember: GuildMember, newMember: G
|
|||||||
return { addedRoles, removedRoles }
|
return { addedRoles, removedRoles }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getMembersWithRoleFromGuild(roleId: string, guild: Guild): Promise<Collection<string, GuildMember>> {
|
||||||
|
const emptyResponse = new Collection<string, GuildMember>
|
||||||
|
const guildRole: Maybe<Role> = guild.roles.resolve(roleId)
|
||||||
|
if (!guildRole) return emptyResponse
|
||||||
|
return guildRole.members
|
||||||
|
}
|
||||||
|
|
||||||
export function getGuildSpecificTriggerRoleId(): Collection<string, PermissionLevel> {
|
export function getGuildSpecificTriggerRoleId(): Collection<string, PermissionLevel> {
|
||||||
const outVal = new Collection<string, PermissionLevel>()
|
const outVal = new Collection<string, PermissionLevel>()
|
||||||
outVal.set(config.bot.watcher_role, "VIEWER")
|
outVal.set(config.bot.watcher_role, "VIEWER")
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { CustomError, errorCodes } from "../interfaces"
|
import { CustomError, errorCodes } from "../interfaces"
|
||||||
|
import { logger } from "../logger"
|
||||||
import { ExtendedClient } from "../structures/client"
|
import { ExtendedClient } from "../structures/client"
|
||||||
|
|
||||||
export async function sendFailureDM(creatorMessage: string, client: ExtendedClient, creatorId?: string): Promise<void> {
|
export async function sendFailureDM(creatorMessage: string, client: ExtendedClient, creatorId?: string): Promise<void> {
|
||||||
if (!creatorId) throw new CustomError('No creator ID present', errorCodes.no_creator_id)
|
if (!creatorId) throw new CustomError('No creator ID present', errorCodes.no_creator_id)
|
||||||
const creator = await client.users.fetch(creatorId)
|
const creator = await client.users.fetch(creatorId)
|
||||||
console.log(`Creator ${JSON.stringify(creator)}`)
|
logger.info(`Creator ${JSON.stringify(creator)}`)
|
||||||
if (creator)
|
if (creator)
|
||||||
if (!creator.dmChannel)
|
if (!creator.dmChannel)
|
||||||
await creator.createDM()
|
await creator.createDM()
|
||||||
|
361
server/helper/vote.controller.ts
Normal file
361
server/helper/vote.controller.ts
Normal file
@ -0,0 +1,361 @@
|
|||||||
|
import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildScheduledEventSetStatusArg, GuildScheduledEventStatus, Message, MessageCreateOptions, MessageReaction, TextChannel } from "discord.js"
|
||||||
|
import { ValidVoteEmotes, NONE_OF_THAT } from "../constants"
|
||||||
|
import { logger, newRequestId } from "../logger"
|
||||||
|
import { getMembersWithRoleFromGuild } from "./roleFilter"
|
||||||
|
import { config } from "../configuration"
|
||||||
|
import { VoteMessage, isVoteEndedMessage, isVoteMessage } from "./messageIdentifiers"
|
||||||
|
import { createDateStringFromEvent } from "./dateHelper"
|
||||||
|
import { Maybe, prepareVoteMessageInput } from "../interfaces"
|
||||||
|
import format from "date-fns/format"
|
||||||
|
import toDate from "date-fns/toDate"
|
||||||
|
import differenceInDays from "date-fns/differenceInDays"
|
||||||
|
import addDays from "date-fns/addDays"
|
||||||
|
import isAfter from "date-fns/isAfter"
|
||||||
|
import { ExtendedClient } from "../structures/client"
|
||||||
|
import { JellyfinHandler } from "../jellyfin/handler"
|
||||||
|
|
||||||
|
export type Vote = {
|
||||||
|
emote: string, //todo habs nicht hinbekommen hier Emotes zu nutzen
|
||||||
|
count: number,
|
||||||
|
movie: string
|
||||||
|
}
|
||||||
|
export type VoteMessageInfo = {
|
||||||
|
votes: Vote[],
|
||||||
|
event: GuildScheduledEvent,
|
||||||
|
}
|
||||||
|
export default class VoteController {
|
||||||
|
private client: ExtendedClient
|
||||||
|
private yavinJellyfinHandler: JellyfinHandler
|
||||||
|
|
||||||
|
public constructor(_client: ExtendedClient, _yavin: JellyfinHandler) {
|
||||||
|
this.client = _client
|
||||||
|
this.yavinJellyfinHandler = _yavin
|
||||||
|
}
|
||||||
|
|
||||||
|
public async handleNoneOfThatVote(messageReaction: MessageReaction, reactedUponMessage: VoteMessage, requestId: string, guildId: string) {
|
||||||
|
if (!messageReaction.message.guild) return 'No guild'
|
||||||
|
const guild = messageReaction.message.guild
|
||||||
|
logger.debug(`${reactedUponMessage.id} is vote message`, { requestId, guildId })
|
||||||
|
|
||||||
|
const watcherRoleMember = await getMembersWithRoleFromGuild(config.bot.announcement_role, messageReaction.message.guild)
|
||||||
|
logger.info("ROLE MEMBERS " + JSON.stringify(watcherRoleMember), { requestId, guildId })
|
||||||
|
|
||||||
|
const watcherRoleMemberCount = watcherRoleMember.size
|
||||||
|
logger.info(`MEMBER COUNT: ${watcherRoleMemberCount}`, { requestId, guildId })
|
||||||
|
|
||||||
|
const noneOfThatReactions = reactedUponMessage.reactions.cache.get(NONE_OF_THAT)?.users.cache.filter(x => x.id !== this.client.user?.id).size ?? 0
|
||||||
|
|
||||||
|
const memberThreshold = (watcherRoleMemberCount / 2)
|
||||||
|
logger.info(`Reroll ${noneOfThatReactions} > ${memberThreshold} ?`, { requestId, guildId })
|
||||||
|
if (noneOfThatReactions > memberThreshold)
|
||||||
|
logger.info(`No reroll`, { requestId, guildId })
|
||||||
|
else {
|
||||||
|
logger.info('Starting poll reroll', { requestId, guildId })
|
||||||
|
await this.handleReroll(reactedUponMessage, guild.id, requestId)
|
||||||
|
logger.info(`Finished handling NONE_OF_THAT vote`, { requestId, guildId })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async removeMessage(message: Message): Promise<Message<boolean>> {
|
||||||
|
if (message.pinned) {
|
||||||
|
await message.unpin()
|
||||||
|
}
|
||||||
|
return await message.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns true if a Vote object contains at least one vote
|
||||||
|
* @param {Vote} vote
|
||||||
|
*/
|
||||||
|
private hasAtLeastOneVote(vote: Vote): boolean {
|
||||||
|
// subtracting the bots initial vote
|
||||||
|
const overOneVote = (vote.count - 1) >= 1
|
||||||
|
logger.debug(`${vote.movie} : ${vote.count} -> above: ${overOneVote}`)
|
||||||
|
return overOneVote
|
||||||
|
}
|
||||||
|
|
||||||
|
public async generateRerollMovieList(voteInfo: VoteMessageInfo, guildId: string, requestId: string) {
|
||||||
|
if (config.bot.reroll_retains_top_picks) {
|
||||||
|
const votedOnMovies = voteInfo.votes.filter(this.hasAtLeastOneVote).filter(x => x.emote !== NONE_OF_THAT)
|
||||||
|
logger.info(`Found ${votedOnMovies.length} with votes`, { requestId, guildId })
|
||||||
|
const newMovieCount: number = config.bot.random_movie_count - votedOnMovies.length
|
||||||
|
logger.info(`Fetching ${newMovieCount} from jellyfin`)
|
||||||
|
const newMovies: string[] = await this.yavinJellyfinHandler.getRandomMovieNames(newMovieCount, guildId, requestId)
|
||||||
|
// merge
|
||||||
|
return newMovies.concat(votedOnMovies.map(x => x.movie))
|
||||||
|
} else {
|
||||||
|
// get movies from jellyfin to fill the remaining slots
|
||||||
|
const newMovieCount: number = config.bot.random_movie_count
|
||||||
|
logger.info(`Fetching ${newMovieCount} from jellyfin`)
|
||||||
|
return await this.yavinJellyfinHandler.getRandomMovieNames(newMovieCount, guildId, requestId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async handleReroll(voteMessage: VoteMessage, guildId: string, requestId: string) {
|
||||||
|
// get the movies currently being voted on, their votes, the eventId and its date
|
||||||
|
const voteInfo: VoteMessageInfo = await this.parseVoteInfoFromVoteMessage(voteMessage, requestId)
|
||||||
|
if (!voteInfo.event.scheduledStartAt) {
|
||||||
|
logger.info("Event does not have a start date, cancelling", { guildId: voteInfo.event.guildId, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let movies: string[] = await this.generateRerollMovieList(voteInfo, guildId, requestId)
|
||||||
|
|
||||||
|
const announcementChannel = this.client.getAnnouncementChannelForGuild(guildId)
|
||||||
|
if (!announcementChannel) {
|
||||||
|
logger.error(`No announcementChannel found for ${guildId}, can't post poll`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
logger.info(`Trying to remove old vote Message`, { requestId, guildId })
|
||||||
|
this.removeMessage(voteMessage)
|
||||||
|
} catch (err) {
|
||||||
|
// TODO: integrate failure DM to media Admin to inform about inability to delete old message
|
||||||
|
logger.error(`Error during removeMessage: ${err}`)
|
||||||
|
}
|
||||||
|
const sentMessage = this.prepareAndSendVoteMessage({
|
||||||
|
event: voteInfo.event,
|
||||||
|
movies,
|
||||||
|
announcementChannel,
|
||||||
|
startDate: voteInfo.event.scheduledStartAt,
|
||||||
|
pinAfterSending: true
|
||||||
|
},
|
||||||
|
guildId,
|
||||||
|
requestId)
|
||||||
|
logger.debug(`Sent reroll message: ${JSON.stringify(sentMessage)}`, { requestId, guildId })
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchEventByEventId(guild: Guild, eventId: string, requestId: string): Promise<Maybe<GuildScheduledEvent>> {
|
||||||
|
const guildEvent: GuildScheduledEvent = await guild.scheduledEvents.fetch(eventId)
|
||||||
|
if (!guildEvent) logger.error(`GuildScheduledEvent with id${eventId} could not be found`, { requestId, guildId: guild.id })
|
||||||
|
return guildEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
public async parseVoteInfoFromVoteMessage(message: VoteMessage, requestId: string): Promise<VoteMessageInfo> {
|
||||||
|
const lines = message.cleanContent.split('\n')
|
||||||
|
let parsedIds = this.parseGuildIdAndEventIdFromWholeMessage(message.cleanContent)
|
||||||
|
|
||||||
|
if (!message.guild)
|
||||||
|
throw new Error(`Message ${message.id} not a guild message`)
|
||||||
|
|
||||||
|
const event: Maybe<GuildScheduledEvent> = await this.fetchEventByEventId(message.guild, parsedIds.eventId, requestId)
|
||||||
|
|
||||||
|
let votes: Vote[] = []
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.slice(0, 5).includes(':')) {
|
||||||
|
const splitLine = line.split(":")
|
||||||
|
const [emoji, movie] = splitLine
|
||||||
|
const fetchedVoteFromMessage = message.reactions.cache.get(emoji)
|
||||||
|
if (fetchedVoteFromMessage) {
|
||||||
|
if (emoji === NONE_OF_THAT) {
|
||||||
|
votes.push({ movie: NONE_OF_THAT, emote: NONE_OF_THAT, count: fetchedVoteFromMessage.count })
|
||||||
|
} else
|
||||||
|
votes.push({ movie: movie.trim(), emote: emoji, count: fetchedVoteFromMessage.count })
|
||||||
|
} else {
|
||||||
|
logger.error(`No vote reaction found for movie, assuming 0`, requestId)
|
||||||
|
votes.push({ movie, emote: emoji, count: 0 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return <VoteMessageInfo>{ event, votes }
|
||||||
|
}
|
||||||
|
public parseEventDateFromMessage(message: string, guildId: string, requestId: string): Date {
|
||||||
|
logger.warn(`Falling back to RegEx parsing to get Event Date`, { guildId, requestId })
|
||||||
|
const datematcher = RegExp(/((?:0[1-9]|[12][0-9]|3[01])\.(?:0[1-9]|1[012])\.)(?:\ um\ )((?:(?:[01][0-9]|[2][0-3])\:[0-5][0-9])|(?:[2][4]\:00))!/i)
|
||||||
|
const result: RegExpMatchArray | null = message.match(datematcher)
|
||||||
|
const timeFromResult = result?.at(-1)
|
||||||
|
const dateFromResult = result?.at(1)?.concat(format(new Date(), 'yyyy')).concat(" " + timeFromResult) ?? ""
|
||||||
|
return new Date(dateFromResult)
|
||||||
|
}
|
||||||
|
public parseGuildIdAndEventIdFromWholeMessage(message: string) {
|
||||||
|
const idmatch = RegExp(/(?:http|https):\/\/discord\.com\/events\/(\d*)\/(\d*)/)
|
||||||
|
const matches = message.match(idmatch)
|
||||||
|
if (matches && matches.length == 3)
|
||||||
|
return { guildId: matches[1], eventId: matches[2] }
|
||||||
|
throw Error(`Could not find eventId in Vote Message`)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async prepareAndSendVoteMessage(inputInfo: prepareVoteMessageInput, guildId: string, requestId: string) {
|
||||||
|
const messageText = this.createVoteMessageText(inputInfo.event, inputInfo.movies, guildId, requestId)
|
||||||
|
const sentMessage = await this.sendVoteMessage(messageText, inputInfo.movies.length, inputInfo.announcementChannel)
|
||||||
|
if (inputInfo.pinAfterSending)
|
||||||
|
sentMessage.pin()
|
||||||
|
return sentMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
public createVoteMessageText(event: GuildScheduledEvent, movies: string[], guildId: string, requestId: string): string {
|
||||||
|
let message = `[Abstimmung] für https://discord.com/events/${guildId}/${event.id} \n<@&${config.bot.announcement_role}> Es gibt eine neue Abstimmung für die nächste Watchparty ${createDateStringFromEvent(event.scheduledStartAt, guildId, requestId)}! Stimme hierunter für den nächsten Film ab!\n`
|
||||||
|
|
||||||
|
for (let i = 0; i < movies.length; i++) {
|
||||||
|
message = message.concat(ValidVoteEmotes[i]).concat(": ").concat(movies[i]).concat("\n")
|
||||||
|
}
|
||||||
|
message = message.concat(NONE_OF_THAT).concat(": Wenn dir nichts davon gefällt.")
|
||||||
|
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Refactor into separate message controller
|
||||||
|
public async sendVoteMessage(messageText: string, movieCount: number, announcementChannel: TextChannel) {
|
||||||
|
|
||||||
|
const options: MessageCreateOptions = {
|
||||||
|
allowedMentions: { parse: ["roles"] },
|
||||||
|
content: messageText,
|
||||||
|
}
|
||||||
|
|
||||||
|
const sentMessage: Message<true> = await (await announcementChannel.fetch()).send(options)
|
||||||
|
|
||||||
|
for (let i = 0; i < movieCount; i++) {
|
||||||
|
sentMessage.react(ValidVoteEmotes[i])
|
||||||
|
}
|
||||||
|
sentMessage.react(NONE_OF_THAT)
|
||||||
|
|
||||||
|
return sentMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
public async closePoll(guild: Guild, requestId: string) {
|
||||||
|
const guildId = guild.id
|
||||||
|
logger.info("stopping poll", { guildId, requestId })
|
||||||
|
|
||||||
|
const announcementChannel: Maybe<TextChannel> = this.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 => !isVoteEndedMessage(message) && isVoteMessage(message))
|
||||||
|
.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]
|
||||||
|
|
||||||
|
if (!isVoteMessage(lastMessage)) {
|
||||||
|
logger.error(`Found message that is not a vote message, can't proceed`, { guildId, requestId })
|
||||||
|
logger.debug(`Found messages: ${JSON.stringify(messages, null, 2)}`, { guildId, requestId })
|
||||||
|
logger.debug(`Last message: ${JSON.stringify(lastMessage, null, 2)}`, { guildId, requestId })
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const votes = (await this.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")
|
||||||
|
lastMessage.unpin()
|
||||||
|
await lastMessage.delete()
|
||||||
|
const event = await this.getOpenPollEvent(guild, guild.id, requestId)
|
||||||
|
if (event && votes?.length > 0) {
|
||||||
|
this.updateOpenPollEventWithVoteResults(event, votes, guild, guildId, requestId)
|
||||||
|
this.sendVoteClosedMessage(event, votes[0].movie, guildId, requestId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* gets votes for the movies without the NONE_OF_THAT votes
|
||||||
|
*/
|
||||||
|
public async getVotesByEmote(message: VoteMessage, guildId: string, requestId: string): Promise<Vote[]> {
|
||||||
|
const votes: Vote[] = []
|
||||||
|
logger.debug(`Number of items in emotes: ${Object.values(ValidVoteEmotes).length}`, { guildId, requestId })
|
||||||
|
for (let i = 0; i < Object.keys(ValidVoteEmotes).length / 2; i++) {
|
||||||
|
const emote = ValidVoteEmotes[i]
|
||||||
|
logger.debug(`Getting reaction for emote ${emote}`, { guildId, requestId })
|
||||||
|
const reaction = 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: this.extractMovieFromMessageByEmote(message, emote) }
|
||||||
|
votes.push(vote)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return votes
|
||||||
|
}
|
||||||
|
public async getOpenPollEvent(guild: Guild, guildId: string, requestId: string): Promise<Maybe<GuildScheduledEvent>> {
|
||||||
|
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 an open vote event.", { guildId, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return voteEvents[0]
|
||||||
|
}
|
||||||
|
public async updateOpenPollEventWithVoteResults(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)
|
||||||
|
}
|
||||||
|
public async sendVoteClosedMessage(event: GuildScheduledEvent, movie: string, guildId: string, requestId: string): Promise<Message<boolean>> {
|
||||||
|
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 keine Uhrzeit"
|
||||||
|
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 = {
|
||||||
|
content: body,
|
||||||
|
allowedMentions: { parse: ["roles"] }
|
||||||
|
}
|
||||||
|
const announcementChannel = this.client.getAnnouncementChannelForGuild(guildId)
|
||||||
|
logger.info("Sending vote closed message.", { guildId, requestId })
|
||||||
|
if (!announcementChannel) {
|
||||||
|
const errorMessageText = "Could not find announcement channel. Please fix!"
|
||||||
|
logger.error(errorMessageText, { guildId, requestId })
|
||||||
|
throw errorMessageText
|
||||||
|
}
|
||||||
|
return announcementChannel.send(options)
|
||||||
|
}
|
||||||
|
private extractMovieFromMessageByEmote(voteMessage: VoteMessage, emote: string): string {
|
||||||
|
const lines = voteMessage.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
|
||||||
|
}
|
||||||
|
public async checkForPollsToClose(guild: Guild): Promise<void> {
|
||||||
|
const requestId = newRequestId()
|
||||||
|
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 })
|
||||||
|
this.closePoll(guild, requestId)
|
||||||
|
} else {
|
||||||
|
logger.info(`ScheduledStart: ${closePollDate}. Now: ${toDate(Date.now())}`, { guildId: guild.id, requestId })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import { Collection } from "@discordjs/collection"
|
import { Collection } from "@discordjs/collection"
|
||||||
import { Role } from "discord.js"
|
import { GuildScheduledEvent, Role, TextChannel } from "discord.js"
|
||||||
|
|
||||||
export type Maybe<T> = T | undefined | null
|
export type Maybe<T> = T | undefined | null
|
||||||
export interface Player {
|
export interface Player {
|
||||||
@ -39,3 +39,10 @@ export interface JellyfinConfig {
|
|||||||
collectionUser: string
|
collectionUser: string
|
||||||
}
|
}
|
||||||
export type PermissionLevel = "VIEWER" | "ADMIN" | "TEMPORARY"
|
export type PermissionLevel = "VIEWER" | "ADMIN" | "TEMPORARY"
|
||||||
|
export interface prepareVoteMessageInput {
|
||||||
|
movies: string[],
|
||||||
|
startDate: Date,
|
||||||
|
event: GuildScheduledEvent,
|
||||||
|
announcementChannel: TextChannel,
|
||||||
|
pinAfterSending: boolean,
|
||||||
|
}
|
||||||
|
@ -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 })
|
||||||
@ -242,10 +264,21 @@ export class JellyfinHandler {
|
|||||||
const index = Math.floor(Math.random() * allMovies.length)
|
const index = Math.floor(Math.random() * allMovies.length)
|
||||||
movies.push(...allMovies.splice(index, 1)) // maybe out of bounds? ?
|
movies.push(...allMovies.splice(index, 1)) // maybe out of bounds? ?
|
||||||
}
|
}
|
||||||
|
|
||||||
return movies
|
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 }
|
export enum UserUpsertResult { enabled, created }
|
||||||
|
@ -253,22 +253,22 @@ function isFormData(value: any): value is FormData {
|
|||||||
|
|
||||||
export class ResponseError extends Error {
|
export class ResponseError extends Error {
|
||||||
override name: "ResponseError" = "ResponseError";
|
override name: "ResponseError" = "ResponseError";
|
||||||
constructor(public response: Response, msg?: string) {
|
constructor(public response: Response, errorMessage?: string) {
|
||||||
super(msg);
|
super(errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FetchError extends Error {
|
export class FetchError extends Error {
|
||||||
override name: "FetchError" = "FetchError";
|
override name: "FetchError" = "FetchError";
|
||||||
constructor(public cause: Error, msg?: string) {
|
constructor(public cause: Error, errorMessage?: string) {
|
||||||
super(msg);
|
super(errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RequiredError extends Error {
|
export class RequiredError extends Error {
|
||||||
override name: "RequiredError" = "RequiredError";
|
override name: "RequiredError" = "RequiredError";
|
||||||
constructor(public field: string, msg?: string) {
|
constructor(public field: string, errorMessage?: string) {
|
||||||
super(msg);
|
super(errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import { createLogger, format, transports } from "winston"
|
import { createLogger, format, transports } from "winston"
|
||||||
import { config } from "./configuration"
|
import { config } from "./configuration"
|
||||||
|
import { v4 } from "uuid"
|
||||||
|
export function newRequestId() { return v4() }
|
||||||
|
export const noGuildId = 'NoGuildId'
|
||||||
|
|
||||||
|
|
||||||
const printFn = format.printf(({ guildId, level, message, errorCode, requestId, timestamp: logTimestamp }: { [k: string]: string }) => {
|
const printFn = format.printf(({ guildId, level, message, errorCode, requestId, timestamp: logTimestamp }: { [k: string]: string }) => {
|
||||||
return `[${guildId ?? ''}][${level}][${logTimestamp}][${errorCode ?? ''}][${requestId ?? ''}]:${message}`
|
return `[${guildId ?? ''}][${level.padStart(5, " ")}][${logTimestamp}][${errorCode ?? ''}][${requestId ?? ''}]:${message}`
|
||||||
})
|
})
|
||||||
|
|
||||||
const logFormat = format.combine(
|
const logFormat = format.combine(
|
||||||
@ -13,7 +16,8 @@ const logFormat = format.combine(
|
|||||||
|
|
||||||
const consoleTransports = [
|
const consoleTransports = [
|
||||||
new transports.Console({
|
new transports.Console({
|
||||||
format: logFormat
|
format: logFormat,
|
||||||
|
silent: process.env.NODE_ENV === 'testing'
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
export const logger = createLogger({
|
export const logger = createLogger({
|
||||||
|
@ -2,12 +2,15 @@ import { ApplicationCommandDataResolvable, Client, ClientOptions, Collection, Gu
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { ScheduledTask, schedule } from "node-cron";
|
import { ScheduledTask, schedule } from "node-cron";
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { manageAnnouncementRoles } from "../commands/announce";
|
|
||||||
import { config } from "../configuration";
|
import { config } from "../configuration";
|
||||||
import { Maybe } from "../interfaces";
|
import { Maybe } from "../interfaces";
|
||||||
import { JellyfinHandler } from "../jellyfin/handler";
|
import { JellyfinHandler } from "../jellyfin/handler";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
import { CommandType } from "../types/commandTypes";
|
import { CommandType } from "../types/commandTypes";
|
||||||
|
import { isInitialAnnouncement } from "../helper/messageIdentifiers";
|
||||||
|
import VoteController from "../helper/vote.controller";
|
||||||
|
import { yavinJellyfinHandler } from "../..";
|
||||||
|
import RoleController from "../helper/role.controller";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -15,12 +18,15 @@ 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 voteController: VoteController = new VoteController(this, yavinJellyfinHandler)
|
||||||
|
public roleController: RoleController = new RoleController()
|
||||||
public commands: Collection<string, CommandType> = new Collection()
|
public commands: Collection<string, CommandType> = new Collection()
|
||||||
private announcementChannels: Collection<string, TextChannel> = new Collection //guildId to TextChannel
|
private announcementChannels: Collection<string, TextChannel> = new Collection() //guildId to TextChannel
|
||||||
private announcementRoleHandlerTask: Collection<string, ScheduledTask> = new Collection //one task per guild
|
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, IntentsBitField.Flags.GuildScheduledEvents, IntentsBitField.Flags.GuildVoiceStates)
|
intents.add(IntentsBitField.Flags.GuildMembers, IntentsBitField.Flags.MessageContent, IntentsBitField.Flags.Guilds, IntentsBitField.Flags.DirectMessages, IntentsBitField.Flags.GuildScheduledEvents, IntentsBitField.Flags.GuildMessageReactions, IntentsBitField.Flags.GuildVoiceStates)
|
||||||
const options: ClientOptions = { intents }
|
const options: ClientOptions = { intents }
|
||||||
super(options)
|
super(options)
|
||||||
this.jellyfin = jf
|
this.jellyfin = jf
|
||||||
@ -72,12 +78,29 @@ export class ExtendedClient extends Client {
|
|||||||
this.registerCommands(slashCommands, guilds)
|
this.registerCommands(slashCommands, guilds)
|
||||||
this.cacheUsers(guilds)
|
this.cacheUsers(guilds)
|
||||||
await this.cacheAnnouncementServer(guilds)
|
await this.cacheAnnouncementServer(guilds)
|
||||||
|
this.fetchAnnouncementChannelMessage(this.announcementChannels)
|
||||||
this.startAnnouncementRoleBackgroundTask(guilds)
|
this.startAnnouncementRoleBackgroundTask(guilds)
|
||||||
|
this.startPollCloseBackgroundTasks()
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.info(`Error refreshing slash commands: ${error}`)
|
logger.info(`Error refreshing slash commands: ${error}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Fetches all messages from the provided channel collection.
|
||||||
|
* This is necessary for announcementChannels, because 'old' messages don't receive
|
||||||
|
* messageReactionAdd Events, only messages that were sent while the bot is online are tracked
|
||||||
|
* automatically.
|
||||||
|
* To prevent the need for a dedicated 'Collector' implementation which would listen on specific
|
||||||
|
* it's easiest to just fetch all messages from the backlog, which automatically makes the bot track them
|
||||||
|
* again.
|
||||||
|
* @param {Collection<string, TextChannel>} channels - All channels which should be fecthed for reactionTracking
|
||||||
|
*/
|
||||||
|
private async fetchAnnouncementChannelMessage(channels: Collection<string, TextChannel>): Promise<void> {
|
||||||
|
channels.each(async ch => {
|
||||||
|
ch.messages.fetch()
|
||||||
|
})
|
||||||
|
}
|
||||||
private async cacheAnnouncementServer(guilds: Collection<Snowflake, Guild>) {
|
private async cacheAnnouncementServer(guilds: Collection<Snowflake, Guild>) {
|
||||||
for (const guild of guilds.values()) {
|
for (const guild of guilds.values()) {
|
||||||
const channels: TextChannel[] = <TextChannel[]>(await guild.channels.fetch())
|
const channels: TextChannel[] = <TextChannel[]>(await guild.channels.fetch())
|
||||||
@ -133,7 +156,7 @@ export class ExtendedClient extends Client {
|
|||||||
}
|
}
|
||||||
this.announcementRoleHandlerTask.set(guild.id, schedule("*/10 * * * * *", async () => {
|
this.announcementRoleHandlerTask.set(guild.id, schedule("*/10 * * * * *", async () => {
|
||||||
const requestId = uuid()
|
const requestId = uuid()
|
||||||
const messages = (await textChannel.messages.fetchPinned()).filter(message => message.cleanContent.includes("[initial]"))
|
const messages = (await textChannel.messages.fetchPinned()).filter(message => isInitialAnnouncement(message))
|
||||||
|
|
||||||
if (messages.size > 1) {
|
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 })
|
logger.error("More than one pinned announcement Messages found. Unable to know which one people react to. Please fix!", { guildId: guild.id, requestId })
|
||||||
@ -150,10 +173,10 @@ export class ExtendedClient extends Client {
|
|||||||
}
|
}
|
||||||
//logger.debug(`Message: ${JSON.stringify(message, null, 2)}`, { guildId: guild.id, requestId })
|
//logger.debug(`Message: ${JSON.stringify(message, null, 2)}`, { guildId: guild.id, requestId })
|
||||||
|
|
||||||
const reactions = message.reactions.resolve("🎫")
|
const ticketReaction = message.reactions.resolve("🎫")
|
||||||
//logger.debug(`reactions: ${JSON.stringify(reactions, null, 2)}`, { guildId: guild.id, requestId })
|
//logger.debug(`reactions: ${JSON.stringify(reactions, null, 2)}`, { guildId: guild.id, requestId })
|
||||||
if (reactions) {
|
if (ticketReaction) {
|
||||||
manageAnnouncementRoles(message.guild, reactions, requestId)
|
this.roleController.assignAnnouncementRolesFromReaction(message.guild, ticketReaction, requestId)
|
||||||
} else {
|
} else {
|
||||||
logger.error("Did not get reactions! Aborting!", { guildId: guild.id, requestId })
|
logger.error("Did not get reactions! Aborting!", { guildId: guild.id, requestId })
|
||||||
}
|
}
|
||||||
@ -169,4 +192,10 @@ export class ExtendedClient extends Client {
|
|||||||
}
|
}
|
||||||
task.stop()
|
task.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async startPollCloseBackgroundTasks() {
|
||||||
|
for (const guild of this.guilds.cache) {
|
||||||
|
this.pollCloseBackgroundTasks.set(guild[1].id, schedule("0 * * * * *", () => this.voteController.checkForPollsToClose(guild[1])))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
81
tests/discord/noneofthat.test.ts
Normal file
81
tests/discord/noneofthat.test.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { Guild, GuildScheduledEvent, Message } from "discord.js"
|
||||||
|
import VoteController from "../../server/helper/vote.controller"
|
||||||
|
import { JellyfinHandler } from "../../server/jellyfin/handler"
|
||||||
|
import { ExtendedClient } from "../../server/structures/client"
|
||||||
|
import { Emoji, NONE_OF_THAT } from "../../server/constants"
|
||||||
|
import { isVoteMessage } from "../../server/helper/messageIdentifiers"
|
||||||
|
|
||||||
|
describe('vote controller - none_of_that functions', () => {
|
||||||
|
const testEventId = '1234321'
|
||||||
|
const testEventDate = new Date('2023-01-01')
|
||||||
|
const testGuildId = "888999888"
|
||||||
|
const testMovies = [
|
||||||
|
'Movie1',
|
||||||
|
'Movie2',
|
||||||
|
'Movie3',
|
||||||
|
'Movie4',
|
||||||
|
'Movie5',
|
||||||
|
]
|
||||||
|
const votesList = [
|
||||||
|
{ emote: Emoji.one, count: 1, movie: testMovies[0] },
|
||||||
|
{ emote: Emoji.two, count: 2, movie: testMovies[1] },
|
||||||
|
{ emote: Emoji.three, count: 3, movie: testMovies[2] },
|
||||||
|
{ emote: Emoji.four, count: 1, movie: testMovies[3] },
|
||||||
|
{ emote: Emoji.five, count: 1, movie: testMovies[4] },
|
||||||
|
{ emote: NONE_OF_THAT, count: 2, movie: NONE_OF_THAT },
|
||||||
|
]
|
||||||
|
const mockClient: ExtendedClient = <ExtendedClient><unknown>{
|
||||||
|
user: {
|
||||||
|
id: 'mockId'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const mockEvent: GuildScheduledEvent = <GuildScheduledEvent><unknown>{
|
||||||
|
scheduledStartAt: testEventDate,
|
||||||
|
id: testEventId,
|
||||||
|
guild: testGuildId
|
||||||
|
}
|
||||||
|
const mockJellyfinHandler: JellyfinHandler = <JellyfinHandler><unknown>{
|
||||||
|
getRandomMovieNames: jest.fn().mockReturnValue(["movie1"])
|
||||||
|
}
|
||||||
|
const votes = new VoteController(mockClient, mockJellyfinHandler)
|
||||||
|
const mockMessageContent = votes.createVoteMessageText(mockEvent, testMovies, testGuildId, "requestId")
|
||||||
|
|
||||||
|
test('sendVoteClosedMessage', async () => {
|
||||||
|
mockClient.getAnnouncementChannelForGuild = jest.fn().mockReturnValue({
|
||||||
|
send: jest.fn().mockImplementation((options: any) => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
resolve(options)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const scheduledEvent: GuildScheduledEvent = <GuildScheduledEvent>{
|
||||||
|
scheduledStartAt: testEventDate,
|
||||||
|
guildId: testGuildId,
|
||||||
|
id: testEventId
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await votes.sendVoteClosedMessage(scheduledEvent, 'MovieNew', 'guild', 'request')
|
||||||
|
expect(res).toEqual({
|
||||||
|
allowedMentions: {
|
||||||
|
parse: ["roles"]
|
||||||
|
},
|
||||||
|
content: `[Abstimmung beendet] für https://discord.com/events/${testGuildId}/${testEventId}\n<@&WATCHPARTY_ANNOUNCEMENT_ROLE> Wir gucken MovieNew am 01.01. um 01:00`
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('getVotesByEmote', async () => {
|
||||||
|
const mockMessage: Message = <Message><unknown>{
|
||||||
|
cleanContent: mockMessageContent,
|
||||||
|
reactions: {
|
||||||
|
resolve: jest.fn().mockImplementation((input: any) => {
|
||||||
|
return votesList.find(e => e.emote === input)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isVoteMessage(mockMessage)) {
|
||||||
|
const result = await votes.getVotesByEmote(mockMessage, 'guildId', 'requestId')
|
||||||
|
expect(result.length).toEqual(5)
|
||||||
|
expect(result).toEqual(votesList.filter(x => x.movie != NONE_OF_THAT))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
192
tests/discord/votes.test.ts
Normal file
192
tests/discord/votes.test.ts
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
import { Emoji, NONE_OF_THAT } from "../../server/constants"
|
||||||
|
import VoteController, { VoteMessageInfo } from "../../server/helper/vote.controller"
|
||||||
|
import { JellyfinHandler } from "../../server/jellyfin/handler"
|
||||||
|
import { ExtendedClient } from "../../server/structures/client"
|
||||||
|
import { VoteMessage } from "../../server/helper/messageIdentifiers"
|
||||||
|
import { GuildScheduledEvent, MessageReaction } from "discord.js"
|
||||||
|
test('parse votes from vote message', async () => {
|
||||||
|
const testMovies = [
|
||||||
|
'Movie1',
|
||||||
|
'Movie2',
|
||||||
|
'Movie3',
|
||||||
|
'Movie4',
|
||||||
|
'Movie5',
|
||||||
|
]
|
||||||
|
const testEventId = '1234321'
|
||||||
|
const testEventDate = new Date('2023-01-01')
|
||||||
|
const testGuildId = "888999888"
|
||||||
|
const voteController: VoteController = new VoteController(<ExtendedClient>{}, <JellyfinHandler>{})
|
||||||
|
const mockEvent: GuildScheduledEvent = <GuildScheduledEvent><unknown>{
|
||||||
|
scheduledStartAt: testEventDate,
|
||||||
|
id: testEventId,
|
||||||
|
guild: testGuildId
|
||||||
|
}
|
||||||
|
const testMessage = voteController.createVoteMessageText(mockEvent, testMovies, testGuildId, "requestId")
|
||||||
|
|
||||||
|
|
||||||
|
const expectedResult: VoteMessageInfo = {
|
||||||
|
event: mockEvent,
|
||||||
|
votes: [
|
||||||
|
{ emote: Emoji.one, count: 1, movie: testMovies[0] },
|
||||||
|
{ emote: Emoji.two, count: 2, movie: testMovies[1] },
|
||||||
|
{ emote: Emoji.three, count: 3, movie: testMovies[2] },
|
||||||
|
{ emote: Emoji.four, count: 1, movie: testMovies[3] },
|
||||||
|
{ emote: Emoji.five, count: 1, movie: testMovies[4] },
|
||||||
|
{ emote: NONE_OF_THAT, count: 1, movie: NONE_OF_THAT },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const message: VoteMessage = <VoteMessage><unknown>{
|
||||||
|
cleanContent: testMessage,
|
||||||
|
guild: {
|
||||||
|
id: testGuildId,
|
||||||
|
scheduledEvents: {
|
||||||
|
fetch: jest.fn().mockImplementation((input: any) => {
|
||||||
|
if (input === testEventId)
|
||||||
|
return {
|
||||||
|
id: testEventId,
|
||||||
|
guild: testGuildId,
|
||||||
|
scheduledStartAt: testEventDate
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reactions: {
|
||||||
|
cache: {
|
||||||
|
get: jest.fn().mockImplementation((input: any) => {
|
||||||
|
// Abusing duck typing
|
||||||
|
// Message Reaction has a method `count` and the expected votes
|
||||||
|
// have a field `count`
|
||||||
|
// this will evaluate to the same 'result'
|
||||||
|
return expectedResult.votes.find(e => e.emote === input)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await voteController.parseVoteInfoFromVoteMessage(message, 'requestId')
|
||||||
|
console.log(JSON.stringify(result))
|
||||||
|
expect(Array.isArray(result)).toBe(false)
|
||||||
|
expect(result.event.id).toEqual(testEventId)
|
||||||
|
expect(result.event.scheduledStartAt).toEqual(testEventDate)
|
||||||
|
expect(result.votes.length).toEqual(expectedResult.votes.length)
|
||||||
|
expect(result).toEqual(expectedResult)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('parse votes from vote message', () => {
|
||||||
|
const testMovies = [
|
||||||
|
'Movie1',
|
||||||
|
'Movie2',
|
||||||
|
'Movie3',
|
||||||
|
'Movie4',
|
||||||
|
'Movie5',
|
||||||
|
]
|
||||||
|
const testEventId = '1234321'
|
||||||
|
const testEventDate = new Date('2023-01-01')
|
||||||
|
const testGuildId = "888999888"
|
||||||
|
const voteController: VoteController = new VoteController(<ExtendedClient>{}, <JellyfinHandler>{})
|
||||||
|
const mockEvent: GuildScheduledEvent = <GuildScheduledEvent><unknown>{
|
||||||
|
scheduledStartAt: testEventDate,
|
||||||
|
id: testEventId,
|
||||||
|
guild: testGuildId
|
||||||
|
}
|
||||||
|
const testMessage = voteController.createVoteMessageText(mockEvent, testMovies, testGuildId, "requestId")
|
||||||
|
|
||||||
|
const result = voteController.parseGuildIdAndEventIdFromWholeMessage(testMessage)
|
||||||
|
expect(result).toEqual({ guildId: testGuildId, eventId: testEventId })
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
test.skip('handles complete none_of_that vote', () => {
|
||||||
|
|
||||||
|
const mockJellyfinHandler: JellyfinHandler = <JellyfinHandler><unknown>{
|
||||||
|
getRandomMovieNames: jest.fn().mockReturnValue(["movie1"])
|
||||||
|
}
|
||||||
|
|
||||||
|
const testMovies = [
|
||||||
|
'Movie1',
|
||||||
|
'Movie2',
|
||||||
|
'Movie3',
|
||||||
|
'Movie4',
|
||||||
|
'Movie5',
|
||||||
|
]
|
||||||
|
const testEventId = '1234321'
|
||||||
|
const testEventDate = new Date('2023-01-01')
|
||||||
|
const testGuildId = "888999888"
|
||||||
|
const mockClient: ExtendedClient = <ExtendedClient><unknown>{
|
||||||
|
user: {
|
||||||
|
id: 'mockId'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const voteController = new VoteController(mockClient, mockJellyfinHandler)
|
||||||
|
const mockEvent: GuildScheduledEvent = <GuildScheduledEvent><unknown>{
|
||||||
|
scheduledStartAt: testEventDate,
|
||||||
|
id: testEventId,
|
||||||
|
guild: testGuildId
|
||||||
|
}
|
||||||
|
const mockMessageContent = voteController.createVoteMessageText(mockEvent, testMovies, testGuildId, "requestId")
|
||||||
|
const reactedUponMessage: VoteMessage = <VoteMessage><unknown>{
|
||||||
|
cleanContent: mockMessageContent,
|
||||||
|
guild: {
|
||||||
|
id: 'id',
|
||||||
|
roles: {
|
||||||
|
resolve: jest.fn().mockReturnValue({
|
||||||
|
members: [{}, {}, {}, {}, {}]//content does not matter
|
||||||
|
})
|
||||||
|
},
|
||||||
|
scheduledEvents: {
|
||||||
|
fetch: jest.fn().mockReturnValue([
|
||||||
|
{
|
||||||
|
name: 'voting offen'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
unpin: jest.fn().mockImplementation(() => {
|
||||||
|
|
||||||
|
}),
|
||||||
|
delete: jest.fn().mockImplementation(() => {
|
||||||
|
|
||||||
|
}),
|
||||||
|
reactions: {
|
||||||
|
resolve: jest.fn().mockImplementation((input: any) => {
|
||||||
|
console.log(JSON.stringify(input))
|
||||||
|
}),
|
||||||
|
cache: {
|
||||||
|
get: jest.fn().mockReturnValue({
|
||||||
|
users: {
|
||||||
|
cache: [
|
||||||
|
{
|
||||||
|
id: "mockId"//to filter out
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "userId1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "userId2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "userId3"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const messageReaction: MessageReaction = <MessageReaction><unknown>{
|
||||||
|
message: reactedUponMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
mockClient.getAnnouncementChannelForGuild = jest.fn().mockReturnValue({
|
||||||
|
messages: {
|
||||||
|
fetch: jest.fn().mockReturnValue([
|
||||||
|
reactedUponMessage
|
||||||
|
])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = voteController.handleNoneOfThatVote(messageReaction, reactedUponMessage, 'requestId', 'guildId')
|
||||||
|
|
||||||
|
|
||||||
|
})
|
15
tests/helpers/date.test.ts
Normal file
15
tests/helpers/date.test.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { createDateStringFromEvent } from "../../server/helper/dateHelper"
|
||||||
|
import MockDate from 'mockdate'
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
MockDate.set('01-01-2023')
|
||||||
|
})
|
||||||
|
|
||||||
|
function getTestDate(date: string): Date {
|
||||||
|
return 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')
|
||||||
|
})
|
28
tests/helpers/memberRoles.test.ts
Normal file
28
tests/helpers/memberRoles.test.ts
Normal 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)
|
||||||
|
})
|
15
tests/testenv.js
Normal file
15
tests/testenv.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
process.env.CLIENT_ID = "CLIENT_ID"
|
||||||
|
process.env.SECRET = "SECRET"
|
||||||
|
process.env.BOT_TOKEN = "BOT_TOKEN"
|
||||||
|
process.env.WATCHER_ROLE = "WATCHER_ROLE"
|
||||||
|
process.env.ADMIN_ROLE = "ADMIN_ROLE"
|
||||||
|
process.env.CHANNEL_ID = "CHANNEL_ID"
|
||||||
|
process.env.WATCHPARTY_ANNOUNCEMENT_ROLE = "WATCHPARTY_ANNOUNCEMENT_ROLE"
|
||||||
|
process.env.YAVIN_JELLYFIN_URL = "YAVIN_JELLYFIN_URL"
|
||||||
|
process.env.YAVIN_COLLECTION_ID = "YAVIN_COLLECTION_ID"
|
||||||
|
process.env.YAVIN_COLLECTION_USER = "YAVIN_COLLECTION_USER"
|
||||||
|
process.env.YAVIN_TOKEN = "YAVIN_TOKEN"
|
||||||
|
process.env.TOKEN = "TOKEN"
|
||||||
|
process.env.JELLYFIN_USER = "JELLYFIN_USER"
|
||||||
|
process.env.JELLYFIN_COLLECTION_ID = "JELLYFIN_COLLECTION_ID"
|
||||||
|
process.env.JELLYFIN_URL = "JELLYFIN_URL"
|
@ -1,61 +1,44 @@
|
|||||||
{
|
{
|
||||||
"extends": "@tsconfig/recommended/tsconfig.json",
|
"extends": "@tsconfig/recommended/tsconfig.json",
|
||||||
"exclude":["node_modules"],
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
/* Basic Options */
|
/* Basic Options */
|
||||||
"target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
|
"target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
|
||||||
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
|
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
// "lib": [], /* Specify library files to be included in the compilation. */
|
|
||||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
|
||||||
// "checkJs": true, /* Report errors in .js files. */
|
|
||||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
|
||||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
|
||||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
|
||||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
|
||||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
|
||||||
"outDir": "./build" /* Redirect output structure to the directory. */,
|
"outDir": "./build" /* Redirect output structure to the directory. */,
|
||||||
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||||
// "composite": true, /* Enable project compilation */
|
// "composite": true, /* Enable project compilation */
|
||||||
// "removeComments": true, /* Do not emit comments to output. */
|
"removeComments": true, /* Do not emit comments to output. */
|
||||||
// "noEmit": true, /* Do not emit outputs. */
|
|
||||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
|
||||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
|
||||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
|
||||||
|
|
||||||
/* Strict Type-Checking Options */
|
/* Strict Type-Checking Options */
|
||||||
"strict": true /* Enable all strict type-checking options. */,
|
"strict": true /* Enable all strict type-checking options. */,
|
||||||
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
"strictNullChecks": true, /* Enable strict null checks. */
|
||||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
"strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||||
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
"alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||||
|
//"noUncheckedIndexedAccess": true,
|
||||||
/* Additional Checks */
|
/* Additional Checks */
|
||||||
//"noUnusedLocals": true, /* Report errors on unused locals. */
|
//"noUnusedLocals": true, /* Report errors on unused locals. */
|
||||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||||
|
|
||||||
/* Module Resolution Options */
|
/* Module Resolution Options */
|
||||||
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
|
||||||
// "types": [], /* Type declaration files to be included in compilation. */
|
|
||||||
"allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
"allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||||
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
|
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
|
||||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||||
|
|
||||||
/* Source Map Options */
|
/* Source Map Options */
|
||||||
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||||
"inlineSourceMap": true /* Emit a single file with source maps instead of having a separate file. */
|
"inlineSourceMap": true /* Emit a single file with source maps instead of having a separate file. */
|
||||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
|
||||||
|
|
||||||
/* Experimental Options */
|
/* Experimental Options */
|
||||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||||
|
Reference in New Issue
Block a user