Compare commits
	
		
			103 Commits
		
	
	
		
			v1.0.1
			...
			e66aebc88c
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | 
							
								
								
									
										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
 | 
				
			||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
name: Compile the repository
 | 
					name: Compile the repository
 | 
				
			||||||
on: [push]
 | 
					on: [pull_request]
 | 
				
			||||||
env:
 | 
					env:
 | 
				
			||||||
  REGISTRY: gitea.brudi.xyz
 | 
					  REGISTRY: gitea.brudi.xyz
 | 
				
			||||||
  IMAGE_NAME: ${{ gitea.repository }}
 | 
					  IMAGE_NAME: ${{ gitea.repository }}
 | 
				
			||||||
@@ -14,4 +14,4 @@ jobs:
 | 
				
			|||||||
      - name: Checkout repository
 | 
					      - name: Checkout repository
 | 
				
			||||||
        uses: actions/checkout@v3
 | 
					        uses: actions/checkout@v3
 | 
				
			||||||
      - name: Build Container
 | 
					      - name: Build Container
 | 
				
			||||||
        run: docker build -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" .
 | 
					        run: docker build --target compile .
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,7 +11,6 @@ env:
 | 
				
			|||||||
jobs:
 | 
					jobs:
 | 
				
			||||||
  build-docker-image:
 | 
					  build-docker-image:
 | 
				
			||||||
    runs-on: ubuntu-latest
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
    if: gitea.ref == 'refs/heads/master'
 | 
					 | 
				
			||||||
    container: catthehacker/ubuntu:act-latest
 | 
					    container: catthehacker/ubuntu:act-latest
 | 
				
			||||||
    permissions:
 | 
					    permissions:
 | 
				
			||||||
      contents: read
 | 
					      contents: read
 | 
				
			||||||
@@ -19,11 +18,11 @@ jobs:
 | 
				
			|||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
      - name: Checkout repository
 | 
					      - name: Checkout repository
 | 
				
			||||||
        uses: actions/checkout@v3
 | 
					        uses: actions/checkout@v3
 | 
				
			||||||
      - name: Get Package Version
 | 
					 | 
				
			||||||
        run: VERSION = node -p "require('./package.json').version"
 | 
					 | 
				
			||||||
      - name: Log in to the Container registry
 | 
					      - name: Log in to the Container registry
 | 
				
			||||||
        run: docker login -u ${{ env.USER }} -p ${{ secrets.TOKEN }} ${{ env.REGISTRY }}
 | 
					        run: docker login -u ${{ env.USER }} -p ${{ secrets.TOKEN }} ${{ env.REGISTRY }}
 | 
				
			||||||
      - name: Build Container
 | 
					      - name: Build Container
 | 
				
			||||||
        run: docker build -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}".
 | 
					        run: docker build --target compile -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}" .
 | 
				
			||||||
 | 
					        env:
 | 
				
			||||||
 | 
					          version: $(cat package.json | awk 'match($0, /version/) {print $2}' | sed 's/[\",]//g') # extracts the version number from the package.json with bash magic
 | 
				
			||||||
      - name: Push Container
 | 
					      - name: Push Container
 | 
				
			||||||
        run: docker push "${{ 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
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										4
									
								
								index.ts
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								index.ts
									
									
									
									
									
								
							@@ -5,8 +5,8 @@ import { JellyfinHandler } from "./server/jellyfin/handler"
 | 
				
			|||||||
import { attachedImages } from "./server/assets/attachments"
 | 
					import { attachedImages } from "./server/assets/attachments"
 | 
				
			||||||
const requestId = 'startup'
 | 
					const requestId = 'startup'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const jellyfinHandler = new JellyfinHandler({jellyfinToken: config.bot.workaround_token, jellyfinUrl: config.bot.jellyfin_url, movieCollectionId: config.bot.jf_collection_id, collectionUser: config.bot.jf_user})
 | 
					export const jellyfinHandler = new JellyfinHandler({ jellyfinToken: config.bot.workaround_token, jellyfinUrl: config.bot.jellyfin_url, movieCollectionId: config.bot.jf_collection_id, collectionUser: config.bot.jf_user })
 | 
				
			||||||
export const yavinJellyfinHandler = new JellyfinHandler({jellyfinToken: config.bot.yavin_jellyfin_token, jellyfinUrl: config.bot.yavin_jellyfin_url, movieCollectionId: config.bot.yavin_collection_id, collectionUser: config.bot.yavin_jellyfin_collection_user})
 | 
					export const yavinJellyfinHandler = new JellyfinHandler({ jellyfinToken: config.bot.yavin_jellyfin_token, jellyfinUrl: config.bot.yavin_jellyfin_url, movieCollectionId: config.bot.yavin_collection_id, collectionUser: config.bot.yavin_jellyfin_collection_user })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const client = new ExtendedClient(jellyfinHandler)
 | 
					export const client = new ExtendedClient(jellyfinHandler)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										18
									
								
								jest.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								jest.config.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					module.exports = {
 | 
				
			||||||
 | 
					  'roots': [
 | 
				
			||||||
 | 
					    '<rootDir>/tests',
 | 
				
			||||||
 | 
					    '<rootDir>/server'
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  'transform': {
 | 
				
			||||||
 | 
					    '^.+\\.tsx?$': 'ts-jest'
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  'testRegex': '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',
 | 
				
			||||||
 | 
					  'moduleFileExtensions': [
 | 
				
			||||||
 | 
					    'ts',
 | 
				
			||||||
 | 
					    'tsx',
 | 
				
			||||||
 | 
					    'js',
 | 
				
			||||||
 | 
					    'jsx',
 | 
				
			||||||
 | 
					    'json',
 | 
				
			||||||
 | 
					    'node'
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										46
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										46
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -1,12 +1,12 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "name": "node-jellyfin-discord-bot",
 | 
					  "name": "node-jellyfin-discord-bot",
 | 
				
			||||||
  "version": "1.0.1",
 | 
					  "version": "1.1.3",
 | 
				
			||||||
  "lockfileVersion": 2,
 | 
					  "lockfileVersion": 2,
 | 
				
			||||||
  "requires": true,
 | 
					  "requires": true,
 | 
				
			||||||
  "packages": {
 | 
					  "packages": {
 | 
				
			||||||
    "": {
 | 
					    "": {
 | 
				
			||||||
      "name": "node-jellyfin-discord-bot",
 | 
					      "name": "node-jellyfin-discord-bot",
 | 
				
			||||||
      "version": "1.0.1",
 | 
					      "version": "1.1.3",
 | 
				
			||||||
      "license": "MIT",
 | 
					      "license": "MIT",
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "@discordjs/rest": "^1.7.0",
 | 
					        "@discordjs/rest": "^1.7.0",
 | 
				
			||||||
@@ -17,6 +17,7 @@
 | 
				
			|||||||
        "@types/uuid": "^9.0.1",
 | 
					        "@types/uuid": "^9.0.1",
 | 
				
			||||||
        "axios": "^1.3.5",
 | 
					        "axios": "^1.3.5",
 | 
				
			||||||
        "date-fns": "^2.29.3",
 | 
					        "date-fns": "^2.29.3",
 | 
				
			||||||
 | 
					        "date-fns-tz": "^2.0.0",
 | 
				
			||||||
        "discord-api-types": "^0.37.38",
 | 
					        "discord-api-types": "^0.37.38",
 | 
				
			||||||
        "discord.js": "^14.9.0",
 | 
					        "discord.js": "^14.9.0",
 | 
				
			||||||
        "dotenv": "^16.0.3",
 | 
					        "dotenv": "^16.0.3",
 | 
				
			||||||
@@ -29,12 +30,13 @@
 | 
				
			|||||||
        "winston": "^3.8.2"
 | 
					        "winston": "^3.8.2"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "devDependencies": {
 | 
					      "devDependencies": {
 | 
				
			||||||
        "@types/jest": "^29.5.0",
 | 
					        "@types/jest": "^29.5.2",
 | 
				
			||||||
        "@typescript-eslint/eslint-plugin": "^5.58.0",
 | 
					        "@typescript-eslint/eslint-plugin": "^5.58.0",
 | 
				
			||||||
        "@typescript-eslint/parser": "^5.58.0",
 | 
					        "@typescript-eslint/parser": "^5.58.0",
 | 
				
			||||||
        "eslint": "^8.38.0",
 | 
					        "eslint": "^8.38.0",
 | 
				
			||||||
        "jest": "^29.5.0",
 | 
					        "jest": "^29.5.0",
 | 
				
			||||||
        "jest-cli": "^29.5.0",
 | 
					        "jest-cli": "^29.5.0",
 | 
				
			||||||
 | 
					        "mockdate": "^3.0.5",
 | 
				
			||||||
        "nodemon": "^2.0.22",
 | 
					        "nodemon": "^2.0.22",
 | 
				
			||||||
        "rimraf": "^5.0.0",
 | 
					        "rimraf": "^5.0.0",
 | 
				
			||||||
        "ts-jest": "^29.1.0"
 | 
					        "ts-jest": "^29.1.0"
 | 
				
			||||||
@@ -1567,9 +1569,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@types/jest": {
 | 
					    "node_modules/@types/jest": {
 | 
				
			||||||
      "version": "29.5.0",
 | 
					      "version": "29.5.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.2.tgz",
 | 
				
			||||||
      "integrity": "sha512-3Emr5VOl/aoBwnWcH/EFQvlSAmjV+XtV9GGu5mwdYew5vhQh0IUZx/60x0TzHDu09Bi7HMx10t/namdJw5QIcg==",
 | 
					      "integrity": "sha512-mSoZVJF5YzGVCk+FsDxzDuH7s+SCkzrgKZzf0Z0T2WudhBUPoF6ktoTPC4R0ZoCPCV5xUvuU6ias5NvxcBcMMg==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "expect": "^29.0.0",
 | 
					        "expect": "^29.0.0",
 | 
				
			||||||
@@ -2626,6 +2628,14 @@
 | 
				
			|||||||
        "url": "https://opencollective.com/date-fns"
 | 
					        "url": "https://opencollective.com/date-fns"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/date-fns-tz": {
 | 
				
			||||||
 | 
					      "version": "2.0.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-2.0.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-OAtcLdB9vxSXTWHdT8b398ARImVwQMyjfYGkKD2zaGpHseG2UPHbHjXELReErZFxWdSLph3c2zOaaTyHfOhERQ==",
 | 
				
			||||||
 | 
					      "peerDependencies": {
 | 
				
			||||||
 | 
					        "date-fns": ">=2.0.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/debug": {
 | 
					    "node_modules/debug": {
 | 
				
			||||||
      "version": "4.3.4",
 | 
					      "version": "4.3.4",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
 | 
				
			||||||
@@ -4980,6 +4990,12 @@
 | 
				
			|||||||
        "node": ">=10"
 | 
					        "node": ">=10"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/mockdate": {
 | 
				
			||||||
 | 
					      "version": "3.0.5",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/mockdate/-/mockdate-3.0.5.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-iniQP4rj1FhBdBYS/+eQv7j1tadJ9lJtdzgOpvsOHng/GbcDh2Fhdeq+ZRldrPYdXvCyfFUmFeEwEGXZB5I/AQ==",
 | 
				
			||||||
 | 
					      "dev": true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/ms": {
 | 
					    "node_modules/ms": {
 | 
				
			||||||
      "version": "2.1.2",
 | 
					      "version": "2.1.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
 | 
				
			||||||
@@ -8130,9 +8146,9 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "@types/jest": {
 | 
					    "@types/jest": {
 | 
				
			||||||
      "version": "29.5.0",
 | 
					      "version": "29.5.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.2.tgz",
 | 
				
			||||||
      "integrity": "sha512-3Emr5VOl/aoBwnWcH/EFQvlSAmjV+XtV9GGu5mwdYew5vhQh0IUZx/60x0TzHDu09Bi7HMx10t/namdJw5QIcg==",
 | 
					      "integrity": "sha512-mSoZVJF5YzGVCk+FsDxzDuH7s+SCkzrgKZzf0Z0T2WudhBUPoF6ktoTPC4R0ZoCPCV5xUvuU6ias5NvxcBcMMg==",
 | 
				
			||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "requires": {
 | 
					      "requires": {
 | 
				
			||||||
        "expect": "^29.0.0",
 | 
					        "expect": "^29.0.0",
 | 
				
			||||||
@@ -8905,6 +8921,12 @@
 | 
				
			|||||||
      "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
 | 
				
			||||||
      "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA=="
 | 
					      "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "date-fns-tz": {
 | 
				
			||||||
 | 
					      "version": "2.0.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-2.0.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-OAtcLdB9vxSXTWHdT8b398ARImVwQMyjfYGkKD2zaGpHseG2UPHbHjXELReErZFxWdSLph3c2zOaaTyHfOhERQ==",
 | 
				
			||||||
 | 
					      "requires": {}
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "debug": {
 | 
					    "debug": {
 | 
				
			||||||
      "version": "4.3.4",
 | 
					      "version": "4.3.4",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
 | 
				
			||||||
@@ -10705,6 +10727,12 @@
 | 
				
			|||||||
      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
 | 
				
			||||||
      "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
 | 
					      "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "mockdate": {
 | 
				
			||||||
 | 
					      "version": "3.0.5",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/mockdate/-/mockdate-3.0.5.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-iniQP4rj1FhBdBYS/+eQv7j1tadJ9lJtdzgOpvsOHng/GbcDh2Fhdeq+ZRldrPYdXvCyfFUmFeEwEGXZB5I/AQ==",
 | 
				
			||||||
 | 
					      "dev": true
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "ms": {
 | 
					    "ms": {
 | 
				
			||||||
      "version": "2.1.2",
 | 
					      "version": "2.1.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										10
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								package.json
									
									
									
									
									
								
							@@ -1,6 +1,6 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
	"name": "node-jellyfin-discord-bot",
 | 
						"name": "node-jellyfin-discord-bot",
 | 
				
			||||||
  "version": "1.0.1",
 | 
						"version": "1.1.3",
 | 
				
			||||||
	"description": "A discord bot to sync jellyfin accounts with discord roles",
 | 
						"description": "A discord bot to sync jellyfin accounts with discord roles",
 | 
				
			||||||
	"main": "index.js",
 | 
						"main": "index.js",
 | 
				
			||||||
	"license": "MIT",
 | 
						"license": "MIT",
 | 
				
			||||||
@@ -13,6 +13,7 @@
 | 
				
			|||||||
		"@types/uuid": "^9.0.1",
 | 
							"@types/uuid": "^9.0.1",
 | 
				
			||||||
		"axios": "^1.3.5",
 | 
							"axios": "^1.3.5",
 | 
				
			||||||
		"date-fns": "^2.29.3",
 | 
							"date-fns": "^2.29.3",
 | 
				
			||||||
 | 
							"date-fns-tz": "^2.0.0",
 | 
				
			||||||
		"discord-api-types": "^0.37.38",
 | 
							"discord-api-types": "^0.37.38",
 | 
				
			||||||
		"discord.js": "^14.9.0",
 | 
							"discord.js": "^14.9.0",
 | 
				
			||||||
		"dotenv": "^16.0.3",
 | 
							"dotenv": "^16.0.3",
 | 
				
			||||||
@@ -32,15 +33,18 @@
 | 
				
			|||||||
		"debuggable": "node build/index.js --inspect-brk",
 | 
							"debuggable": "node build/index.js --inspect-brk",
 | 
				
			||||||
		"monitor": "nodemon build/index.js",
 | 
							"monitor": "nodemon build/index.js",
 | 
				
			||||||
		"lint": "eslint . --ext .ts",
 | 
							"lint": "eslint . --ext .ts",
 | 
				
			||||||
    "lint-fix": "eslint . --ext .ts --fix"
 | 
							"lint-fix": "eslint . --ext .ts --fix",
 | 
				
			||||||
 | 
							"test": "jest --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',
 | 
				
			||||||
@@ -13,22 +14,22 @@ export default new Command({
 | 
				
			|||||||
	options: [{
 | 
						options: [{
 | 
				
			||||||
		name: "typ",
 | 
							name: "typ",
 | 
				
			||||||
		type: ApplicationCommandOptionType.String,
 | 
							type: ApplicationCommandOptionType.String,
 | 
				
			||||||
        description:"Was für ein announcement?",
 | 
							description: "Was für ein announcement?",
 | 
				
			||||||
        choices: [{name: "initial", value:"initial"},{name: "votepls", value:"votepls"},{name: "cancel", value:"cancel"}],
 | 
							choices: [{ name: "initial", value: "initial" }, { name: "votepls", value: "votepls" }, { name: "cancel", value: "cancel" }],
 | 
				
			||||||
		required: true
 | 
							required: true
 | 
				
			||||||
	}],
 | 
						}],
 | 
				
			||||||
	run: async (interaction: RunOptions) => {
 | 
						run: async (interaction: RunOptions) => {
 | 
				
			||||||
		const command = interaction.interaction
 | 
							const command = interaction.interaction
 | 
				
			||||||
		const requestId = uuid()
 | 
							const requestId = uuid()
 | 
				
			||||||
        if(!command.guildId) {
 | 
							if (!command.guildId) {
 | 
				
			||||||
            logger.error("COMMAND DOES NOT HAVE A GUILD ID; CANCELLING!!!", {requestId})
 | 
								logger.error("COMMAND DOES NOT HAVE A GUILD ID; CANCELLING!!!", { requestId })
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		const guildId = command.guildId
 | 
							const guildId = command.guildId
 | 
				
			||||||
		const announcementType = command.options.data.find(option => option.name.includes("typ"))
 | 
							const announcementType = command.options.data.find(option => option.name.includes("typ"))
 | 
				
			||||||
		logger.info(`Got command for announcing ${announcementType?.value}!`, { guildId, requestId })
 | 
							logger.info(`Got command for announcing ${announcementType?.value}!`, { guildId, requestId })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if(!announcementType) {
 | 
							if (!announcementType) {
 | 
				
			||||||
			logger.error("Did not get an announcement type!", { guildId, requestId })
 | 
								logger.error("Did not get an announcement type!", { guildId, requestId })
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -40,7 +41,7 @@ export default new Command({
 | 
				
			|||||||
			logger.info(`User ${command.member.displayName} seems to be admin`)
 | 
								logger.info(`User ${command.member.displayName} seems to be admin`)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if((<string>announcementType.value).includes("initial")) {
 | 
							if ((<string>announcementType.value).includes("initial")) {
 | 
				
			||||||
			sendInitialAnnouncement(guildId, requestId)
 | 
								sendInitialAnnouncement(guildId, requestId)
 | 
				
			||||||
			command.followUp("Ist rausgeschickt!")
 | 
								command.followUp("Ist rausgeschickt!")
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
@@ -56,12 +57,12 @@ function isAdmin(member: GuildMember): boolean {
 | 
				
			|||||||
async function sendInitialAnnouncement(guildId: string, requestId: string): Promise<void> {
 | 
					async function sendInitialAnnouncement(guildId: string, requestId: string): Promise<void> {
 | 
				
			||||||
	logger.info("Sending initial announcement")
 | 
						logger.info("Sending initial announcement")
 | 
				
			||||||
	const announcementChannel: Maybe<TextChannel> = client.getAnnouncementChannelForGuild(guildId)
 | 
						const announcementChannel: Maybe<TextChannel> = client.getAnnouncementChannelForGuild(guildId)
 | 
				
			||||||
    if(!announcementChannel) {
 | 
						if (!announcementChannel) {
 | 
				
			||||||
		logger.error("Could not find announcement channel. Aborting", { guildId, requestId })
 | 
							logger.error("Could not find announcement channel. Aborting", { guildId, requestId })
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -96,7 +97,7 @@ export async function manageAnnouncementRoles(guild: Guild, reaction: MessageRea
 | 
				
			|||||||
	const allUsers = (await guild.members.fetch())
 | 
						const allUsers = (await guild.members.fetch())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const usersWhoHaveRole: GuildMember[] = allUsers
 | 
						const usersWhoHaveRole: GuildMember[] = allUsers
 | 
				
			||||||
        .filter(member=> member.roles.cache
 | 
							.filter(member => member.roles.cache
 | 
				
			||||||
			.find(role => role.id === config.bot.announcement_role) !== undefined)
 | 
								.find(role => role.id === config.bot.announcement_role) !== undefined)
 | 
				
			||||||
		.map(member => member)
 | 
							.map(member => member)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -105,15 +106,15 @@ export async function manageAnnouncementRoles(guild: Guild, reaction: MessageRea
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	const usersWhoDontHaveRole: GuildMember[] = allUsers
 | 
						const usersWhoDontHaveRole: GuildMember[] = allUsers
 | 
				
			||||||
		.filter(member => member.roles.cache
 | 
							.filter(member => member.roles.cache
 | 
				
			||||||
            .find(role=> role.id === config.bot.announcement_role) === undefined)
 | 
								.find(role => role.id === config.bot.announcement_role) === undefined)
 | 
				
			||||||
		.map(member => member)
 | 
							.map(member => member)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const usersWhoNeedRole: GuildMember[] = usersWhoDontHaveRole
 | 
						const usersWhoNeedRole: GuildMember[] = usersWhoDontHaveRole
 | 
				
			||||||
		.filter(userWhoNeeds => usersWhoWantRole.map(wanter => wanter.id).includes(userWhoNeeds.id))
 | 
							.filter(userWhoNeeds => usersWhoWantRole.map(wanter => wanter.id).includes(userWhoNeeds.id))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    logger.debug(`Theses users will get the role removed: ${JSON.stringify(usersWhoNeedRoleRevoked)}`, {guildId, requestId})
 | 
						logger.debug(`Theses users will get the role removed: ${JSON.stringify(usersWhoNeedRoleRevoked)}`, { guildId, requestId })
 | 
				
			||||||
    logger.debug(`Theses users will get the role added: ${JSON.stringify(usersWhoNeedRole)}`, {guildId, requestId})
 | 
						logger.debug(`Theses users will get the role added: ${JSON.stringify(usersWhoNeedRole)}`, { guildId, requestId })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	usersWhoNeedRoleRevoked.forEach(user => user.roles.remove(announcementRole))
 | 
						usersWhoNeedRoleRevoked.forEach(user => user.roles.remove(announcementRole))
 | 
				
			||||||
	usersWhoNeedRole.forEach(user => user.roles.add(announcementRole))
 | 
						usersWhoNeedRole.forEach(user => user.roles.add(announcementRole))
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,10 +1,5 @@
 | 
				
			|||||||
import { addDays, differenceInDays, format, isAfter, toDate } from 'date-fns'
 | 
					 | 
				
			||||||
import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildScheduledEventSetStatusArg, GuildScheduledEventStatus, Message, MessageCreateOptions, TextChannel } from 'discord.js'
 | 
					 | 
				
			||||||
import { v4 as uuid } from 'uuid'
 | 
					import { v4 as uuid } from 'uuid'
 | 
				
			||||||
import { client } from '../..'
 | 
					import { client } from '../..'
 | 
				
			||||||
import { config } from '../configuration'
 | 
					 | 
				
			||||||
import { Emotes } from '../events/guildScheduledEventCreate'
 | 
					 | 
				
			||||||
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'
 | 
				
			||||||
@@ -25,160 +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
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function checkForPollsToClose(guild: Guild): Promise<void> {
 | 
					 | 
				
			||||||
    const requestId = uuid()
 | 
					 | 
				
			||||||
    logger.info(`Automatic check for poll closing.`, { guildId: guild.id, requestId })
 | 
					 | 
				
			||||||
    const events = (await guild.scheduledEvents.fetch()).filter(event => event.name.toLocaleLowerCase().includes("voting offen")).map(event => event)
 | 
					 | 
				
			||||||
    if (events.length > 1) {
 | 
					 | 
				
			||||||
        logger.error("Handling more than one Event is not implemented yet. Found more than one poll to close")
 | 
					 | 
				
			||||||
        return
 | 
					 | 
				
			||||||
    } else if (events.length == 0) {
 | 
					 | 
				
			||||||
        logger.info("Could not find any events. Cancelling", { guildId: guild.id, requestId })
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const updatedEvent = events[0]             //add two hours because of different timezones in discord api and Date.now()
 | 
					 | 
				
			||||||
    if (!updatedEvent.scheduledStartTimestamp) {
 | 
					 | 
				
			||||||
        logger.error("Event does not have a scheduled start time. Cancelling", { guildId: guild.id, requestId })
 | 
					 | 
				
			||||||
        return
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const createDate: Date = toDate(updatedEvent.createdTimestamp)
 | 
					 | 
				
			||||||
    const eventDate: Date = toDate(updatedEvent.scheduledStartTimestamp)
 | 
					 | 
				
			||||||
    const difference: number = differenceInDays(createDate, eventDate)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (difference <= 2) {
 | 
					 | 
				
			||||||
        logger.info("Less than two days between event create and event start. Not closing poll.", { guildId: guild.id, requestId })
 | 
					 | 
				
			||||||
        return
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const closePollDate: Date = addDays(eventDate, -2)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (isAfter(Date.now(), closePollDate)) {
 | 
					 | 
				
			||||||
        logger.info("Less than two days until event. Closing poll", { guildId: guild.id, requestId })
 | 
					 | 
				
			||||||
        closePoll(guild, requestId)
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        logger.info(`ScheduledStart: ${closePollDate}. Now: ${toDate(Date.now())}`, { guildId: guild.id, requestId })
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -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())
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 = {
 | 
				
			||||||
@@ -69,6 +71,8 @@ export const config: Config = {
 | 
				
			|||||||
		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"
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										16
									
								
								server/constants.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								server/constants.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					export enum Emotes { "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟" }
 | 
				
			||||||
 | 
					export const NONE_OF_THAT = "❌"
 | 
				
			||||||
 | 
					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 })
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										39
									
								
								server/events/autoCreateVoteByWPEvent.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								server/events/autoCreateVoteByWPEvent.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
				
			|||||||
 | 
					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 STARTDATE; CANCELLING", { guildId: event.guildId, requestId })
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							const sentMessageText = client.voteController.createVoteMessageText(event.id, event.scheduledStartAt, movies, event.guild?.id ?? "", requestId)
 | 
				
			||||||
 | 
							const sentMessage = await client.voteController.sendVoteMessage(sentMessageText, movies.length, announcementChannel)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							sentMessage.pin()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										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,63 +0,0 @@
 | 
				
			|||||||
import { format } from "date-fns";
 | 
					 | 
				
			||||||
import { GuildScheduledEvent, Message, MessageCreateOptions, TextChannel } from "discord.js";
 | 
					 | 
				
			||||||
import { ScheduledTask } from "node-cron";
 | 
					 | 
				
			||||||
import { v4 as uuid } from "uuid";
 | 
					 | 
				
			||||||
import { client, yavinJellyfinHandler } from "../..";
 | 
					 | 
				
			||||||
import { config } from "../configuration";
 | 
					 | 
				
			||||||
import { Maybe } from "../interfaces";
 | 
					 | 
				
			||||||
import { logger } from "../logger";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const name = 'guildScheduledEventCreate'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export enum Emotes { "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟" }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export let task: ScheduledTask | undefined
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function execute(event: GuildScheduledEvent) {
 | 
					 | 
				
			||||||
    const requestId = uuid()
 | 
					 | 
				
			||||||
    logger.debug(`New event created: ${JSON.stringify(event, null, 2)}`, { guildId: event.guildId, requestId })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (event.name.toLowerCase().includes("!nextwp")) {
 | 
					 | 
				
			||||||
        logger.info("Event was a placeholder event to start a new watchparty and voting. Creating vote!", { guildId: event.guildId, requestId })
 | 
					 | 
				
			||||||
        logger.debug("Renaming event", { guildId: event.guildId, requestId })
 | 
					 | 
				
			||||||
        event.edit({ name: "Watchparty - Voting offen" })
 | 
					 | 
				
			||||||
        const movies = await yavinJellyfinHandler.getRandomMovieNames(5, event.guildId, requestId)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        logger.info(`Got ${movies.length} random movies. Creating voting`, { guildId: event.guildId, requestId })
 | 
					 | 
				
			||||||
        logger.debug(`Movies: ${JSON.stringify(movies)}`, { guildId: event.guildId, requestId })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const announcementChannel: Maybe<TextChannel> = client.getAnnouncementChannelForGuild(event.guildId)
 | 
					 | 
				
			||||||
        if(!announcementChannel) {
 | 
					 | 
				
			||||||
            logger.error("Could not find announcement channel. Aborting", { guildId: event.guildId, requestId })
 | 
					 | 
				
			||||||
            return
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        logger.debug(`Found channel ${JSON.stringify(announcementChannel, null, 2)}`, { guildId: event.guildId, requestId })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if(!event.scheduledStartAt) {
 | 
					 | 
				
			||||||
            logger.info("EVENT DOES NOT HAVE STARTDATE; CANCELLING", {guildId: event.guildId, requestId})
 | 
					 | 
				
			||||||
            return
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        const date = format(event.scheduledStartAt, "dd.MM")
 | 
					 | 
				
			||||||
        const time = format(event.scheduledStartAt, "HH:mm")
 | 
					 | 
				
			||||||
        let message = `[Abstimmung]\n<@&${config.bot.announcement_role}> Es gibt eine neue Abstimmung für die nächste Watchparty am ${date} um ${time}}! Stimme hierunter für den nächsten Film ab!\n`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        for (let i = 0; i < movies.length; i++) {
 | 
					 | 
				
			||||||
            message = message.concat(Emotes[i]).concat(": ").concat(movies[i]).concat("\n")
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const options: MessageCreateOptions = {
 | 
					 | 
				
			||||||
            allowedMentions: { parse: ["roles"]},
 | 
					 | 
				
			||||||
            content: message
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const sentMessage: Message<true> = await (await announcementChannel.fetch()).send(options)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        for (let i = 0; i < movies.length; i++) {
 | 
					 | 
				
			||||||
            sentMessage.react(Emotes[i])
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // sentMessage.pin() //todo: uncomment when bot has permission to pin messages. Also update closepoll.ts to only fetch pinned messages
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@@ -1,64 +0,0 @@
 | 
				
			|||||||
import { GuildMember, GuildScheduledEvent, GuildScheduledEventStatus } from "discord.js";
 | 
					 | 
				
			||||||
import { v4 as uuid } from "uuid";
 | 
					 | 
				
			||||||
import { client, jellyfinHandler } from "../..";
 | 
					 | 
				
			||||||
import { getGuildSpecificTriggerRoleId } from "../helper/roleFilter";
 | 
					 | 
				
			||||||
import { logger } from "../logger";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const name = 'guildScheduledEventUpdate'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export async function execute(oldEvent: GuildScheduledEvent, newEvent: GuildScheduledEvent) {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
        const requestId = uuid()
 | 
					 | 
				
			||||||
        logger.debug(`Got scheduledEvent update. New Event: ${JSON.stringify(newEvent, null, 2)}`, { guildId: newEvent.guildId, requestId })
 | 
					 | 
				
			||||||
        if (!newEvent.guild) {
 | 
					 | 
				
			||||||
            logger.error("Event has no guild, aborting.", { guildId: newEvent.guildId, requestId })
 | 
					 | 
				
			||||||
            return
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (newEvent.description?.toLowerCase().includes("!wp") && [GuildScheduledEventStatus.Active, GuildScheduledEventStatus.Completed].includes(newEvent.status)) {
 | 
					 | 
				
			||||||
            const roles = getGuildSpecificTriggerRoleId().map((key, value) => value)
 | 
					 | 
				
			||||||
            const eventMembers = (await newEvent.fetchSubscribers({ withMember: true })).filter(member => !member.member.roles.cache.hasAny(...roles)).map((value) => value.member)
 | 
					 | 
				
			||||||
            const channelMembers = newEvent.channel?.members.filter(member => !member.roles.cache.hasAny(...roles)).map((value) => value)
 | 
					 | 
				
			||||||
            const allMembers = eventMembers.concat(channelMembers ?? [])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            const members: GuildMember[] = []
 | 
					 | 
				
			||||||
            for (const member of allMembers) {
 | 
					 | 
				
			||||||
                if (!members.find(x => x.id == member.id))
 | 
					 | 
				
			||||||
                    members.push(member)
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (newEvent.status === GuildScheduledEventStatus.Active)
 | 
					 | 
				
			||||||
                createJFUsers(members, newEvent.name, requestId)
 | 
					 | 
				
			||||||
            else {
 | 
					 | 
				
			||||||
                const announcementChannel = await client.getAnnouncementChannelForGuild(newEvent.guild.id)
 | 
					 | 
				
			||||||
                if(!announcementChannel) {
 | 
					 | 
				
			||||||
                    logger.error("Could not find announcement channel. Aborting", { guildId: newEvent.guild.id, requestId })
 | 
					 | 
				
			||||||
                    return
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                const announcements = (await announcementChannel.messages.fetch()).filter(message => !message.pinned)
 | 
					 | 
				
			||||||
                announcements.forEach(message => message.delete())
 | 
					 | 
				
			||||||
                members.forEach(member => {
 | 
					 | 
				
			||||||
                    member.createDM().then(channel => channel.send(`Die Watchparty ist vorbei, dein Account wurde wieder gelöscht. Wenn du einen permanenten Account haben möchtest, melde dich bei Samantha oder Marukus.`))
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
                deleteJFUsers(newEvent.guildId, requestId)
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    } catch (error) {
 | 
					 | 
				
			||||||
        logger.error(error)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function createJFUsers(members: GuildMember[], movieName: string, requestId?: string) {
 | 
					 | 
				
			||||||
    logger.info(`Creating users for: \n ${JSON.stringify(members, null, 2)}`)
 | 
					 | 
				
			||||||
    members.forEach(member => {
 | 
					 | 
				
			||||||
        member.createDM().then(channel => channel.send(`Hey! Du hast dich für die Watchparty von ${movieName} angemeldet! Es geht gleich los!`))
 | 
					 | 
				
			||||||
        jellyfinHandler.upsertUser(member, "TEMPORARY", requestId)
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function deleteJFUsers(guildId: string, requestId?: string) {
 | 
					 | 
				
			||||||
    logger.info(`Watchparty ended, deleting tmp users`, { guildId, requestId })
 | 
					 | 
				
			||||||
    jellyfinHandler.purge(guildId, requestId)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										49
									
								
								server/events/handleMessageReactionAdd.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								server/events/handleMessageReactionAdd.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,49 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					import { Message, MessageReaction, User } from "discord.js";
 | 
				
			||||||
 | 
					import { logger, newRequestId, noGuildId } from "../logger";
 | 
				
			||||||
 | 
					import { Emoji, Emotes, 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.debug(`reactedUponMessage payload: ${JSON.stringify(reactedUponMessage)}`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						logger.info(`emoji: ${messageReaction.emoji.toString()}`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!Object.values(Emotes).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)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if (messageReaction.emoji.toString() === Emoji.one) {
 | 
				
			||||||
 | 
								// do something
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						else if (isInitialAnnouncement(reactedUponMessage)) {
 | 
				
			||||||
 | 
							if (messageReaction.emoji.toString() === Emoji.ticket) {
 | 
				
			||||||
 | 
								logger.error(`Got a role emoji. Not implemented yet. ${reactedUponMessage.id}`)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										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 { GuildScheduledEvent } from "discord.js";
 | 
				
			||||||
 | 
					import { logger } from "../logger";
 | 
				
			||||||
 | 
					import de from "date-fns/locale/de";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function createDateStringFromEvent(eventStartDate: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 & { readonly __brand: 'vote' }
 | 
				
			||||||
 | 
					export type AnnouncementMessage = Message & { readonly __brand: 'announcement' }
 | 
				
			||||||
 | 
					export type VoteMessage = Message & { readonly __brand: 'voteend' }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type DiscordMessage = VoteMessage | VoteEndMessage | AnnouncementMessage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function isVoteMessage(msg: Message): msg is VoteMessage {
 | 
				
			||||||
 | 
						return msg.cleanContent.includes('[Abstimmung]')
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export function isInitialAnnouncement(msg: Message): msg is AnnouncementMessage {
 | 
				
			||||||
 | 
						return msg.cleanContent.includes("[initial]")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					export function isVoteEndedMessage(msg: Message): msg is VoteEndMessage {
 | 
				
			||||||
 | 
						return msg.cleanContent.includes("[Abstimmung beendet]")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -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()
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										335
									
								
								server/helper/vote.controller.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										335
									
								
								server/helper/vote.controller.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,335 @@
 | 
				
			|||||||
 | 
					import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildScheduledEventSetStatusArg, GuildScheduledEventStatus, Message, MessageCreateOptions, MessageReaction, TextChannel } from "discord.js"
 | 
				
			||||||
 | 
					import { Emotes, 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 } 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[],
 | 
				
			||||||
 | 
						eventId: string,
 | 
				
			||||||
 | 
						eventDate: Date
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					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(msg: Message): Promise<Message<boolean>> {
 | 
				
			||||||
 | 
							if (msg.pinned) {
 | 
				
			||||||
 | 
								await msg.unpin()
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return await msg.delete()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						public isAboveThreshold(vote: Vote): boolean {
 | 
				
			||||||
 | 
							const aboveThreshold = (vote.count - 1) >= 1
 | 
				
			||||||
 | 
							logger.debug(`${vote.movie} : ${vote.count} -> above: ${aboveThreshold}`)
 | 
				
			||||||
 | 
							return aboveThreshold
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						public async handleReroll(voteMessage: VoteMessage, guildId: string, requestId: string) {
 | 
				
			||||||
 | 
							//get movies that already had votes to give them a second chance
 | 
				
			||||||
 | 
							const voteInfo: VoteMessageInfo = await this.parseVoteInfoFromVoteMessage(voteMessage, requestId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							let movies: string[] = Array()
 | 
				
			||||||
 | 
							if (config.bot.reroll_retains_top_picks) {
 | 
				
			||||||
 | 
								const votedOnMovies = voteInfo.votes.filter(this.isAboveThreshold).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
 | 
				
			||||||
 | 
								movies = 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`)
 | 
				
			||||||
 | 
								movies = await this.yavinJellyfinHandler.getRandomMovieNames(newMovieCount, guildId, requestId)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							// create new message
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							logger.info(`Creating new poll message with new movies: ${movies}`, { requestId, guildId })
 | 
				
			||||||
 | 
							const message = this.createVoteMessageText(voteInfo.eventId, voteInfo.eventDate, movies, 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) {
 | 
				
			||||||
 | 
								logger.error(`Error during removeMessage: ${err}`)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const sentMessage = await this.sendVoteMessage(message, movies.length, announcementChannel)
 | 
				
			||||||
 | 
							sentMessage.pin()
 | 
				
			||||||
 | 
							logger.info(`Sent and pinned new poll message`, { requestId, guildId })
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						private async fetchEventStartDateByEventId(guild: Guild, eventId: string, requestId: string): Promise<Maybe<Date>> {
 | 
				
			||||||
 | 
							const guildEvent: GuildScheduledEvent = await guild.scheduledEvents.fetch(eventId)
 | 
				
			||||||
 | 
							if (!guildEvent) logger.error(`GuildScheduledEvent with id${eventId} could not be found`, { requestId, guildId: guild.id })
 | 
				
			||||||
 | 
							if (guildEvent.scheduledStartAt)
 | 
				
			||||||
 | 
								return guildEvent.scheduledStartAt
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						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`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							let eventStartDate: Maybe<Date> = await this.fetchEventStartDateByEventId(message.guild, parsedIds.eventId, requestId)
 | 
				
			||||||
 | 
							if (!eventStartDate) eventStartDate = this.parseEventDateFromMessage(message.cleanContent, message.guild.id, 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>{ eventId: parsedIds.eventId, eventDate: eventStartDate, 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 createVoteMessageText(eventId: string, eventStartDate: Date, movies: string[], guildId: string, requestId: string): string {
 | 
				
			||||||
 | 
							let message = `[Abstimmung] für https://discord.com/events/${guildId}/${eventId} \n<@&${config.bot.announcement_role}> Es gibt eine neue Abstimmung für die nächste Watchparty ${createDateStringFromEvent(eventStartDate, guildId, requestId)}! Stimme hierunter für den nächsten Film ab!\n`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for (let i = 0; i < movies.length; i++) {
 | 
				
			||||||
 | 
								message = message.concat(Emotes[i]).concat(": ").concat(movies[i]).concat("\n")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							message = message.concat(NONE_OF_THAT).concat(": Wenn dir nichts davon gefällt.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return message
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						public async sendVoteMessage(message: string, movieCount: number, announcementChannel: TextChannel) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const options: MessageCreateOptions = {
 | 
				
			||||||
 | 
								allowedMentions: { parse: ["roles"] },
 | 
				
			||||||
 | 
								content: message,
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const sentMessage: Message<true> = await (await announcementChannel.fetch()).send(options)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							for (let i = 0; i < movieCount; i++) {
 | 
				
			||||||
 | 
								sentMessage.react(Emotes[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]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							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 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")
 | 
				
			||||||
 | 
							await lastMessage.delete()
 | 
				
			||||||
 | 
							const event = await this.getEvent(guild, guild.id, requestId)
 | 
				
			||||||
 | 
							if (event && votes?.length > 0) {
 | 
				
			||||||
 | 
								this.updateEvent(event, votes, guild, guildId, requestId)
 | 
				
			||||||
 | 
								this.sendVoteClosedMessage(event, votes[0].movie, guildId, requestId)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							lastMessage.unpin() //todo: uncomment when bot has permission to pin/unpin
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * gets votes for the movies without the NONE_OF_THAT votes
 | 
				
			||||||
 | 
						*/
 | 
				
			||||||
 | 
						public async 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 = 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 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]
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						public async 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)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						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 kein Datum"
 | 
				
			||||||
 | 
							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 errorMessages = "Could not find announcement channel. Please fix!"
 | 
				
			||||||
 | 
								logger.error(errorMessages, { guildId, requestId })
 | 
				
			||||||
 | 
								throw errorMessages
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return announcementChannel.send(options)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						private extractMovieFromMessageByEmote(lastMessages: Message, emote: string): string {
 | 
				
			||||||
 | 
							const lines = lastMessages.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 })
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -2,7 +2,7 @@ import { GuildMember } from "discord.js";
 | 
				
			|||||||
import { JellyfinConfig, Maybe, PermissionLevel } from "../interfaces";
 | 
					import { JellyfinConfig, Maybe, PermissionLevel } from "../interfaces";
 | 
				
			||||||
import { logger } from "../logger";
 | 
					import { logger } from "../logger";
 | 
				
			||||||
import { CreateUserByNameOperationRequest, DeleteUserRequest, GetItemsRequest, ItemsApi, SystemApi, UpdateUserPasswordOperationRequest, UpdateUserPolicyOperationRequest, UserApi } from "./apis";
 | 
					import { CreateUserByNameOperationRequest, DeleteUserRequest, GetItemsRequest, ItemsApi, SystemApi, UpdateUserPasswordOperationRequest, UpdateUserPolicyOperationRequest, UserApi } from "./apis";
 | 
				
			||||||
import { BaseItemDto, UpdateUserPasswordRequest } from "./models";
 | 
					import { BaseItemDto, UpdateUserPasswordRequest, UpdateUserPolicyRequest } from "./models";
 | 
				
			||||||
import { UserDto } from "./models/UserDto";
 | 
					import { UserDto } from "./models/UserDto";
 | 
				
			||||||
import { Configuration, ConfigurationParameters } from "./runtime";
 | 
					import { Configuration, ConfigurationParameters } from "./runtime";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -52,24 +52,46 @@ export class JellyfinHandler {
 | 
				
			|||||||
		return (Math.random() * 10000 + 10000).toFixed(0)
 | 
							return (Math.random() * 10000 + 10000).toFixed(0)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public async createUserAccountForDiscordUser(discordUser: GuildMember, level: PermissionLevel, guildId?: string, requestId?: string): Promise<UserDto> {
 | 
						public async createUserAccountForDiscordUser(discordUser: GuildMember, level: PermissionLevel, requestId: string, guildId?: string): Promise<UserDto> {
 | 
				
			||||||
		const newUserName = this.generateJFUserName(discordUser, level)
 | 
							const newUserName = this.generateJFUserName(discordUser, level)
 | 
				
			||||||
		logger.info(`New Username for ${discordUser.displayName}: ${newUserName}`, { guildId, requestId })
 | 
							logger.info(`New Username for ${discordUser.displayName}: ${newUserName}`, { guildId, requestId })
 | 
				
			||||||
		const req: CreateUserByNameOperationRequest = {
 | 
							const req: CreateUserByNameOperationRequest = {
 | 
				
			||||||
			createUserByNameRequest: {
 | 
								createUserByNameRequest: {
 | 
				
			||||||
				name: newUserName,
 | 
									name: newUserName,
 | 
				
			||||||
        password: this.generatePasswordForUser(),
 | 
									password: this.generatePasswordForUser()
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		logger.debug(JSON.stringify(req), { requestId, guildId })
 | 
							logger.debug(JSON.stringify(req), { requestId, guildId })
 | 
				
			||||||
		const createResult = await this.userApi.createUserByName(req)
 | 
							const createResult = await this.userApi.createUserByName(req)
 | 
				
			||||||
		if (createResult) {
 | 
							if (createResult) {
 | 
				
			||||||
 | 
								if (createResult.policy) {
 | 
				
			||||||
 | 
									this.setUserPermissions(createResult, requestId, guildId)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
			(await discordUser.createDM()).send(`Ich hab dir mal nen Account angelegt :)\nDein Username ist ${createResult.name}, dein Password ist "${req.createUserByNameRequest.password}"!`)
 | 
								(await discordUser.createDM()).send(`Ich hab dir mal nen Account angelegt :)\nDein Username ist ${createResult.name}, dein Password ist "${req.createUserByNameRequest.password}"!`)
 | 
				
			||||||
			return createResult
 | 
								return createResult
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		else throw new Error('Could not create User in Jellyfin')
 | 
							else throw new Error('Could not create User in Jellyfin')
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public async setUserPermissions(user: UserDto, requestId: string, guildId?: string) {
 | 
				
			||||||
 | 
							if (!user.policy || !user.id) {
 | 
				
			||||||
 | 
								logger.error(`Cannot update user policy. User ${user.name} has no policy to modify`, { guildId, requestId })
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							user.policy.enableVideoPlaybackTranscoding = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const operation: UpdateUserPolicyRequest = {
 | 
				
			||||||
 | 
								...user.policy,
 | 
				
			||||||
 | 
								enableVideoPlaybackTranscoding: false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const request: UpdateUserPolicyOperationRequest = {
 | 
				
			||||||
 | 
								userId: user.id,
 | 
				
			||||||
 | 
								updateUserPolicyRequest: operation
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							this.userApi.updateUserPolicy(request)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public async isUserAlreadyPresent(discordUser: GuildMember, requestId?: string): Promise<boolean> {
 | 
						public async isUserAlreadyPresent(discordUser: GuildMember, requestId?: string): Promise<boolean> {
 | 
				
			||||||
		const jfuser = await this.getUser(discordUser, requestId)
 | 
							const jfuser = await this.getUser(discordUser, requestId)
 | 
				
			||||||
		logger.debug(`Presence for DiscordUser ${discordUser.id}:${jfuser !== undefined}`, { guildId: discordUser.guild.id, requestId })
 | 
							logger.debug(`Presence for DiscordUser ${discordUser.id}:${jfuser !== undefined}`, { guildId: discordUser.guild.id, requestId })
 | 
				
			||||||
@@ -251,7 +273,7 @@ export class JellyfinHandler {
 | 
				
			|||||||
		let movieCount = 0
 | 
							let movieCount = 0
 | 
				
			||||||
		let movieNames: string[]
 | 
							let movieNames: string[]
 | 
				
			||||||
		do {
 | 
							do {
 | 
				
			||||||
      movieNames = (await this.getRandomMovies(count, guildId, requestId)).filter(movie => movie.name && movie.name.length > 0).map(movie => <string> movie.name)
 | 
								movieNames = (await this.getRandomMovies(count, guildId, requestId)).filter(movie => movie.name && movie.name.length > 0).map(movie => <string>movie.name)
 | 
				
			||||||
			movieCount = movieNames.length
 | 
								movieCount = movieNames.length
 | 
				
			||||||
		} while (movieCount < count)
 | 
							} while (movieCount < count)
 | 
				
			||||||
		return movieNames
 | 
							return movieNames
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,7 +29,7 @@ export interface ConfigurationParameters {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class Configuration {
 | 
					export class Configuration {
 | 
				
			||||||
    constructor(private configuration: ConfigurationParameters = {}) {}
 | 
						constructor(private configuration: ConfigurationParameters = {}) { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	set config(configuration: Configuration) {
 | 
						set config(configuration: Configuration) {
 | 
				
			||||||
		this.configuration = configuration;
 | 
							this.configuration = configuration;
 | 
				
			||||||
@@ -393,7 +393,7 @@ export interface ResponseTransformer<T> {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class JSONApiResponse<T> {
 | 
					export class JSONApiResponse<T> {
 | 
				
			||||||
    constructor(public raw: Response, private transformer: ResponseTransformer<T> = (jsonValue: any) => jsonValue) {}
 | 
						constructor(public raw: Response, private transformer: ResponseTransformer<T> = (jsonValue: any) => jsonValue) { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async value(): Promise<T> {
 | 
						async value(): Promise<T> {
 | 
				
			||||||
		return this.transformer(await this.raw.json());
 | 
							return this.transformer(await this.raw.json());
 | 
				
			||||||
@@ -401,7 +401,7 @@ export class JSONApiResponse<T> {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class VoidApiResponse {
 | 
					export class VoidApiResponse {
 | 
				
			||||||
    constructor(public raw: Response) {}
 | 
						constructor(public raw: Response) { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async value(): Promise<void> {
 | 
						async value(): Promise<void> {
 | 
				
			||||||
		return undefined;
 | 
							return undefined;
 | 
				
			||||||
@@ -409,7 +409,7 @@ export class VoidApiResponse {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class BlobApiResponse {
 | 
					export class BlobApiResponse {
 | 
				
			||||||
    constructor(public raw: Response) {}
 | 
						constructor(public raw: Response) { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async value(): Promise<Blob> {
 | 
						async value(): Promise<Blob> {
 | 
				
			||||||
		return await this.raw.blob();
 | 
							return await this.raw.blob();
 | 
				
			||||||
@@ -417,7 +417,7 @@ export class BlobApiResponse {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class TextApiResponse {
 | 
					export class TextApiResponse {
 | 
				
			||||||
    constructor(public raw: Response) {}
 | 
						constructor(public raw: Response) { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async value(): Promise<string> {
 | 
						async value(): Promise<string> {
 | 
				
			||||||
		return await this.raw.text();
 | 
							return await this.raw.text();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,8 @@
 | 
				
			|||||||
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 }) => {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,9 @@ 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 { checkForPollsToClose } from "../commands/closepoll";
 | 
					import { isInitialAnnouncement } from "../helper/messageIdentifiers";
 | 
				
			||||||
 | 
					import VoteController from "../helper/vote.controller";
 | 
				
			||||||
 | 
					import { yavinJellyfinHandler } from "../..";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -16,13 +18,14 @@ 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 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()
 | 
						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
 | 
				
			||||||
@@ -74,6 +77,7 @@ 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()
 | 
									this.startPollCloseBackgroundTasks()
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
@@ -81,6 +85,21 @@ export class ExtendedClient extends Client {
 | 
				
			|||||||
			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())
 | 
				
			||||||
@@ -130,13 +149,13 @@ export class ExtendedClient extends Client {
 | 
				
			|||||||
		for (const guild of guilds.values()) {
 | 
							for (const guild of guilds.values()) {
 | 
				
			||||||
			logger.info("Starting background task for announcement role", { guildId: guild.id })
 | 
								logger.info("Starting background task for announcement role", { guildId: guild.id })
 | 
				
			||||||
			const textChannel: Maybe<TextChannel> = this.getAnnouncementChannelForGuild(guild.id)
 | 
								const textChannel: Maybe<TextChannel> = this.getAnnouncementChannelForGuild(guild.id)
 | 
				
			||||||
      if(!textChannel) {
 | 
								if (!textChannel) {
 | 
				
			||||||
				logger.error("Could not find announcement channel. Aborting", { guildId: guild.id })
 | 
									logger.error("Could not find announcement channel. Aborting", { guildId: guild.id })
 | 
				
			||||||
				return
 | 
									return
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			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 })
 | 
				
			||||||
@@ -174,8 +193,8 @@ export class ExtendedClient extends Client {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private async startPollCloseBackgroundTasks() {
 | 
						private async startPollCloseBackgroundTasks() {
 | 
				
			||||||
    for(const guild of this.guilds.cache) {
 | 
							for (const guild of this.guilds.cache) {
 | 
				
			||||||
      this.pollCloseBackgroundTasks.set(guild[1].id, schedule("0 * * * * *", () => checkForPollsToClose(guild[1])))
 | 
								this.pollCloseBackgroundTasks.set(guild[1].id, schedule("0 * * * * *", () => this.voteController.checkForPollsToClose(guild[1])))
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										96
									
								
								tests/discord/noneofthat.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								tests/discord/noneofthat.test.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,96 @@
 | 
				
			|||||||
 | 
					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"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 mockJellyfinHandler: JellyfinHandler = <JellyfinHandler><unknown>{
 | 
				
			||||||
 | 
							getRandomMovieNames: jest.fn().mockReturnValue(["movie1"])
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						const votes = new VoteController(mockClient, mockJellyfinHandler)
 | 
				
			||||||
 | 
						const mockMessageContent = votes.createVoteMessageText(testEventId, testEventDate, 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<@&1117915290781626398> Wir gucken MovieNew am 01.01. um 01:00`
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						//	test('checkForPollsToClose', async () => {
 | 
				
			||||||
 | 
						//
 | 
				
			||||||
 | 
						//		const testGuild: Guild = <Guild><unknown>{
 | 
				
			||||||
 | 
						//			scheduledEvents: {
 | 
				
			||||||
 | 
						//				fetch: jest.fn().mockImplementation(() => {
 | 
				
			||||||
 | 
						//					return new Promise(resolve => {
 | 
				
			||||||
 | 
						//						resolve([
 | 
				
			||||||
 | 
						//							{ name: "Event Name" },
 | 
				
			||||||
 | 
						//							{ name: "Event: VOTING OFFEN", scheduledStartTimestamp: "" },
 | 
				
			||||||
 | 
						//							{ name: "another voting" },
 | 
				
			||||||
 | 
						//						]
 | 
				
			||||||
 | 
						//						)
 | 
				
			||||||
 | 
						//					})
 | 
				
			||||||
 | 
						//				})
 | 
				
			||||||
 | 
						//			}
 | 
				
			||||||
 | 
						//		}
 | 
				
			||||||
 | 
						//
 | 
				
			||||||
 | 
						//		const result = await votes.checkForPollsToClose(testGuild)
 | 
				
			||||||
 | 
						//
 | 
				
			||||||
 | 
						//
 | 
				
			||||||
 | 
						//
 | 
				
			||||||
 | 
						//
 | 
				
			||||||
 | 
						//	})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						test('getVotesByEmote', async () => {
 | 
				
			||||||
 | 
							const mockMessage: Message = <Message><unknown>{
 | 
				
			||||||
 | 
								cleanContent: mockMessageContent,
 | 
				
			||||||
 | 
								reactions: {
 | 
				
			||||||
 | 
									resolve: jest.fn().mockImplementation((input: any) => {
 | 
				
			||||||
 | 
										return votesList.find(e => e.emote === input)
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							const result = await votes.getVotesByEmote(mockMessage, 'guildId', 'requestId')
 | 
				
			||||||
 | 
							expect(result.length).toEqual(5)
 | 
				
			||||||
 | 
							expect(result).toEqual(votesList.filter(x => x.movie != NONE_OF_THAT))
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
							
								
								
									
										176
									
								
								tests/discord/votes.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								tests/discord/votes.test.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,176 @@
 | 
				
			|||||||
 | 
					import { Emoji, NONE_OF_THAT } from "../../server/constants"
 | 
				
			||||||
 | 
					import VoteController, { Vote, 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 { Message, 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 testMessage = voteController.createVoteMessageText(testEventId, testEventDate, testMovies, testGuildId, "requestId")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const expectedResult: VoteMessageInfo = {
 | 
				
			||||||
 | 
							eventId: testEventId,
 | 
				
			||||||
 | 
							eventDate: testEventDate,
 | 
				
			||||||
 | 
							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 msg: VoteMessage = <VoteMessage><unknown>{
 | 
				
			||||||
 | 
							cleanContent: testMessage,
 | 
				
			||||||
 | 
							guild: {
 | 
				
			||||||
 | 
								id: testGuildId,
 | 
				
			||||||
 | 
								scheduledEvents: {
 | 
				
			||||||
 | 
									fetch: jest.fn().mockImplementation((input: any) => {
 | 
				
			||||||
 | 
										if (input === testEventId)
 | 
				
			||||||
 | 
											return {
 | 
				
			||||||
 | 
												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(msg, 'requestId')
 | 
				
			||||||
 | 
						console.log(JSON.stringify(result))
 | 
				
			||||||
 | 
						expect(Array.isArray(result)).toBe(false)
 | 
				
			||||||
 | 
						expect(result.eventId).toEqual(testEventId)
 | 
				
			||||||
 | 
						expect(result.eventDate).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 testMessage = voteController.createVoteMessageText(testEventId, testEventDate, 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 mockMessageContent = voteController.createVoteMessageText(testEventId, testEventDate, 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 msgReaction: MessageReaction = <MessageReaction><unknown>{
 | 
				
			||||||
 | 
							message: reactedUponMessage
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						mockClient.getAnnouncementChannelForGuild = jest.fn().mockReturnValue({
 | 
				
			||||||
 | 
							messages: {
 | 
				
			||||||
 | 
								fetch: jest.fn().mockReturnValue([
 | 
				
			||||||
 | 
									reactedUponMessage
 | 
				
			||||||
 | 
								])
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const res = voteController.handleNoneOfThatVote(msgReaction, 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)
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
@@ -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