Compare commits
49 Commits
v1.0.0
...
e7b21fa658
Author | SHA1 | Date | |
---|---|---|---|
e7b21fa658 | |||
2d32f9b680 | |||
5503aa8713 | |||
0e67252976 | |||
fa49dc0f76 | |||
e52e845851 | |||
61544feaba | |||
1966640239 | |||
fa9998e92c | |||
c1a449bafe | |||
d5d82043f0 | |||
51ebf2e939 | |||
f314b2f355 | |||
a4d7c57d10 | |||
2802afa7d5 | |||
3a5ea5d4ff | |||
45d87275bf | |||
31e440434e | |||
3d70b56eb7 | |||
3298c7a244 | |||
5b98c9bf2f | |||
ee363e065c | |||
9af847f234 | |||
a18406e7e4 | |||
b9f65125dc | |||
d61457cb5f | |||
9da8f47784 | |||
e8c58d5ff8 | |||
8569a3e1e6 | |||
8d0dda0fa9 | |||
777ae330ad | |||
111ccaa880 | |||
c00453d3d3 | |||
8a7973a2e3 | |||
0b67b126dd | |||
6d5725be90 | |||
59f5b34e5a | |||
670a64af22 | |||
4cc332820f | |||
f5928049ea | |||
99905f98d0 | |||
07849d331a | |||
1e6a75687a | |||
2c09033c3f | |||
ce4441cee3 | |||
7c8072b295 | |||
7899aac5ce | |||
26c2d91252 | |||
d6300e8bec |
7
.editorconfig
Normal file
7
.editorconfig
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
root = true
|
||||||
|
[*]
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
[*.ts]
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
17
.gitea/workflows/compile.yaml
Normal file
17
.gitea/workflows/compile.yaml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
name: Compile the repository
|
||||||
|
on: [pull_request]
|
||||||
|
env:
|
||||||
|
REGISTRY: gitea.brudi.xyz
|
||||||
|
IMAGE_NAME: ${{ gitea.repository }}
|
||||||
|
USER: ${{ gitea.actor }}
|
||||||
|
jobs:
|
||||||
|
compile:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container: catthehacker/ubuntu:act-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Build Container
|
||||||
|
run: docker build .
|
@ -1,6 +1,9 @@
|
|||||||
name: Build a docker image for node-jellyfin-role-bot
|
name: Build a docker image for node-jellyfin-role-bot
|
||||||
run-name: ${{ gitea.actor }} is building an image
|
run-name: ${{ gitea.actor }} is building an image
|
||||||
on: [push]
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
env:
|
env:
|
||||||
REGISTRY: gitea.brudi.xyz
|
REGISTRY: gitea.brudi.xyz
|
||||||
IMAGE_NAME: ${{ gitea.repository }}
|
IMAGE_NAME: ${{ gitea.repository }}
|
||||||
@ -18,6 +21,8 @@ jobs:
|
|||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry
|
||||||
run: docker login -u ${{ env.USER }} -p ${{ secrets.TOKEN }} ${{ env.REGISTRY }}
|
run: docker login -u ${{ env.USER }} -p ${{ secrets.TOKEN }} ${{ env.REGISTRY }}
|
||||||
- name: Build Container
|
- name: Build Container
|
||||||
run: docker build -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" .
|
run: docker build -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}" .
|
||||||
|
env:
|
||||||
|
version: $(cat package.json | awk 'match($0, /version/) {print $2}' | sed 's/[\",]//g') # extracts the version number from the package.json with bash magic
|
||||||
- name: Push Container
|
- name: Push Container
|
||||||
run: docker push "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest"
|
run: docker push --all-tags "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
|
||||||
|
4
index.ts
4
index.ts
@ -5,8 +5,8 @@ import { JellyfinHandler } from "./server/jellyfin/handler"
|
|||||||
import { attachedImages } from "./server/assets/attachments"
|
import { attachedImages } from "./server/assets/attachments"
|
||||||
const requestId = 'startup'
|
const requestId = 'startup'
|
||||||
|
|
||||||
export const jellyfinHandler = new JellyfinHandler({jellyfinToken: config.bot.workaround_token, jellyfinUrl: config.bot.jellyfin_url, movieCollectionId: config.bot.jf_collection_id, collectionUser: config.bot.jf_user})
|
export const jellyfinHandler = new JellyfinHandler({ jellyfinToken: config.bot.workaround_token, jellyfinUrl: config.bot.jellyfin_url, movieCollectionId: config.bot.jf_collection_id, collectionUser: config.bot.jf_user })
|
||||||
export const yavinJellyfinHandler = new JellyfinHandler({jellyfinToken: config.bot.yavin_jellyfin_token, jellyfinUrl: config.bot.yavin_jellyfin_url, movieCollectionId: config.bot.yavin_collection_id, collectionUser: config.bot.yavin_jellyfin_collection_user})
|
export const yavinJellyfinHandler = new JellyfinHandler({ jellyfinToken: config.bot.yavin_jellyfin_token, jellyfinUrl: config.bot.yavin_jellyfin_url, movieCollectionId: config.bot.yavin_collection_id, collectionUser: config.bot.yavin_jellyfin_collection_user })
|
||||||
|
|
||||||
export const client = new ExtendedClient(jellyfinHandler)
|
export const client = new ExtendedClient(jellyfinHandler)
|
||||||
|
|
||||||
|
19
package-lock.json
generated
19
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "node-jellyfin-discord-bot",
|
"name": "node-jellyfin-discord-bot",
|
||||||
"version": "1.0.0",
|
"version": "1.1.3",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "node-jellyfin-discord-bot",
|
"name": "node-jellyfin-discord-bot",
|
||||||
"version": "1.0.0",
|
"version": "1.1.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordjs/rest": "^1.7.0",
|
"@discordjs/rest": "^1.7.0",
|
||||||
@ -17,6 +17,7 @@
|
|||||||
"@types/uuid": "^9.0.1",
|
"@types/uuid": "^9.0.1",
|
||||||
"axios": "^1.3.5",
|
"axios": "^1.3.5",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
|
"date-fns-tz": "^2.0.0",
|
||||||
"discord-api-types": "^0.37.38",
|
"discord-api-types": "^0.37.38",
|
||||||
"discord.js": "^14.9.0",
|
"discord.js": "^14.9.0",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
@ -2626,6 +2627,14 @@
|
|||||||
"url": "https://opencollective.com/date-fns"
|
"url": "https://opencollective.com/date-fns"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/date-fns-tz": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-OAtcLdB9vxSXTWHdT8b398ARImVwQMyjfYGkKD2zaGpHseG2UPHbHjXELReErZFxWdSLph3c2zOaaTyHfOhERQ==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"date-fns": ">=2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.3.4",
|
"version": "4.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||||
@ -8905,6 +8914,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
|
||||||
"integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA=="
|
"integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA=="
|
||||||
},
|
},
|
||||||
|
"date-fns-tz": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-OAtcLdB9vxSXTWHdT8b398ARImVwQMyjfYGkKD2zaGpHseG2UPHbHjXELReErZFxWdSLph3c2zOaaTyHfOhERQ==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"debug": {
|
"debug": {
|
||||||
"version": "4.3.4",
|
"version": "4.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "node-jellyfin-discord-bot",
|
"name": "node-jellyfin-discord-bot",
|
||||||
"version": "1.0.0",
|
"version": "1.1.3",
|
||||||
"description": "A discord bot to sync jellyfin accounts with discord roles",
|
"description": "A discord bot to sync jellyfin accounts with discord roles",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -13,6 +13,7 @@
|
|||||||
"@types/uuid": "^9.0.1",
|
"@types/uuid": "^9.0.1",
|
||||||
"axios": "^1.3.5",
|
"axios": "^1.3.5",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
|
"date-fns-tz": "^2.0.0",
|
||||||
"discord-api-types": "^0.37.38",
|
"discord-api-types": "^0.37.38",
|
||||||
"discord.js": "^14.9.0",
|
"discord.js": "^14.9.0",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
@ -45,4 +46,4 @@
|
|||||||
"rimraf": "^5.0.0",
|
"rimraf": "^5.0.0",
|
||||||
"ts-jest": "^29.1.0"
|
"ts-jest": "^29.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -8,114 +8,114 @@ import { Command } from '../structures/command'
|
|||||||
import { RunOptions } from '../types/commandTypes'
|
import { RunOptions } from '../types/commandTypes'
|
||||||
|
|
||||||
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 announcementType = command.options.data.find(option => option.name.includes("typ"))
|
|
||||||
logger.info(`Got command for announcing ${announcementType?.value}!`, { guildId, requestId })
|
|
||||||
|
|
||||||
if(!announcementType) {
|
|
||||||
logger.error("Did not get an announcement type!", { guildId, requestId })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isAdmin(command.member)) {
|
|
||||||
logger.info(`Announcement was requested by ${command.member.displayName} but they are not an admin! Not sending announcement.`, { guildId, requestId })
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
logger.info(`User ${command.member.displayName} seems to be admin`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if((<string>announcementType.value).includes("initial")) {
|
|
||||||
sendInitialAnnouncement(guildId, requestId)
|
|
||||||
command.followUp("Ist rausgeschickt!")
|
|
||||||
} else {
|
|
||||||
command.followUp(`${announcementType.value} ist aktuell noch nicht implementiert`)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
const guildId = command.guildId
|
||||||
|
const announcementType = command.options.data.find(option => option.name.includes("typ"))
|
||||||
|
logger.info(`Got command for announcing ${announcementType?.value}!`, { guildId, requestId })
|
||||||
|
|
||||||
|
if (!announcementType) {
|
||||||
|
logger.error("Did not get an announcement type!", { guildId, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAdmin(command.member)) {
|
||||||
|
logger.info(`Announcement was requested by ${command.member.displayName} but they are not an admin! Not sending announcement.`, { guildId, requestId })
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
logger.info(`User ${command.member.displayName} seems to be admin`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((<string>announcementType.value).includes("initial")) {
|
||||||
|
sendInitialAnnouncement(guildId, requestId)
|
||||||
|
command.followUp("Ist rausgeschickt!")
|
||||||
|
} else {
|
||||||
|
command.followUp(`${announcementType.value} ist aktuell noch nicht implementiert`)
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function isAdmin(member: GuildMember): boolean {
|
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 => message.cleanContent.includes("[initial]"))
|
||||||
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,148 +1,184 @@
|
|||||||
|
import { addDays, differenceInDays, format, isAfter, toDate } from 'date-fns'
|
||||||
import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildScheduledEventSetStatusArg, GuildScheduledEventStatus, Message, MessageCreateOptions, TextChannel } from 'discord.js'
|
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 { config } from '../configuration'
|
||||||
import { Emotes } from '../events/guildScheduledEventCreate'
|
import { Emotes } from '../events/autoCreateVoteByWPEvent'
|
||||||
|
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 { format } from 'date-fns'
|
|
||||||
import { Maybe } from '../interfaces'
|
|
||||||
|
|
||||||
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
|
|
||||||
logger.info("Got command for closing poll!", { guildId, requestId })
|
|
||||||
|
|
||||||
command.followUp("Alles klar, beende die Umfrage :)")
|
|
||||||
closePoll(command.guild, requestId)
|
|
||||||
}
|
}
|
||||||
|
const guildId = command.guildId
|
||||||
|
logger.info("Got command for closing poll!", { guildId, requestId })
|
||||||
|
|
||||||
|
command.followUp("Alles klar, beende die Umfrage :)")
|
||||||
|
closePoll(command.guild, requestId)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function closePoll(guild: Guild, requestId: string) {
|
export async function closePoll(guild: Guild, requestId: string) {
|
||||||
const guildId = guild.id
|
const guildId = guild.id
|
||||||
logger.info("stopping poll", { guildId, requestId })
|
logger.info("stopping poll", { guildId, requestId })
|
||||||
|
|
||||||
const announcementChannel: Maybe<TextChannel> = client.getAnnouncementChannelForGuild(guildId)
|
const announcementChannel: Maybe<TextChannel> = client.getAnnouncementChannelForGuild(guildId)
|
||||||
if(!announcementChannel) {
|
if (!announcementChannel) {
|
||||||
logger.error("Could not find the textchannel. Unable to close poll.", { guildId, requestId })
|
logger.error("Could not find the textchannel. Unable to close poll.", { guildId, requestId })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const messages: Message<true>[] = (await announcementChannel.messages.fetch()) //todo: fetch only pinned messages
|
const messages: Message<true>[] = (await announcementChannel.messages.fetch()) //todo: fetch only pinned messages
|
||||||
.map((value) => value)
|
.map((value) => value)
|
||||||
.filter(message => !message.cleanContent.includes("[Abstimmung beendet]") && message.cleanContent.includes("[Abstimmung]"))
|
.filter(message => !message.cleanContent.includes("[Abstimmung beendet]") && message.cleanContent.includes("[Abstimmung]"))
|
||||||
.sort((a, b) => b.createdTimestamp - a.createdTimestamp)
|
.sort((a, b) => b.createdTimestamp - a.createdTimestamp)
|
||||||
|
|
||||||
if (!messages || messages.length <= 0) {
|
if (!messages || messages.length <= 0) {
|
||||||
logger.info("Could not find any vote messages. Cancelling pollClose", { guildId, requestId })
|
logger.info("Could not find any vote messages. Cancelling pollClose", { guildId, requestId })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastMessage: Message<true> = messages[0]
|
const lastMessage: Message<true> = messages[0]
|
||||||
|
|
||||||
logger.debug(`Found messages: ${JSON.stringify(messages, null, 2)}`, { guildId, requestId })
|
logger.debug(`Found messages: ${JSON.stringify(messages, null, 2)}`, { guildId, requestId })
|
||||||
|
|
||||||
logger.debug(`Last message: ${JSON.stringify(lastMessage, null, 2)}`, { guildId, requestId })
|
logger.debug(`Last message: ${JSON.stringify(lastMessage, null, 2)}`, { guildId, requestId })
|
||||||
|
|
||||||
|
|
||||||
const votes = await (await getVotesByEmote(lastMessage, guildId, requestId))
|
const votes = await (await getVotesByEmote(lastMessage, guildId, requestId))
|
||||||
.sort((a, b) => b.count - a.count)
|
.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
|
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) {
|
async function sendVoteClosedMessage(event: GuildScheduledEvent, movie: string, guildId: string, requestId: string) {
|
||||||
const date = event.scheduledStartAt ? format(event.scheduledStartAt, "dd.MM") : "Fehler, event hatte kein Datum"
|
const date = event.scheduledStartAt ? format(event.scheduledStartAt, "dd.MM") : "Fehler, event hatte kein Datum"
|
||||||
const time = event.scheduledStartAt ? format(event.scheduledStartAt, "HH:mm") : "Fehler, event hatte kein Datum"
|
const time = event.scheduledStartAt ? format(event.scheduledStartAt, "HH:mm") : "Fehler, event hatte kein Datum"
|
||||||
const body = `[Abstimmung beendet] <@&${config.bot.announcement_role}> Wir gucken ${movie} am ${date} um ${time}`
|
const body = `[Abstimmung beendet] für https://discord.com/events/${event.guildId}/${event.id}\n<@&${config.bot.announcement_role}> Wir gucken ${movie} am ${date} um ${time}`
|
||||||
const options: MessageCreateOptions = {
|
const options: MessageCreateOptions = {
|
||||||
content: body,
|
content: body,
|
||||||
allowedMentions: { parse: ["roles"] }
|
allowedMentions: { parse: ["roles"] }
|
||||||
}
|
}
|
||||||
const announcementChannel = client.getAnnouncementChannelForGuild(guildId)
|
const announcementChannel = client.getAnnouncementChannelForGuild(guildId)
|
||||||
logger.info("Sending vote closed message.", { guildId, requestId })
|
logger.info("Sending vote closed message.", { guildId, requestId })
|
||||||
if(!announcementChannel) {
|
if (!announcementChannel) {
|
||||||
logger.error("Could not find announcement channel. Please fix!", { guildId, requestId })
|
logger.error("Could not find announcement channel. Please fix!", { guildId, requestId })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
announcementChannel.send(options)
|
announcementChannel.send(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateEvent(voteEvent: GuildScheduledEvent, votes: Vote[], guild: Guild, guildId: string, requestId: string) {
|
async function updateEvent(voteEvent: GuildScheduledEvent, votes: Vote[], guild: Guild, guildId: string, requestId: string) {
|
||||||
logger.info(`Updating event with movie ${votes[0].movie}.`, { guildId, requestId })
|
logger.info(`Updating event with movie ${votes[0].movie}.`, { guildId, requestId })
|
||||||
const options: GuildScheduledEventEditOptions<GuildScheduledEventStatus.Scheduled, GuildScheduledEventSetStatusArg<GuildScheduledEventStatus.Scheduled>> = {
|
const options: GuildScheduledEventEditOptions<GuildScheduledEventStatus.Scheduled, GuildScheduledEventSetStatusArg<GuildScheduledEventStatus.Scheduled>> = {
|
||||||
name: votes[0].movie,
|
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`
|
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.debug(`Updating event: ${JSON.stringify(voteEvent, null, 2)}`, { guildId, requestId })
|
||||||
logger.info("Updating event.", { guildId, requestId })
|
logger.info("Updating event.", { guildId, requestId })
|
||||||
voteEvent.edit(options)
|
voteEvent.edit(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getEvent(guild: Guild, guildId: string, requestId: string): Promise<GuildScheduledEvent | null> {
|
async function getEvent(guild: Guild, guildId: string, requestId: string): Promise<GuildScheduledEvent | null> {
|
||||||
const voteEvents = (await guild.scheduledEvents.fetch())
|
const voteEvents = (await guild.scheduledEvents.fetch())
|
||||||
.map((value) => value)
|
.map((value) => value)
|
||||||
.filter(event => event.name.toLowerCase().includes("voting offen"))
|
.filter(event => event.name.toLowerCase().includes("voting offen"))
|
||||||
logger.debug(`Found events: ${JSON.stringify(voteEvents, null, 2)}`, { guildId, requestId })
|
logger.debug(`Found events: ${JSON.stringify(voteEvents, null, 2)}`, { guildId, requestId })
|
||||||
|
|
||||||
if (!voteEvents || voteEvents.length <= 0) {
|
if (!voteEvents || voteEvents.length <= 0) {
|
||||||
logger.error("Could not find vote event. Cancelling update!", { guildId, requestId })
|
logger.error("Could not find vote event. Cancelling update!", { guildId, requestId })
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return voteEvents[0]
|
return voteEvents[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
type Vote = {
|
type Vote = {
|
||||||
emote: string, //todo habs nicht hinbekommen hier Emotes zu nutzen
|
emote: string, //todo habs nicht hinbekommen hier Emotes zu nutzen
|
||||||
count: number,
|
count: number,
|
||||||
movie: string
|
movie: string
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getVotesByEmote(message: Message, guildId: string, requestId: string): Promise<Vote[]> {
|
async function getVotesByEmote(message: Message, guildId: string, requestId: string): Promise<Vote[]> {
|
||||||
const votes: Vote[] = []
|
const votes: Vote[] = []
|
||||||
logger.debug(`Number of items in emotes: ${Object.values(Emotes).length}`, { guildId, requestId })
|
logger.debug(`Number of items in emotes: ${Object.values(Emotes).length}`, { guildId, requestId })
|
||||||
for (let i = 0; i < Object.keys(Emotes).length / 2; i++) {
|
for (let i = 0; i < Object.keys(Emotes).length / 2; i++) {
|
||||||
const emote = Emotes[i]
|
const emote = Emotes[i]
|
||||||
logger.debug(`Getting reaction for emote ${emote}`, { guildId, requestId })
|
logger.debug(`Getting reaction for emote ${emote}`, { guildId, requestId })
|
||||||
const reaction = await message.reactions.resolve(emote)
|
const reaction = await message.reactions.resolve(emote)
|
||||||
logger.debug(`Reaction for emote ${emote}: ${JSON.stringify(reaction, null, 2)}`, { guildId, requestId })
|
logger.debug(`Reaction for emote ${emote}: ${JSON.stringify(reaction, null, 2)}`, { guildId, requestId })
|
||||||
if (reaction) {
|
if (reaction) {
|
||||||
const vote: Vote = { emote: emote, count: reaction.count, movie: extractMovieFromMessageByEmote(message, emote) }
|
const vote: Vote = { emote: emote, count: reaction.count, movie: extractMovieFromMessageByEmote(message, emote) }
|
||||||
votes.push(vote)
|
votes.push(vote)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return votes
|
}
|
||||||
|
return votes
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractMovieFromMessageByEmote(message: Message, emote: string): string {
|
function extractMovieFromMessageByEmote(message: Message, emote: string): string {
|
||||||
const lines = message.cleanContent.split("\n")
|
const lines = message.cleanContent.split("\n")
|
||||||
const emoteLines = lines.filter(line => line.includes(emote))
|
const emoteLines = lines.filter(line => line.includes(emote))
|
||||||
|
|
||||||
if (!emoteLines) {
|
if (!emoteLines) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
const movie = emoteLines[0].substring(emoteLines[0].indexOf(emote) + emote.length + 2) // plus colon and space
|
const movie = emoteLines[0].substring(emoteLines[0].indexOf(emote) + emote.length + 2) // plus colon and space
|
||||||
|
|
||||||
return movie
|
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 })
|
||||||
|
}
|
||||||
}
|
}
|
@ -2,18 +2,18 @@ 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'
|
||||||
export default new Command({
|
export default new Command({
|
||||||
name: 'echo',
|
name: 'echo',
|
||||||
description: 'Echoes a text',
|
description: 'Echoes a text',
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
name: 'echo',
|
name: 'echo',
|
||||||
description: 'The text to echo',
|
description: 'The text to echo',
|
||||||
type: ApplicationCommandOptionType.String,
|
type: ApplicationCommandOptionType.String,
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
run: async (interaction: RunOptions) => {
|
run: async (interaction: RunOptions) => {
|
||||||
console.log('echo called')
|
console.log('echo called')
|
||||||
interaction.interaction.reply(interaction.toString())
|
interaction.interaction.reply(interaction.toString())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -5,7 +5,7 @@ import { accountChoice, joingroup, leavegroup, loginScreen, overview, resume, se
|
|||||||
import { logger } from '../logger'
|
import { logger } from '../logger'
|
||||||
import { Command } from '../structures/command'
|
import { Command } from '../structures/command'
|
||||||
import { RunOptions } from '../types/commandTypes'
|
import { RunOptions } from '../types/commandTypes'
|
||||||
import { configureServer, explainRoles, installation, loginInfo, useSyncgroup } from './mitgucken'
|
import { configureServer, explainRole, installation, loginInfo, useSyncgroup } from './mitgucken'
|
||||||
|
|
||||||
export default new Command({
|
export default new Command({
|
||||||
name: 'guides',
|
name: 'guides',
|
||||||
@ -70,7 +70,7 @@ export default new Command({
|
|||||||
userDMChannel.send({ embeds: useSyncgroup(), files: [overview, joingroup, resume, leavegroup] })
|
userDMChannel.send({ embeds: useSyncgroup(), files: [overview, joingroup, resume, leavegroup] })
|
||||||
} else if (guideSelection.customId === 'explainRoles') {
|
} else if (guideSelection.customId === 'explainRoles') {
|
||||||
const userDMChannel = await guideSelection.user.createDM()
|
const userDMChannel = await guideSelection.user.createDM()
|
||||||
userDMChannel.send(explainRoles())
|
userDMChannel.send({ embeds: explainRole() })
|
||||||
}
|
}
|
||||||
|
|
||||||
guideSelection.update({ content: "Hab ich dir per DM geschickt :)", components: [] })
|
guideSelection.update({ content: "Hab ich dir per DM geschickt :)", components: [] })
|
||||||
|
@ -16,13 +16,9 @@ export default new Command({
|
|||||||
const embedList: APIEmbed[] = []
|
const embedList: APIEmbed[] = []
|
||||||
embedList.push(...installation())
|
embedList.push(...installation())
|
||||||
embedList.push(...configureServer())
|
embedList.push(...configureServer())
|
||||||
|
embedList.push(...explainRole())
|
||||||
embedList.push(...loginInfo())
|
embedList.push(...loginInfo())
|
||||||
embedList.push(...useSyncgroup())
|
embedList.push(...useSyncgroup())
|
||||||
embedList.push({
|
|
||||||
color,
|
|
||||||
title: "Wie du an einen Account kommst",
|
|
||||||
description: explainRoles()
|
|
||||||
})
|
|
||||||
|
|
||||||
//logger.info(`Trying to use ${splashScreen.name}`, { requestId, guildId: interaction.interaction.guild?.id })
|
//logger.info(`Trying to use ${splashScreen.name}`, { requestId, guildId: interaction.interaction.guild?.id })
|
||||||
logger.info(`Sending guide to ${interaction.interaction.user.id}`, { requestId, guildId: interaction.interaction.guild?.id })
|
logger.info(`Sending guide to ${interaction.interaction.user.id}`, { requestId, guildId: interaction.interaction.guild?.id })
|
||||||
@ -32,6 +28,13 @@ export default new Command({
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export function explainRole(): APIEmbed[] {
|
||||||
|
return [{
|
||||||
|
color,
|
||||||
|
title: "Wie du an einen Account kommst",
|
||||||
|
description: roleExplanation
|
||||||
|
}]
|
||||||
|
}
|
||||||
export function installation(): APIEmbed[] {
|
export function installation(): APIEmbed[] {
|
||||||
const embedList: APIEmbed[] = []
|
const embedList: APIEmbed[] = []
|
||||||
// DownloadLink and installation
|
// DownloadLink and installation
|
||||||
@ -67,7 +70,7 @@ export function configureServer(): APIEmbed[] {
|
|||||||
title: "Server Verbindung",
|
title: "Server Verbindung",
|
||||||
description: "Stelle eine Verbindung zum Hartzarett Jellyfin Server her",
|
description: "Stelle eine Verbindung zum Hartzarett Jellyfin Server her",
|
||||||
fields: [
|
fields: [
|
||||||
{ name: "Server Adresse", value: "https://media.hartzarett.ruhr" }
|
{ name: "Server Adresse", value: "`https://media.hartzarett.ruhr`" }
|
||||||
],
|
],
|
||||||
image: {
|
image: {
|
||||||
url: 'attachment://server_verbindung.png'
|
url: 'attachment://server_verbindung.png'
|
||||||
@ -91,7 +94,7 @@ export function loginInfo(): APIEmbed[] {
|
|||||||
embedList.push({
|
embedList.push({
|
||||||
color,
|
color,
|
||||||
title: "Login",
|
title: "Login",
|
||||||
description: "Melde dich mit dem Usernamen und Passwort an, welches dir von mir zugeschickt wird. Falls du ein neues brauchst führe einmal /reset_passwort aus :)",
|
description: "Melde dich mit dem Usernamen und Passwort an, welches dir von mir zugeschickt wird. Falls du ein neues brauchst führe einmal `/passwort_reset` aus :)",
|
||||||
image: {
|
image: {
|
||||||
url: 'attachment://login_screen.png'
|
url: 'attachment://login_screen.png'
|
||||||
}
|
}
|
||||||
@ -139,8 +142,7 @@ export function useSyncgroup(): APIEmbed[] {
|
|||||||
return embedList
|
return embedList
|
||||||
}
|
}
|
||||||
|
|
||||||
export function explainRoles(): string {
|
const roleExplanation = `Mit einer Rolle kann dafür gesorgt werden, dass du einen dauerhaften Account auf dem Mediaserver hast. Wende dich bei Bedarf an Samantha oder Markus.\n
|
||||||
return `Mit einer Rolle kann dafür gesorgt werden, dass du einen dauerhaften Account auf dem Mediaserver hast. Wende dich bei Bedarf an Samantha oder Markus.\n
|
|
||||||
Für eine watchparty bekommst du allerdings automatisch einen Account. Hierfür melde einfach Interesse an dem Event an. Wenn du für das Event Interesse angemeldet hast bekommst du automatisch beim Start des Events einen Benutzernamen und das dazugehörige Passwort zugesendet.\n
|
Für eine watchparty bekommst du allerdings automatisch einen Account. Hierfür melde einfach Interesse an dem Event an. Wenn du für das Event Interesse angemeldet hast bekommst du automatisch beim Start des Events einen Benutzernamen und das dazugehörige Passwort zugesendet.\n
|
||||||
Hast du kein Interesse angemeldet bekommst du automatisch einen Nutzernamen und Passwort zugeschickt wenn du den Channel betrittst in dem das Event stattfindet.`
|
Hast du kein Interesse angemeldet bekommst du automatisch einen Nutzernamen und Passwort zugeschickt wenn du den Channel betrittst in dem das Event stattfindet.`
|
||||||
}
|
|
||||||
|
@ -4,13 +4,13 @@ import { Command } from '../structures/command'
|
|||||||
import { RunOptions } from '../types/commandTypes'
|
import { RunOptions } from '../types/commandTypes'
|
||||||
|
|
||||||
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')
|
console.log('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))
|
console.log(JSON.stringify(interaction.interaction.member, null, 2))
|
||||||
jellyfinHandler.resetUserPasswort(interaction.interaction.member, uuid())
|
jellyfinHandler.resetUserPasswort(interaction.interaction.member, uuid())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
48
server/events/announceManualWatchparty.ts
Normal file
48
server/events/announceManualWatchparty.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { GuildScheduledEvent, TextChannel } from "discord.js";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
|
import { client } from "../..";
|
||||||
|
import { config } from "../configuration";
|
||||||
|
import { createDateStringFromEvent } from "../helper/dateHelper";
|
||||||
|
import { Maybe } from "../interfaces";
|
||||||
|
import { logger } from "../logger";
|
||||||
|
|
||||||
|
|
||||||
|
export const name = 'guildScheduledEventCreate'
|
||||||
|
|
||||||
|
export async function execute(event: GuildScheduledEvent) {
|
||||||
|
const guildId = event.guildId
|
||||||
|
const requestId = uuid()
|
||||||
|
try {
|
||||||
|
if (!event.description) {
|
||||||
|
logger.debug("Got GuildScheduledEventCreate event. But has no description. Aborting.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.description.includes("!wp")) {
|
||||||
|
logger.info("Got manual create event of watchparty event!", { guildId, requestId })
|
||||||
|
if (event.description.includes("!private")) {
|
||||||
|
logger.info("Event description contains \"!private\". Won't announce.", { guildId, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel: Maybe<TextChannel> = client.getAnnouncementChannelForGuild(guildId)
|
||||||
|
|
||||||
|
if (!channel) {
|
||||||
|
logger.error("Could not obtain announcement channel. Aborting announcement.", { guildId, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = `[Watchparty] https://discord.com/events/${event.guildId}/${event.id} \nHey <@&${config.bot.announcement_role}>, wir gucken ${event.name} ${createDateStringFromEvent(event, guildId, requestId)}`
|
||||||
|
|
||||||
|
channel.send(message)
|
||||||
|
} else {
|
||||||
|
logger.debug("Got GuildScheduledEventCreate event but no !wp in description. Not creating manual wp announcement.", { guildId, requestId })
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// sendFailureDM(error)
|
||||||
|
logger.error(<string>error, { guildId, requestId })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
63
server/events/autoCreateVoteByWPEvent.ts
Normal file
63
server/events/autoCreateVoteByWPEvent.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { GuildScheduledEvent, Message, MessageCreateOptions, TextChannel } from "discord.js";
|
||||||
|
import { ScheduledTask } from "node-cron";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
|
import { client, yavinJellyfinHandler } from "../..";
|
||||||
|
import { config } from "../configuration";
|
||||||
|
import { createDateStringFromEvent } from "../helper/dateHelper";
|
||||||
|
import { Maybe } from "../interfaces";
|
||||||
|
import { logger } from "../logger";
|
||||||
|
|
||||||
|
|
||||||
|
export const name = 'guildScheduledEventCreate'
|
||||||
|
|
||||||
|
export enum Emotes { "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟" }
|
||||||
|
export const NONE_OF_THAT = "❌"
|
||||||
|
|
||||||
|
export let task: ScheduledTask | undefined
|
||||||
|
|
||||||
|
export async function execute(event: GuildScheduledEvent) {
|
||||||
|
const requestId = uuid()
|
||||||
|
|
||||||
|
if (event.name.toLowerCase().includes("!nextwp")) {
|
||||||
|
logger.info("Event was a placeholder event to start a new watchparty and voting. Creating vote!", { guildId: event.guildId, requestId })
|
||||||
|
logger.debug("Renaming event", { guildId: event.guildId, requestId })
|
||||||
|
event.edit({ name: "Watchparty - Voting offen" })
|
||||||
|
const movies = await yavinJellyfinHandler.getRandomMovieNames(5, event.guildId, requestId)
|
||||||
|
|
||||||
|
logger.info(`Got ${movies.length} random movies. Creating voting`, { guildId: event.guildId, requestId })
|
||||||
|
logger.debug(`Movies: ${JSON.stringify(movies)}`, { guildId: event.guildId, requestId })
|
||||||
|
|
||||||
|
const announcementChannel: Maybe<TextChannel> = client.getAnnouncementChannelForGuild(event.guildId)
|
||||||
|
if (!announcementChannel) {
|
||||||
|
logger.error("Could not find announcement channel. Aborting", { guildId: event.guildId, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.debug(`Found channel ${JSON.stringify(announcementChannel, null, 2)}`, { guildId: event.guildId, requestId })
|
||||||
|
|
||||||
|
if (!event.scheduledStartAt) {
|
||||||
|
logger.info("EVENT DOES NOT HAVE STARTDATE; CANCELLING", { guildId: event.guildId, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let message = `[Abstimmung] für https://discord.com/events/${event.guildId}/${event.id}\n<@&${config.bot.announcement_role}> Es gibt eine neue Abstimmung für die nächste Watchparty ${createDateStringFromEvent(event, event.guildId, requestId)}! Stimme hierunter für den nächsten Film ab!\n`
|
||||||
|
|
||||||
|
for (let i = 0; i < movies.length; i++) {
|
||||||
|
message = message.concat(Emotes[i]).concat(": ").concat(movies[i]).concat("\n")
|
||||||
|
}
|
||||||
|
message = message.concat(NONE_OF_THAT).concat(": Wenn dir nichts davon gefällt.")
|
||||||
|
|
||||||
|
const options: MessageCreateOptions = {
|
||||||
|
allowedMentions: { parse: ["roles"] },
|
||||||
|
content: message,
|
||||||
|
}
|
||||||
|
|
||||||
|
const sentMessage: Message<true> = await (await announcementChannel.fetch()).send(options)
|
||||||
|
|
||||||
|
for (let i = 0; i < movies.length; i++) {
|
||||||
|
sentMessage.react(Emotes[i])
|
||||||
|
}
|
||||||
|
sentMessage.react(NONE_OF_THAT)
|
||||||
|
|
||||||
|
// sentMessage.pin() //todo: uncomment when bot has permission to pin messages. Also update closepoll.ts to only fetch pinned messages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
52
server/events/deleteAnnouncementsWhenWPEnds.ts
Normal file
52
server/events/deleteAnnouncementsWhenWPEnds.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { Collection, GuildScheduledEvent, GuildScheduledEventStatus, Message } from "discord.js";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
|
import { client } from "../..";
|
||||||
|
import { logger } from "../logger";
|
||||||
|
|
||||||
|
|
||||||
|
export const name = 'guildScheduledEventUpdate'
|
||||||
|
|
||||||
|
export async function execute(oldEvent: GuildScheduledEvent, newEvent: GuildScheduledEvent) {
|
||||||
|
const requestId = uuid()
|
||||||
|
try {
|
||||||
|
if (!newEvent.guild) {
|
||||||
|
logger.error("Event has no guild, aborting.", { guildId: newEvent.guildId, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const guildId = newEvent.guildId
|
||||||
|
|
||||||
|
if (newEvent.description?.toLowerCase().includes("!wp") && newEvent.status === GuildScheduledEventStatus.Completed) {
|
||||||
|
logger.info("A watchparty ended. Cleaning up announcements!", { guildId, requestId })
|
||||||
|
const announcementChannel = client.getAnnouncementChannelForGuild(newEvent.guild.id)
|
||||||
|
if (!announcementChannel) {
|
||||||
|
logger.error("Could not find announcement channel. Aborting", { guildId: newEvent.guild.id, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const events = await newEvent.guild.scheduledEvents.fetch()
|
||||||
|
|
||||||
|
const wpAnnouncements = (await announcementChannel.messages.fetch()).filter(message => !message.cleanContent.includes("[initial]"))
|
||||||
|
const announcementsWithoutEvent = filterAnnouncementsByPendingWPs(wpAnnouncements, events)
|
||||||
|
logger.info(`Deleting ${announcementsWithoutEvent.length} announcements.`, { guildId, requestId })
|
||||||
|
announcementsWithoutEvent.forEach(message => message.delete())
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(<string>error, { guildId: newEvent.guildId, requestId })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterAnnouncementsByPendingWPs(messages: Collection<string, Message<true>>, events: Collection<string, GuildScheduledEvent<GuildScheduledEventStatus>>): Message<true>[] {
|
||||||
|
const filteredMessages: Message<true>[] = []
|
||||||
|
for (const message of messages.values()) {
|
||||||
|
let foundEventForMessage = false
|
||||||
|
for (const event of events.values()) {
|
||||||
|
if (message.cleanContent.includes(event.id)) { //announcement always has eventid because of eventbox
|
||||||
|
foundEventForMessage = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!foundEventForMessage) {
|
||||||
|
filteredMessages.push(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filteredMessages
|
||||||
|
}
|
@ -1,104 +0,0 @@
|
|||||||
import { addDays, format, isAfter } from "date-fns";
|
|
||||||
import toDate from "date-fns/fp/toDate";
|
|
||||||
import { GuildScheduledEvent, Message, MessageCreateOptions, TextChannel } from "discord.js";
|
|
||||||
import { ScheduledTask, schedule } from "node-cron";
|
|
||||||
import { v4 as uuid } from "uuid";
|
|
||||||
import { client, yavinJellyfinHandler } from "../..";
|
|
||||||
import { closePoll } from "../commands/closepoll";
|
|
||||||
import { config } from "../configuration";
|
|
||||||
import { Maybe } from "../interfaces";
|
|
||||||
import { logger } from "../logger";
|
|
||||||
|
|
||||||
|
|
||||||
export const name = 'guildScheduledEventCreate'
|
|
||||||
|
|
||||||
export enum Emotes { "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟" }
|
|
||||||
|
|
||||||
export let task: ScheduledTask | undefined
|
|
||||||
|
|
||||||
export async function execute(event: GuildScheduledEvent) {
|
|
||||||
const requestId = uuid()
|
|
||||||
logger.debug(`New event created: ${JSON.stringify(event, null, 2)}`, { guildId: event.guildId, requestId })
|
|
||||||
|
|
||||||
if (event.name.toLowerCase().includes("!nextwp")) {
|
|
||||||
logger.info("Event was a placeholder event to start a new watchparty and voting. Creating vote!", { guildId: event.guildId, requestId })
|
|
||||||
logger.debug("Renaming event", { guildId: event.guildId, requestId })
|
|
||||||
event.edit({ name: "Watchparty - Voting offen" })
|
|
||||||
const movies = await yavinJellyfinHandler.getRandomMovies(5, event.guildId, requestId)
|
|
||||||
|
|
||||||
logger.info(`Got ${movies.length} random movies. Creating voting`, { guildId: event.guildId, requestId })
|
|
||||||
logger.debug(`Movies: ${JSON.stringify(movies.map(movie => movie.name))}`, { guildId: event.guildId, requestId })
|
|
||||||
|
|
||||||
const announcementChannel: Maybe<TextChannel> = client.getAnnouncementChannelForGuild(event.guildId)
|
|
||||||
if(!announcementChannel) {
|
|
||||||
logger.error("Could not find announcement channel. Aborting", { guildId: event.guildId, requestId })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
logger.debug(`Found channel ${JSON.stringify(announcementChannel, null, 2)}`, { guildId: event.guildId, requestId })
|
|
||||||
|
|
||||||
if(!event.scheduledStartAt) {
|
|
||||||
logger.info("EVENT DOES NOT HAVE STARTDATE; CANCELLING", {guildId: event.guildId, requestId})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const date = format(event.scheduledStartAt, "dd.MM")
|
|
||||||
const time = format(event.scheduledStartAt, "HH:mm")
|
|
||||||
let message = `[Abstimmung]\n<@&${config.bot.announcement_role}> Es gibt eine neue Abstimmung für die nächste Watchparty am ${date} um ${time}}! Stimme hierunter für den nächsten Film ab!\n`
|
|
||||||
|
|
||||||
for (let i = 0; i < movies.length; i++) {
|
|
||||||
message = message.concat(Emotes[i]).concat(": ").concat(movies[i].name ?? "Film hatte keinen Namen :(").concat("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
const options: MessageCreateOptions = {
|
|
||||||
allowedMentions: { parse: ["roles"]},
|
|
||||||
content: message
|
|
||||||
}
|
|
||||||
|
|
||||||
const sentMessage: Message<true> = await (await announcementChannel.fetch()).send(options)
|
|
||||||
|
|
||||||
for (let i = 0; i < movies.length; i++) {
|
|
||||||
sentMessage.react(Emotes[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!task) {
|
|
||||||
task = schedule("0 * * * * *", () => checkForPollsToClose(event))
|
|
||||||
}
|
|
||||||
|
|
||||||
// sentMessage.pin() //todo: uncomment when bot has permission to pin messages. Also update closepoll.ts to only fetch pinned messages
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkForPollsToClose(event: GuildScheduledEvent): Promise<void> {
|
|
||||||
const requestId = uuid()
|
|
||||||
logger.info(`Automatic check for poll closing.`, { guildId: event.guildId, requestId })
|
|
||||||
if (!event.guild) {
|
|
||||||
logger.error("No guild in event. Cancelling.", { guildId: event.guildId, requestId })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
//refetch event in case the time changed or the poll is already closed
|
|
||||||
const events = (await event.guild.scheduledEvents.fetch())
|
|
||||||
.filter(event => event.name.toLowerCase().includes("voting offen"))
|
|
||||||
.map((value) => value)
|
|
||||||
|
|
||||||
if (!events || events.length <= 0) {
|
|
||||||
logger.info("Did not find any events. Cancelling", { guildId: event.guildId, requestId })
|
|
||||||
return
|
|
||||||
} else if (events.length > 1) {
|
|
||||||
logger.error(`More than one event found. Don't know which one is the right one :( Events: ${JSON.stringify(events, null, 2)}`, { guildId: event.guildId, requestId })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const updatedEvent = events[0] //add two hours because of different timezones in discord api and Date.now()
|
|
||||||
if (!updatedEvent.scheduledStartTimestamp) {
|
|
||||||
logger.error("Event does not have a scheduled start time. Cancelling", { guildId: event.guildId, requestId })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const eventDate: Date = toDate(updatedEvent.scheduledStartTimestamp)
|
|
||||||
const closePollDate: Date = addDays(eventDate, -2)
|
|
||||||
|
|
||||||
if (isAfter(Date.now(), closePollDate)) {
|
|
||||||
logger.info("Less than two days until event. Closing poll", { guildId: event.guildId, requestId })
|
|
||||||
closePoll(event.guild, requestId)
|
|
||||||
} else {
|
|
||||||
logger.info(`ScheduledStart: ${closePollDate}. Now: ${toDate(Date.now())}`, { guildId: event.guildId, requestId })
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
import { GuildMember, GuildScheduledEvent, GuildScheduledEventStatus } from "discord.js";
|
|
||||||
import { v4 as uuid } from "uuid";
|
|
||||||
import { client, jellyfinHandler } from "../..";
|
|
||||||
import { getGuildSpecificTriggerRoleId } from "../helper/roleFilter";
|
|
||||||
import { logger } from "../logger";
|
|
||||||
|
|
||||||
|
|
||||||
export const name = 'guildScheduledEventUpdate'
|
|
||||||
|
|
||||||
export async function execute(oldEvent: GuildScheduledEvent, newEvent: GuildScheduledEvent) {
|
|
||||||
try {
|
|
||||||
const requestId = uuid()
|
|
||||||
logger.debug(`Got scheduledEvent update. New Event: ${JSON.stringify(newEvent, null, 2)}`, { guildId: newEvent.guildId, requestId })
|
|
||||||
if (!newEvent.guild) {
|
|
||||||
logger.error("Event has no guild, aborting.", { guildId: newEvent.guildId, requestId })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newEvent.description?.toLowerCase().includes("!wp") && [GuildScheduledEventStatus.Active, GuildScheduledEventStatus.Completed].includes(newEvent.status)) {
|
|
||||||
const roles = getGuildSpecificTriggerRoleId().map((key, value) => value)
|
|
||||||
const eventMembers = (await newEvent.fetchSubscribers({ withMember: true })).filter(member => !member.member.roles.cache.hasAny(...roles)).map((value) => value.member)
|
|
||||||
const channelMembers = newEvent.channel?.members.filter(member => !member.roles.cache.hasAny(...roles)).map((value) => value)
|
|
||||||
const allMembers = eventMembers.concat(channelMembers ?? [])
|
|
||||||
|
|
||||||
const members: GuildMember[] = []
|
|
||||||
for (const member of allMembers) {
|
|
||||||
if (!members.find(x => x.id == member.id))
|
|
||||||
members.push(member)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newEvent.status === GuildScheduledEventStatus.Active)
|
|
||||||
createJFUsers(members, newEvent.name, requestId)
|
|
||||||
else {
|
|
||||||
const announcementChannel = await client.getAnnouncementChannelForGuild(newEvent.guild.id)
|
|
||||||
if(!announcementChannel) {
|
|
||||||
logger.error("Could not find announcement channel. Aborting", { guildId: newEvent.guild.id, requestId })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const announcements = (await announcementChannel.messages.fetch()).filter(message => !message.pinned)
|
|
||||||
announcements.forEach(message => message.delete())
|
|
||||||
members.forEach(member => {
|
|
||||||
member.createDM().then(channel => channel.send(`Die Watchparty ist vorbei, dein Account wurde wieder gelöscht. Wenn du einen permanenten Account haben möchtest, melde dich bei Samantha oder Marukus.`))
|
|
||||||
})
|
|
||||||
deleteJFUsers(newEvent.guildId, requestId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createJFUsers(members: GuildMember[], movieName: string, requestId?: string) {
|
|
||||||
logger.info(`Creating users for: \n ${JSON.stringify(members, null, 2)}`)
|
|
||||||
members.forEach(member => {
|
|
||||||
member.createDM().then(channel => channel.send(`Hey! Du hast dich für die Watchparty von ${movieName} angemeldet! Es geht gleich los!`))
|
|
||||||
jellyfinHandler.upsertUser(member, "TEMPORARY", requestId)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteJFUsers(guildId: string, requestId?: string) {
|
|
||||||
logger.info(`Watchparty ended, deleting tmp users`, { guildId, requestId })
|
|
||||||
jellyfinHandler.purge(guildId, requestId)
|
|
||||||
}
|
|
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)
|
||||||
|
}
|
@ -2,5 +2,5 @@ import { Message } from "discord.js"
|
|||||||
|
|
||||||
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`)
|
console.log(`${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, toDate } from "date-fns";
|
||||||
|
import { utcToZonedTime } from "date-fns-tz"
|
||||||
|
import { GuildScheduledEvent } from "discord.js";
|
||||||
|
import { logger } from "../logger";
|
||||||
|
import de from "date-fns/locale/de";
|
||||||
|
|
||||||
|
export function createDateStringFromEvent(event: GuildScheduledEvent, requestId: string, guildId?: string): string {
|
||||||
|
if (!event.scheduledStartAt) {
|
||||||
|
logger.error("Event has no start. Cannot create dateString.", { guildId, requestId })
|
||||||
|
return `"habe keinen Startzeitpunkt ermitteln können"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeZone = 'Europe/Berlin'
|
||||||
|
const zonedDateTime = utcToZonedTime(event.scheduledStartAt, timeZone)
|
||||||
|
const time = format(zonedDateTime, "HH:mm", { locale: de })
|
||||||
|
|
||||||
|
if (isToday(zonedDateTime)) {
|
||||||
|
return `heute um ${time}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = format(zonedDateTime, "eeee dd.MM", { locale: de })
|
||||||
|
return `am ${date} um ${time}`
|
||||||
|
}
|
@ -2,7 +2,7 @@ import { GuildMember } from "discord.js";
|
|||||||
import { JellyfinConfig, Maybe, PermissionLevel } from "../interfaces";
|
import { JellyfinConfig, Maybe, PermissionLevel } from "../interfaces";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
import { CreateUserByNameOperationRequest, DeleteUserRequest, GetItemsRequest, ItemsApi, SystemApi, UpdateUserPasswordOperationRequest, UpdateUserPolicyOperationRequest, UserApi } from "./apis";
|
import { CreateUserByNameOperationRequest, DeleteUserRequest, GetItemsRequest, ItemsApi, SystemApi, UpdateUserPasswordOperationRequest, UpdateUserPolicyOperationRequest, UserApi } from "./apis";
|
||||||
import { BaseItemDto, UpdateUserPasswordRequest } from "./models";
|
import { BaseItemDto, UpdateUserPasswordRequest, UpdateUserPolicyRequest } from "./models";
|
||||||
import { UserDto } from "./models/UserDto";
|
import { UserDto } from "./models/UserDto";
|
||||||
import { Configuration, ConfigurationParameters } from "./runtime";
|
import { Configuration, ConfigurationParameters } from "./runtime";
|
||||||
|
|
||||||
@ -52,24 +52,46 @@ export class JellyfinHandler {
|
|||||||
return (Math.random() * 10000 + 10000).toFixed(0)
|
return (Math.random() * 10000 + 10000).toFixed(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createUserAccountForDiscordUser(discordUser: GuildMember, level: PermissionLevel, guildId?: string, requestId?: string): Promise<UserDto> {
|
public async createUserAccountForDiscordUser(discordUser: GuildMember, level: PermissionLevel, requestId: string, guildId?: string): Promise<UserDto> {
|
||||||
const newUserName = this.generateJFUserName(discordUser, level)
|
const newUserName = this.generateJFUserName(discordUser, level)
|
||||||
logger.info(`New Username for ${discordUser.displayName}: ${newUserName}`, { guildId, requestId })
|
logger.info(`New Username for ${discordUser.displayName}: ${newUserName}`, { guildId, requestId })
|
||||||
const req: CreateUserByNameOperationRequest = {
|
const req: CreateUserByNameOperationRequest = {
|
||||||
createUserByNameRequest: {
|
createUserByNameRequest: {
|
||||||
name: newUserName,
|
name: newUserName,
|
||||||
password: this.generatePasswordForUser(),
|
password: this.generatePasswordForUser()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.debug(JSON.stringify(req), { requestId, guildId })
|
logger.debug(JSON.stringify(req), { requestId, guildId })
|
||||||
const createResult = await this.userApi.createUserByName(req)
|
const createResult = await this.userApi.createUserByName(req)
|
||||||
if (createResult) {
|
if (createResult) {
|
||||||
|
if (createResult.policy) {
|
||||||
|
this.setUserPermissions(createResult, requestId, guildId)
|
||||||
|
}
|
||||||
(await discordUser.createDM()).send(`Ich hab dir mal nen Account angelegt :)\nDein Username ist ${createResult.name}, dein Password ist "${req.createUserByNameRequest.password}"!`)
|
(await discordUser.createDM()).send(`Ich hab dir mal nen Account angelegt :)\nDein Username ist ${createResult.name}, dein Password ist "${req.createUserByNameRequest.password}"!`)
|
||||||
return createResult
|
return createResult
|
||||||
}
|
}
|
||||||
else throw new Error('Could not create User in Jellyfin')
|
else throw new Error('Could not create User in Jellyfin')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async setUserPermissions(user: UserDto, requestId: string, guildId?: string) {
|
||||||
|
if (!user.policy || !user.id) {
|
||||||
|
logger.error(`Cannot update user policy. User ${user.name} has no policy to modify`, { guildId, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user.policy.enableVideoPlaybackTranscoding = false
|
||||||
|
|
||||||
|
const operation: UpdateUserPolicyRequest = {
|
||||||
|
...user.policy,
|
||||||
|
enableVideoPlaybackTranscoding: false
|
||||||
|
}
|
||||||
|
|
||||||
|
const request: UpdateUserPolicyOperationRequest = {
|
||||||
|
userId: user.id,
|
||||||
|
updateUserPolicyRequest: operation
|
||||||
|
}
|
||||||
|
this.userApi.updateUserPolicy(request)
|
||||||
|
}
|
||||||
|
|
||||||
public async isUserAlreadyPresent(discordUser: GuildMember, requestId?: string): Promise<boolean> {
|
public async isUserAlreadyPresent(discordUser: GuildMember, requestId?: string): Promise<boolean> {
|
||||||
const jfuser = await this.getUser(discordUser, requestId)
|
const jfuser = await this.getUser(discordUser, requestId)
|
||||||
logger.debug(`Presence for DiscordUser ${discordUser.id}:${jfuser !== undefined}`, { guildId: discordUser.guild.id, requestId })
|
logger.debug(`Presence for DiscordUser ${discordUser.id}:${jfuser !== undefined}`, { guildId: discordUser.guild.id, requestId })
|
||||||
@ -242,10 +264,21 @@ export class JellyfinHandler {
|
|||||||
const index = Math.floor(Math.random() * allMovies.length)
|
const index = Math.floor(Math.random() * allMovies.length)
|
||||||
movies.push(...allMovies.splice(index, 1)) // maybe out of bounds? ?
|
movies.push(...allMovies.splice(index, 1)) // maybe out of bounds? ?
|
||||||
}
|
}
|
||||||
|
|
||||||
return movies
|
return movies
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getRandomMovieNames(count: number, guildId: string, requestId: string): Promise<string[]> {
|
||||||
|
logger.info(`${count} random movie names requested`, { guildId, requestId })
|
||||||
|
|
||||||
|
let movieCount = 0
|
||||||
|
let movieNames: string[]
|
||||||
|
do {
|
||||||
|
movieNames = (await this.getRandomMovies(count, guildId, requestId)).filter(movie => movie.name && movie.name.length > 0).map(movie => <string>movie.name)
|
||||||
|
movieCount = movieNames.length
|
||||||
|
} while (movieCount < count)
|
||||||
|
return movieNames
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
export enum UserUpsertResult { enabled, created }
|
export enum UserUpsertResult { enabled, created }
|
||||||
|
@ -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[]) {
|
||||||
|
const next = this.clone<T>();
|
||||||
|
next.middleware = next.middleware.concat(...middlewares);
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
withPreMiddleware<T extends BaseAPI>(this: T, ...preMiddlewares: Array<Middleware['pre']>) {
|
||||||
|
const middlewares = preMiddlewares.map((pre) => ({ pre }));
|
||||||
|
return this.withMiddleware<T>(...middlewares);
|
||||||
|
}
|
||||||
|
|
||||||
|
withPostMiddleware<T extends BaseAPI>(this: T, ...postMiddlewares: Array<Middleware['post']>) {
|
||||||
|
const middlewares = postMiddlewares.map((post) => ({ post }));
|
||||||
|
return this.withMiddleware<T>(...middlewares);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given MIME is a JSON MIME.
|
||||||
|
* JSON MIME examples:
|
||||||
|
* application/json
|
||||||
|
* application/json; charset=UTF8
|
||||||
|
* APPLICATION/JSON
|
||||||
|
* application/vnd.company+json
|
||||||
|
* @param mime - MIME (Multipurpose Internet Mail Extensions)
|
||||||
|
* @return True if the given MIME is JSON, false otherwise.
|
||||||
|
*/
|
||||||
|
protected isJsonMime(mime: string | null | undefined): boolean {
|
||||||
|
if (!mime) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return BaseAPI.jsonRegex.test(mime);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async request(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction): Promise<Response> {
|
||||||
|
const { url, init } = await this.createFetchParams(context, initOverrides);
|
||||||
|
const response = await this.fetchApi(url, init);
|
||||||
|
if (response && (response.status >= 200 && response.status < 300)) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
throw new ResponseError(response, 'Response returned an error code');
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createFetchParams(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction) {
|
||||||
|
let url = this.configuration.basePath + context.path;
|
||||||
|
if (context.query !== undefined && Object.keys(context.query).length !== 0) {
|
||||||
|
// 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
|
||||||
|
// do not handle correctly sometimes.
|
||||||
|
url += '?' + this.configuration.queryParamsStringify(context.query);
|
||||||
}
|
}
|
||||||
|
|
||||||
withMiddleware<T extends BaseAPI>(this: T, ...middlewares: Middleware[]) {
|
const headers = Object.assign({}, this.configuration.headers, context.headers);
|
||||||
const next = this.clone<T>();
|
Object.keys(headers).forEach(key => headers[key] === undefined ? delete headers[key] : {});
|
||||||
next.middleware = next.middleware.concat(...middlewares);
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
|
|
||||||
withPreMiddleware<T extends BaseAPI>(this: T, ...preMiddlewares: Array<Middleware['pre']>) {
|
const initOverrideFn =
|
||||||
const middlewares = preMiddlewares.map((pre) => ({ pre }));
|
typeof initOverrides === "function"
|
||||||
return this.withMiddleware<T>(...middlewares);
|
? initOverrides
|
||||||
}
|
: async () => initOverrides;
|
||||||
|
|
||||||
withPostMiddleware<T extends BaseAPI>(this: T, ...postMiddlewares: Array<Middleware['post']>) {
|
const initParams = {
|
||||||
const middlewares = postMiddlewares.map((post) => ({ post }));
|
method: context.method,
|
||||||
return this.withMiddleware<T>(...middlewares);
|
headers,
|
||||||
}
|
body: context.body,
|
||||||
|
credentials: this.configuration.credentials,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
const overriddenInit: RequestInit = {
|
||||||
* Check if the given MIME is a JSON MIME.
|
...initParams,
|
||||||
* JSON MIME examples:
|
...(await initOverrideFn({
|
||||||
* application/json
|
init: initParams,
|
||||||
* application/json; charset=UTF8
|
context,
|
||||||
* APPLICATION/JSON
|
}))
|
||||||
* application/vnd.company+json
|
};
|
||||||
* @param mime - MIME (Multipurpose Internet Mail Extensions)
|
|
||||||
* @return True if the given MIME is JSON, false otherwise.
|
const init: RequestInit = {
|
||||||
*/
|
...overriddenInit,
|
||||||
protected isJsonMime(mime: string | null | undefined): boolean {
|
body:
|
||||||
if (!mime) {
|
isFormData(overriddenInit.body) ||
|
||||||
return false;
|
overriddenInit.body instanceof URLSearchParams ||
|
||||||
|
isBlob(overriddenInit.body)
|
||||||
|
? overriddenInit.body
|
||||||
|
: JSON.stringify(overriddenInit.body),
|
||||||
|
};
|
||||||
|
|
||||||
|
return { url, init };
|
||||||
|
}
|
||||||
|
|
||||||
|
private fetchApi = async (url: string, init: RequestInit) => {
|
||||||
|
let fetchParams = { url, init };
|
||||||
|
for (const middleware of this.middleware) {
|
||||||
|
if (middleware.pre) {
|
||||||
|
fetchParams = await middleware.pre({
|
||||||
|
fetch: this.fetchApi,
|
||||||
|
...fetchParams,
|
||||||
|
}) || fetchParams;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let response: Response | undefined = undefined;
|
||||||
|
try {
|
||||||
|
response = await (this.configuration.fetchApi || fetch)(fetchParams.url, fetchParams.init);
|
||||||
|
} catch (e) {
|
||||||
|
for (const middleware of this.middleware) {
|
||||||
|
if (middleware.onError) {
|
||||||
|
response = await middleware.onError({
|
||||||
|
fetch: this.fetchApi,
|
||||||
|
url: fetchParams.url,
|
||||||
|
init: fetchParams.init,
|
||||||
|
error: e,
|
||||||
|
response: response ? response.clone() : undefined,
|
||||||
|
}) || response;
|
||||||
}
|
}
|
||||||
return BaseAPI.jsonRegex.test(mime);
|
}
|
||||||
}
|
if (response === undefined) {
|
||||||
|
if (e instanceof Error) {
|
||||||
protected async request(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction): Promise<Response> {
|
throw new FetchError(e, 'The request failed and the interceptors did not return an alternative response');
|
||||||
const { url, init } = await this.createFetchParams(context, initOverrides);
|
} else {
|
||||||
const response = await this.fetchApi(url, init);
|
throw e;
|
||||||
if (response && (response.status >= 200 && response.status < 300)) {
|
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
throw new ResponseError(response, 'Response returned an error code');
|
}
|
||||||
}
|
}
|
||||||
|
for (const middleware of this.middleware) {
|
||||||
private async createFetchParams(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction) {
|
if (middleware.post) {
|
||||||
let url = this.configuration.basePath + context.path;
|
response = await middleware.post({
|
||||||
if (context.query !== undefined && Object.keys(context.query).length !== 0) {
|
fetch: this.fetchApi,
|
||||||
// only add the querystring to the URL if there are query parameters.
|
url: fetchParams.url,
|
||||||
// this is done to avoid urls ending with a "?" character which buggy webservers
|
init: fetchParams.init,
|
||||||
// do not handle correctly sometimes.
|
response: response.clone(),
|
||||||
url += '?' + this.configuration.queryParamsStringify(context.query);
|
}) || response;
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = Object.assign({}, this.configuration.headers, context.headers);
|
|
||||||
Object.keys(headers).forEach(key => headers[key] === undefined ? delete headers[key] : {});
|
|
||||||
|
|
||||||
const initOverrideFn =
|
|
||||||
typeof initOverrides === "function"
|
|
||||||
? initOverrides
|
|
||||||
: async () => initOverrides;
|
|
||||||
|
|
||||||
const initParams = {
|
|
||||||
method: context.method,
|
|
||||||
headers,
|
|
||||||
body: context.body,
|
|
||||||
credentials: this.configuration.credentials,
|
|
||||||
};
|
|
||||||
|
|
||||||
const overriddenInit: RequestInit = {
|
|
||||||
...initParams,
|
|
||||||
...(await initOverrideFn({
|
|
||||||
init: initParams,
|
|
||||||
context,
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
|
|
||||||
const init: RequestInit = {
|
|
||||||
...overriddenInit,
|
|
||||||
body:
|
|
||||||
isFormData(overriddenInit.body) ||
|
|
||||||
overriddenInit.body instanceof URLSearchParams ||
|
|
||||||
isBlob(overriddenInit.body)
|
|
||||||
? overriddenInit.body
|
|
||||||
: JSON.stringify(overriddenInit.body),
|
|
||||||
};
|
|
||||||
|
|
||||||
return { url, init };
|
|
||||||
}
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
private fetchApi = async (url: string, init: RequestInit) => {
|
/**
|
||||||
let fetchParams = { url, init };
|
* Create a shallow clone of `this` by constructing a new instance
|
||||||
for (const middleware of this.middleware) {
|
* and then shallow cloning data members.
|
||||||
if (middleware.pre) {
|
*/
|
||||||
fetchParams = await middleware.pre({
|
private clone<T extends BaseAPI>(this: T): T {
|
||||||
fetch: this.fetchApi,
|
const constructor = this.constructor as any;
|
||||||
...fetchParams,
|
const next = new constructor(this.configuration);
|
||||||
}) || fetchParams;
|
next.middleware = this.middleware.slice();
|
||||||
}
|
return next;
|
||||||
}
|
}
|
||||||
let response: Response | undefined = undefined;
|
|
||||||
try {
|
|
||||||
response = await (this.configuration.fetchApi || fetch)(fetchParams.url, fetchParams.init);
|
|
||||||
} catch (e) {
|
|
||||||
for (const middleware of this.middleware) {
|
|
||||||
if (middleware.onError) {
|
|
||||||
response = await middleware.onError({
|
|
||||||
fetch: this.fetchApi,
|
|
||||||
url: fetchParams.url,
|
|
||||||
init: fetchParams.init,
|
|
||||||
error: e,
|
|
||||||
response: response ? response.clone() : undefined,
|
|
||||||
}) || response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (response === undefined) {
|
|
||||||
if (e instanceof Error) {
|
|
||||||
throw new FetchError(e, 'The request failed and the interceptors did not return an alternative response');
|
|
||||||
} else {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const middleware of this.middleware) {
|
|
||||||
if (middleware.post) {
|
|
||||||
response = await middleware.post({
|
|
||||||
fetch: this.fetchApi,
|
|
||||||
url: fetchParams.url,
|
|
||||||
init: fetchParams.init,
|
|
||||||
response: response.clone(),
|
|
||||||
}) || response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a shallow clone of `this` by constructing a new instance
|
|
||||||
* and then shallow cloning data members.
|
|
||||||
*/
|
|
||||||
private clone<T extends BaseAPI>(this: T): T {
|
|
||||||
const constructor = this.constructor as any;
|
|
||||||
const next = new constructor(this.configuration);
|
|
||||||
next.middleware = this.middleware.slice();
|
|
||||||
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, msg?: string) {
|
||||||
super(msg);
|
super(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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, msg?: string) {
|
||||||
super(msg);
|
super(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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, msg?: string) {
|
||||||
super(msg);
|
super(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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,48 +292,48 @@ 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) {
|
||||||
@ -344,82 +344,82 @@ export function mapValues(data: any, fn: (item: any) => any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ 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";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -16,8 +17,9 @@ export class ExtendedClient extends Client {
|
|||||||
private commandFilePath = `${__dirname}/../commands`
|
private commandFilePath = `${__dirname}/../commands`
|
||||||
private jellyfin: JellyfinHandler
|
private jellyfin: JellyfinHandler
|
||||||
public commands: Collection<string, CommandType> = new Collection()
|
public commands: Collection<string, CommandType> = new Collection()
|
||||||
private announcementChannels: Collection<string, TextChannel> = new Collection //guildId to TextChannel
|
private announcementChannels: Collection<string, TextChannel> = new Collection() //guildId to TextChannel
|
||||||
private announcementRoleHandlerTask: Collection<string, ScheduledTask> = new Collection //one task per guild
|
private announcementRoleHandlerTask: Collection<string, ScheduledTask> = new Collection() //one task per guild
|
||||||
|
private pollCloseBackgroundTasks: Collection<string, ScheduledTask> = new Collection()
|
||||||
public constructor(jf: JellyfinHandler) {
|
public constructor(jf: JellyfinHandler) {
|
||||||
const intents: IntentsBitField = new IntentsBitField()
|
const intents: IntentsBitField = new IntentsBitField()
|
||||||
intents.add(IntentsBitField.Flags.GuildMembers, IntentsBitField.Flags.MessageContent, IntentsBitField.Flags.Guilds, IntentsBitField.Flags.DirectMessages, IntentsBitField.Flags.GuildScheduledEvents, IntentsBitField.Flags.GuildVoiceStates)
|
intents.add(IntentsBitField.Flags.GuildMembers, IntentsBitField.Flags.MessageContent, IntentsBitField.Flags.Guilds, IntentsBitField.Flags.DirectMessages, IntentsBitField.Flags.GuildScheduledEvents, IntentsBitField.Flags.GuildVoiceStates)
|
||||||
@ -73,6 +75,7 @@ export class ExtendedClient extends Client {
|
|||||||
this.cacheUsers(guilds)
|
this.cacheUsers(guilds)
|
||||||
await this.cacheAnnouncementServer(guilds)
|
await this.cacheAnnouncementServer(guilds)
|
||||||
this.startAnnouncementRoleBackgroundTask(guilds)
|
this.startAnnouncementRoleBackgroundTask(guilds)
|
||||||
|
this.startPollCloseBackgroundTasks()
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.info(`Error refreshing slash commands: ${error}`)
|
logger.info(`Error refreshing slash commands: ${error}`)
|
||||||
@ -127,10 +130,10 @@ export class ExtendedClient extends Client {
|
|||||||
for (const guild of guilds.values()) {
|
for (const guild of guilds.values()) {
|
||||||
logger.info("Starting background task for announcement role", { guildId: guild.id })
|
logger.info("Starting background task for announcement role", { guildId: guild.id })
|
||||||
const textChannel: Maybe<TextChannel> = this.getAnnouncementChannelForGuild(guild.id)
|
const textChannel: Maybe<TextChannel> = this.getAnnouncementChannelForGuild(guild.id)
|
||||||
if(!textChannel) {
|
if (!textChannel) {
|
||||||
logger.error("Could not find announcement channel. Aborting", { guildId: guild.id })
|
logger.error("Could not find announcement channel. Aborting", { guildId: guild.id })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.announcementRoleHandlerTask.set(guild.id, schedule("*/10 * * * * *", async () => {
|
this.announcementRoleHandlerTask.set(guild.id, schedule("*/10 * * * * *", async () => {
|
||||||
const requestId = uuid()
|
const requestId = uuid()
|
||||||
const messages = (await textChannel.messages.fetchPinned()).filter(message => message.cleanContent.includes("[initial]"))
|
const messages = (await textChannel.messages.fetchPinned()).filter(message => message.cleanContent.includes("[initial]"))
|
||||||
@ -169,4 +172,10 @@ export class ExtendedClient extends Client {
|
|||||||
}
|
}
|
||||||
task.stop()
|
task.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async startPollCloseBackgroundTasks() {
|
||||||
|
for (const guild of this.guilds.cache) {
|
||||||
|
this.pollCloseBackgroundTasks.set(guild[1].id, schedule("0 * * * * *", () => checkForPollsToClose(guild[1])))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { CommandType } from "../types/commandTypes";
|
import { CommandType } from "../types/commandTypes";
|
||||||
|
|
||||||
export class Command {
|
export class Command {
|
||||||
constructor(commandOptions: CommandType) {
|
constructor(commandOptions: CommandType) {
|
||||||
Object.assign(this, commandOptions)
|
Object.assign(this, commandOptions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ApplicationCommandDataResolvable } from "discord.js"
|
import { ApplicationCommandDataResolvable } from "discord.js"
|
||||||
|
|
||||||
export interface RegisterCommandOptions {
|
export interface RegisterCommandOptions {
|
||||||
guildId?: string
|
guildId?: string
|
||||||
commands: ApplicationCommandDataResolvable[]
|
commands: ApplicationCommandDataResolvable[]
|
||||||
}
|
}
|
||||||
|
@ -2,16 +2,16 @@ import { PermissionResolvable, ChatInputApplicationCommandData, CommandInteracti
|
|||||||
import { ExtendedClient } from "../structures/client";
|
import { ExtendedClient } from "../structures/client";
|
||||||
|
|
||||||
export interface ExtendedInteraction extends CommandInteraction {
|
export interface ExtendedInteraction extends CommandInteraction {
|
||||||
member: GuildMember
|
member: GuildMember
|
||||||
}
|
}
|
||||||
export interface RunOptions {
|
export interface RunOptions {
|
||||||
client: ExtendedClient
|
client: ExtendedClient
|
||||||
interaction: ExtendedInteraction
|
interaction: ExtendedInteraction
|
||||||
args: CommandInteractionOptionResolver
|
args: CommandInteractionOptionResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
type RunFunction = (options: RunOptions) => unknown
|
type RunFunction = (options: RunOptions) => unknown
|
||||||
export type CommandType = {
|
export type CommandType = {
|
||||||
userPermissions?: PermissionResolvable[]
|
userPermissions?: PermissionResolvable[]
|
||||||
run: RunFunction
|
run: RunFunction
|
||||||
} & ChatInputApplicationCommandData
|
} & ChatInputApplicationCommandData
|
||||||
|
@ -2,100 +2,100 @@ import { add } from "date-fns"
|
|||||||
import { CustomError, errorCodes } from "../interfaces"
|
import { CustomError, errorCodes } from "../interfaces"
|
||||||
|
|
||||||
export interface RepetitonInfo {
|
export interface RepetitonInfo {
|
||||||
startDate?: Date, // If defined will take precedence over repetitonAmount
|
startDate?: Date, // If defined will take precedence over repetitonAmount
|
||||||
endDate?: Date,// If defined will take precedence over repetitonAmount
|
endDate?: Date,// If defined will take precedence over repetitonAmount
|
||||||
totalAmount: number,
|
totalAmount: number,
|
||||||
alreadyOccured: number,
|
alreadyOccured: number,
|
||||||
schedule: Schedule
|
schedule: Schedule
|
||||||
}
|
}
|
||||||
export const scheduleNames = ['daily', 'weekly', 'monthly', 'everyNWeeks', 'everyNDays', 'everyNMonths']
|
export const scheduleNames = ['daily', 'weekly', 'monthly', 'everyNWeeks', 'everyNDays', 'everyNMonths']
|
||||||
export type supportedSchedule = typeof scheduleNames[number]
|
export type supportedSchedule = typeof scheduleNames[number]
|
||||||
export interface IScheduleType {
|
export interface IScheduleType {
|
||||||
name: supportedSchedule,
|
name: supportedSchedule,
|
||||||
multiplier: number,
|
multiplier: number,
|
||||||
duration: Duration
|
duration: Duration
|
||||||
}
|
}
|
||||||
export const scheduleTypes: IScheduleType[] = [
|
export const scheduleTypes: IScheduleType[] = [
|
||||||
{
|
{
|
||||||
name: 'daily',
|
name: 'daily',
|
||||||
multiplier: 1,
|
multiplier: 1,
|
||||||
duration: {
|
duration: {
|
||||||
days: 1
|
days: 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'weekly',
|
name: 'weekly',
|
||||||
multiplier: 1,
|
multiplier: 1,
|
||||||
duration: {
|
duration: {
|
||||||
weeks: 1
|
weeks: 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
export class Schedule {
|
export class Schedule {
|
||||||
private scheduleName: string
|
private scheduleName: string
|
||||||
private multiplier = 1
|
private multiplier = 1
|
||||||
private duration: Duration
|
private duration: Duration
|
||||||
private baseScheduleTypes = ['daily', 'weekly', 'monthly', 'yearly']
|
private baseScheduleTypes = ['daily', 'weekly', 'monthly', 'yearly']
|
||||||
private _scheduleString: string
|
private _scheduleString: string
|
||||||
constructor(scheduleString: string) {
|
constructor(scheduleString: string) {
|
||||||
this._scheduleString = scheduleString.toLowerCase()
|
this._scheduleString = scheduleString.toLowerCase()
|
||||||
this.scheduleName = this._scheduleString
|
this.scheduleName = this._scheduleString
|
||||||
if (this.baseScheduleTypes.includes(this._scheduleString)) {
|
if (this.baseScheduleTypes.includes(this._scheduleString)) {
|
||||||
this.multiplier = 1
|
this.multiplier = 1
|
||||||
}
|
}
|
||||||
if (this._scheduleString.includes('every')) {
|
if (this._scheduleString.includes('every')) {
|
||||||
this.scheduleName = this.getBaseScheduleNameFromVariableString()
|
this.scheduleName = this.getBaseScheduleNameFromVariableString()
|
||||||
this.multiplier = this.getMultiplierFromVariableString()
|
this.multiplier = this.getMultiplierFromVariableString()
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (this.scheduleName) {
|
switch (this.scheduleName) {
|
||||||
case 'daily':
|
case 'daily':
|
||||||
this.duration = { days: 1 }
|
this.duration = { days: 1 }
|
||||||
break
|
break
|
||||||
case 'weekly':
|
case 'weekly':
|
||||||
this.duration = { weeks: 1 }
|
this.duration = { weeks: 1 }
|
||||||
break
|
break
|
||||||
case 'monthly':
|
case 'monthly':
|
||||||
this.duration = { months: 1 }
|
this.duration = { months: 1 }
|
||||||
break
|
break
|
||||||
case 'yearly':
|
case 'yearly':
|
||||||
this.duration = { years: 1 }
|
this.duration = { years: 1 }
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
throw new CustomError('Schedule type not supported', errorCodes.schedule_not_supported)
|
throw new CustomError('Schedule type not supported', errorCodes.schedule_not_supported)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public getSanitizedScheduleString(): string {
|
public getSanitizedScheduleString(): string {
|
||||||
return this._scheduleString
|
return this._scheduleString
|
||||||
}
|
}
|
||||||
private getBaseScheduleNameFromVariableString(): string {
|
private getBaseScheduleNameFromVariableString(): string {
|
||||||
if (this._scheduleString.includes('week')) return 'weekly'
|
if (this._scheduleString.includes('week')) return 'weekly'
|
||||||
if (this._scheduleString.includes('day')) return 'daily'
|
if (this._scheduleString.includes('day')) return 'daily'
|
||||||
if (this._scheduleString.includes('month')) return 'monthly'
|
if (this._scheduleString.includes('month')) return 'monthly'
|
||||||
if (this._scheduleString.includes('year')) return 'yearly'
|
if (this._scheduleString.includes('year')) return 'yearly'
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
public getMultiplierFromVariableString(): number {
|
public getMultiplierFromVariableString(): number {
|
||||||
const matches = this._scheduleString.match(/\d+/)
|
const matches = this._scheduleString.match(/\d+/)
|
||||||
if (matches) {
|
if (matches) {
|
||||||
const multi = matches[0]
|
const multi = matches[0]
|
||||||
if (multi)
|
if (multi)
|
||||||
return parseInt(multi)
|
return parseInt(multi)
|
||||||
}
|
}
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
public calculateDuration(): Duration {
|
public calculateDuration(): Duration {
|
||||||
const dur: Duration = {
|
const dur: Duration = {
|
||||||
days: this.duration.days ? this.duration.days * this.multiplier : undefined,
|
days: this.duration.days ? this.duration.days * this.multiplier : undefined,
|
||||||
weeks: this.duration.weeks ? this.duration.weeks * this.multiplier : undefined,
|
weeks: this.duration.weeks ? this.duration.weeks * this.multiplier : undefined,
|
||||||
months: this.duration.months ? this.duration.months * this.multiplier : undefined,
|
months: this.duration.months ? this.duration.months * this.multiplier : undefined,
|
||||||
years: this.duration.years ? this.duration.years * this.multiplier : undefined,
|
years: this.duration.years ? this.duration.years * this.multiplier : undefined,
|
||||||
}
|
}
|
||||||
return dur
|
return dur
|
||||||
}
|
}
|
||||||
|
|
||||||
public getNewDate(oldDate: Date): Date {
|
public getNewDate(oldDate: Date): Date {
|
||||||
const newDate = add(oldDate, this.calculateDuration())
|
const newDate = add(oldDate, this.calculateDuration())
|
||||||
return newDate
|
return newDate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user