Compare commits
129 Commits
Author | SHA1 | Date | |
---|---|---|---|
fec0bc31f1 | |||
1bfcaa95f9 | |||
fb4ab59dc6 | |||
6d40930dc1 | |||
4e9fe587b0 | |||
03b6a30ffa | |||
7d794a8001 | |||
8df180898e | |||
976175242b | |||
68546b0b50 | |||
1348abbd48 | |||
fce9091114 | |||
081f3c6201 | |||
ca99987a20 | |||
fc64728a78 | |||
20da25f2bf | |||
a455fd8ff7 | |||
119343c916 | |||
296a490e93 | |||
66507cb08f | |||
4600820889 | |||
4a3e8809be | |||
690ba697b6 | |||
71343d6742 | |||
3f6e558d39 | |||
ca259c5f24 | |||
b1c581ca6e | |||
96189c2392 | |||
700353cff4 | |||
f705b97804 | |||
9cdc6e1934 | |||
c73cd20ccf | |||
e66aebc88c | |||
599243990e | |||
eef3a9c358 | |||
1e912b20ef | |||
ce4dc81f7d | |||
b76df79d2a | |||
4e563d57fd | |||
b6a1e06b03 | |||
2ebc7fbdbe | |||
8ff5aeff03 | |||
1101a84501 | |||
91ec2ece7e | |||
5e58765cf4 | |||
a2adef808f | |||
dc66c277b2 | |||
c022cc32d5 | |||
e763e76413 | |||
137d156981 | |||
fdfe7ce404 | |||
146848b759 | |||
e54f03292e | |||
fe45445811 | |||
8f02e11dba | |||
878c81bfa7 | |||
ca19168cf4 | |||
e8893646f0 | |||
e61b3a7b16 | |||
9383cee4a0 | |||
0748097a1f | |||
ffba737e5a | |||
4cd9c771f0 | |||
8c3cf7829b | |||
1a13638ed9 | |||
c351e27fdd | |||
6d3bea169e | |||
3f071c8a4e | |||
98d1ca73b5 | |||
ee742018e9 | |||
8ad651c753 | |||
a4a834ad27 | |||
e8dcfd8340 | |||
d9d1d74ef9 | |||
331ff89060 | |||
f6476c609b | |||
6220268b14 | |||
b6034d4fb7 | |||
ca0a9e3cb8 | |||
b8a32aab40 | |||
e3e755011d | |||
5a6c66cb3e | |||
0d3c62c6ad | |||
5816db48e6 | |||
66f843b399 | |||
d82a7cffd2 | |||
8a06a661fa | |||
4084f675cd | |||
3bd26a9d6c | |||
e7b21fa658 | |||
2d32f9b680 | |||
5503aa8713 | |||
25bb676fda | |||
9f5abb8a90 | |||
0e67252976 | |||
37b798818c | |||
af414d0bad | |||
c32434a7eb | |||
c133570d8c | |||
65cdee36e9 | |||
6b0e84669a | |||
dd72f8e165 | |||
a6f19ccd2b | |||
c39f9c6ee1 | |||
f41194ba71 | |||
fa49dc0f76 | |||
e52e845851 | |||
61544feaba | |||
1966640239 | |||
fa9998e92c | |||
c1a449bafe | |||
d5d82043f0 | |||
51ebf2e939 | |||
f314b2f355 | |||
a4d7c57d10 | |||
2802afa7d5 | |||
3a5ea5d4ff | |||
45d87275bf | |||
31e440434e | |||
3d70b56eb7 | |||
3298c7a244 | |||
5b98c9bf2f | |||
ee363e065c | |||
9af847f234 | |||
a18406e7e4 | |||
b9f65125dc | |||
9da8f47784 | |||
e8c58d5ff8 | |||
8569a3e1e6 |
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
|
@ -14,4 +14,4 @@ jobs:
|
|||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
- name: Build Container
|
- name: Build Container
|
||||||
run: docker build .
|
run: docker build --target compile .
|
||||||
|
@ -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 --all-tags "${{ 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
|
||||||
|
20
index.ts
20
index.ts
@ -5,21 +5,21 @@ 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)
|
||||||
|
|
||||||
export const attachmentImages = attachedImages
|
export const attachmentImages = attachedImages
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
try {
|
try {
|
||||||
const users = await jellyfinHandler.getCurrentUsers("", requestId)
|
const users = await jellyfinHandler.getCurrentUsers("", requestId)
|
||||||
logger.info(`Fetched ${users.map(x => x.name).join(', ')} from JF`, { requestId })
|
logger.info(`Fetched ${users.map(x => x.name).join(', ')} from JF`, { requestId })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error fetching existing users from Jellyfin`, { requestId })
|
logger.error(`Error fetching existing users from Jellyfin`, { requestId })
|
||||||
}
|
}
|
||||||
logger.info(`Starting client`, { requestId })
|
logger.info(`Starting client`, { requestId })
|
||||||
client.start()
|
client.start()
|
||||||
}
|
}
|
||||||
init()
|
init()
|
||||||
|
19
jest.config.js
Normal file
19
jest.config.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
module.exports = {
|
||||||
|
'roots': [
|
||||||
|
'<rootDir>/tests',
|
||||||
|
'<rootDir>/server'
|
||||||
|
],
|
||||||
|
'transform': {
|
||||||
|
'^.+\\.tsx?$': 'ts-jest'
|
||||||
|
},
|
||||||
|
'testRegex': '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',
|
||||||
|
'setupFiles': ["<rootDir>/tests/testenv.js"],
|
||||||
|
'moduleFileExtensions': [
|
||||||
|
'ts',
|
||||||
|
'tsx',
|
||||||
|
'js',
|
||||||
|
'jsx',
|
||||||
|
'json',
|
||||||
|
'node'
|
||||||
|
],
|
||||||
|
};
|
24202
package-lock.json
generated
24202
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
96
package.json
96
package.json
@ -1,48 +1,52 @@
|
|||||||
{
|
{
|
||||||
"name": "node-jellyfin-discord-bot",
|
"name": "node-jellyfin-discord-bot",
|
||||||
"version": "1.0.2",
|
"version": "1.1.4",
|
||||||
"description": "A discord bot to sync jellyfin accounts with discord roles",
|
"description": "A discord bot to sync jellyfin accounts with discord roles",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordjs/rest": "^1.7.0",
|
"@discordjs/rest": "^1.7.0",
|
||||||
"@tsconfig/recommended": "^1.0.2",
|
"@tsconfig/recommended": "^1.0.2",
|
||||||
"@types/node": "^18.15.11",
|
"@types/node": "^18.15.11",
|
||||||
"@types/node-cron": "^3.0.7",
|
"@types/node-cron": "^3.0.7",
|
||||||
"@types/request": "^2.48.8",
|
"@types/request": "^2.48.8",
|
||||||
"@types/uuid": "^9.0.1",
|
"@types/uuid": "^9.0.1",
|
||||||
"axios": "^1.3.5",
|
"axios": "^1.3.5",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
"discord-api-types": "^0.37.38",
|
"date-fns-tz": "^2.0.0",
|
||||||
"discord.js": "^14.9.0",
|
"discord-api-types": "^0.37.38",
|
||||||
"dotenv": "^16.0.3",
|
"discord.js": "^14.9.0",
|
||||||
"jellyfin-apiclient": "^1.10.0",
|
"dotenv": "^16.0.3",
|
||||||
"node-cron": "^3.0.2",
|
"jellyfin-apiclient": "^1.10.0",
|
||||||
"sqlite3": "^5.1.6",
|
"node-cron": "^3.0.2",
|
||||||
"ts-node": "^10.9.1",
|
"sqlite3": "^5.1.6",
|
||||||
"typescript": "^5.0.4",
|
"ts-node": "^10.9.1",
|
||||||
"uuid": "^9.0.0",
|
"typescript": "^5.0.4",
|
||||||
"winston": "^3.8.2"
|
"uuid": "^9.0.0",
|
||||||
},
|
"winston": "^3.8.2"
|
||||||
"scripts": {
|
},
|
||||||
"build": "tsc",
|
"scripts": {
|
||||||
"buildwatch": "tsc --watch",
|
"build": "tsc",
|
||||||
"clean": "rimraf build",
|
"buildwatch": "tsc --watch",
|
||||||
"start": "node build/index.js",
|
"clean": "rimraf build",
|
||||||
"debuggable": "node build/index.js --inspect-brk",
|
"start": "node build/index.js",
|
||||||
"monitor": "nodemon build/index.js",
|
"debuggable": "node build/index.js --inspect-brk",
|
||||||
"lint": "eslint . --ext .ts",
|
"monitor": "nodemon build/index.js",
|
||||||
"lint-fix": "eslint . --ext .ts --fix"
|
"lint": "eslint . --ext .ts",
|
||||||
},
|
"lint-fix": "eslint . --ext .ts --fix",
|
||||||
"devDependencies": {
|
"test": "jest --runInBand",
|
||||||
"@types/jest": "^29.5.0",
|
"test-watch": "jest --watch"
|
||||||
"@typescript-eslint/eslint-plugin": "^5.58.0",
|
},
|
||||||
"@typescript-eslint/parser": "^5.58.0",
|
"devDependencies": {
|
||||||
"eslint": "^8.38.0",
|
"@types/jest": "^29.5.2",
|
||||||
"jest": "^29.5.0",
|
"@typescript-eslint/eslint-plugin": "^5.58.0",
|
||||||
"jest-cli": "^29.5.0",
|
"@typescript-eslint/parser": "^5.58.0",
|
||||||
"nodemon": "^2.0.22",
|
"eslint": "^8.38.0",
|
||||||
"rimraf": "^5.0.0",
|
"jest": "^29.5.0",
|
||||||
"ts-jest": "^29.1.0"
|
"jest-cli": "^29.5.0",
|
||||||
}
|
"mockdate": "^3.0.5",
|
||||||
|
"nodemon": "^2.0.22",
|
||||||
|
"rimraf": "^5.0.0",
|
||||||
|
"ts-jest": "^29.1.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,116 +6,117 @@ 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',
|
||||||
description: 'Neues announcement im announcement Channel an alle senden.',
|
description: 'Neues announcement im announcement Channel an alle senden.',
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isAdmin(command.member)) {
|
if (!isAdmin(command.member)) {
|
||||||
logger.info(`Announcement was requested by ${command.member.displayName} but they are not an admin! Not sending announcement.`, { guildId, requestId })
|
logger.info(`Announcement was requested by ${command.member.displayName} but they are not an admin! Not sending announcement.`, { guildId, requestId })
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
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 {
|
||||||
command.followUp(`${announcementType.value} ist aktuell noch nicht implementiert`)
|
command.followUp(`${announcementType.value} ist aktuell noch nicht implementiert`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function isAdmin(member: GuildMember): boolean {
|
function isAdmin(member: GuildMember): boolean {
|
||||||
return member.roles.cache.find((role) => role.id === config.bot.jf_admin_role) !== undefined
|
return member.roles.cache.find((role) => role.id === config.bot.jf_admin_role) !== undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
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())
|
||||||
|
|
||||||
const body = `[initial] Hey! @everyone! Hier ist der Watchparty Bot vom Hartzarett.
|
const body = `[initial] Hey! @everyone! Hier ist der Watchparty Bot vom Hartzarett.
|
||||||
|
|
||||||
Wir machen in Zukunft regelmäßig Watchparties in denen wir zusammen Filme gucken! Falls du mitmachen möchtest, reagiere einfach auf diesen Post mit 🎫, dann bekommst du automatisch eine Rolle zugewiesen und wirst benachrichtigt sobald es in der Zukunft weitere Watchparties und Filme zum abstimmen gibt.
|
Wir machen in Zukunft regelmäßig Watchparties in denen wir zusammen Filme gucken! Falls du mitmachen möchtest, reagiere einfach auf diesen Post mit 🎫, dann bekommst du automatisch eine Rolle zugewiesen und wirst benachrichtigt sobald es in der Zukunft weitere Watchparties und Filme zum abstimmen gibt.
|
||||||
|
|
||||||
Für eine Erklärung wie das alles funktioniert mach einfach /mitgucken für eine lange Erklärung am Stück oder /guides wenn du auswählen möchtest wozu du Infos bekommst.`
|
Für eine Erklärung wie das alles funktioniert mach einfach /mitgucken für eine lange Erklärung am Stück oder /guides wenn du auswählen möchtest wozu du Infos bekommst.`
|
||||||
|
|
||||||
const options: MessageCreateOptions = {
|
const options: MessageCreateOptions = {
|
||||||
allowedMentions: { parse: ['everyone'] },
|
allowedMentions: { parse: ['everyone'] },
|
||||||
content: body
|
content: body
|
||||||
}
|
}
|
||||||
const message: Message<true> = await announcementChannel.send(options)
|
const message: Message<true> = await announcementChannel.send(options)
|
||||||
await message.react("🎫")
|
await message.react("🎫")
|
||||||
await message.pin()
|
await message.pin()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function manageAnnouncementRoles(guild: Guild, reaction: MessageReaction, requestId: string) {
|
export async function manageAnnouncementRoles(guild: Guild, reaction: MessageReaction, requestId: string) {
|
||||||
const guildId = guild.id
|
const guildId = guild.id
|
||||||
logger.info("Managing roles", { guildId, requestId })
|
logger.info("Managing roles", { guildId, requestId })
|
||||||
|
|
||||||
const announcementRole: Role | undefined = (await guild.roles.fetch()).find(role => role.id === config.bot.announcement_role)
|
const announcementRole: Role | undefined = (await guild.roles.fetch()).find(role => role.id === config.bot.announcement_role)
|
||||||
if (!announcementRole) {
|
if (!announcementRole) {
|
||||||
logger.error(`Could not find announcement role! Aborting! Was looking for role with id: ${config.bot.announcement_role}`, { guildId, requestId })
|
logger.error(`Could not find announcement role! Aborting! Was looking for role with id: ${config.bot.announcement_role}`, { guildId, requestId })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const usersWhoWantRole: User[] = (await reaction.users.fetch()).filter(user => !user.bot).map(user => user)
|
const usersWhoWantRole: User[] = (await reaction.users.fetch()).filter(user => !user.bot).map(user => user)
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
const usersWhoNeedRoleRevoked: GuildMember[] = usersWhoHaveRole
|
const usersWhoNeedRoleRevoked: GuildMember[] = usersWhoHaveRole
|
||||||
.filter(userWhoHas => !usersWhoWantRole.map(wanter => wanter.id).includes(userWhoHas.id))
|
.filter(userWhoHas => !usersWhoWantRole.map(wanter => wanter.id).includes(userWhoHas.id))
|
||||||
|
|
||||||
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,184 +1,25 @@
|
|||||||
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'
|
||||||
|
|
||||||
export default new Command({
|
export default new Command({
|
||||||
name: 'closepoll',
|
name: 'closepoll',
|
||||||
description: 'Aktuelle Umfrage für nächste Watchparty beenden und Gewinner in Event eintragen.',
|
description: 'Aktuelle Umfrage für nächste Watchparty beenden und Gewinner in Event eintragen.',
|
||||||
options: [],
|
options: [],
|
||||||
run: async (interaction: RunOptions) => {
|
run: async (interaction: RunOptions) => {
|
||||||
const command = interaction.interaction
|
const command = interaction.interaction
|
||||||
const requestId = uuid()
|
const requestId = uuid()
|
||||||
if (!command.guild) {
|
if (!command.guild) {
|
||||||
logger.error("No guild found in interaction. Cancelling closing request", { requestId })
|
logger.error("No guild found in interaction. Cancelling closing request", { requestId })
|
||||||
command.followUp("Es gab leider ein Problem. Ich konnte deine Anfrage nicht bearbeiten :(")
|
command.followUp("Es gab leider ein Problem. Ich konnte deine Anfrage nicht bearbeiten :(")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const guildId = command.guildId
|
const guildId = command.guildId
|
||||||
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())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -8,76 +8,76 @@ import { RunOptions } from '../types/commandTypes'
|
|||||||
import { configureServer, explainRole, installation, loginInfo, useSyncgroup } from './mitgucken'
|
import { configureServer, explainRole, installation, loginInfo, useSyncgroup } from './mitgucken'
|
||||||
|
|
||||||
export default new Command({
|
export default new Command({
|
||||||
name: 'guides',
|
name: 'guides',
|
||||||
description: 'Bekomme eine Auswahl von Guides per DM',
|
description: 'Bekomme eine Auswahl von Guides per DM',
|
||||||
options: [],
|
options: [],
|
||||||
run: async (interaction: RunOptions) => {
|
run: async (interaction: RunOptions) => {
|
||||||
const requestId = uuid()
|
const requestId = uuid()
|
||||||
const guildId = interaction.interaction.guild?.id
|
const guildId = interaction.interaction.guild?.id
|
||||||
logger.info(`Starting guides interaction for user ${interaction.interaction.user.id}`, { requestId, guildId })
|
logger.info(`Starting guides interaction for user ${interaction.interaction.user.id}`, { requestId, guildId })
|
||||||
|
|
||||||
const mediaPlayerGuideButton = new ButtonBuilder()
|
const mediaPlayerGuideButton = new ButtonBuilder()
|
||||||
.setCustomId('jfInstallation')
|
.setCustomId('jfInstallation')
|
||||||
.setLabel('Media Player Installation')
|
.setLabel('Media Player Installation')
|
||||||
.setStyle(ButtonStyle.Primary)
|
.setStyle(ButtonStyle.Primary)
|
||||||
|
|
||||||
const accountSetupGuideButton = new ButtonBuilder()
|
const accountSetupGuideButton = new ButtonBuilder()
|
||||||
.setCustomId('configureServer')
|
.setCustomId('configureServer')
|
||||||
.setLabel('Server einstellen')
|
.setLabel('Server einstellen')
|
||||||
.setStyle(ButtonStyle.Primary)
|
.setStyle(ButtonStyle.Primary)
|
||||||
|
|
||||||
const loginGuideButton = new ButtonBuilder()
|
const loginGuideButton = new ButtonBuilder()
|
||||||
.setCustomId('login')
|
.setCustomId('login')
|
||||||
.setLabel('Einloggen')
|
.setLabel('Einloggen')
|
||||||
.setStyle(ButtonStyle.Primary)
|
.setStyle(ButtonStyle.Primary)
|
||||||
|
|
||||||
const useSyncGroupGuideButton = new ButtonBuilder()
|
const useSyncGroupGuideButton = new ButtonBuilder()
|
||||||
.setCustomId('useSyncGroup')
|
.setCustomId('useSyncGroup')
|
||||||
.setLabel('Watch Parties nutzen')
|
.setLabel('Watch Parties nutzen')
|
||||||
.setStyle(ButtonStyle.Primary)
|
.setStyle(ButtonStyle.Primary)
|
||||||
|
|
||||||
const roleExplanationButton = new ButtonBuilder()
|
const roleExplanationButton = new ButtonBuilder()
|
||||||
.setCustomId('explainRoles')
|
.setCustomId('explainRoles')
|
||||||
.setLabel('Wie bekomme ich Zugang')
|
.setLabel('Wie bekomme ich Zugang')
|
||||||
.setStyle(ButtonStyle.Primary)
|
.setStyle(ButtonStyle.Primary)
|
||||||
|
|
||||||
|
|
||||||
const row = new ActionRowBuilder<ButtonBuilder>()
|
const row = new ActionRowBuilder<ButtonBuilder>()
|
||||||
.addComponents(mediaPlayerGuideButton, accountSetupGuideButton, loginGuideButton, useSyncGroupGuideButton, roleExplanationButton)
|
.addComponents(mediaPlayerGuideButton, accountSetupGuideButton, loginGuideButton, useSyncGroupGuideButton, roleExplanationButton)
|
||||||
|
|
||||||
|
|
||||||
//const userDMchannel = await interaction.interaction.user.createDM()
|
//const userDMchannel = await interaction.interaction.user.createDM()
|
||||||
const response = await interaction.interaction.followUp({
|
const response = await interaction.interaction.followUp({
|
||||||
content: `Hier ist eine Auswahl von Guides.`,
|
content: `Hier ist eine Auswahl von Guides.`,
|
||||||
components: [row]
|
components: [row]
|
||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const guideSelection = await response.awaitMessageComponent({ time: 60_000 })
|
const guideSelection = await response.awaitMessageComponent({ time: 60_000 })
|
||||||
|
|
||||||
if (guideSelection.customId === 'jfInstallation') {
|
if (guideSelection.customId === 'jfInstallation') {
|
||||||
const userDMChannel = await guideSelection.user.createDM()
|
const userDMChannel = await guideSelection.user.createDM()
|
||||||
userDMChannel.send({ embeds: installation(), files: [splashScreen] })
|
userDMChannel.send({ embeds: installation(), files: [splashScreen] })
|
||||||
} else if (guideSelection.customId === 'configureServer') {
|
} else if (guideSelection.customId === 'configureServer') {
|
||||||
const userDMChannel = await guideSelection.user.createDM()
|
const userDMChannel = await guideSelection.user.createDM()
|
||||||
userDMChannel.send({ embeds: configureServer(), files: [startScreen, serverConnection] })
|
userDMChannel.send({ embeds: configureServer(), files: [startScreen, serverConnection] })
|
||||||
} else if (guideSelection.customId === 'login') {
|
} else if (guideSelection.customId === 'login') {
|
||||||
const userDMChannel = await guideSelection.user.createDM()
|
const userDMChannel = await guideSelection.user.createDM()
|
||||||
userDMChannel.send({ embeds: loginInfo(), files: [accountChoice, loginScreen] })
|
userDMChannel.send({ embeds: loginInfo(), files: [accountChoice, loginScreen] })
|
||||||
} else if (guideSelection.customId === 'useSyncGroup') {
|
} else if (guideSelection.customId === 'useSyncGroup') {
|
||||||
const userDMChannel = await guideSelection.user.createDM()
|
const userDMChannel = await guideSelection.user.createDM()
|
||||||
userDMChannel.send({ embeds: useSyncgroup(), files: [overview, joingroup, resume, leavegroup] })
|
userDMChannel.send({ embeds: useSyncgroup(), files: [overview, joingroup, resume, leavegroup] })
|
||||||
} else if (guideSelection.customId === 'explainRoles') {
|
} else if (guideSelection.customId === 'explainRoles') {
|
||||||
const userDMChannel = await guideSelection.user.createDM()
|
const userDMChannel = await guideSelection.user.createDM()
|
||||||
userDMChannel.send({ embeds: explainRole() })
|
userDMChannel.send({ embeds: explainRole() })
|
||||||
}
|
}
|
||||||
|
|
||||||
guideSelection.update({ content: "Hab ich dir per DM geschickt :)", components: [] })
|
guideSelection.update({ content: "Hab ich dir per DM geschickt :)", components: [] })
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await interaction.interaction.editReply({ content: 'Das dauert mir zu lange, frag mich nochmal wenn du nen Guide brauchst', components: [] });
|
await interaction.interaction.editReply({ content: 'Das dauert mir zu lange, frag mich nochmal wenn du nen Guide brauchst', components: [] });
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -7,139 +7,139 @@ import { attachmentImages } from '../..'
|
|||||||
|
|
||||||
const color = 0x0099FF
|
const color = 0x0099FF
|
||||||
export default new Command({
|
export default new Command({
|
||||||
name: 'mitgucken',
|
name: 'mitgucken',
|
||||||
description: 'Erfahre wie die Verbindung mit Jellyfin funktioniert und eine WatchTogether Gruppe funktioniert.',
|
description: 'Erfahre wie die Verbindung mit Jellyfin funktioniert und eine WatchTogether Gruppe funktioniert.',
|
||||||
options: [],
|
options: [],
|
||||||
run: async (interaction: RunOptions) => {
|
run: async (interaction: RunOptions) => {
|
||||||
const requestId = uuid()
|
const requestId = uuid()
|
||||||
interaction.interaction.followUp('Ich schicke dir einen Guide per DM!')
|
interaction.interaction.followUp('Ich schicke dir einen Guide per DM!')
|
||||||
const embedList: APIEmbed[] = []
|
const embedList: APIEmbed[] = []
|
||||||
embedList.push(...installation())
|
embedList.push(...installation())
|
||||||
embedList.push(...configureServer())
|
embedList.push(...configureServer())
|
||||||
embedList.push(...explainRole())
|
embedList.push(...explainRole())
|
||||||
embedList.push(...loginInfo())
|
embedList.push(...loginInfo())
|
||||||
embedList.push(...useSyncgroup())
|
embedList.push(...useSyncgroup())
|
||||||
|
|
||||||
//logger.info(`Trying to use ${splashScreen.name}`, { requestId, guildId: interaction.interaction.guild?.id })
|
//logger.info(`Trying to use ${splashScreen.name}`, { requestId, guildId: interaction.interaction.guild?.id })
|
||||||
logger.info(`Sending guide to ${interaction.interaction.user.id}`, { requestId, guildId: interaction.interaction.guild?.id })
|
logger.info(`Sending guide to ${interaction.interaction.user.id}`, { requestId, guildId: interaction.interaction.guild?.id })
|
||||||
const userDMchannel = await interaction.interaction.user.createDM()
|
const userDMchannel = await interaction.interaction.user.createDM()
|
||||||
userDMchannel.send({ embeds: embedList, files: attachmentImages })
|
userDMchannel.send({ embeds: embedList, files: attachmentImages })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
export function explainRole(): APIEmbed[] {
|
export function explainRole(): APIEmbed[] {
|
||||||
return [{
|
return [{
|
||||||
color,
|
color,
|
||||||
title: "Wie du an einen Account kommst",
|
title: "Wie du an einen Account kommst",
|
||||||
description: roleExplanation
|
description: roleExplanation
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
export function installation(): APIEmbed[] {
|
export function installation(): APIEmbed[] {
|
||||||
const embedList: APIEmbed[] = []
|
const embedList: APIEmbed[] = []
|
||||||
// DownloadLink and installation
|
// DownloadLink and installation
|
||||||
embedList.push({
|
embedList.push({
|
||||||
color,
|
color,
|
||||||
title: 'Jellyfin Media Player Installation',
|
title: 'Jellyfin Media Player Installation',
|
||||||
description: 'Du kannst den Jellyfin Media Player von github herunterladen.\n Der Mediaplayer muss genutzt werden, da ein Schauen direkt über das Webinterface den Server zum Schmelzen bringt.\nFühre die Datei aus und installiere den Jellyfin Media Player an den Ort deiner Wahl.',
|
description: 'Du kannst den Jellyfin Media Player von github herunterladen.\n Der Mediaplayer muss genutzt werden, da ein Schauen direkt über das Webinterface den Server zum Schmelzen bringt.\nFühre die Datei aus und installiere den Jellyfin Media Player an den Ort deiner Wahl.',
|
||||||
fields: [
|
fields: [
|
||||||
{ name: "Windows", value: "https://github.com/jellyfin/jellyfin-media-player/releases/download/v1.9.1/JellyfinMediaPlayer-1.9.1-windows-x64.exe" },
|
{ name: "Windows", value: "https://github.com/jellyfin/jellyfin-media-player/releases/download/v1.9.1/JellyfinMediaPlayer-1.9.1-windows-x64.exe" },
|
||||||
{ name: "Mac", value: "https://github.com/jellyfin/jellyfin-media-player/releases/download/v1.9.1/JellyfinMediaPlayer-1.9.1-macos-notarized.dmg" }
|
{ name: "Mac", value: "https://github.com/jellyfin/jellyfin-media-player/releases/download/v1.9.1/JellyfinMediaPlayer-1.9.1-macos-notarized.dmg" }
|
||||||
],
|
],
|
||||||
image: {
|
image: {
|
||||||
url: 'attachment://set_splashscreen.png'
|
url: 'attachment://set_splashscreen.png'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return embedList
|
return embedList
|
||||||
}
|
}
|
||||||
|
|
||||||
export function configureServer(): APIEmbed[] {
|
export function configureServer(): APIEmbed[] {
|
||||||
const embedList: APIEmbed[] = []
|
const embedList: APIEmbed[] = []
|
||||||
// Login
|
// Login
|
||||||
embedList.push({
|
embedList.push({
|
||||||
color,
|
color,
|
||||||
title: "Server Auswahl",
|
title: "Server Auswahl",
|
||||||
description: "Die Jellyfin App kann sich mit mehreren Servern verbinden.\n Hattest du noch nie eine Server Verbindung wähle hier 'Server hinzufügen'.",
|
description: "Die Jellyfin App kann sich mit mehreren Servern verbinden.\n Hattest du noch nie eine Server Verbindung wähle hier 'Server hinzufügen'.",
|
||||||
image: {
|
image: {
|
||||||
url: 'attachment://start_screen.png'
|
url: 'attachment://start_screen.png'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// Server Address
|
// Server Address
|
||||||
embedList.push({
|
embedList.push({
|
||||||
color,
|
color,
|
||||||
title: "Server Verbindung",
|
title: "Server Verbindung",
|
||||||
description: "Stelle eine Verbindung zum Hartzarett Jellyfin Server her",
|
description: "Stelle eine Verbindung zum Hartzarett Jellyfin Server her",
|
||||||
fields: [
|
fields: [
|
||||||
{ name: "Server Adresse", value: "`https://media.hartzarett.ruhr`" }
|
{ name: "Server Adresse", value: "`https://media.hartzarett.ruhr`" }
|
||||||
],
|
],
|
||||||
image: {
|
image: {
|
||||||
url: 'attachment://server_verbindung.png'
|
url: 'attachment://server_verbindung.png'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return embedList
|
return embedList
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loginInfo(): APIEmbed[] {
|
export function loginInfo(): APIEmbed[] {
|
||||||
const embedList: APIEmbed[] = []
|
const embedList: APIEmbed[] = []
|
||||||
// Account choice
|
// Account choice
|
||||||
embedList.push({
|
embedList.push({
|
||||||
color,
|
color,
|
||||||
title: "Account Auswahl",
|
title: "Account Auswahl",
|
||||||
description: "In der Regel sind die Accounts aus Datenschutzgründen versteckt.\nWähle 'Manuelle Anmeldung' aus",
|
description: "In der Regel sind die Accounts aus Datenschutzgründen versteckt.\nWähle 'Manuelle Anmeldung' aus",
|
||||||
image: {
|
image: {
|
||||||
url: 'attachment://auswahl_anmeldung.png'
|
url: 'attachment://auswahl_anmeldung.png'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// password screen
|
// password screen
|
||||||
embedList.push({
|
embedList.push({
|
||||||
color,
|
color,
|
||||||
title: "Login",
|
title: "Login",
|
||||||
description: "Melde dich mit dem Usernamen und Passwort an, welches dir von mir zugeschickt wird. Falls du ein neues brauchst führe einmal `/passwort_reset` aus :)",
|
description: "Melde dich mit dem Usernamen und Passwort an, welches dir von mir zugeschickt wird. Falls du ein neues brauchst führe einmal `/passwort_reset` aus :)",
|
||||||
image: {
|
image: {
|
||||||
url: 'attachment://login_screen.png'
|
url: 'attachment://login_screen.png'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return embedList
|
return embedList
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSyncgroup(): APIEmbed[] {
|
export function useSyncgroup(): APIEmbed[] {
|
||||||
const embedList: APIEmbed[] = []
|
const embedList: APIEmbed[] = []
|
||||||
embedList.push({
|
embedList.push({
|
||||||
color,
|
color,
|
||||||
title: "SyncPlay Menü",
|
title: "SyncPlay Menü",
|
||||||
image: {
|
image: {
|
||||||
url: 'attachment://jellyfin_ubersicht.png'
|
url: 'attachment://jellyfin_ubersicht.png'
|
||||||
},
|
},
|
||||||
description: "Im Hauptbildschirm findest du die 'SyncPlay' Einstellungen oben rechts.",
|
description: "Im Hauptbildschirm findest du die 'SyncPlay' Einstellungen oben rechts.",
|
||||||
})
|
})
|
||||||
// join group
|
// join group
|
||||||
embedList.push({
|
embedList.push({
|
||||||
color,
|
color,
|
||||||
title: "Gruppe beitreten",
|
title: "Gruppe beitreten",
|
||||||
image: {
|
image: {
|
||||||
url: 'attachment://gruppe_beitreten.png'
|
url: 'attachment://gruppe_beitreten.png'
|
||||||
},
|
},
|
||||||
description: "Suche dir aus dem Dropdown die SyncPlay Gruppe aus, die zu deinem Event gehört.",
|
description: "Suche dir aus dem Dropdown die SyncPlay Gruppe aus, die zu deinem Event gehört.",
|
||||||
})
|
})
|
||||||
// leave group
|
// leave group
|
||||||
embedList.push({
|
embedList.push({
|
||||||
color,
|
color,
|
||||||
title: "Gruppe verlassen",
|
title: "Gruppe verlassen",
|
||||||
image: {
|
image: {
|
||||||
url: 'attachment://gruppe_verlassen.png'
|
url: 'attachment://gruppe_verlassen.png'
|
||||||
},
|
},
|
||||||
description: "Wenn du die Watchparty verlassen möchtest, kannst du das ebenfalls über das Menü oben rechts tun.",
|
description: "Wenn du die Watchparty verlassen möchtest, kannst du das ebenfalls über das Menü oben rechts tun.",
|
||||||
})
|
})
|
||||||
//resume playback
|
//resume playback
|
||||||
embedList.push({
|
embedList.push({
|
||||||
color,
|
color,
|
||||||
title: "Wiedergabe fortsetzen",
|
title: "Wiedergabe fortsetzen",
|
||||||
image: {
|
image: {
|
||||||
url: 'attachment://wiedergabe_fortsetzen.png'
|
url: 'attachment://wiedergabe_fortsetzen.png'
|
||||||
},
|
},
|
||||||
description: "Wenn du aus der Watchparty rausgeflogen bist, oder die Wiedergabe verlassen hast, kannst du über das Menü oben rechts auch wieder zurückkehren.",
|
description: "Wenn du aus der Watchparty rausgeflogen bist, oder die Wiedergabe verlassen hast, kannst du über das Menü oben rechts auch wieder zurückkehren.",
|
||||||
})
|
})
|
||||||
return embedList
|
return embedList
|
||||||
}
|
}
|
||||||
|
|
||||||
const roleExplanation = `Mit einer Rolle kann dafür gesorgt werden, dass du einen dauerhaften Account auf dem Mediaserver hast. Wende dich bei Bedarf an Samantha oder Markus.\n
|
const roleExplanation = `Mit einer Rolle kann dafür gesorgt werden, dass du einen dauerhaften Account auf dem Mediaserver hast. Wende dich bei Bedarf an Samantha oder Markus.\n
|
||||||
|
@ -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())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -2,73 +2,77 @@ import dotenv from "dotenv"
|
|||||||
dotenv.config()
|
dotenv.config()
|
||||||
|
|
||||||
interface options {
|
interface options {
|
||||||
[k: string]: boolean | number | string | undefined
|
[k: string]: boolean | number | string | undefined
|
||||||
}
|
}
|
||||||
interface bodyParserOptions {
|
interface bodyParserOptions {
|
||||||
urlEncodedOptions: options,
|
urlEncodedOptions: options,
|
||||||
jsonOptions: options
|
jsonOptions: options
|
||||||
}
|
}
|
||||||
export interface Config {
|
export interface Config {
|
||||||
server: { bodyParser: bodyParserOptions },
|
server: { bodyParser: bodyParserOptions },
|
||||||
bot: {
|
bot: {
|
||||||
debug: boolean
|
debug: boolean
|
||||||
silent: boolean
|
silent: boolean
|
||||||
token: string
|
token: string
|
||||||
guild_id: string
|
guild_id: string
|
||||||
client_id: string
|
client_id: string
|
||||||
jellfin_token: string
|
jellfin_token: string
|
||||||
jellyfin_url: string
|
jellyfin_url: string
|
||||||
port: number
|
port: number
|
||||||
workaround_token: string
|
workaround_token: string
|
||||||
watcher_role: string
|
watcher_role: string
|
||||||
jf_admin_role: string
|
jf_admin_role: string
|
||||||
announcement_role: string
|
announcement_role: string
|
||||||
announcement_channel_id: string
|
announcement_channel_id: string
|
||||||
jf_collection_id: string
|
jf_collection_id: string
|
||||||
jf_user: string
|
jf_user: string
|
||||||
yavin_collection_id: string
|
yavin_collection_id: string
|
||||||
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 = {
|
||||||
server: {
|
server: {
|
||||||
bodyParser: {
|
bodyParser: {
|
||||||
urlEncodedOptions: {
|
urlEncodedOptions: {
|
||||||
inflate: true,
|
inflate: true,
|
||||||
limit: '5mb',
|
limit: '5mb',
|
||||||
type: 'application/x-www-form-urlencoded',
|
type: 'application/x-www-form-urlencoded',
|
||||||
extended: true,
|
extended: true,
|
||||||
parameterLimit: 1000
|
parameterLimit: 1000
|
||||||
},
|
},
|
||||||
jsonOptions: {
|
jsonOptions: {
|
||||||
inflate: true,
|
inflate: true,
|
||||||
limit: '5mb',
|
limit: '5mb',
|
||||||
type: 'application/json',
|
type: 'application/json',
|
||||||
strict: true
|
strict: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
bot: {
|
bot: {
|
||||||
debug: true,
|
debug: true,
|
||||||
silent: false,
|
silent: false,
|
||||||
port: 1234,
|
port: 1234,
|
||||||
token: process.env.BOT_TOKEN ?? "",
|
token: process.env.BOT_TOKEN ?? "",
|
||||||
guild_id: process.env.GUILD_ID ?? "",
|
guild_id: process.env.GUILD_ID ?? "",
|
||||||
client_id: process.env.CLIENT_ID ?? "",
|
client_id: process.env.CLIENT_ID ?? "",
|
||||||
jellfin_token: process.env.JELLYFIN_TOKEN ?? "",
|
jellfin_token: process.env.JELLYFIN_TOKEN ?? "",
|
||||||
jellyfin_url: process.env.JELLYFIN_URL ?? "",
|
jellyfin_url: process.env.JELLYFIN_URL ?? "",
|
||||||
workaround_token: process.env.TOKEN ?? "",
|
workaround_token: process.env.TOKEN ?? "TOKEN",
|
||||||
watcher_role: process.env.WATCHER_ROLE ?? "",
|
watcher_role: process.env.WATCHER_ROLE ?? "WATCHER_ROLE",
|
||||||
jf_admin_role: process.env.ADMIN_ROLE ?? "",
|
jf_admin_role: process.env.ADMIN_ROLE ?? "ADMIN_ROLE",
|
||||||
announcement_role: process.env.WATCHPARTY_ANNOUNCEMENT_ROLE ?? "",
|
announcement_role: process.env.WATCHPARTY_ANNOUNCEMENT_ROLE ?? "ANNOUNCE_ROLE",
|
||||||
announcement_channel_id: process.env.CHANNEL_ID ?? "",
|
announcement_channel_id: process.env.CHANNEL_ID ?? "ANNOUNCE_CHANNEL",
|
||||||
jf_collection_id: process.env.JELLYFIN_COLLECTION_ID ?? "",
|
jf_collection_id: process.env.JELLYFIN_COLLECTION_ID ?? "",
|
||||||
yavin_collection_id: process.env.YAVIN_COLLECTION_ID ?? "",
|
yavin_collection_id: process.env.YAVIN_COLLECTION_ID ?? "",
|
||||||
yavin_jellyfin_url: process.env.YAVIN_JELLYFIN_URL ?? "",
|
yavin_jellyfin_url: process.env.YAVIN_JELLYFIN_URL ?? "",
|
||||||
yavin_jellyfin_token: process.env.YAVIN_TOKEN ?? "",
|
yavin_jellyfin_token: process.env.YAVIN_TOKEN ?? "",
|
||||||
yavin_jellyfin_collection_user: process.env.YAVIN_COLLECTION_USER ?? "",
|
yavin_jellyfin_collection_user: process.env.YAVIN_COLLECTION_USER ?? "",
|
||||||
jf_user: process.env.JELLYFIN_USER ?? ""
|
jf_user: process.env.JELLYFIN_USER ?? "",
|
||||||
}
|
random_movie_count: parseInt(process.env.RANDOM_MOVIE_COUNT ?? "5") ?? 5,
|
||||||
|
reroll_retains_top_picks: process.env.REROLL_RETAIN === "true"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
17
server/constants.ts
Normal file
17
server/constants.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
export enum ValidVoteEmotes { "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟" }
|
||||||
|
export const NONE_OF_THAT = "❌"
|
||||||
|
// WIP
|
||||||
|
export const Emoji = {
|
||||||
|
"one": "\u0031\uFE0F\u20E3",
|
||||||
|
"two": "\u0032\uFE0F\u20E3",
|
||||||
|
"three": "\u0033\uFE0F\u20E3",
|
||||||
|
"four": "\u0034\uFE0F\u20E3",
|
||||||
|
"five": "\u0035\uFE0F\u20E3",
|
||||||
|
"six": "\u0036\uFE0F\u20E3",
|
||||||
|
"seven": "\u0037\uFE0F\u20E3",
|
||||||
|
"eight": "\u0038\uFE0F\u20E3",
|
||||||
|
"nine": "\u0039\uFE0F\u20E3",
|
||||||
|
"ten": "\uD83D\uDD1F",
|
||||||
|
"ticket": "🎫"
|
||||||
|
}
|
52
server/events/announceManualWatchparty.ts
Normal file
52
server/events/announceManualWatchparty.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { GuildScheduledEvent, TextChannel } from "discord.js";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
|
import { client } from "../..";
|
||||||
|
import { config } from "../configuration";
|
||||||
|
import { createDateStringFromEvent } from "../helper/dateHelper";
|
||||||
|
import { Maybe } from "../interfaces";
|
||||||
|
import { logger } from "../logger";
|
||||||
|
|
||||||
|
|
||||||
|
export const name = 'guildScheduledEventCreate'
|
||||||
|
|
||||||
|
export async function execute(event: GuildScheduledEvent) {
|
||||||
|
const guildId = event.guildId
|
||||||
|
const requestId = uuid()
|
||||||
|
try {
|
||||||
|
if (!event.description) {
|
||||||
|
logger.debug("Got GuildScheduledEventCreate event. But has no description. Aborting.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.description.includes("!wp")) {
|
||||||
|
logger.info("Got manual create event of watchparty event!", { guildId, requestId })
|
||||||
|
if (event.description.includes("!private")) {
|
||||||
|
logger.info("Event description contains \"!private\". Won't announce.", { guildId, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel: Maybe<TextChannel> = client.getAnnouncementChannelForGuild(guildId)
|
||||||
|
|
||||||
|
if (!channel) {
|
||||||
|
logger.error("Could not obtain announcement channel. Aborting announcement.", { guildId, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!event.scheduledStartAt) {
|
||||||
|
logger.error('Event has no start date, bailing out')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const message = `[Watchparty] https://discord.com/events/${event.guildId}/${event.id} \nHey <@&${config.bot.announcement_role}>, wir gucken ${event.name} ${createDateStringFromEvent(event.scheduledStartAt, guildId, requestId)}`
|
||||||
|
|
||||||
|
channel.send(message)
|
||||||
|
} else {
|
||||||
|
logger.debug("Got GuildScheduledEventCreate event but no !wp in description. Not creating manual wp announcement.", { guildId, requestId })
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// sendFailureDM(error)
|
||||||
|
logger.error(<string>error, { guildId, requestId })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
44
server/events/autoCreateVoteByWPEvent.ts
Normal file
44
server/events/autoCreateVoteByWPEvent.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { GuildScheduledEvent, TextChannel } from "discord.js";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
|
import { client, yavinJellyfinHandler } from "../..";
|
||||||
|
import { Maybe } from "../interfaces";
|
||||||
|
import { logger } from "../logger";
|
||||||
|
|
||||||
|
export const name = 'guildScheduledEventCreate'
|
||||||
|
|
||||||
|
export async function execute(event: GuildScheduledEvent) {
|
||||||
|
const requestId = uuid()
|
||||||
|
|
||||||
|
if (event.name.toLowerCase().includes("!nextwp")) {
|
||||||
|
logger.info("Event was a placeholder event to start a new watchparty and voting. Creating vote!", { guildId: event.guildId, requestId })
|
||||||
|
logger.debug("Renaming event", { guildId: event.guildId, requestId })
|
||||||
|
event.edit({ name: "Watchparty - Voting offen" })
|
||||||
|
const movies = await yavinJellyfinHandler.getRandomMovieNames(5, event.guildId, requestId)
|
||||||
|
|
||||||
|
logger.info(`Got ${movies.length} random movies. Creating voting`, { guildId: event.guildId, requestId })
|
||||||
|
logger.debug(`Movies: ${JSON.stringify(movies)}`, { guildId: event.guildId, requestId })
|
||||||
|
|
||||||
|
const announcementChannel: Maybe<TextChannel> = client.getAnnouncementChannelForGuild(event.guildId)
|
||||||
|
if (!announcementChannel) {
|
||||||
|
logger.error("Could not find announcement channel. Aborting", { guildId: event.guildId, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.debug(`Found channel ${JSON.stringify(announcementChannel, null, 2)}`, { guildId: event.guildId, requestId })
|
||||||
|
if (!event.scheduledStartAt) {
|
||||||
|
logger.info("Event does not have a start date, cancelling", { guildId: event.guildId, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const sentMessage = await client.voteController.prepareAndSendVoteMessage({
|
||||||
|
movies,
|
||||||
|
startDate: event.scheduledStartAt,
|
||||||
|
event,
|
||||||
|
announcementChannel,
|
||||||
|
pinAfterSending: true
|
||||||
|
},
|
||||||
|
event.guildId,
|
||||||
|
requestId)
|
||||||
|
|
||||||
|
logger.debug(JSON.stringify(sentMessage))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
53
server/events/deleteAnnouncementsWhenWPEnds.ts
Normal file
53
server/events/deleteAnnouncementsWhenWPEnds.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { Collection, GuildScheduledEvent, GuildScheduledEventStatus, Message } from "discord.js";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
|
import { client } from "../..";
|
||||||
|
import { logger } from "../logger";
|
||||||
|
import { isInitialAnnouncement } from "../helper/messageIdentifiers";
|
||||||
|
|
||||||
|
|
||||||
|
export const name = 'guildScheduledEventUpdate'
|
||||||
|
|
||||||
|
export async function execute(oldEvent: GuildScheduledEvent, newEvent: GuildScheduledEvent) {
|
||||||
|
const requestId = uuid()
|
||||||
|
try {
|
||||||
|
if (!newEvent.guild) {
|
||||||
|
logger.error("Event has no guild, aborting.", { guildId: newEvent.guildId, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const guildId = newEvent.guildId
|
||||||
|
|
||||||
|
if (newEvent.description?.toLowerCase().includes("!wp") && newEvent.status === GuildScheduledEventStatus.Completed) {
|
||||||
|
logger.info("A watchparty ended. Cleaning up announcements!", { guildId, requestId })
|
||||||
|
const announcementChannel = client.getAnnouncementChannelForGuild(newEvent.guild.id)
|
||||||
|
if (!announcementChannel) {
|
||||||
|
logger.error("Could not find announcement channel. Aborting", { guildId: newEvent.guild.id, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const events = await newEvent.guild.scheduledEvents.fetch()
|
||||||
|
|
||||||
|
const wpAnnouncements = (await announcementChannel.messages.fetch()).filter(message => !isInitialAnnouncement(message))
|
||||||
|
const announcementsWithoutEvent = filterAnnouncementsByPendingWPs(wpAnnouncements, events)
|
||||||
|
logger.info(`Deleting ${announcementsWithoutEvent.length} announcements.`, { guildId, requestId })
|
||||||
|
announcementsWithoutEvent.forEach(message => message.delete())
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(<string>error, { guildId: newEvent.guildId, requestId })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterAnnouncementsByPendingWPs(messages: Collection<string, Message<true>>, events: Collection<string, GuildScheduledEvent<GuildScheduledEventStatus>>): Message<true>[] {
|
||||||
|
const filteredMessages: Message<true>[] = []
|
||||||
|
for (const message of messages.values()) {
|
||||||
|
let foundEventForMessage = false
|
||||||
|
for (const event of events.values()) {
|
||||||
|
if (message.cleanContent.includes(event.id)) { //announcement always has eventid because of eventbox
|
||||||
|
foundEventForMessage = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!foundEventForMessage) {
|
||||||
|
filteredMessages.push(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filteredMessages
|
||||||
|
}
|
@ -1,30 +0,0 @@
|
|||||||
import { Collection, GuildMember } from "discord.js"
|
|
||||||
import { filterRolesFromMemberUpdate, getGuildSpecificTriggerRoleId } from "../helper/roleFilter"
|
|
||||||
import { ChangedRoles, PermissionLevel } from "../interfaces"
|
|
||||||
import { jellyfinHandler } from "../.."
|
|
||||||
import { v4 as uuid } from "uuid"
|
|
||||||
|
|
||||||
export const name = 'guildMemberUpdate'
|
|
||||||
export async function execute(oldMember: GuildMember, newMember: GuildMember) {
|
|
||||||
try {
|
|
||||||
const requestId = uuid()
|
|
||||||
const changedRoles: ChangedRoles = filterRolesFromMemberUpdate(oldMember, newMember)
|
|
||||||
const triggerRoleIds: Collection<string, PermissionLevel> = getGuildSpecificTriggerRoleId()
|
|
||||||
|
|
||||||
triggerRoleIds.forEach((level, key) => {
|
|
||||||
const addedRoleMatches = changedRoles.addedRoles.find(aRole => aRole.id === key)
|
|
||||||
if (addedRoleMatches) {
|
|
||||||
jellyfinHandler.upsertUser(newMember, level, requestId)
|
|
||||||
}
|
|
||||||
const removedRoleMatches = changedRoles.removedRoles.find(rRole => rRole.id === key)
|
|
||||||
if (removedRoleMatches) {
|
|
||||||
jellyfinHandler.removeUser(newMember, level, requestId)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,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)
|
|
||||||
}
|
|
46
server/events/handleMessageReactionAdd.ts
Normal file
46
server/events/handleMessageReactionAdd.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
|
||||||
|
import { Message, MessageReaction, User } from "discord.js";
|
||||||
|
import { logger, newRequestId, noGuildId } from "../logger";
|
||||||
|
import { Emoji, ValidVoteEmotes, NONE_OF_THAT } from "../constants";
|
||||||
|
import { client } from "../..";
|
||||||
|
import { isInitialAnnouncement, isVoteMessage } from "../helper/messageIdentifiers";
|
||||||
|
|
||||||
|
|
||||||
|
export const name = 'messageReactionAdd'
|
||||||
|
|
||||||
|
export async function execute(messageReaction: MessageReaction, user: User) {
|
||||||
|
if (user.id == client.user?.id) {
|
||||||
|
logger.info('Skipping bot reaction')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const requestId = newRequestId()
|
||||||
|
const guildId = messageReaction.message.inGuild() ? messageReaction.message.guildId : noGuildId
|
||||||
|
const reactedUponMessage: Message = messageReaction.message.partial ? await messageReaction.message.fetch() : messageReaction.message
|
||||||
|
if (!messageReaction.message.guild) {
|
||||||
|
logger.warn(`Received messageReactionAdd on non-guild message.`, { requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Got reaction on message`, { requestId, guildId })
|
||||||
|
//logger.debug(`reactedUponMessage payload: ${JSON.stringify(reactedUponMessage)}`)
|
||||||
|
|
||||||
|
logger.info(`emoji: ${messageReaction.emoji.toString()}`)
|
||||||
|
|
||||||
|
if (!Object.values(ValidVoteEmotes).includes(messageReaction.emoji.toString()) && messageReaction.emoji.toString() !== NONE_OF_THAT) {
|
||||||
|
logger.info(`${messageReaction.emoji.toString()} currently not handled`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.info(`Found a match for ${messageReaction.emoji.toString()}`)
|
||||||
|
if (isVoteMessage(reactedUponMessage)) {
|
||||||
|
if (messageReaction.emoji.toString() === NONE_OF_THAT) {
|
||||||
|
logger.info(`Reaction is NONE_OF_THAT on a vote message. Handling`, { requestId, guildId })
|
||||||
|
return client.voteController.handleNoneOfThatVote(messageReaction, reactedUponMessage, requestId, guildId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (isInitialAnnouncement(reactedUponMessage)) {
|
||||||
|
if (messageReaction.emoji.toString() === Emoji.ticket) {
|
||||||
|
logger.error(`Got a role emoji. 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)
|
||||||
|
}
|
@ -5,17 +5,17 @@ import { logger } from "../logger"
|
|||||||
|
|
||||||
export const name = 'interactionCreate'
|
export const name = 'interactionCreate'
|
||||||
export async function execute(interaction: ExtendedInteraction) {
|
export async function execute(interaction: ExtendedInteraction) {
|
||||||
//console.dir(interaction, { depth: null })
|
//console.dir(interaction, { depth: null })
|
||||||
if (interaction.isCommand()) {
|
if (interaction.isCommand()) {
|
||||||
logger.info(`Interaction is a command.`, { guildId: interaction.guild?.id })
|
logger.info(`Interaction is a command.`, { guildId: interaction.guild?.id })
|
||||||
await interaction.deferReply({ ephemeral: true })
|
await interaction.deferReply({ ephemeral: true })
|
||||||
const command = client.commands.get(interaction.commandName)
|
const command = client.commands.get(interaction.commandName)
|
||||||
if (!command)
|
if (!command)
|
||||||
return interaction.followUp('Invalid command')
|
return interaction.followUp('Invalid command')
|
||||||
command.run({
|
command.run({
|
||||||
args: interaction.options as CommandInteractionOptionResolver,
|
args: interaction.options as CommandInteractionOptionResolver,
|
||||||
client,
|
client,
|
||||||
interaction
|
interaction
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Message } from "discord.js"
|
import { Message } from "discord.js"
|
||||||
|
import { logger } from "../logger"
|
||||||
|
|
||||||
export const name = 'messageCreate'
|
export const name = 'messageCreate'
|
||||||
export function execute(message: Message) {
|
export function execute(message: Message) {
|
||||||
console.log(`${JSON.stringify(message)} has been created`)
|
logger.info(`${JSON.stringify(message)} has been created`)
|
||||||
}
|
}
|
||||||
|
@ -1,59 +0,0 @@
|
|||||||
import { VoiceState } from "discord.js";
|
|
||||||
import { v4 as uuid } from "uuid";
|
|
||||||
import { jellyfinHandler } from "../..";
|
|
||||||
import { UserUpsertResult } from "../jellyfin/handler";
|
|
||||||
import { logger } from "../logger";
|
|
||||||
|
|
||||||
|
|
||||||
export const name = 'voiceStateUpdate'
|
|
||||||
|
|
||||||
export async function execute(oldState: VoiceState, newState: VoiceState) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
logger.info(JSON.stringify(newState, null, 2))
|
|
||||||
//ignore events like mute/unmute
|
|
||||||
if(newState.channel?.id === oldState.channel?.id) {
|
|
||||||
logger.info("Not handling VoiceState event because channelid of old and new was the same (i.e. mute/unmute event)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const scheduledEvents = (await newState.guild.scheduledEvents.fetch())
|
|
||||||
.filter((key) => key.description?.toLowerCase().includes("!wp") && key.isActive())
|
|
||||||
.map((key) => key)
|
|
||||||
|
|
||||||
const scheduledEventUsers = (await Promise.all(scheduledEvents.map(event => event.fetchSubscribers({withMember: true}))))
|
|
||||||
|
|
||||||
//Dont handle users, that are already subscribed to the event. We only want to handle unsubscribed users here
|
|
||||||
let userFound = false;
|
|
||||||
scheduledEventUsers.forEach(collection => {
|
|
||||||
collection.each(key => {
|
|
||||||
logger.info(JSON.stringify(key, null, 2))
|
|
||||||
if(key.member.user.id === newState.member?.user.id)
|
|
||||||
userFound = true;
|
|
||||||
})
|
|
||||||
})
|
|
||||||
if(userFound) {
|
|
||||||
logger.info(`Not handling VoiceState event because user was already subscribed and got an account from there. User: ${JSON.stringify(newState.member, null, 2)}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (scheduledEvents.find(event => event.channelId === newState.channelId)) {
|
|
||||||
if(newState.member){
|
|
||||||
logger.info("YO! Da ist jemand dem Channel mit dem Event beigetreten, ich kümmer mich mal um nen Account!")
|
|
||||||
const result = await jellyfinHandler.upsertUser(newState.member, "TEMPORARY", uuid())
|
|
||||||
if (result === UserUpsertResult.created) {
|
|
||||||
newState.member.createDM().then(channel => channel.send(`Hey! Du bist unserer Watchparty beigetreten, ich hab dir gerade die Zugangsdaten für den Mediaserver geschickt!`))
|
|
||||||
} else {
|
|
||||||
newState.member.createDM().then(channel => channel.send(`Hey! Du bist unserer Watchparty beigetreten aber du hast bereits einen Account. Falls du ein neues Passwort brauchst nutze /reset_passwort!`))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.error("WTF? Expected Member?? When doing things")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.info("VoiceState channelId was not the id of any channel with events")
|
|
||||||
}
|
|
||||||
}catch(error){
|
|
||||||
logger.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
23
server/helper/dateHelper.ts
Normal file
23
server/helper/dateHelper.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { format, isToday } from "date-fns";
|
||||||
|
import { utcToZonedTime } from "date-fns-tz"
|
||||||
|
import { logger } from "../logger";
|
||||||
|
import de from "date-fns/locale/de";
|
||||||
|
import { Maybe } from "../interfaces";
|
||||||
|
|
||||||
|
export function createDateStringFromEvent(eventStartDate: Maybe<Date>, requestId: string, guildId?: string): string {
|
||||||
|
if (!eventStartDate) {
|
||||||
|
logger.error("Event has no start. Cannot create dateString.", { guildId, requestId })
|
||||||
|
return `"habe keinen Startzeitpunkt ermitteln können"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeZone = 'Europe/Berlin'
|
||||||
|
const zonedDateTime = utcToZonedTime(eventStartDate, timeZone)
|
||||||
|
const time = format(zonedDateTime, "HH:mm", { locale: de })
|
||||||
|
|
||||||
|
if (isToday(zonedDateTime)) {
|
||||||
|
return `heute um ${time}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = format(zonedDateTime, "eeee dd.MM.", { locale: de })
|
||||||
|
return `am ${date} um ${time}`
|
||||||
|
}
|
20
server/helper/messageIdentifiers.ts
Normal file
20
server/helper/messageIdentifiers.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Message } from "discord.js";
|
||||||
|
|
||||||
|
|
||||||
|
// branded types to differentiate objects of identical Type but different contents
|
||||||
|
export type VoteEndMessage = Message<true> & { readonly __brand: 'voteend' }
|
||||||
|
export type AnnouncementMessage = Message<true> & { readonly __brand: 'announcement' }
|
||||||
|
export type VoteMessage = Message<true> & { readonly __brand: 'vote' }
|
||||||
|
|
||||||
|
export type KnownDiscordMessage = VoteMessage | VoteEndMessage | AnnouncementMessage
|
||||||
|
|
||||||
|
export function isVoteMessage(message: Message): message is VoteMessage {
|
||||||
|
return message.cleanContent.includes('[Abstimmung]')
|
||||||
|
}
|
||||||
|
export function isInitialAnnouncement(message: Message): message is AnnouncementMessage {
|
||||||
|
return message.cleanContent.includes("[initial]")
|
||||||
|
}
|
||||||
|
export function isVoteEndedMessage(message: Message): message is VoteEndMessage {
|
||||||
|
return message.cleanContent.includes("[Abstimmung beendet]")
|
||||||
|
}
|
||||||
|
|
@ -1,24 +1,31 @@
|
|||||||
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"
|
||||||
|
|
||||||
export function filterRolesFromMemberUpdate(oldMember: GuildMember, newMember: GuildMember): ChangedRoles {
|
export function filterRolesFromMemberUpdate(oldMember: GuildMember, newMember: GuildMember): ChangedRoles {
|
||||||
|
|
||||||
const oldRoles = oldMember.roles.cache
|
const oldRoles = oldMember.roles.cache
|
||||||
const newRoles = newMember.roles.cache
|
const newRoles = newMember.roles.cache
|
||||||
|
|
||||||
const removedRoles = oldRoles.filter(x => newRoles.find(y => y.id === x.id) == undefined)
|
const removedRoles = oldRoles.filter(x => newRoles.find(y => y.id === x.id) == undefined)
|
||||||
const addedRoles = newRoles.filter(x => oldRoles.find(y => y.id === x.id) == undefined)
|
const addedRoles = newRoles.filter(x => oldRoles.find(y => y.id === x.id) == undefined)
|
||||||
logger.info(`Member ${oldMember.id} RemovedRoles: ${removedRoles.map(x => x.name)}`, { guildId: oldMember.guild.id })
|
logger.info(`Member ${oldMember.id} RemovedRoles: ${removedRoles.map(x => x.name)}`, { guildId: oldMember.guild.id })
|
||||||
logger.info(`Member ${oldMember.id} AddedRoles: ${addedRoles.map(x => x.name)}`, { guildId: oldMember.guild.id })
|
logger.info(`Member ${oldMember.id} AddedRoles: ${addedRoles.map(x => x.name)}`, { guildId: oldMember.guild.id })
|
||||||
|
|
||||||
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")
|
||||||
outVal.set(config.bot.jf_admin_role, "ADMIN")
|
outVal.set(config.bot.jf_admin_role, "ADMIN")
|
||||||
return outVal
|
return outVal
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
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()
|
||||||
await creator.dmChannel?.send(creatorMessage)
|
await creator.dmChannel?.send(creatorMessage)
|
||||||
}
|
}
|
||||||
|
361
server/helper/vote.controller.ts
Normal file
361
server/helper/vote.controller.ts
Normal file
@ -0,0 +1,361 @@
|
|||||||
|
import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildScheduledEventSetStatusArg, GuildScheduledEventStatus, Message, MessageCreateOptions, MessageReaction, TextChannel } from "discord.js"
|
||||||
|
import { ValidVoteEmotes, NONE_OF_THAT } from "../constants"
|
||||||
|
import { logger, newRequestId } from "../logger"
|
||||||
|
import { getMembersWithRoleFromGuild } from "./roleFilter"
|
||||||
|
import { config } from "../configuration"
|
||||||
|
import { VoteMessage, isVoteEndedMessage, isVoteMessage } from "./messageIdentifiers"
|
||||||
|
import { createDateStringFromEvent } from "./dateHelper"
|
||||||
|
import { Maybe, voteMessageInputInformation as prepareVoteMessageInput } from "../interfaces"
|
||||||
|
import format from "date-fns/format"
|
||||||
|
import toDate from "date-fns/toDate"
|
||||||
|
import differenceInDays from "date-fns/differenceInDays"
|
||||||
|
import addDays from "date-fns/addDays"
|
||||||
|
import isAfter from "date-fns/isAfter"
|
||||||
|
import { ExtendedClient } from "../structures/client"
|
||||||
|
import { JellyfinHandler } from "../jellyfin/handler"
|
||||||
|
|
||||||
|
export type Vote = {
|
||||||
|
emote: string, //todo habs nicht hinbekommen hier Emotes zu nutzen
|
||||||
|
count: number,
|
||||||
|
movie: string
|
||||||
|
}
|
||||||
|
export type VoteMessageInfo = {
|
||||||
|
votes: Vote[],
|
||||||
|
event: GuildScheduledEvent,
|
||||||
|
}
|
||||||
|
export default class VoteController {
|
||||||
|
private client: ExtendedClient
|
||||||
|
private yavinJellyfinHandler: JellyfinHandler
|
||||||
|
|
||||||
|
public constructor(_client: ExtendedClient, _yavin: JellyfinHandler) {
|
||||||
|
this.client = _client
|
||||||
|
this.yavinJellyfinHandler = _yavin
|
||||||
|
}
|
||||||
|
|
||||||
|
public async handleNoneOfThatVote(messageReaction: MessageReaction, reactedUponMessage: VoteMessage, requestId: string, guildId: string) {
|
||||||
|
if (!messageReaction.message.guild) return 'No guild'
|
||||||
|
const guild = messageReaction.message.guild
|
||||||
|
logger.debug(`${reactedUponMessage.id} is vote message`, { requestId, guildId })
|
||||||
|
|
||||||
|
const watcherRoleMember = await getMembersWithRoleFromGuild(config.bot.announcement_role, messageReaction.message.guild)
|
||||||
|
logger.info("ROLE MEMBERS " + JSON.stringify(watcherRoleMember), { requestId, guildId })
|
||||||
|
|
||||||
|
const watcherRoleMemberCount = watcherRoleMember.size
|
||||||
|
logger.info(`MEMBER COUNT: ${watcherRoleMemberCount}`, { requestId, guildId })
|
||||||
|
|
||||||
|
const noneOfThatReactions = reactedUponMessage.reactions.cache.get(NONE_OF_THAT)?.users.cache.filter(x => x.id !== this.client.user?.id).size ?? 0
|
||||||
|
|
||||||
|
const memberThreshold = (watcherRoleMemberCount / 2)
|
||||||
|
logger.info(`Reroll ${noneOfThatReactions} > ${memberThreshold} ?`, { requestId, guildId })
|
||||||
|
if (noneOfThatReactions > memberThreshold)
|
||||||
|
logger.info(`No reroll`, { requestId, guildId })
|
||||||
|
else {
|
||||||
|
logger.info('Starting poll reroll', { requestId, guildId })
|
||||||
|
await this.handleReroll(reactedUponMessage, guild.id, requestId)
|
||||||
|
logger.info(`Finished handling NONE_OF_THAT vote`, { requestId, guildId })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async removeMessage(message: Message): Promise<Message<boolean>> {
|
||||||
|
if (message.pinned) {
|
||||||
|
await message.unpin()
|
||||||
|
}
|
||||||
|
return await message.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns true if a Vote object contains at least one vote
|
||||||
|
* @param {Vote} vote
|
||||||
|
*/
|
||||||
|
private hasAtLeastOneVote(vote: Vote): boolean {
|
||||||
|
// subtracting the bots initial vote
|
||||||
|
const overOneVote = (vote.count - 1) >= 1
|
||||||
|
logger.debug(`${vote.movie} : ${vote.count} -> above: ${overOneVote}`)
|
||||||
|
return overOneVote
|
||||||
|
}
|
||||||
|
|
||||||
|
public async generateRerollMovieList(voteInfo: VoteMessageInfo, guildId: string, requestId: string) {
|
||||||
|
if (config.bot.reroll_retains_top_picks) {
|
||||||
|
const votedOnMovies = voteInfo.votes.filter(this.hasAtLeastOneVote).filter(x => x.emote !== NONE_OF_THAT)
|
||||||
|
logger.info(`Found ${votedOnMovies.length} with votes`, { requestId, guildId })
|
||||||
|
const newMovieCount: number = config.bot.random_movie_count - votedOnMovies.length
|
||||||
|
logger.info(`Fetching ${newMovieCount} from jellyfin`)
|
||||||
|
const newMovies: string[] = await this.yavinJellyfinHandler.getRandomMovieNames(newMovieCount, guildId, requestId)
|
||||||
|
// merge
|
||||||
|
return newMovies.concat(votedOnMovies.map(x => x.movie))
|
||||||
|
} else {
|
||||||
|
// get movies from jellyfin to fill the remaining slots
|
||||||
|
const newMovieCount: number = config.bot.random_movie_count
|
||||||
|
logger.info(`Fetching ${newMovieCount} from jellyfin`)
|
||||||
|
return await this.yavinJellyfinHandler.getRandomMovieNames(newMovieCount, guildId, requestId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async handleReroll(voteMessage: VoteMessage, guildId: string, requestId: string) {
|
||||||
|
// get the movies currently being voted on, their votes, the eventId and its date
|
||||||
|
const voteInfo: VoteMessageInfo = await this.parseVoteInfoFromVoteMessage(voteMessage, requestId)
|
||||||
|
if (!voteInfo.event.scheduledStartAt) {
|
||||||
|
logger.info("Event does not have a start date, cancelling", { guildId: voteInfo.event.guildId, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let movies: string[] = await this.generateRerollMovieList(voteInfo, guildId, requestId)
|
||||||
|
|
||||||
|
const announcementChannel = this.client.getAnnouncementChannelForGuild(guildId)
|
||||||
|
if (!announcementChannel) {
|
||||||
|
logger.error(`No announcementChannel found for ${guildId}, can't post poll`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
logger.info(`Trying to remove old vote Message`, { requestId, guildId })
|
||||||
|
this.removeMessage(voteMessage)
|
||||||
|
} catch (err) {
|
||||||
|
// TODO: integrate failure DM to media Admin to inform about inability to delete old message
|
||||||
|
logger.error(`Error during removeMessage: ${err}`)
|
||||||
|
}
|
||||||
|
const sentMessage = this.prepareAndSendVoteMessage({
|
||||||
|
event: voteInfo.event,
|
||||||
|
movies,
|
||||||
|
announcementChannel,
|
||||||
|
startDate: voteInfo.event.scheduledStartAt,
|
||||||
|
pinAfterSending: true
|
||||||
|
},
|
||||||
|
guildId,
|
||||||
|
requestId)
|
||||||
|
logger.debug(`Sent reroll message: ${JSON.stringify(sentMessage)}`, { requestId, guildId })
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchEventByEventId(guild: Guild, eventId: string, requestId: string): Promise<Maybe<GuildScheduledEvent>> {
|
||||||
|
const guildEvent: GuildScheduledEvent = await guild.scheduledEvents.fetch(eventId)
|
||||||
|
if (!guildEvent) logger.error(`GuildScheduledEvent with id${eventId} could not be found`, { requestId, guildId: guild.id })
|
||||||
|
return guildEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
public async parseVoteInfoFromVoteMessage(message: VoteMessage, requestId: string): Promise<VoteMessageInfo> {
|
||||||
|
const lines = message.cleanContent.split('\n')
|
||||||
|
let parsedIds = this.parseGuildIdAndEventIdFromWholeMessage(message.cleanContent)
|
||||||
|
|
||||||
|
if (!message.guild)
|
||||||
|
throw new Error(`Message ${message.id} not a guild message`)
|
||||||
|
|
||||||
|
const event: Maybe<GuildScheduledEvent> = await this.fetchEventByEventId(message.guild, parsedIds.eventId, requestId)
|
||||||
|
|
||||||
|
let votes: Vote[] = []
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.slice(0, 5).includes(':')) {
|
||||||
|
const splitLine = line.split(":")
|
||||||
|
const [emoji, movie] = splitLine
|
||||||
|
const fetchedVoteFromMessage = message.reactions.cache.get(emoji)
|
||||||
|
if (fetchedVoteFromMessage) {
|
||||||
|
if (emoji === NONE_OF_THAT) {
|
||||||
|
votes.push({ movie: NONE_OF_THAT, emote: NONE_OF_THAT, count: fetchedVoteFromMessage.count })
|
||||||
|
} else
|
||||||
|
votes.push({ movie: movie.trim(), emote: emoji, count: fetchedVoteFromMessage.count })
|
||||||
|
} else {
|
||||||
|
logger.error(`No vote reaction found for movie, assuming 0`, requestId)
|
||||||
|
votes.push({ movie, emote: emoji, count: 0 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return <VoteMessageInfo>{ event, votes }
|
||||||
|
}
|
||||||
|
public parseEventDateFromMessage(message: string, guildId: string, requestId: string): Date {
|
||||||
|
logger.warn(`Falling back to RegEx parsing to get Event Date`, { guildId, requestId })
|
||||||
|
const datematcher = RegExp(/((?:0[1-9]|[12][0-9]|3[01])\.(?:0[1-9]|1[012])\.)(?:\ um\ )((?:(?:[01][0-9]|[2][0-3])\:[0-5][0-9])|(?:[2][4]\:00))!/i)
|
||||||
|
const result: RegExpMatchArray | null = message.match(datematcher)
|
||||||
|
const timeFromResult = result?.at(-1)
|
||||||
|
const dateFromResult = result?.at(1)?.concat(format(new Date(), 'yyyy')).concat(" " + timeFromResult) ?? ""
|
||||||
|
return new Date(dateFromResult)
|
||||||
|
}
|
||||||
|
public parseGuildIdAndEventIdFromWholeMessage(message: string) {
|
||||||
|
const idmatch = RegExp(/(?:http|https):\/\/discord\.com\/events\/(\d*)\/(\d*)/)
|
||||||
|
const matches = message.match(idmatch)
|
||||||
|
if (matches && matches.length == 3)
|
||||||
|
return { guildId: matches[1], eventId: matches[2] }
|
||||||
|
throw Error(`Could not find eventId in Vote Message`)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async prepareAndSendVoteMessage(inputInfo: prepareVoteMessageInput, guildId: string, requestId: string) {
|
||||||
|
const messageText = this.createVoteMessageText(inputInfo.event, inputInfo.movies, guildId, requestId)
|
||||||
|
const sentMessage = await this.sendVoteMessage(messageText, inputInfo.movies.length, inputInfo.announcementChannel)
|
||||||
|
if (inputInfo.pinAfterSending)
|
||||||
|
sentMessage.pin()
|
||||||
|
return sentMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
public createVoteMessageText(event: GuildScheduledEvent, movies: string[], guildId: string, requestId: string): string {
|
||||||
|
let message = `[Abstimmung] für https://discord.com/events/${guildId}/${event.id} \n<@&${config.bot.announcement_role}> Es gibt eine neue Abstimmung für die nächste Watchparty ${createDateStringFromEvent(event.scheduledStartAt, guildId, requestId)}! Stimme hierunter für den nächsten Film ab!\n`
|
||||||
|
|
||||||
|
for (let i = 0; i < movies.length; i++) {
|
||||||
|
message = message.concat(ValidVoteEmotes[i]).concat(": ").concat(movies[i]).concat("\n")
|
||||||
|
}
|
||||||
|
message = message.concat(NONE_OF_THAT).concat(": Wenn dir nichts davon gefällt.")
|
||||||
|
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Refactor into separate message controller
|
||||||
|
public async sendVoteMessage(messageText: string, movieCount: number, announcementChannel: TextChannel) {
|
||||||
|
|
||||||
|
const options: MessageCreateOptions = {
|
||||||
|
allowedMentions: { parse: ["roles"] },
|
||||||
|
content: messageText,
|
||||||
|
}
|
||||||
|
|
||||||
|
const sentMessage: Message<true> = await (await announcementChannel.fetch()).send(options)
|
||||||
|
|
||||||
|
for (let i = 0; i < movieCount; i++) {
|
||||||
|
sentMessage.react(ValidVoteEmotes[i])
|
||||||
|
}
|
||||||
|
sentMessage.react(NONE_OF_THAT)
|
||||||
|
|
||||||
|
return sentMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
public async closePoll(guild: Guild, requestId: string) {
|
||||||
|
const guildId = guild.id
|
||||||
|
logger.info("stopping poll", { guildId, requestId })
|
||||||
|
|
||||||
|
const announcementChannel: Maybe<TextChannel> = this.client.getAnnouncementChannelForGuild(guildId)
|
||||||
|
if (!announcementChannel) {
|
||||||
|
logger.error("Could not find the textchannel. Unable to close poll.", { guildId, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages: Message<true>[] = (await announcementChannel.messages.fetch()) //todo: fetch only pinned messages
|
||||||
|
.map((value) => value)
|
||||||
|
.filter(message => !isVoteEndedMessage(message) && isVoteMessage(message))
|
||||||
|
.sort((a, b) => b.createdTimestamp - a.createdTimestamp)
|
||||||
|
|
||||||
|
if (!messages || messages.length <= 0) {
|
||||||
|
logger.info("Could not find any vote messages. Cancelling pollClose", { guildId, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastMessage: Message<true> = messages[0]
|
||||||
|
|
||||||
|
if (!isVoteMessage(lastMessage)) {
|
||||||
|
logger.error(`Found message that is not a vote message, can't proceed`, { guildId, requestId })
|
||||||
|
logger.debug(`Found messages: ${JSON.stringify(messages, null, 2)}`, { guildId, requestId })
|
||||||
|
logger.debug(`Last message: ${JSON.stringify(lastMessage, null, 2)}`, { guildId, requestId })
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const votes = (await this.getVotesByEmote(lastMessage, guildId, requestId))
|
||||||
|
.sort((a, b) => b.count - a.count)
|
||||||
|
|
||||||
|
logger.debug(`votes: ${JSON.stringify(votes, null, 2)}`, { guildId, requestId })
|
||||||
|
|
||||||
|
logger.info("Deleting vote message")
|
||||||
|
lastMessage.unpin()
|
||||||
|
await lastMessage.delete()
|
||||||
|
const event = await this.getOpenPollEvent(guild, guild.id, requestId)
|
||||||
|
if (event && votes?.length > 0) {
|
||||||
|
this.updateOpenPollEventWithVoteResults(event, votes, guild, guildId, requestId)
|
||||||
|
this.sendVoteClosedMessage(event, votes[0].movie, guildId, requestId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* gets votes for the movies without the NONE_OF_THAT votes
|
||||||
|
*/
|
||||||
|
public async getVotesByEmote(message: VoteMessage, guildId: string, requestId: string): Promise<Vote[]> {
|
||||||
|
const votes: Vote[] = []
|
||||||
|
logger.debug(`Number of items in emotes: ${Object.values(ValidVoteEmotes).length}`, { guildId, requestId })
|
||||||
|
for (let i = 0; i < Object.keys(ValidVoteEmotes).length / 2; i++) {
|
||||||
|
const emote = ValidVoteEmotes[i]
|
||||||
|
logger.debug(`Getting reaction for emote ${emote}`, { guildId, requestId })
|
||||||
|
const reaction = message.reactions.resolve(emote)
|
||||||
|
logger.debug(`Reaction for emote ${emote}: ${JSON.stringify(reaction, null, 2)}`, { guildId, requestId })
|
||||||
|
if (reaction) {
|
||||||
|
const vote: Vote = { emote: emote, count: reaction.count, movie: this.extractMovieFromMessageByEmote(message, emote) }
|
||||||
|
votes.push(vote)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return votes
|
||||||
|
}
|
||||||
|
public async getOpenPollEvent(guild: Guild, guildId: string, requestId: string): Promise<Maybe<GuildScheduledEvent>> {
|
||||||
|
const voteEvents = (await guild.scheduledEvents.fetch())
|
||||||
|
.map((value) => value)
|
||||||
|
.filter(event => event.name.toLowerCase().includes("voting offen"))
|
||||||
|
logger.debug(`Found events: ${JSON.stringify(voteEvents, null, 2)}`, { guildId, requestId })
|
||||||
|
|
||||||
|
if (!voteEvents || voteEvents.length <= 0) {
|
||||||
|
logger.error("Could not find an open vote event.", { guildId, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return voteEvents[0]
|
||||||
|
}
|
||||||
|
public async updateOpenPollEventWithVoteResults(voteEvent: GuildScheduledEvent, votes: Vote[], guild: Guild, guildId: string, requestId: string) {
|
||||||
|
logger.info(`Updating event with movie ${votes[0].movie}.`, { guildId, requestId })
|
||||||
|
const options: GuildScheduledEventEditOptions<GuildScheduledEventStatus.Scheduled, GuildScheduledEventSetStatusArg<GuildScheduledEventStatus.Scheduled>> = {
|
||||||
|
name: votes[0].movie,
|
||||||
|
description: `!wp\nNummer 2: ${votes[1].movie} mit ${votes[1].count - 1} Stimmen\nNummer 3: ${votes[2].movie} mit ${votes[2].count - 1} Stimmen`
|
||||||
|
}
|
||||||
|
logger.debug(`Updating event: ${JSON.stringify(voteEvent, null, 2)}`, { guildId, requestId })
|
||||||
|
logger.info("Updating event.", { guildId, requestId })
|
||||||
|
voteEvent.edit(options)
|
||||||
|
}
|
||||||
|
public async sendVoteClosedMessage(event: GuildScheduledEvent, movie: string, guildId: string, requestId: string): Promise<Message<boolean>> {
|
||||||
|
const date = event.scheduledStartAt ? format(event.scheduledStartAt, "dd.MM.") : "Fehler, Event hatte kein Datum"
|
||||||
|
const time = event.scheduledStartAt ? format(event.scheduledStartAt, "HH:mm") : "Fehler, Event hatte keine Uhrzeit"
|
||||||
|
const body = `[Abstimmung beendet] für https://discord.com/events/${event.guildId}/${event.id}\n<@&${config.bot.announcement_role}> Wir gucken ${movie} am ${date} um ${time}`
|
||||||
|
const options: MessageCreateOptions = {
|
||||||
|
content: body,
|
||||||
|
allowedMentions: { parse: ["roles"] }
|
||||||
|
}
|
||||||
|
const announcementChannel = this.client.getAnnouncementChannelForGuild(guildId)
|
||||||
|
logger.info("Sending vote closed message.", { guildId, requestId })
|
||||||
|
if (!announcementChannel) {
|
||||||
|
const errorMessageText = "Could not find announcement channel. Please fix!"
|
||||||
|
logger.error(errorMessageText, { guildId, requestId })
|
||||||
|
throw errorMessageText
|
||||||
|
}
|
||||||
|
return announcementChannel.send(options)
|
||||||
|
}
|
||||||
|
private extractMovieFromMessageByEmote(voteMessage: VoteMessage, emote: string): string {
|
||||||
|
const lines = voteMessage.cleanContent.split("\n")
|
||||||
|
const emoteLines = lines.filter(line => line.includes(emote))
|
||||||
|
|
||||||
|
if (!emoteLines) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
const movie = emoteLines[0].substring(emoteLines[0].indexOf(emote) + emote.length + 2) // plus colon and space
|
||||||
|
|
||||||
|
return movie
|
||||||
|
}
|
||||||
|
public async checkForPollsToClose(guild: Guild): Promise<void> {
|
||||||
|
const requestId = newRequestId()
|
||||||
|
logger.info(`Automatic check for poll closing.`, { guildId: guild.id, requestId })
|
||||||
|
const events = (await guild.scheduledEvents.fetch()).filter(event => event.name.toLocaleLowerCase().includes("voting offen")).map(event => event)
|
||||||
|
if (events.length > 1) {
|
||||||
|
logger.error("Handling more than one Event is not implemented yet. Found more than one poll to close")
|
||||||
|
return
|
||||||
|
} else if (events.length == 0) {
|
||||||
|
logger.info("Could not find any events. Cancelling", { guildId: guild.id, requestId })
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedEvent = events[0] //add two hours because of different timezones in discord api and Date.now()
|
||||||
|
if (!updatedEvent.scheduledStartTimestamp) {
|
||||||
|
logger.error("Event does not have a scheduled start time. Cancelling", { guildId: guild.id, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const createDate: Date = toDate(updatedEvent.createdTimestamp)
|
||||||
|
const eventDate: Date = toDate(updatedEvent.scheduledStartTimestamp)
|
||||||
|
const difference: number = differenceInDays(createDate, eventDate)
|
||||||
|
|
||||||
|
if (difference <= 2) {
|
||||||
|
logger.info("Less than two days between event create and event start. Not closing poll.", { guildId: guild.id, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const closePollDate: Date = addDays(eventDate, -2)
|
||||||
|
|
||||||
|
if (isAfter(Date.now(), closePollDate)) {
|
||||||
|
logger.info("Less than two days until event. Closing poll", { guildId: guild.id, requestId })
|
||||||
|
this.closePoll(guild, requestId)
|
||||||
|
} else {
|
||||||
|
logger.info(`ScheduledStart: ${closePollDate}. Now: ${toDate(Date.now())}`, { guildId: guild.id, requestId })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,41 +1,48 @@
|
|||||||
import { Collection } from "@discordjs/collection"
|
import { Collection } from "@discordjs/collection"
|
||||||
import { Role } from "discord.js"
|
import { GuildScheduledEvent, Role, TextChannel } from "discord.js"
|
||||||
|
|
||||||
export type Maybe<T> = T | undefined | null
|
export type Maybe<T> = T | undefined | null
|
||||||
export interface Player {
|
export interface Player {
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
export type supported_languages = "german" | "english"
|
export type supported_languages = "german" | "english"
|
||||||
export interface localized_string {
|
export interface localized_string {
|
||||||
[k: string]: {
|
[k: string]: {
|
||||||
[k in supported_languages]: string
|
[k in supported_languages]: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export class CustomError extends Error {
|
export class CustomError extends Error {
|
||||||
private code: string
|
private code: string
|
||||||
public constructor(message: string, errorCode: string) {
|
public constructor(message: string, errorCode: string) {
|
||||||
super(message)
|
super(message)
|
||||||
this.code = errorCode
|
this.code = errorCode
|
||||||
}
|
}
|
||||||
public getCode() { return this.code }
|
public getCode() { return this.code }
|
||||||
}
|
}
|
||||||
export const errorCodes = {
|
export const errorCodes = {
|
||||||
no_end_date: 'no_end_date',
|
no_end_date: 'no_end_date',
|
||||||
no_string_present: 'no_string_present',
|
no_string_present: 'no_string_present',
|
||||||
no_schedule: 'no_schedule',
|
no_schedule: 'no_schedule',
|
||||||
schedule_not_supported: 'schedule_not_supported',
|
schedule_not_supported: 'schedule_not_supported',
|
||||||
no_repetition_amount: 'no_repetition_amount',
|
no_repetition_amount: 'no_repetition_amount',
|
||||||
invalid_repetition_string: 'invalid_repetition_string',
|
invalid_repetition_string: 'invalid_repetition_string',
|
||||||
no_creator_id: "no_creator_id",
|
no_creator_id: "no_creator_id",
|
||||||
}
|
}
|
||||||
export interface ChangedRoles {
|
export interface ChangedRoles {
|
||||||
addedRoles: Collection<string, Role>
|
addedRoles: Collection<string, Role>
|
||||||
removedRoles: Collection<string, Role>
|
removedRoles: Collection<string, Role>
|
||||||
}
|
}
|
||||||
export interface JellyfinConfig {
|
export interface JellyfinConfig {
|
||||||
jellyfinUrl: string,
|
jellyfinUrl: string,
|
||||||
jellyfinToken: string,
|
jellyfinToken: string,
|
||||||
movieCollectionId: string,
|
movieCollectionId: string,
|
||||||
collectionUser: string
|
collectionUser: string
|
||||||
}
|
}
|
||||||
export type PermissionLevel = "VIEWER" | "ADMIN" | "TEMPORARY"
|
export type PermissionLevel = "VIEWER" | "ADMIN" | "TEMPORARY"
|
||||||
|
export interface voteMessageInputInformation {
|
||||||
|
movies: string[],
|
||||||
|
startDate: Date,
|
||||||
|
event: GuildScheduledEvent,
|
||||||
|
announcementChannel: TextChannel,
|
||||||
|
pinAfterSending: boolean,
|
||||||
|
}
|
||||||
|
@ -2,260 +2,282 @@ 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";
|
||||||
|
|
||||||
|
|
||||||
export class JellyfinHandler {
|
export class JellyfinHandler {
|
||||||
|
|
||||||
private userApi: UserApi
|
private userApi: UserApi
|
||||||
private systemApi: SystemApi
|
private systemApi: SystemApi
|
||||||
private moviesApi: ItemsApi
|
private moviesApi: ItemsApi
|
||||||
private token: string
|
private token: string
|
||||||
private authHeader: { headers: { 'X-Emby-Authorization': string } }
|
private authHeader: { headers: { 'X-Emby-Authorization': string } }
|
||||||
private config: JellyfinConfig
|
private config: JellyfinConfig
|
||||||
private serverName = "";
|
private serverName = "";
|
||||||
|
|
||||||
constructor(_config: JellyfinConfig, _userApi?: UserApi, _systemApi?: SystemApi, _itemsApi?: ItemsApi) {
|
constructor(_config: JellyfinConfig, _userApi?: UserApi, _systemApi?: SystemApi, _itemsApi?: ItemsApi) {
|
||||||
this.config = _config
|
this.config = _config
|
||||||
this.token = this.config.jellyfinToken
|
this.token = this.config.jellyfinToken
|
||||||
this.authHeader = {
|
this.authHeader = {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Emby-Authorization": this.config.jellyfinToken
|
"X-Emby-Authorization": this.config.jellyfinToken
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const userApiConfigurationParams: ConfigurationParameters = {
|
const userApiConfigurationParams: ConfigurationParameters = {
|
||||||
basePath: this.config.jellyfinUrl,
|
basePath: this.config.jellyfinUrl,
|
||||||
headers: this.authHeader.headers
|
headers: this.authHeader.headers
|
||||||
}
|
}
|
||||||
const systemApiConfigurationParams: ConfigurationParameters = {
|
const systemApiConfigurationParams: ConfigurationParameters = {
|
||||||
basePath: this.config.jellyfinUrl,
|
basePath: this.config.jellyfinUrl,
|
||||||
headers: this.authHeader.headers
|
headers: this.authHeader.headers
|
||||||
}
|
}
|
||||||
const libraryApiConfigurationParams: ConfigurationParameters = {
|
const libraryApiConfigurationParams: ConfigurationParameters = {
|
||||||
basePath: this.config.jellyfinUrl,
|
basePath: this.config.jellyfinUrl,
|
||||||
headers: this.authHeader.headers
|
headers: this.authHeader.headers
|
||||||
}
|
}
|
||||||
|
|
||||||
this.userApi = _userApi ?? new UserApi(new Configuration(userApiConfigurationParams))
|
this.userApi = _userApi ?? new UserApi(new Configuration(userApiConfigurationParams))
|
||||||
this.systemApi = _systemApi ?? new SystemApi(new Configuration(systemApiConfigurationParams))
|
this.systemApi = _systemApi ?? new SystemApi(new Configuration(systemApiConfigurationParams))
|
||||||
this.moviesApi = _itemsApi ?? new ItemsApi(new Configuration(libraryApiConfigurationParams))
|
this.moviesApi = _itemsApi ?? new ItemsApi(new Configuration(libraryApiConfigurationParams))
|
||||||
logger.info(`Initialized Jellyfin handler`, { requestId: 'Init' })
|
logger.info(`Initialized Jellyfin handler`, { requestId: 'Init' })
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateJFUserName(discordUser: GuildMember, level: PermissionLevel): string {
|
private generateJFUserName(discordUser: GuildMember, level: PermissionLevel): string {
|
||||||
return `${discordUser.displayName}${level == "TEMPORARY" ? "_tmp" : ""}`
|
return `${discordUser.displayName}${level == "TEMPORARY" ? "_tmp" : ""}`
|
||||||
}
|
}
|
||||||
|
|
||||||
private generatePasswordForUser(): string {
|
private generatePasswordForUser(): string {
|
||||||
return (Math.random() * 10000 + 10000).toFixed(0)
|
return (Math.random() * 10000 + 10000).toFixed(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createUserAccountForDiscordUser(discordUser: GuildMember, 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) {
|
||||||
(await discordUser.createDM()).send(`Ich hab dir mal nen Account angelegt :)\nDein Username ist ${createResult.name}, dein Password ist "${req.createUserByNameRequest.password}"!`)
|
if (createResult.policy) {
|
||||||
return createResult
|
this.setUserPermissions(createResult, requestId, guildId)
|
||||||
}
|
}
|
||||||
else throw new Error('Could not create User in Jellyfin')
|
(await discordUser.createDM()).send(`Ich hab dir mal nen Account angelegt :)\nDein Username ist ${createResult.name}, dein Password ist "${req.createUserByNameRequest.password}"!`)
|
||||||
}
|
return createResult
|
||||||
|
}
|
||||||
|
else throw new Error('Could not create User in Jellyfin')
|
||||||
|
}
|
||||||
|
|
||||||
public async isUserAlreadyPresent(discordUser: GuildMember, requestId?: string): Promise<boolean> {
|
public async setUserPermissions(user: UserDto, requestId: string, guildId?: string) {
|
||||||
const jfuser = await this.getUser(discordUser, requestId)
|
if (!user.policy || !user.id) {
|
||||||
logger.debug(`Presence for DiscordUser ${discordUser.id}:${jfuser !== undefined}`, { guildId: discordUser.guild.id, requestId })
|
logger.error(`Cannot update user policy. User ${user.name} has no policy to modify`, { guildId, requestId })
|
||||||
return jfuser !== undefined
|
return
|
||||||
}
|
}
|
||||||
|
user.policy.enableVideoPlaybackTranscoding = false
|
||||||
|
|
||||||
public async getCurrentUsers(guildId: string, requestId?: string): Promise<UserDto[]> {
|
const operation: UpdateUserPolicyRequest = {
|
||||||
try {
|
...user.policy,
|
||||||
logger.info(`Fetching current users from Jellyfin`, { requestId, guildId })
|
enableVideoPlaybackTranscoding: false
|
||||||
const result = await this.userApi.getUsers(undefined, this.authHeader)
|
}
|
||||||
return result
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Could not fetch current users from jellyfin`, { guildId, requestId })
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getUser(discordUser: GuildMember, requestId?: string): Promise<Maybe<UserDto>> {
|
const request: UpdateUserPolicyOperationRequest = {
|
||||||
logger.info(`Getting user for discord member ${discordUser.displayName}`, { requestId, guildId: discordUser.guild.id })
|
userId: user.id,
|
||||||
const jfUsers = await this.getCurrentUsers(discordUser.guild.id, requestId)
|
updateUserPolicyRequest: operation
|
||||||
const foundUser = jfUsers.find(x => x.name?.includes(discordUser.displayName))
|
}
|
||||||
return foundUser
|
this.userApi.updateUserPolicy(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async removeUser(newMember: GuildMember, level: PermissionLevel, requestId?: string) {
|
public async isUserAlreadyPresent(discordUser: GuildMember, requestId?: string): Promise<boolean> {
|
||||||
logger.info(`${level == "TEMPORARY" ? "Deleting" : "Disabling"} user ${newMember.displayName}, but method is not implemented`, { requestId, guildId: newMember.guild.id })
|
const jfuser = await this.getUser(discordUser, requestId)
|
||||||
const jfuser = await this.getUser(newMember, requestId)
|
logger.debug(`Presence for DiscordUser ${discordUser.id}:${jfuser !== undefined}`, { guildId: discordUser.guild.id, requestId })
|
||||||
if (jfuser && jfuser.id) {
|
return jfuser !== undefined
|
||||||
if (level === "TEMPORARY") {
|
}
|
||||||
const r: DeleteUserRequest = {
|
|
||||||
userId: jfuser.id
|
|
||||||
}
|
|
||||||
this.userApi.deleteUser(r)
|
|
||||||
}
|
|
||||||
else
|
|
||||||
await this.disableUser(jfuser, newMember.guild.id, requestId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async purge(guildId: string, requestId?: string) {
|
public async getCurrentUsers(guildId: string, requestId?: string): Promise<UserDto[]> {
|
||||||
logger.info("Deleting tmp users", { requestId, guildId })
|
try {
|
||||||
const users = (await this.userApi.getUsers()).filter(user => user.name?.endsWith("_tmp"))
|
logger.info(`Fetching current users from Jellyfin`, { requestId, guildId })
|
||||||
|
const result = await this.userApi.getUsers(undefined, this.authHeader)
|
||||||
|
return result
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Could not fetch current users from jellyfin`, { guildId, requestId })
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
users.forEach(user => {
|
public async getUser(discordUser: GuildMember, requestId?: string): Promise<Maybe<UserDto>> {
|
||||||
if (user.id) {
|
logger.info(`Getting user for discord member ${discordUser.displayName}`, { requestId, guildId: discordUser.guild.id })
|
||||||
const r: DeleteUserRequest = {
|
const jfUsers = await this.getCurrentUsers(discordUser.guild.id, requestId)
|
||||||
userId: user.id
|
const foundUser = jfUsers.find(x => x.name?.includes(discordUser.displayName))
|
||||||
}
|
return foundUser
|
||||||
this.userApi.deleteUser(r)
|
}
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
public async resetUserPasswort(member: GuildMember, requestId?: string) {
|
public async removeUser(newMember: GuildMember, level: PermissionLevel, requestId?: string) {
|
||||||
logger.info(`Resetting password for user ${member.displayName}`, { requestId, guildId: member.guild.id })
|
logger.info(`${level == "TEMPORARY" ? "Deleting" : "Disabling"} user ${newMember.displayName}, but method is not implemented`, { requestId, guildId: newMember.guild.id })
|
||||||
const jfUser = await this.getUser(member, requestId)
|
const jfuser = await this.getUser(newMember, requestId)
|
||||||
if (jfUser && jfUser.id) {
|
if (jfuser && jfuser.id) {
|
||||||
|
if (level === "TEMPORARY") {
|
||||||
|
const r: DeleteUserRequest = {
|
||||||
|
userId: jfuser.id
|
||||||
|
}
|
||||||
|
this.userApi.deleteUser(r)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
await this.disableUser(jfuser, newMember.guild.id, requestId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// const reset: UpdateUserPasswordRequest = {
|
public async purge(guildId: string, requestId?: string) {
|
||||||
// resetPassword: true
|
logger.info("Deleting tmp users", { requestId, guildId })
|
||||||
// }
|
const users = (await this.userApi.getUsers()).filter(user => user.name?.endsWith("_tmp"))
|
||||||
|
|
||||||
// const shit: UpdateUserPasswordOperationRequest = {
|
users.forEach(user => {
|
||||||
// updateUserPasswordRequest: reset,
|
if (user.id) {
|
||||||
// userId: jfUser.id
|
const r: DeleteUserRequest = {
|
||||||
// }
|
userId: user.id
|
||||||
|
}
|
||||||
|
this.userApi.deleteUser(r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// logger.info(JSON.stringify(jfUser.policy, null, 2))
|
public async resetUserPasswort(member: GuildMember, requestId?: string) {
|
||||||
|
logger.info(`Resetting password for user ${member.displayName}`, { requestId, guildId: member.guild.id })
|
||||||
|
const jfUser = await this.getUser(member, requestId)
|
||||||
|
if (jfUser && jfUser.id) {
|
||||||
|
|
||||||
// logger.info("Resetting password", {requestId})
|
// const reset: UpdateUserPasswordRequest = {
|
||||||
// await this.userApi.updateUserPassword(shit);
|
// resetPassword: true
|
||||||
|
// }
|
||||||
|
|
||||||
const password = this.generatePasswordForUser()
|
// const shit: UpdateUserPasswordOperationRequest = {
|
||||||
const passwordRequest: UpdateUserPasswordRequest = {
|
// updateUserPasswordRequest: reset,
|
||||||
// resetPassword: true,
|
// userId: jfUser.id
|
||||||
currentPw: "",
|
// }
|
||||||
newPw: password
|
|
||||||
}
|
|
||||||
|
|
||||||
const passwordOperationRequest: UpdateUserPasswordOperationRequest = {
|
// logger.info(JSON.stringify(jfUser.policy, null, 2))
|
||||||
updateUserPasswordRequest: passwordRequest,
|
|
||||||
userId: jfUser.id
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("Setting new password", { requestId, guildId: member.guild.id })
|
// logger.info("Resetting password", {requestId})
|
||||||
await this.userApi.updateUserPassword(passwordOperationRequest);
|
// await this.userApi.updateUserPassword(shit);
|
||||||
|
|
||||||
|
const password = this.generatePasswordForUser()
|
||||||
|
const passwordRequest: UpdateUserPasswordRequest = {
|
||||||
|
// resetPassword: true,
|
||||||
|
currentPw: "",
|
||||||
|
newPw: password
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordOperationRequest: UpdateUserPasswordOperationRequest = {
|
||||||
|
updateUserPasswordRequest: passwordRequest,
|
||||||
|
userId: jfUser.id
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Setting new password", { requestId, guildId: member.guild.id })
|
||||||
|
await this.userApi.updateUserPassword(passwordOperationRequest);
|
||||||
|
|
||||||
|
|
||||||
(await member.createDM()).send(`Hier ist dein neues Passwort: ${password}`)
|
(await member.createDM()).send(`Hier ist dein neues Passwort: ${password}`)
|
||||||
} else {
|
} else {
|
||||||
(await member.createDM()).send("Ich konnte leider keinen User von dir auf Jellyfin finden. Bitte melde dich bei Markus oder Samantha!")
|
(await member.createDM()).send("Ich konnte leider keinen User von dir auf Jellyfin finden. Bitte melde dich bei Markus oder Samantha!")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async disableUser(user: UserDto, guildId?: string, requestId?: string): Promise<void> {
|
public async disableUser(user: UserDto, guildId?: string, requestId?: string): Promise<void> {
|
||||||
if (user.id) {
|
if (user.id) {
|
||||||
const jfUser = await this.getUser(<GuildMember>{ displayName: user.name, guild: { id: guildId } }, requestId)
|
const jfUser = await this.getUser(<GuildMember>{ displayName: user.name, guild: { id: guildId } }, requestId)
|
||||||
logger.info(`Trying to disable user: ${user.name}|${user.id}|${JSON.stringify(jfUser, null, 2)}`, { guildId, requestId })
|
logger.info(`Trying to disable user: ${user.name}|${user.id}|${JSON.stringify(jfUser, null, 2)}`, { guildId, requestId })
|
||||||
const r: UpdateUserPolicyOperationRequest = {
|
const r: UpdateUserPolicyOperationRequest = {
|
||||||
userId: user.id ?? "",
|
userId: user.id ?? "",
|
||||||
updateUserPolicyRequest: {
|
updateUserPolicyRequest: {
|
||||||
...jfUser?.policy,
|
...jfUser?.policy,
|
||||||
isDisabled: true,
|
isDisabled: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await this.userApi.updateUserPolicy(r)
|
await this.userApi.updateUserPolicy(r)
|
||||||
logger.info(`Succeeded with disabling user: ${user.name}`, { guildId, requestId })
|
logger.info(`Succeeded with disabling user: ${user.name}`, { guildId, requestId })
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
logger.error(`Can not disable user ${JSON.stringify(user)}, has no id?!`, { requestId, guildId })
|
logger.error(`Can not disable user ${JSON.stringify(user)}, has no id?!`, { requestId, guildId })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async enableUser(user: UserDto, guildId: string, requestId?: string): Promise<void> {
|
public async enableUser(user: UserDto, guildId: string, requestId?: string): Promise<void> {
|
||||||
if (user.id) {
|
if (user.id) {
|
||||||
const jfUser = await this.getUser(<GuildMember>{ displayName: user.name, guild: { id: guildId } }, requestId)
|
const jfUser = await this.getUser(<GuildMember>{ displayName: user.name, guild: { id: guildId } }, requestId)
|
||||||
logger.info(`Trying to enable user: ${user.name}|${user.id}|${JSON.stringify(jfUser, null, 2)}`, { guildId, requestId })
|
logger.info(`Trying to enable user: ${user.name}|${user.id}|${JSON.stringify(jfUser, null, 2)}`, { guildId, requestId })
|
||||||
const r: UpdateUserPolicyOperationRequest = {
|
const r: UpdateUserPolicyOperationRequest = {
|
||||||
userId: user.id ?? "",
|
userId: user.id ?? "",
|
||||||
updateUserPolicyRequest: {
|
updateUserPolicyRequest: {
|
||||||
...jfUser?.policy,
|
...jfUser?.policy,
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await this.userApi.updateUserPolicy(r)
|
await this.userApi.updateUserPolicy(r)
|
||||||
logger.info(`Succeeded with enabling user: ${user.name}`, { guildId, requestId })
|
logger.info(`Succeeded with enabling user: ${user.name}`, { guildId, requestId })
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
logger.error(`Can not enable user ${JSON.stringify(user)}, has no id?!`, { requestId, guildId })
|
logger.error(`Can not enable user ${JSON.stringify(user)}, has no id?!`, { requestId, guildId })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async upsertUser(newMember: GuildMember, level: PermissionLevel, requestId?: string): Promise<UserUpsertResult> {
|
public async upsertUser(newMember: GuildMember, level: PermissionLevel, requestId?: string): Promise<UserUpsertResult> {
|
||||||
logger.info(`Trying to upsert user ${newMember.displayName}, with permissionLevel ${level}`, { guildId: newMember.guild.id, requestId })
|
logger.info(`Trying to upsert user ${newMember.displayName}, with permissionLevel ${level}`, { guildId: newMember.guild.id, requestId })
|
||||||
const jfuser = await this.getUser(newMember, requestId)
|
const jfuser = await this.getUser(newMember, requestId)
|
||||||
if (jfuser && !jfuser.policy?.isDisabled) {
|
if (jfuser && !jfuser.policy?.isDisabled) {
|
||||||
logger.info(`User with name ${newMember.displayName} is already present`, { guildId: newMember.guild.id, requestId })
|
logger.info(`User with name ${newMember.displayName} is already present`, { guildId: newMember.guild.id, requestId })
|
||||||
await this.enableUser(jfuser, newMember.guild.id, requestId)
|
await this.enableUser(jfuser, newMember.guild.id, requestId)
|
||||||
return UserUpsertResult.enabled
|
return UserUpsertResult.enabled
|
||||||
} else {
|
} else {
|
||||||
this.createUserAccountForDiscordUser(newMember, level, newMember.guild.id, requestId)
|
this.createUserAccountForDiscordUser(newMember, level, newMember.guild.id, requestId)
|
||||||
return UserUpsertResult.created
|
return UserUpsertResult.created
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAllMovies(guildId: string, requestId: string): Promise<BaseItemDto[]> {
|
public async getAllMovies(guildId: string, requestId: string): Promise<BaseItemDto[]> {
|
||||||
logger.info("requesting all movies from jellyfin", { guildId, requestId })
|
logger.info("requesting all movies from jellyfin", { guildId, requestId })
|
||||||
|
|
||||||
const searchParams: GetItemsRequest = {
|
const searchParams: GetItemsRequest = {
|
||||||
userId: this.config.collectionUser,
|
userId: this.config.collectionUser,
|
||||||
parentId: this.config.movieCollectionId // collection ID for all movies
|
parentId: this.config.movieCollectionId // collection ID for all movies
|
||||||
}
|
}
|
||||||
const movies = (await (this.moviesApi.getItems(searchParams))).items?.filter(item => !item.isFolder)
|
const movies = (await (this.moviesApi.getItems(searchParams))).items?.filter(item => !item.isFolder)
|
||||||
// logger.debug(JSON.stringify(movies, null, 2), { guildId: guildId, requestId })
|
// logger.debug(JSON.stringify(movies, null, 2), { guildId: guildId, requestId })
|
||||||
logger.info(`Found ${movies?.length} movies in total`, { guildId, requestId })
|
logger.info(`Found ${movies?.length} movies in total`, { guildId, requestId })
|
||||||
return movies ?? []
|
return movies ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getRandomMovies(count: number, guildId: string, requestId: string): Promise<BaseItemDto[]> {
|
public async getRandomMovies(count: number, guildId: string, requestId: string): Promise<BaseItemDto[]> {
|
||||||
logger.info(`${count} random movies requested.`, { guildId, requestId })
|
logger.info(`${count} random movies requested.`, { guildId, requestId })
|
||||||
const allMovies = await this.getAllMovies(guildId, requestId)
|
const allMovies = await this.getAllMovies(guildId, requestId)
|
||||||
if (count >= allMovies.length) {
|
if (count >= allMovies.length) {
|
||||||
logger.info(`${count} random movies requested but found only ${allMovies.length}. Returning all Movies.`, { guildId, requestId })
|
logger.info(`${count} random movies requested but found only ${allMovies.length}. Returning all Movies.`, { guildId, requestId })
|
||||||
return allMovies
|
return allMovies
|
||||||
}
|
}
|
||||||
const movies: BaseItemDto[] = []
|
const movies: BaseItemDto[] = []
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
const index = Math.floor(Math.random() * allMovies.length)
|
const index = Math.floor(Math.random() * allMovies.length)
|
||||||
movies.push(...allMovies.splice(index, 1)) // maybe out of bounds? ?
|
movies.push(...allMovies.splice(index, 1)) // maybe out of bounds? ?
|
||||||
}
|
}
|
||||||
return movies
|
return movies
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getRandomMovieNames(count: number, guildId: string, requestId: string): Promise<string[]> {
|
public async getRandomMovieNames(count: number, guildId: string, requestId: string): Promise<string[]> {
|
||||||
logger.info(`${count} random movie names requested`, { guildId, requestId })
|
logger.info(`${count} random movie names requested`, { guildId, requestId })
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -16,72 +16,72 @@
|
|||||||
export const BASE_PATH = "http://localhost".replace(/\/+$/, "");
|
export const BASE_PATH = "http://localhost".replace(/\/+$/, "");
|
||||||
|
|
||||||
export interface ConfigurationParameters {
|
export interface ConfigurationParameters {
|
||||||
basePath?: string; // override base path
|
basePath?: string; // override base path
|
||||||
fetchApi?: FetchAPI; // override for fetch implementation
|
fetchApi?: FetchAPI; // override for fetch implementation
|
||||||
middleware?: Middleware[]; // middleware to apply before/after fetch requests
|
middleware?: Middleware[]; // middleware to apply before/after fetch requests
|
||||||
queryParamsStringify?: (params: HTTPQuery) => string; // stringify function for query strings
|
queryParamsStringify?: (params: HTTPQuery) => string; // stringify function for query strings
|
||||||
username?: string; // parameter for basic security
|
username?: string; // parameter for basic security
|
||||||
password?: string; // parameter for basic security
|
password?: string; // parameter for basic security
|
||||||
apiKey?: string | ((name: string) => string); // parameter for apiKey security
|
apiKey?: string | ((name: string) => string); // parameter for apiKey security
|
||||||
accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string | Promise<string>); // parameter for oauth2 security
|
accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string | Promise<string>); // parameter for oauth2 security
|
||||||
headers?: HTTPHeaders; //header params we want to use on every request
|
headers?: HTTPHeaders; //header params we want to use on every request
|
||||||
credentials?: RequestCredentials; //value for the credentials param we want to use on each request
|
credentials?: RequestCredentials; //value for the credentials param we want to use on each request
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
get basePath(): string {
|
get basePath(): string {
|
||||||
return this.configuration.basePath != null ? this.configuration.basePath : BASE_PATH;
|
return this.configuration.basePath != null ? this.configuration.basePath : BASE_PATH;
|
||||||
}
|
}
|
||||||
|
|
||||||
get fetchApi(): FetchAPI | undefined {
|
get fetchApi(): FetchAPI | undefined {
|
||||||
return this.configuration.fetchApi;
|
return this.configuration.fetchApi;
|
||||||
}
|
}
|
||||||
|
|
||||||
get middleware(): Middleware[] {
|
get middleware(): Middleware[] {
|
||||||
return this.configuration.middleware || [];
|
return this.configuration.middleware || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
get queryParamsStringify(): (params: HTTPQuery) => string {
|
get queryParamsStringify(): (params: HTTPQuery) => string {
|
||||||
return this.configuration.queryParamsStringify || querystring;
|
return this.configuration.queryParamsStringify || querystring;
|
||||||
}
|
}
|
||||||
|
|
||||||
get username(): string | undefined {
|
get username(): string | undefined {
|
||||||
return this.configuration.username;
|
return this.configuration.username;
|
||||||
}
|
}
|
||||||
|
|
||||||
get password(): string | undefined {
|
get password(): string | undefined {
|
||||||
return this.configuration.password;
|
return this.configuration.password;
|
||||||
}
|
}
|
||||||
|
|
||||||
get apiKey(): ((name: string) => string) | undefined {
|
get apiKey(): ((name: string) => string) | undefined {
|
||||||
const apiKey = this.configuration.apiKey;
|
const apiKey = this.configuration.apiKey;
|
||||||
if (apiKey) {
|
if (apiKey) {
|
||||||
return typeof apiKey === 'function' ? apiKey : () => apiKey;
|
return typeof apiKey === 'function' ? apiKey : () => apiKey;
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
get accessToken(): ((name?: string, scopes?: string[]) => string | Promise<string>) | undefined {
|
get accessToken(): ((name?: string, scopes?: string[]) => string | Promise<string>) | undefined {
|
||||||
const accessToken = this.configuration.accessToken;
|
const accessToken = this.configuration.accessToken;
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
return typeof accessToken === 'function' ? accessToken : async () => accessToken;
|
return typeof accessToken === 'function' ? accessToken : async () => accessToken;
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
get headers(): HTTPHeaders | undefined {
|
get headers(): HTTPHeaders | undefined {
|
||||||
return this.configuration.headers;
|
return this.configuration.headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
get credentials(): RequestCredentials | undefined {
|
get credentials(): RequestCredentials | undefined {
|
||||||
return this.configuration.credentials;
|
return this.configuration.credentials;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DefaultConfig = new Configuration();
|
export const DefaultConfig = new Configuration();
|
||||||
@ -91,192 +91,192 @@ export const DefaultConfig = new Configuration();
|
|||||||
*/
|
*/
|
||||||
export class BaseAPI {
|
export class BaseAPI {
|
||||||
|
|
||||||
private static readonly jsonRegex = new RegExp('^(:?application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(:?;.*)?$', 'i');
|
private static readonly jsonRegex = new RegExp('^(:?application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(:?;.*)?$', 'i');
|
||||||
private middleware: Middleware[];
|
private middleware: Middleware[];
|
||||||
|
|
||||||
constructor(protected configuration = DefaultConfig) {
|
constructor(protected configuration = DefaultConfig) {
|
||||||
this.middleware = configuration.middleware;
|
this.middleware = configuration.middleware;
|
||||||
}
|
}
|
||||||
|
|
||||||
withMiddleware<T extends BaseAPI>(this: T, ...middlewares: Middleware[]) {
|
withMiddleware<T extends BaseAPI>(this: T, ...middlewares: Middleware[]) {
|
||||||
const next = this.clone<T>();
|
const next = this.clone<T>();
|
||||||
next.middleware = next.middleware.concat(...middlewares);
|
next.middleware = next.middleware.concat(...middlewares);
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
withPreMiddleware<T extends BaseAPI>(this: T, ...preMiddlewares: Array<Middleware['pre']>) {
|
withPreMiddleware<T extends BaseAPI>(this: T, ...preMiddlewares: Array<Middleware['pre']>) {
|
||||||
const middlewares = preMiddlewares.map((pre) => ({ pre }));
|
const middlewares = preMiddlewares.map((pre) => ({ pre }));
|
||||||
return this.withMiddleware<T>(...middlewares);
|
return this.withMiddleware<T>(...middlewares);
|
||||||
}
|
}
|
||||||
|
|
||||||
withPostMiddleware<T extends BaseAPI>(this: T, ...postMiddlewares: Array<Middleware['post']>) {
|
withPostMiddleware<T extends BaseAPI>(this: T, ...postMiddlewares: Array<Middleware['post']>) {
|
||||||
const middlewares = postMiddlewares.map((post) => ({ post }));
|
const middlewares = postMiddlewares.map((post) => ({ post }));
|
||||||
return this.withMiddleware<T>(...middlewares);
|
return this.withMiddleware<T>(...middlewares);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the given MIME is a JSON MIME.
|
* Check if the given MIME is a JSON MIME.
|
||||||
* JSON MIME examples:
|
* JSON MIME examples:
|
||||||
* application/json
|
* application/json
|
||||||
* application/json; charset=UTF8
|
* application/json; charset=UTF8
|
||||||
* APPLICATION/JSON
|
* APPLICATION/JSON
|
||||||
* application/vnd.company+json
|
* application/vnd.company+json
|
||||||
* @param mime - MIME (Multipurpose Internet Mail Extensions)
|
* @param mime - MIME (Multipurpose Internet Mail Extensions)
|
||||||
* @return True if the given MIME is JSON, false otherwise.
|
* @return True if the given MIME is JSON, false otherwise.
|
||||||
*/
|
*/
|
||||||
protected isJsonMime(mime: string | null | undefined): boolean {
|
protected isJsonMime(mime: string | null | undefined): boolean {
|
||||||
if (!mime) {
|
if (!mime) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return BaseAPI.jsonRegex.test(mime);
|
return BaseAPI.jsonRegex.test(mime);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async request(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction): Promise<Response> {
|
protected async request(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction): Promise<Response> {
|
||||||
const { url, init } = await this.createFetchParams(context, initOverrides);
|
const { url, init } = await this.createFetchParams(context, initOverrides);
|
||||||
const response = await this.fetchApi(url, init);
|
const response = await this.fetchApi(url, init);
|
||||||
if (response && (response.status >= 200 && response.status < 300)) {
|
if (response && (response.status >= 200 && response.status < 300)) {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
throw new ResponseError(response, 'Response returned an error code');
|
throw new ResponseError(response, 'Response returned an error code');
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createFetchParams(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction) {
|
private async createFetchParams(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction) {
|
||||||
let url = this.configuration.basePath + context.path;
|
let url = this.configuration.basePath + context.path;
|
||||||
if (context.query !== undefined && Object.keys(context.query).length !== 0) {
|
if (context.query !== undefined && Object.keys(context.query).length !== 0) {
|
||||||
// only add the querystring to the URL if there are query parameters.
|
// only add the querystring to the URL if there are query parameters.
|
||||||
// this is done to avoid urls ending with a "?" character which buggy webservers
|
// this is done to avoid urls ending with a "?" character which buggy webservers
|
||||||
// do not handle correctly sometimes.
|
// do not handle correctly sometimes.
|
||||||
url += '?' + this.configuration.queryParamsStringify(context.query);
|
url += '?' + this.configuration.queryParamsStringify(context.query);
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = Object.assign({}, this.configuration.headers, context.headers);
|
const headers = Object.assign({}, this.configuration.headers, context.headers);
|
||||||
Object.keys(headers).forEach(key => headers[key] === undefined ? delete headers[key] : {});
|
Object.keys(headers).forEach(key => headers[key] === undefined ? delete headers[key] : {});
|
||||||
|
|
||||||
const initOverrideFn =
|
const initOverrideFn =
|
||||||
typeof initOverrides === "function"
|
typeof initOverrides === "function"
|
||||||
? initOverrides
|
? initOverrides
|
||||||
: async () => initOverrides;
|
: async () => initOverrides;
|
||||||
|
|
||||||
const initParams = {
|
const initParams = {
|
||||||
method: context.method,
|
method: context.method,
|
||||||
headers,
|
headers,
|
||||||
body: context.body,
|
body: context.body,
|
||||||
credentials: this.configuration.credentials,
|
credentials: this.configuration.credentials,
|
||||||
};
|
};
|
||||||
|
|
||||||
const overriddenInit: RequestInit = {
|
const overriddenInit: RequestInit = {
|
||||||
...initParams,
|
...initParams,
|
||||||
...(await initOverrideFn({
|
...(await initOverrideFn({
|
||||||
init: initParams,
|
init: initParams,
|
||||||
context,
|
context,
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
|
|
||||||
const init: RequestInit = {
|
const init: RequestInit = {
|
||||||
...overriddenInit,
|
...overriddenInit,
|
||||||
body:
|
body:
|
||||||
isFormData(overriddenInit.body) ||
|
isFormData(overriddenInit.body) ||
|
||||||
overriddenInit.body instanceof URLSearchParams ||
|
overriddenInit.body instanceof URLSearchParams ||
|
||||||
isBlob(overriddenInit.body)
|
isBlob(overriddenInit.body)
|
||||||
? overriddenInit.body
|
? overriddenInit.body
|
||||||
: JSON.stringify(overriddenInit.body),
|
: JSON.stringify(overriddenInit.body),
|
||||||
};
|
};
|
||||||
|
|
||||||
return { url, init };
|
return { url, init };
|
||||||
}
|
}
|
||||||
|
|
||||||
private fetchApi = async (url: string, init: RequestInit) => {
|
private fetchApi = async (url: string, init: RequestInit) => {
|
||||||
let fetchParams = { url, init };
|
let fetchParams = { url, init };
|
||||||
for (const middleware of this.middleware) {
|
for (const middleware of this.middleware) {
|
||||||
if (middleware.pre) {
|
if (middleware.pre) {
|
||||||
fetchParams = await middleware.pre({
|
fetchParams = await middleware.pre({
|
||||||
fetch: this.fetchApi,
|
fetch: this.fetchApi,
|
||||||
...fetchParams,
|
...fetchParams,
|
||||||
}) || fetchParams;
|
}) || fetchParams;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let response: Response | undefined = undefined;
|
let response: Response | undefined = undefined;
|
||||||
try {
|
try {
|
||||||
response = await (this.configuration.fetchApi || fetch)(fetchParams.url, fetchParams.init);
|
response = await (this.configuration.fetchApi || fetch)(fetchParams.url, fetchParams.init);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
for (const middleware of this.middleware) {
|
for (const middleware of this.middleware) {
|
||||||
if (middleware.onError) {
|
if (middleware.onError) {
|
||||||
response = await middleware.onError({
|
response = await middleware.onError({
|
||||||
fetch: this.fetchApi,
|
fetch: this.fetchApi,
|
||||||
url: fetchParams.url,
|
url: fetchParams.url,
|
||||||
init: fetchParams.init,
|
init: fetchParams.init,
|
||||||
error: e,
|
error: e,
|
||||||
response: response ? response.clone() : undefined,
|
response: response ? response.clone() : undefined,
|
||||||
}) || response;
|
}) || response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (response === undefined) {
|
if (response === undefined) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
throw new FetchError(e, 'The request failed and the interceptors did not return an alternative response');
|
throw new FetchError(e, 'The request failed and the interceptors did not return an alternative response');
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const middleware of this.middleware) {
|
for (const middleware of this.middleware) {
|
||||||
if (middleware.post) {
|
if (middleware.post) {
|
||||||
response = await middleware.post({
|
response = await middleware.post({
|
||||||
fetch: this.fetchApi,
|
fetch: this.fetchApi,
|
||||||
url: fetchParams.url,
|
url: fetchParams.url,
|
||||||
init: fetchParams.init,
|
init: fetchParams.init,
|
||||||
response: response.clone(),
|
response: response.clone(),
|
||||||
}) || response;
|
}) || response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a shallow clone of `this` by constructing a new instance
|
* Create a shallow clone of `this` by constructing a new instance
|
||||||
* and then shallow cloning data members.
|
* and then shallow cloning data members.
|
||||||
*/
|
*/
|
||||||
private clone<T extends BaseAPI>(this: T): T {
|
private clone<T extends BaseAPI>(this: T): T {
|
||||||
const constructor = this.constructor as any;
|
const constructor = this.constructor as any;
|
||||||
const next = new constructor(this.configuration);
|
const next = new constructor(this.configuration);
|
||||||
next.middleware = this.middleware.slice();
|
next.middleware = this.middleware.slice();
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function isBlob(value: any): value is Blob {
|
function isBlob(value: any): value is Blob {
|
||||||
return typeof Blob !== 'undefined' && value instanceof Blob;
|
return typeof Blob !== 'undefined' && value instanceof Blob;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isFormData(value: any): value is FormData {
|
function isFormData(value: any): value is FormData {
|
||||||
return typeof FormData !== "undefined" && value instanceof FormData;
|
return typeof FormData !== "undefined" && value instanceof FormData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ResponseError extends Error {
|
export class ResponseError extends Error {
|
||||||
override name: "ResponseError" = "ResponseError";
|
override name: "ResponseError" = "ResponseError";
|
||||||
constructor(public response: Response, msg?: string) {
|
constructor(public response: Response, errorMessage?: string) {
|
||||||
super(msg);
|
super(errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FetchError extends Error {
|
export class FetchError extends Error {
|
||||||
override name: "FetchError" = "FetchError";
|
override name: "FetchError" = "FetchError";
|
||||||
constructor(public cause: Error, msg?: string) {
|
constructor(public cause: Error, errorMessage?: string) {
|
||||||
super(msg);
|
super(errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RequiredError extends Error {
|
export class RequiredError extends Error {
|
||||||
override name: "RequiredError" = "RequiredError";
|
override name: "RequiredError" = "RequiredError";
|
||||||
constructor(public field: string, msg?: string) {
|
constructor(public field: string, errorMessage?: string) {
|
||||||
super(msg);
|
super(errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const COLLECTION_FORMATS = {
|
export const COLLECTION_FORMATS = {
|
||||||
csv: ",",
|
csv: ",",
|
||||||
ssv: " ",
|
ssv: " ",
|
||||||
tsv: "\t",
|
tsv: "\t",
|
||||||
pipes: "|",
|
pipes: "|",
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FetchAPI = WindowOrWorkerGlobalScope['fetch'];
|
export type FetchAPI = WindowOrWorkerGlobalScope['fetch'];
|
||||||
@ -292,134 +292,134 @@ export type ModelPropertyNaming = 'camelCase' | 'snake_case' | 'PascalCase' | 'o
|
|||||||
export type InitOverrideFunction = (requestContext: { init: HTTPRequestInit, context: RequestOpts }) => Promise<RequestInit>
|
export type InitOverrideFunction = (requestContext: { init: HTTPRequestInit, context: RequestOpts }) => Promise<RequestInit>
|
||||||
|
|
||||||
export interface FetchParams {
|
export interface FetchParams {
|
||||||
url: string;
|
url: string;
|
||||||
init: RequestInit;
|
init: RequestInit;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RequestOpts {
|
export interface RequestOpts {
|
||||||
path: string;
|
path: string;
|
||||||
method: HTTPMethod;
|
method: HTTPMethod;
|
||||||
headers: HTTPHeaders;
|
headers: HTTPHeaders;
|
||||||
query?: HTTPQuery;
|
query?: HTTPQuery;
|
||||||
body?: HTTPBody;
|
body?: HTTPBody;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function exists(json: any, key: string) {
|
export function exists(json: any, key: string) {
|
||||||
const value = json[key];
|
const value = json[key];
|
||||||
return value !== null && value !== undefined;
|
return value !== null && value !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function querystring(params: HTTPQuery, prefix: string = ''): string {
|
export function querystring(params: HTTPQuery, prefix: string = ''): string {
|
||||||
return Object.keys(params)
|
return Object.keys(params)
|
||||||
.map(key => querystringSingleKey(key, params[key], prefix))
|
.map(key => querystringSingleKey(key, params[key], prefix))
|
||||||
.filter(part => part.length > 0)
|
.filter(part => part.length > 0)
|
||||||
.join('&');
|
.join('&');
|
||||||
}
|
}
|
||||||
|
|
||||||
function querystringSingleKey(key: string, value: string | number | null | undefined | boolean | Array<string | number | null | boolean> | Set<string | number | null | boolean> | HTTPQuery, keyPrefix: string = ''): string {
|
function querystringSingleKey(key: string, value: string | number | null | undefined | boolean | Array<string | number | null | boolean> | Set<string | number | null | boolean> | HTTPQuery, keyPrefix: string = ''): string {
|
||||||
const fullKey = keyPrefix + (keyPrefix.length ? `[${key}]` : key);
|
const fullKey = keyPrefix + (keyPrefix.length ? `[${key}]` : key);
|
||||||
if (value instanceof Array) {
|
if (value instanceof Array) {
|
||||||
const multiValue = value.map(singleValue => encodeURIComponent(String(singleValue)))
|
const multiValue = value.map(singleValue => encodeURIComponent(String(singleValue)))
|
||||||
.join(`&${encodeURIComponent(fullKey)}=`);
|
.join(`&${encodeURIComponent(fullKey)}=`);
|
||||||
return `${encodeURIComponent(fullKey)}=${multiValue}`;
|
return `${encodeURIComponent(fullKey)}=${multiValue}`;
|
||||||
}
|
}
|
||||||
if (value instanceof Set) {
|
if (value instanceof Set) {
|
||||||
const valueAsArray = Array.from(value);
|
const valueAsArray = Array.from(value);
|
||||||
return querystringSingleKey(key, valueAsArray, keyPrefix);
|
return querystringSingleKey(key, valueAsArray, keyPrefix);
|
||||||
}
|
}
|
||||||
if (value instanceof Date) {
|
if (value instanceof Date) {
|
||||||
return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`;
|
return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`;
|
||||||
}
|
}
|
||||||
if (value instanceof Object) {
|
if (value instanceof Object) {
|
||||||
return querystring(value as HTTPQuery, fullKey);
|
return querystring(value as HTTPQuery, fullKey);
|
||||||
}
|
}
|
||||||
return `${encodeURIComponent(fullKey)}=${encodeURIComponent(String(value))}`;
|
return `${encodeURIComponent(fullKey)}=${encodeURIComponent(String(value))}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapValues(data: any, fn: (item: any) => any) {
|
export function mapValues(data: any, fn: (item: any) => any) {
|
||||||
return Object.keys(data).reduce(
|
return Object.keys(data).reduce(
|
||||||
(acc, key) => ({ ...acc, [key]: fn(data[key]) }),
|
(acc, key) => ({ ...acc, [key]: fn(data[key]) }),
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function canConsumeForm(consumes: Consume[]): boolean {
|
export function canConsumeForm(consumes: Consume[]): boolean {
|
||||||
for (const consume of consumes) {
|
for (const consume of consumes) {
|
||||||
if ('multipart/form-data' === consume.contentType) {
|
if ('multipart/form-data' === consume.contentType) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Consume {
|
export interface Consume {
|
||||||
contentType: string;
|
contentType: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RequestContext {
|
export interface RequestContext {
|
||||||
fetch: FetchAPI;
|
fetch: FetchAPI;
|
||||||
url: string;
|
url: string;
|
||||||
init: RequestInit;
|
init: RequestInit;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResponseContext {
|
export interface ResponseContext {
|
||||||
fetch: FetchAPI;
|
fetch: FetchAPI;
|
||||||
url: string;
|
url: string;
|
||||||
init: RequestInit;
|
init: RequestInit;
|
||||||
response: Response;
|
response: Response;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ErrorContext {
|
export interface ErrorContext {
|
||||||
fetch: FetchAPI;
|
fetch: FetchAPI;
|
||||||
url: string;
|
url: string;
|
||||||
init: RequestInit;
|
init: RequestInit;
|
||||||
error: unknown;
|
error: unknown;
|
||||||
response?: Response;
|
response?: Response;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Middleware {
|
export interface Middleware {
|
||||||
pre?(context: RequestContext): Promise<FetchParams | void>;
|
pre?(context: RequestContext): Promise<FetchParams | void>;
|
||||||
post?(context: ResponseContext): Promise<Response | void>;
|
post?(context: ResponseContext): Promise<Response | void>;
|
||||||
onError?(context: ErrorContext): Promise<Response | void>;
|
onError?(context: ErrorContext): Promise<Response | void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiResponse<T> {
|
export interface ApiResponse<T> {
|
||||||
raw: Response;
|
raw: Response;
|
||||||
value(): Promise<T>;
|
value(): Promise<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResponseTransformer<T> {
|
export interface ResponseTransformer<T> {
|
||||||
(json: any): T;
|
(json: any): 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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
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,24 +1,28 @@
|
|||||||
import { createLogger, format, transports } from "winston"
|
import { createLogger, format, transports } from "winston"
|
||||||
import { config } from "./configuration"
|
import { config } from "./configuration"
|
||||||
|
import { v4 } from "uuid"
|
||||||
|
export function newRequestId() { return v4() }
|
||||||
|
export const noGuildId = 'NoGuildId'
|
||||||
|
|
||||||
|
|
||||||
const printFn = format.printf(({ guildId, level, message, errorCode, requestId, timestamp: logTimestamp }: { [k: string]: string }) => {
|
const printFn = format.printf(({ guildId, level, message, errorCode, requestId, timestamp: logTimestamp }: { [k: string]: string }) => {
|
||||||
return `[${guildId ?? ''}][${level}][${logTimestamp}][${errorCode ?? ''}][${requestId ?? ''}]:${message}`
|
return `[${guildId ?? ''}][${level.padStart(5, " ")}][${logTimestamp}][${errorCode ?? ''}][${requestId ?? ''}]:${message}`
|
||||||
})
|
})
|
||||||
|
|
||||||
const logFormat = format.combine(
|
const logFormat = format.combine(
|
||||||
format.timestamp(),
|
format.timestamp(),
|
||||||
printFn
|
printFn
|
||||||
)
|
)
|
||||||
|
|
||||||
const consoleTransports = [
|
const consoleTransports = [
|
||||||
new transports.Console({
|
new transports.Console({
|
||||||
format: logFormat
|
format: logFormat,
|
||||||
})
|
silent: process.env.NODE_ENV === 'testing'
|
||||||
|
})
|
||||||
]
|
]
|
||||||
export const logger = createLogger({
|
export const logger = createLogger({
|
||||||
level: config.bot.debug ? 'debug' : 'info',
|
level: config.bot.debug ? 'debug' : 'info',
|
||||||
format: logFormat,
|
format: logFormat,
|
||||||
silent: config.bot.silent,
|
silent: config.bot.silent,
|
||||||
transports: consoleTransports
|
transports: consoleTransports
|
||||||
})
|
})
|
||||||
|
@ -8,174 +8,193 @@ 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 "../..";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export class ExtendedClient extends Client {
|
export class ExtendedClient extends Client {
|
||||||
private eventFilePath = `${__dirname}/../events`
|
private eventFilePath = `${__dirname}/../events`
|
||||||
private commandFilePath = `${__dirname}/../commands`
|
private commandFilePath = `${__dirname}/../commands`
|
||||||
private jellyfin: JellyfinHandler
|
private jellyfin: JellyfinHandler
|
||||||
public commands: Collection<string, CommandType> = new Collection()
|
public voteController: VoteController = new VoteController(this, yavinJellyfinHandler)
|
||||||
private announcementChannels: Collection<string, TextChannel> = new Collection() //guildId to TextChannel
|
public commands: Collection<string, CommandType> = new Collection()
|
||||||
private announcementRoleHandlerTask: Collection<string, ScheduledTask> = new Collection() //one task per guild
|
private announcementChannels: Collection<string, TextChannel> = new Collection() //guildId to TextChannel
|
||||||
private pollCloseBackgroundTasks: Collection<string, ScheduledTask> = new Collection()
|
private announcementRoleHandlerTask: Collection<string, ScheduledTask> = new Collection() //one task per guild
|
||||||
public constructor(jf: JellyfinHandler) {
|
private pollCloseBackgroundTasks: Collection<string, ScheduledTask> = new Collection()
|
||||||
const intents: IntentsBitField = new IntentsBitField()
|
public constructor(jf: JellyfinHandler) {
|
||||||
intents.add(IntentsBitField.Flags.GuildMembers, IntentsBitField.Flags.MessageContent, IntentsBitField.Flags.Guilds, IntentsBitField.Flags.DirectMessages, IntentsBitField.Flags.GuildScheduledEvents, IntentsBitField.Flags.GuildVoiceStates)
|
const intents: IntentsBitField = new IntentsBitField()
|
||||||
const options: ClientOptions = { intents }
|
intents.add(IntentsBitField.Flags.GuildMembers, IntentsBitField.Flags.MessageContent, IntentsBitField.Flags.Guilds, IntentsBitField.Flags.DirectMessages, IntentsBitField.Flags.GuildScheduledEvents, IntentsBitField.Flags.GuildMessageReactions, IntentsBitField.Flags.GuildVoiceStates)
|
||||||
super(options)
|
const options: ClientOptions = { intents }
|
||||||
this.jellyfin = jf
|
super(options)
|
||||||
}
|
this.jellyfin = jf
|
||||||
public async start() {
|
}
|
||||||
if (process.env.NODE_ENV === 'test') return
|
public async start() {
|
||||||
const promises: Promise<any>[] = []
|
if (process.env.NODE_ENV === 'test') return
|
||||||
promises.push(this.registerSlashCommands())
|
const promises: Promise<any>[] = []
|
||||||
promises.push(this.registerEventCallback())
|
promises.push(this.registerSlashCommands())
|
||||||
Promise.all(promises).then(() => {
|
promises.push(this.registerEventCallback())
|
||||||
this.login(config.bot.token)
|
Promise.all(promises).then(() => {
|
||||||
})
|
this.login(config.bot.token)
|
||||||
}
|
})
|
||||||
private async importFile(filepath: string): Promise<any> {
|
}
|
||||||
logger.debug(`Importing ${filepath}`)
|
private async importFile(filepath: string): Promise<any> {
|
||||||
const imported = await import(filepath)
|
logger.debug(`Importing ${filepath}`)
|
||||||
logger.debug(`Imported ${JSON.stringify(imported)}`)
|
const imported = await import(filepath)
|
||||||
return imported.default ?? imported
|
logger.debug(`Imported ${JSON.stringify(imported)}`)
|
||||||
}
|
return imported.default ?? imported
|
||||||
public async registerCommands(cmds: ApplicationCommandDataResolvable[], guildIds: Collection<Snowflake, Guild>) {
|
}
|
||||||
if (guildIds) {
|
public async registerCommands(cmds: ApplicationCommandDataResolvable[], guildIds: Collection<Snowflake, Guild>) {
|
||||||
guildIds.forEach(guild => {
|
if (guildIds) {
|
||||||
this.guilds.cache.get(guild.id)?.commands.set(cmds)
|
guildIds.forEach(guild => {
|
||||||
logger.info(`Registering commands to ${guild.name}|${guild.id}`)
|
this.guilds.cache.get(guild.id)?.commands.set(cmds)
|
||||||
})
|
logger.info(`Registering commands to ${guild.name}|${guild.id}`)
|
||||||
} else {
|
})
|
||||||
this.application?.commands.set(cmds)
|
} else {
|
||||||
logger.info(`Registering global commands`)
|
this.application?.commands.set(cmds)
|
||||||
}
|
logger.info(`Registering global commands`)
|
||||||
return
|
}
|
||||||
}
|
return
|
||||||
public async registerSlashCommands(): Promise<void> {
|
}
|
||||||
try {
|
public async registerSlashCommands(): Promise<void> {
|
||||||
const slashCommands: ApplicationCommandDataResolvable[] = []
|
try {
|
||||||
const commandFiles = fs.readdirSync(this.commandFilePath).filter(file => file.endsWith('.ts') || file.endsWith('.js'))
|
const slashCommands: ApplicationCommandDataResolvable[] = []
|
||||||
for (const commandFile of commandFiles) {
|
const commandFiles = fs.readdirSync(this.commandFilePath).filter(file => file.endsWith('.ts') || file.endsWith('.js'))
|
||||||
const filePath = `${this.commandFilePath}/${commandFile}`
|
for (const commandFile of commandFiles) {
|
||||||
const command = await this.importFile(filePath)
|
const filePath = `${this.commandFilePath}/${commandFile}`
|
||||||
logger.debug(JSON.stringify(command))
|
const command = await this.importFile(filePath)
|
||||||
if (!command.name) return
|
logger.debug(JSON.stringify(command))
|
||||||
this.commands.set(command.name, command)
|
if (!command.name) return
|
||||||
slashCommands.push(command)
|
this.commands.set(command.name, command)
|
||||||
}
|
slashCommands.push(command)
|
||||||
this.on("ready", async (client: Client) => {
|
}
|
||||||
//logger.info(`Ready processing ${JSON.stringify(client)}`)
|
this.on("ready", async (client: Client) => {
|
||||||
logger.info(`SlashCommands: ${JSON.stringify(slashCommands)}`)
|
//logger.info(`Ready processing ${JSON.stringify(client)}`)
|
||||||
const guilds = client.guilds.cache
|
logger.info(`SlashCommands: ${JSON.stringify(slashCommands)}`)
|
||||||
|
const guilds = client.guilds.cache
|
||||||
|
|
||||||
this.registerCommands(slashCommands, guilds)
|
this.registerCommands(slashCommands, guilds)
|
||||||
this.cacheUsers(guilds)
|
this.cacheUsers(guilds)
|
||||||
await this.cacheAnnouncementServer(guilds)
|
await this.cacheAnnouncementServer(guilds)
|
||||||
this.startAnnouncementRoleBackgroundTask(guilds)
|
this.fetchAnnouncementChannelMessage(this.announcementChannels)
|
||||||
this.startPollCloseBackgroundTasks()
|
this.startAnnouncementRoleBackgroundTask(guilds)
|
||||||
})
|
this.startPollCloseBackgroundTasks()
|
||||||
} catch (error) {
|
})
|
||||||
logger.info(`Error refreshing slash commands: ${error}`)
|
} catch (error) {
|
||||||
}
|
logger.info(`Error refreshing slash commands: ${error}`)
|
||||||
}
|
}
|
||||||
private async cacheAnnouncementServer(guilds: Collection<Snowflake, Guild>) {
|
}
|
||||||
for (const guild of guilds.values()) {
|
/**
|
||||||
const channels: TextChannel[] = <TextChannel[]>(await guild.channels.fetch())
|
* Fetches all messages from the provided channel collection.
|
||||||
?.filter(channel => channel?.id === config.bot.announcement_channel_id)
|
* This is necessary for announcementChannels, because 'old' messages don't receive
|
||||||
.map((value) => value)
|
* 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>) {
|
||||||
|
for (const guild of guilds.values()) {
|
||||||
|
const channels: TextChannel[] = <TextChannel[]>(await guild.channels.fetch())
|
||||||
|
?.filter(channel => channel?.id === config.bot.announcement_channel_id)
|
||||||
|
.map((value) => value)
|
||||||
|
|
||||||
if (!channels || channels.length != 1) {
|
if (!channels || channels.length != 1) {
|
||||||
logger.error(`Could not find announcement channel for guild ${guild.name} with guildId ${guild.id}. Found ${channels}`)
|
logger.error(`Could not find announcement channel for guild ${guild.name} with guildId ${guild.id}. Found ${channels}`)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
logger.info(`Fetched announcement channel: ${JSON.stringify(channels[0])}`)
|
logger.info(`Fetched announcement channel: ${JSON.stringify(channels[0])}`)
|
||||||
this.announcementChannels.set(guild.id, channels[0])
|
this.announcementChannels.set(guild.id, channels[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public getAnnouncementChannelForGuild(guildId: string): Maybe<TextChannel> {
|
public getAnnouncementChannelForGuild(guildId: string): Maybe<TextChannel> {
|
||||||
return this.announcementChannels.get(guildId)
|
return this.announcementChannels.get(guildId)
|
||||||
}
|
}
|
||||||
public async cacheUsers(guilds: Collection<Snowflake, Guild>) {
|
public async cacheUsers(guilds: Collection<Snowflake, Guild>) {
|
||||||
guilds.forEach((guild: Guild, id: Snowflake) => {
|
guilds.forEach((guild: Guild, id: Snowflake) => {
|
||||||
logger.info(`Fetching members for ${guild.name}|${id}`)
|
logger.info(`Fetching members for ${guild.name}|${id}`)
|
||||||
guild.members.fetch()
|
guild.members.fetch()
|
||||||
logger.info(`Fetched: ${guild.memberCount} members`)
|
logger.info(`Fetched: ${guild.memberCount} members`)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
public async registerEventCallback() {
|
public async registerEventCallback() {
|
||||||
try {
|
try {
|
||||||
const eventFiles = fs.readdirSync(this.eventFilePath).filter(file => file.endsWith('.ts') || file.endsWith('.js'));
|
const eventFiles = fs.readdirSync(this.eventFilePath).filter(file => file.endsWith('.ts') || file.endsWith('.js'));
|
||||||
for (const file of eventFiles) {
|
for (const file of eventFiles) {
|
||||||
const filePath = `${this.eventFilePath}/${file}`
|
const filePath = `${this.eventFilePath}/${file}`
|
||||||
const event = await this.importFile(filePath)
|
const event = await this.importFile(filePath)
|
||||||
if (event.once) {
|
if (event.once) {
|
||||||
logger.info(`Registering once ${file}`)
|
logger.info(`Registering once ${file}`)
|
||||||
this.once(event.name, (...args: any[]) => event.execute(...args))
|
this.once(event.name, (...args: any[]) => event.execute(...args))
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
logger.info(`Registering on ${file}`)
|
logger.info(`Registering on ${file}`)
|
||||||
this.on(event.name, (...args: any[]) => event.execute(...args))
|
this.on(event.name, (...args: any[]) => event.execute(...args))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.info(`Registered event names ${this.eventNames()}`)
|
logger.info(`Registered event names ${this.eventNames()}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error)
|
logger.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async startAnnouncementRoleBackgroundTask(guilds: Collection<string, Guild>) {
|
public async startAnnouncementRoleBackgroundTask(guilds: Collection<string, Guild>) {
|
||||||
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 })
|
||||||
return
|
return
|
||||||
} else if (messages.size == 0) {
|
} else if (messages.size == 0) {
|
||||||
logger.error("Could not find any pinned announcement messages. Unable to manage roles!", { guildId: guild.id, requestId })
|
logger.error("Could not find any pinned announcement messages. Unable to manage roles!", { guildId: guild.id, requestId })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = await messages.at(0)?.fetch()
|
const message = await messages.at(0)?.fetch()
|
||||||
if (!message) {
|
if (!message) {
|
||||||
logger.error(`No pinned message found`, { guildId: guild.id, requestId })
|
logger.error(`No pinned message found`, { guildId: guild.id, requestId })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
//logger.debug(`Message: ${JSON.stringify(message, null, 2)}`, { guildId: guild.id, requestId })
|
//logger.debug(`Message: ${JSON.stringify(message, null, 2)}`, { guildId: guild.id, requestId })
|
||||||
|
|
||||||
const reactions = message.reactions.resolve("🎫")
|
const reactions = message.reactions.resolve("🎫")
|
||||||
//logger.debug(`reactions: ${JSON.stringify(reactions, null, 2)}`, { guildId: guild.id, requestId })
|
//logger.debug(`reactions: ${JSON.stringify(reactions, null, 2)}`, { guildId: guild.id, requestId })
|
||||||
if (reactions) {
|
if (reactions) {
|
||||||
manageAnnouncementRoles(message.guild, reactions, requestId)
|
manageAnnouncementRoles(message.guild, reactions, requestId)
|
||||||
} else {
|
} else {
|
||||||
logger.error("Did not get reactions! Aborting!", { guildId: guild.id, requestId })
|
logger.error("Did not get reactions! Aborting!", { guildId: guild.id, requestId })
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public stopAnnouncementRoleBackgroundTask(guildId: string, requestId: string) {
|
public stopAnnouncementRoleBackgroundTask(guildId: string, requestId: string) {
|
||||||
const task: Maybe<ScheduledTask> = this.announcementRoleHandlerTask.get(guildId)
|
const task: Maybe<ScheduledTask> = this.announcementRoleHandlerTask.get(guildId)
|
||||||
if (!task) {
|
if (!task) {
|
||||||
logger.error(`No task found for guildID ${guildId}.`, { guildId, requestId })
|
logger.error(`No task found for guildID ${guildId}.`, { guildId, requestId })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
task.stop()
|
task.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
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])))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { ClientEvents } from "discord.js";
|
import { ClientEvents } from "discord.js";
|
||||||
|
|
||||||
export class Event<Key extends keyof ClientEvents>{
|
export class Event<Key extends keyof ClientEvents>{
|
||||||
constructor(
|
constructor(
|
||||||
public event: Key,
|
public event: Key,
|
||||||
public run: (...args: ClientEvents[Key]) => unknown
|
public run: (...args: ClientEvents[Key]) => unknown
|
||||||
) { }
|
) { }
|
||||||
}
|
}
|
||||||
|
81
tests/discord/noneofthat.test.ts
Normal file
81
tests/discord/noneofthat.test.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { Guild, GuildScheduledEvent, Message } from "discord.js"
|
||||||
|
import VoteController from "../../server/helper/vote.controller"
|
||||||
|
import { JellyfinHandler } from "../../server/jellyfin/handler"
|
||||||
|
import { ExtendedClient } from "../../server/structures/client"
|
||||||
|
import { Emoji, NONE_OF_THAT } from "../../server/constants"
|
||||||
|
import { isVoteMessage } from "../../server/helper/messageIdentifiers"
|
||||||
|
|
||||||
|
describe('vote controller - none_of_that functions', () => {
|
||||||
|
const testEventId = '1234321'
|
||||||
|
const testEventDate = new Date('2023-01-01')
|
||||||
|
const testGuildId = "888999888"
|
||||||
|
const testMovies = [
|
||||||
|
'Movie1',
|
||||||
|
'Movie2',
|
||||||
|
'Movie3',
|
||||||
|
'Movie4',
|
||||||
|
'Movie5',
|
||||||
|
]
|
||||||
|
const votesList = [
|
||||||
|
{ emote: Emoji.one, count: 1, movie: testMovies[0] },
|
||||||
|
{ emote: Emoji.two, count: 2, movie: testMovies[1] },
|
||||||
|
{ emote: Emoji.three, count: 3, movie: testMovies[2] },
|
||||||
|
{ emote: Emoji.four, count: 1, movie: testMovies[3] },
|
||||||
|
{ emote: Emoji.five, count: 1, movie: testMovies[4] },
|
||||||
|
{ emote: NONE_OF_THAT, count: 2, movie: NONE_OF_THAT },
|
||||||
|
]
|
||||||
|
const mockClient: ExtendedClient = <ExtendedClient><unknown>{
|
||||||
|
user: {
|
||||||
|
id: 'mockId'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const mockEvent: GuildScheduledEvent = <GuildScheduledEvent><unknown>{
|
||||||
|
scheduledStartAt: testEventDate,
|
||||||
|
id: testEventId,
|
||||||
|
guild: testGuildId
|
||||||
|
}
|
||||||
|
const mockJellyfinHandler: JellyfinHandler = <JellyfinHandler><unknown>{
|
||||||
|
getRandomMovieNames: jest.fn().mockReturnValue(["movie1"])
|
||||||
|
}
|
||||||
|
const votes = new VoteController(mockClient, mockJellyfinHandler)
|
||||||
|
const mockMessageContent = votes.createVoteMessageText(mockEvent, testMovies, testGuildId, "requestId")
|
||||||
|
|
||||||
|
test('sendVoteClosedMessage', async () => {
|
||||||
|
mockClient.getAnnouncementChannelForGuild = jest.fn().mockReturnValue({
|
||||||
|
send: jest.fn().mockImplementation((options: any) => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
resolve(options)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const scheduledEvent: GuildScheduledEvent = <GuildScheduledEvent>{
|
||||||
|
scheduledStartAt: testEventDate,
|
||||||
|
guildId: testGuildId,
|
||||||
|
id: testEventId
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await votes.sendVoteClosedMessage(scheduledEvent, 'MovieNew', 'guild', 'request')
|
||||||
|
expect(res).toEqual({
|
||||||
|
allowedMentions: {
|
||||||
|
parse: ["roles"]
|
||||||
|
},
|
||||||
|
content: `[Abstimmung beendet] für https://discord.com/events/${testGuildId}/${testEventId}\n<@&WATCHPARTY_ANNOUNCEMENT_ROLE> Wir gucken MovieNew am 01.01. um 01:00`
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('getVotesByEmote', async () => {
|
||||||
|
const mockMessage: Message = <Message><unknown>{
|
||||||
|
cleanContent: mockMessageContent,
|
||||||
|
reactions: {
|
||||||
|
resolve: jest.fn().mockImplementation((input: any) => {
|
||||||
|
return votesList.find(e => e.emote === input)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isVoteMessage(mockMessage)) {
|
||||||
|
const result = await votes.getVotesByEmote(mockMessage, 'guildId', 'requestId')
|
||||||
|
expect(result.length).toEqual(5)
|
||||||
|
expect(result).toEqual(votesList.filter(x => x.movie != NONE_OF_THAT))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
192
tests/discord/votes.test.ts
Normal file
192
tests/discord/votes.test.ts
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
import { Emoji, NONE_OF_THAT } from "../../server/constants"
|
||||||
|
import VoteController, { VoteMessageInfo } from "../../server/helper/vote.controller"
|
||||||
|
import { JellyfinHandler } from "../../server/jellyfin/handler"
|
||||||
|
import { ExtendedClient } from "../../server/structures/client"
|
||||||
|
import { VoteMessage } from "../../server/helper/messageIdentifiers"
|
||||||
|
import { GuildScheduledEvent, MessageReaction } from "discord.js"
|
||||||
|
test('parse votes from vote message', async () => {
|
||||||
|
const testMovies = [
|
||||||
|
'Movie1',
|
||||||
|
'Movie2',
|
||||||
|
'Movie3',
|
||||||
|
'Movie4',
|
||||||
|
'Movie5',
|
||||||
|
]
|
||||||
|
const testEventId = '1234321'
|
||||||
|
const testEventDate = new Date('2023-01-01')
|
||||||
|
const testGuildId = "888999888"
|
||||||
|
const voteController: VoteController = new VoteController(<ExtendedClient>{}, <JellyfinHandler>{})
|
||||||
|
const mockEvent: GuildScheduledEvent = <GuildScheduledEvent><unknown>{
|
||||||
|
scheduledStartAt: testEventDate,
|
||||||
|
id: testEventId,
|
||||||
|
guild: testGuildId
|
||||||
|
}
|
||||||
|
const testMessage = voteController.createVoteMessageText(mockEvent, testMovies, testGuildId, "requestId")
|
||||||
|
|
||||||
|
|
||||||
|
const expectedResult: VoteMessageInfo = {
|
||||||
|
event: mockEvent,
|
||||||
|
votes: [
|
||||||
|
{ emote: Emoji.one, count: 1, movie: testMovies[0] },
|
||||||
|
{ emote: Emoji.two, count: 2, movie: testMovies[1] },
|
||||||
|
{ emote: Emoji.three, count: 3, movie: testMovies[2] },
|
||||||
|
{ emote: Emoji.four, count: 1, movie: testMovies[3] },
|
||||||
|
{ emote: Emoji.five, count: 1, movie: testMovies[4] },
|
||||||
|
{ emote: NONE_OF_THAT, count: 1, movie: NONE_OF_THAT },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const message: VoteMessage = <VoteMessage><unknown>{
|
||||||
|
cleanContent: testMessage,
|
||||||
|
guild: {
|
||||||
|
id: testGuildId,
|
||||||
|
scheduledEvents: {
|
||||||
|
fetch: jest.fn().mockImplementation((input: any) => {
|
||||||
|
if (input === testEventId)
|
||||||
|
return {
|
||||||
|
id: testEventId,
|
||||||
|
guild: testGuildId,
|
||||||
|
scheduledStartAt: testEventDate
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reactions: {
|
||||||
|
cache: {
|
||||||
|
get: jest.fn().mockImplementation((input: any) => {
|
||||||
|
// Abusing duck typing
|
||||||
|
// Message Reaction has a method `count` and the expected votes
|
||||||
|
// have a field `count`
|
||||||
|
// this will evaluate to the same 'result'
|
||||||
|
return expectedResult.votes.find(e => e.emote === input)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await voteController.parseVoteInfoFromVoteMessage(message, 'requestId')
|
||||||
|
console.log(JSON.stringify(result))
|
||||||
|
expect(Array.isArray(result)).toBe(false)
|
||||||
|
expect(result.event.id).toEqual(testEventId)
|
||||||
|
expect(result.event.scheduledStartAt).toEqual(testEventDate)
|
||||||
|
expect(result.votes.length).toEqual(expectedResult.votes.length)
|
||||||
|
expect(result).toEqual(expectedResult)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('parse votes from vote message', () => {
|
||||||
|
const testMovies = [
|
||||||
|
'Movie1',
|
||||||
|
'Movie2',
|
||||||
|
'Movie3',
|
||||||
|
'Movie4',
|
||||||
|
'Movie5',
|
||||||
|
]
|
||||||
|
const testEventId = '1234321'
|
||||||
|
const testEventDate = new Date('2023-01-01')
|
||||||
|
const testGuildId = "888999888"
|
||||||
|
const voteController: VoteController = new VoteController(<ExtendedClient>{}, <JellyfinHandler>{})
|
||||||
|
const mockEvent: GuildScheduledEvent = <GuildScheduledEvent><unknown>{
|
||||||
|
scheduledStartAt: testEventDate,
|
||||||
|
id: testEventId,
|
||||||
|
guild: testGuildId
|
||||||
|
}
|
||||||
|
const testMessage = voteController.createVoteMessageText(mockEvent, testMovies, testGuildId, "requestId")
|
||||||
|
|
||||||
|
const result = voteController.parseGuildIdAndEventIdFromWholeMessage(testMessage)
|
||||||
|
expect(result).toEqual({ guildId: testGuildId, eventId: testEventId })
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
test.skip('handles complete none_of_that vote', () => {
|
||||||
|
|
||||||
|
const mockJellyfinHandler: JellyfinHandler = <JellyfinHandler><unknown>{
|
||||||
|
getRandomMovieNames: jest.fn().mockReturnValue(["movie1"])
|
||||||
|
}
|
||||||
|
|
||||||
|
const testMovies = [
|
||||||
|
'Movie1',
|
||||||
|
'Movie2',
|
||||||
|
'Movie3',
|
||||||
|
'Movie4',
|
||||||
|
'Movie5',
|
||||||
|
]
|
||||||
|
const testEventId = '1234321'
|
||||||
|
const testEventDate = new Date('2023-01-01')
|
||||||
|
const testGuildId = "888999888"
|
||||||
|
const mockClient: ExtendedClient = <ExtendedClient><unknown>{
|
||||||
|
user: {
|
||||||
|
id: 'mockId'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const voteController = new VoteController(mockClient, mockJellyfinHandler)
|
||||||
|
const mockEvent: GuildScheduledEvent = <GuildScheduledEvent><unknown>{
|
||||||
|
scheduledStartAt: testEventDate,
|
||||||
|
id: testEventId,
|
||||||
|
guild: testGuildId
|
||||||
|
}
|
||||||
|
const mockMessageContent = voteController.createVoteMessageText(mockEvent, testMovies, testGuildId, "requestId")
|
||||||
|
const reactedUponMessage: VoteMessage = <VoteMessage><unknown>{
|
||||||
|
cleanContent: mockMessageContent,
|
||||||
|
guild: {
|
||||||
|
id: 'id',
|
||||||
|
roles: {
|
||||||
|
resolve: jest.fn().mockReturnValue({
|
||||||
|
members: [{}, {}, {}, {}, {}]//content does not matter
|
||||||
|
})
|
||||||
|
},
|
||||||
|
scheduledEvents: {
|
||||||
|
fetch: jest.fn().mockReturnValue([
|
||||||
|
{
|
||||||
|
name: 'voting offen'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
unpin: jest.fn().mockImplementation(() => {
|
||||||
|
|
||||||
|
}),
|
||||||
|
delete: jest.fn().mockImplementation(() => {
|
||||||
|
|
||||||
|
}),
|
||||||
|
reactions: {
|
||||||
|
resolve: jest.fn().mockImplementation((input: any) => {
|
||||||
|
console.log(JSON.stringify(input))
|
||||||
|
}),
|
||||||
|
cache: {
|
||||||
|
get: jest.fn().mockReturnValue({
|
||||||
|
users: {
|
||||||
|
cache: [
|
||||||
|
{
|
||||||
|
id: "mockId"//to filter out
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "userId1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "userId2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "userId3"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const messageReaction: MessageReaction = <MessageReaction><unknown>{
|
||||||
|
message: reactedUponMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
mockClient.getAnnouncementChannelForGuild = jest.fn().mockReturnValue({
|
||||||
|
messages: {
|
||||||
|
fetch: jest.fn().mockReturnValue([
|
||||||
|
reactedUponMessage
|
||||||
|
])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = voteController.handleNoneOfThatVote(messageReaction, reactedUponMessage, 'requestId', 'guildId')
|
||||||
|
|
||||||
|
|
||||||
|
})
|
15
tests/helpers/date.test.ts
Normal file
15
tests/helpers/date.test.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { createDateStringFromEvent } from "../../server/helper/dateHelper"
|
||||||
|
import MockDate from 'mockdate'
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
MockDate.set('01-01-2023')
|
||||||
|
})
|
||||||
|
|
||||||
|
function getTestDate(date: string): Date {
|
||||||
|
return new Date(date)
|
||||||
|
}
|
||||||
|
test('createDateStringFromEvent - correct formatting', () => {
|
||||||
|
expect(createDateStringFromEvent(getTestDate('01-01-2023 12:30'), "")).toEqual('heute um 12:30')
|
||||||
|
expect(createDateStringFromEvent(getTestDate('01-02-2023 12:30'), "")).toEqual('am Montag 02.01. um 12:30')
|
||||||
|
expect(createDateStringFromEvent(getTestDate('01-03-2023 12:30'), "")).toEqual('am Dienstag 03.01. um 12:30')
|
||||||
|
})
|
28
tests/helpers/memberRoles.test.ts
Normal file
28
tests/helpers/memberRoles.test.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { Collection, GuildMember, Role } from "discord.js"
|
||||||
|
import { filterRolesFromMemberUpdate } from "../../server/helper/roleFilter"
|
||||||
|
|
||||||
|
function buildFakeRole(id: string, name: string): Role {
|
||||||
|
return <Role>{ id, name }
|
||||||
|
|
||||||
|
}
|
||||||
|
test('filterRolesFromMemberUpdate', () => {
|
||||||
|
const oldMemberRoles: Collection<string, Role> = new Collection<string, Role>()
|
||||||
|
oldMemberRoles.set('1', buildFakeRole('01', 'Role01'))
|
||||||
|
oldMemberRoles.set('2', buildFakeRole('02', 'Role02'))
|
||||||
|
|
||||||
|
const newMemberRoles: Collection<string, Role> = new Collection<string, Role>()
|
||||||
|
newMemberRoles.set('1', buildFakeRole('01', 'Role01'))
|
||||||
|
newMemberRoles.set('2', buildFakeRole('02', 'Role02'))
|
||||||
|
newMemberRoles.set('3', buildFakeRole('03', 'Role03'))
|
||||||
|
|
||||||
|
const oldMember: GuildMember = <GuildMember>{ roles: { cache: oldMemberRoles }, guild: { id: "guildid" } }
|
||||||
|
const newMember: GuildMember = <GuildMember>{ roles: { cache: newMemberRoles }, guild: { id: "guildid" } }
|
||||||
|
const output = filterRolesFromMemberUpdate(oldMember, newMember)
|
||||||
|
|
||||||
|
const expectedAddedRoles: Collection<string, Role> = new Collection<string, Role>()
|
||||||
|
expectedAddedRoles.set('3', buildFakeRole('03', 'Role03'))
|
||||||
|
const expectedRemovedRoles: Collection<string, Role> = new Collection<string, Role>()
|
||||||
|
|
||||||
|
expect(output.addedRoles).toEqual(expectedAddedRoles)
|
||||||
|
expect(output.removedRoles).toEqual(expectedRemovedRoles)
|
||||||
|
})
|
15
tests/testenv.js
Normal file
15
tests/testenv.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
process.env.CLIENT_ID = "CLIENT_ID"
|
||||||
|
process.env.SECRET = "SECRET"
|
||||||
|
process.env.BOT_TOKEN = "BOT_TOKEN"
|
||||||
|
process.env.WATCHER_ROLE = "WATCHER_ROLE"
|
||||||
|
process.env.ADMIN_ROLE = "ADMIN_ROLE"
|
||||||
|
process.env.CHANNEL_ID = "CHANNEL_ID"
|
||||||
|
process.env.WATCHPARTY_ANNOUNCEMENT_ROLE = "WATCHPARTY_ANNOUNCEMENT_ROLE"
|
||||||
|
process.env.YAVIN_JELLYFIN_URL = "YAVIN_JELLYFIN_URL"
|
||||||
|
process.env.YAVIN_COLLECTION_ID = "YAVIN_COLLECTION_ID"
|
||||||
|
process.env.YAVIN_COLLECTION_USER = "YAVIN_COLLECTION_USER"
|
||||||
|
process.env.YAVIN_TOKEN = "YAVIN_TOKEN"
|
||||||
|
process.env.TOKEN = "TOKEN"
|
||||||
|
process.env.JELLYFIN_USER = "JELLYFIN_USER"
|
||||||
|
process.env.JELLYFIN_COLLECTION_ID = "JELLYFIN_COLLECTION_ID"
|
||||||
|
process.env.JELLYFIN_URL = "JELLYFIN_URL"
|
105
tsconfig.json
105
tsconfig.json
@ -1,63 +1,46 @@
|
|||||||
{
|
{
|
||||||
"extends":"@tsconfig/recommended/tsconfig.json",
|
"extends": "@tsconfig/recommended/tsconfig.json",
|
||||||
"exclude":["node_modules"],
|
"exclude": [
|
||||||
"compilerOptions": {
|
"node_modules"
|
||||||
/* Basic Options */
|
],
|
||||||
"target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
|
"compilerOptions": {
|
||||||
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
|
/* Basic Options */
|
||||||
"resolveJsonModule": true,
|
"target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
|
||||||
// "lib": [], /* Specify library files to be included in the compilation. */
|
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
|
||||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
"resolveJsonModule": true,
|
||||||
// "checkJs": true, /* Report errors in .js files. */
|
"outDir": "./build" /* Redirect output structure to the directory. */,
|
||||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
// "composite": true, /* Enable project compilation */
|
||||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
"removeComments": true, /* Do not emit comments to output. */
|
||||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
/* Strict Type-Checking Options */
|
||||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
"strict": true /* Enable all strict type-checking options. */,
|
||||||
"outDir": "./build" /* Redirect output structure to the directory. */,
|
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||||
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
"strictNullChecks": true, /* Enable strict null checks. */
|
||||||
// "composite": true, /* Enable project compilation */
|
"strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||||
// "removeComments": true, /* Do not emit comments to output. */
|
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||||
// "noEmit": true, /* Do not emit outputs. */
|
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
"alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
//"noUncheckedIndexedAccess": true,
|
||||||
|
/* Additional Checks */
|
||||||
/* Strict Type-Checking Options */
|
//"noUnusedLocals": true, /* Report errors on unused locals. */
|
||||||
"strict": true /* Enable all strict type-checking options. */,
|
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||||
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
/* Module Resolution Options */
|
||||||
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||||
|
"allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||||
/* Additional Checks */
|
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
|
||||||
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
/* Source Map Options */
|
||||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
// "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. */
|
||||||
/* Module Resolution Options */
|
/* Experimental Options */
|
||||||
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||||
// "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. */
|
|
||||||
// "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. */
|
|
||||||
"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. */
|
|
||||||
|
|
||||||
/* Source Map Options */
|
|
||||||
// "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. */
|
|
||||||
"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 */
|
|
||||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
|
||||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user