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
|
||||
run-name: ${{ gitea.actor }} is building an image
|
||||
on: [push]
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
env:
|
||||
REGISTRY: gitea.brudi.xyz
|
||||
IMAGE_NAME: ${{ gitea.repository }}
|
||||
@ -18,6 +21,8 @@ jobs:
|
||||
- name: Log in to the Container registry
|
||||
run: docker login -u ${{ env.USER }} -p ${{ secrets.TOKEN }} ${{ env.REGISTRY }}
|
||||
- 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
|
||||
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"
|
||||
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 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 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 client = new ExtendedClient(jellyfinHandler)
|
||||
|
||||
|
19
package-lock.json
generated
19
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "node-jellyfin-discord-bot",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.3",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "node-jellyfin-discord-bot",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@discordjs/rest": "^1.7.0",
|
||||
@ -17,6 +17,7 @@
|
||||
"@types/uuid": "^9.0.1",
|
||||
"axios": "^1.3.5",
|
||||
"date-fns": "^2.29.3",
|
||||
"date-fns-tz": "^2.0.0",
|
||||
"discord-api-types": "^0.37.38",
|
||||
"discord.js": "^14.9.0",
|
||||
"dotenv": "^16.0.3",
|
||||
@ -2626,6 +2627,14 @@
|
||||
"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": {
|
||||
"version": "4.3.4",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "node-jellyfin-discord-bot",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.3",
|
||||
"description": "A discord bot to sync jellyfin accounts with discord roles",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
@ -13,6 +13,7 @@
|
||||
"@types/uuid": "^9.0.1",
|
||||
"axios": "^1.3.5",
|
||||
"date-fns": "^2.29.3",
|
||||
"date-fns-tz": "^2.0.0",
|
||||
"discord-api-types": "^0.37.38",
|
||||
"discord.js": "^14.9.0",
|
||||
"dotenv": "^16.0.3",
|
||||
|
@ -13,22 +13,22 @@ export default new Command({
|
||||
options: [{
|
||||
name: "typ",
|
||||
type: ApplicationCommandOptionType.String,
|
||||
description:"Was für ein announcement?",
|
||||
choices: [{name: "initial", value:"initial"},{name: "votepls", value:"votepls"},{name: "cancel", value:"cancel"}],
|
||||
description: "Was für ein announcement?",
|
||||
choices: [{ name: "initial", value: "initial" }, { name: "votepls", value: "votepls" }, { name: "cancel", value: "cancel" }],
|
||||
required: true
|
||||
}],
|
||||
run: async (interaction: RunOptions) => {
|
||||
const command = interaction.interaction
|
||||
const requestId = uuid()
|
||||
if(!command.guildId) {
|
||||
logger.error("COMMAND DOES NOT HAVE A GUILD ID; CANCELLING!!!", {requestId})
|
||||
if (!command.guildId) {
|
||||
logger.error("COMMAND DOES NOT HAVE A GUILD ID; CANCELLING!!!", { requestId })
|
||||
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) {
|
||||
if (!announcementType) {
|
||||
logger.error("Did not get an announcement type!", { guildId, requestId })
|
||||
return
|
||||
}
|
||||
@ -40,7 +40,7 @@ export default new Command({
|
||||
logger.info(`User ${command.member.displayName} seems to be admin`)
|
||||
}
|
||||
|
||||
if((<string>announcementType.value).includes("initial")) {
|
||||
if ((<string>announcementType.value).includes("initial")) {
|
||||
sendInitialAnnouncement(guildId, requestId)
|
||||
command.followUp("Ist rausgeschickt!")
|
||||
} else {
|
||||
@ -56,7 +56,7 @@ function isAdmin(member: GuildMember): boolean {
|
||||
async function sendInitialAnnouncement(guildId: string, requestId: string): Promise<void> {
|
||||
logger.info("Sending initial announcement")
|
||||
const announcementChannel: Maybe<TextChannel> = client.getAnnouncementChannelForGuild(guildId)
|
||||
if(!announcementChannel) {
|
||||
if (!announcementChannel) {
|
||||
logger.error("Could not find announcement channel. Aborting", { guildId, requestId })
|
||||
return
|
||||
}
|
||||
@ -96,7 +96,7 @@ export async function manageAnnouncementRoles(guild: Guild, reaction: MessageRea
|
||||
const allUsers = (await guild.members.fetch())
|
||||
|
||||
const usersWhoHaveRole: GuildMember[] = allUsers
|
||||
.filter(member=> member.roles.cache
|
||||
.filter(member => member.roles.cache
|
||||
.find(role => role.id === config.bot.announcement_role) !== undefined)
|
||||
.map(member => member)
|
||||
|
||||
@ -105,15 +105,15 @@ export async function manageAnnouncementRoles(guild: Guild, reaction: MessageRea
|
||||
|
||||
const usersWhoDontHaveRole: GuildMember[] = allUsers
|
||||
.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)
|
||||
|
||||
const usersWhoNeedRole: GuildMember[] = usersWhoDontHaveRole
|
||||
.filter(userWhoNeeds => usersWhoWantRole.map(wanter => wanter.id).includes(userWhoNeeds.id))
|
||||
|
||||
|
||||
logger.debug(`Theses users will get the role removed: ${JSON.stringify(usersWhoNeedRoleRevoked)}`, {guildId, requestId})
|
||||
logger.debug(`Theses users will get the role added: ${JSON.stringify(usersWhoNeedRole)}`, {guildId, requestId})
|
||||
logger.debug(`Theses users will get the role removed: ${JSON.stringify(usersWhoNeedRoleRevoked)}`, { guildId, requestId })
|
||||
logger.debug(`Theses users will get the role added: ${JSON.stringify(usersWhoNeedRole)}`, { guildId, requestId })
|
||||
|
||||
usersWhoNeedRoleRevoked.forEach(user => user.roles.remove(announcementRole))
|
||||
usersWhoNeedRole.forEach(user => user.roles.add(announcementRole))
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { addDays, differenceInDays, format, isAfter, toDate } from 'date-fns'
|
||||
import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildScheduledEventSetStatusArg, GuildScheduledEventStatus, Message, MessageCreateOptions, TextChannel } from 'discord.js'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
import { client } from '../..'
|
||||
import { config } from '../configuration'
|
||||
import { Emotes } from '../events/guildScheduledEventCreate'
|
||||
import { Emotes } from '../events/autoCreateVoteByWPEvent'
|
||||
import { Maybe } from '../interfaces'
|
||||
import { logger } from '../logger'
|
||||
import { Command } from '../structures/command'
|
||||
import { RunOptions } from '../types/commandTypes'
|
||||
import { format } from 'date-fns'
|
||||
import { Maybe } from '../interfaces'
|
||||
|
||||
export default new Command({
|
||||
name: 'closepoll',
|
||||
@ -34,7 +34,7 @@ export async function closePoll(guild: Guild, requestId: string) {
|
||||
logger.info("stopping poll", { guildId, requestId })
|
||||
|
||||
const announcementChannel: Maybe<TextChannel> = client.getAnnouncementChannelForGuild(guildId)
|
||||
if(!announcementChannel) {
|
||||
if (!announcementChannel) {
|
||||
logger.error("Could not find the textchannel. Unable to close poll.", { guildId, requestId })
|
||||
return
|
||||
}
|
||||
@ -64,7 +64,7 @@ export async function closePoll(guild: Guild, requestId: string) {
|
||||
logger.info("Deleting vote message")
|
||||
await lastMessage.delete()
|
||||
const event = await getEvent(guild, guild.id, requestId)
|
||||
if(event) {
|
||||
if (event) {
|
||||
updateEvent(event, votes, guild, guildId, requestId)
|
||||
sendVoteClosedMessage(event, votes[0].movie, guildId, requestId)
|
||||
}
|
||||
@ -75,14 +75,14 @@ export async function closePoll(guild: Guild, 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 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 = {
|
||||
content: body,
|
||||
allowedMentions: { parse: ["roles"] }
|
||||
}
|
||||
const announcementChannel = client.getAnnouncementChannelForGuild(guildId)
|
||||
logger.info("Sending vote closed message.", { guildId, requestId })
|
||||
if(!announcementChannel) {
|
||||
if (!announcementChannel) {
|
||||
logger.error("Could not find announcement channel. Please fix!", { guildId, requestId })
|
||||
return
|
||||
}
|
||||
@ -146,3 +146,39 @@ function extractMovieFromMessageByEmote(message: Message, emote: string): string
|
||||
|
||||
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 })
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@ import { accountChoice, joingroup, leavegroup, loginScreen, overview, resume, se
|
||||
import { logger } from '../logger'
|
||||
import { Command } from '../structures/command'
|
||||
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({
|
||||
name: 'guides',
|
||||
@ -70,7 +70,7 @@ export default new Command({
|
||||
userDMChannel.send({ embeds: useSyncgroup(), files: [overview, joingroup, resume, leavegroup] })
|
||||
} else if (guideSelection.customId === 'explainRoles') {
|
||||
const userDMChannel = await guideSelection.user.createDM()
|
||||
userDMChannel.send(explainRoles())
|
||||
userDMChannel.send({ embeds: explainRole() })
|
||||
}
|
||||
|
||||
guideSelection.update({ content: "Hab ich dir per DM geschickt :)", components: [] })
|
||||
|
@ -16,13 +16,9 @@ export default new Command({
|
||||
const embedList: APIEmbed[] = []
|
||||
embedList.push(...installation())
|
||||
embedList.push(...configureServer())
|
||||
embedList.push(...explainRole())
|
||||
embedList.push(...loginInfo())
|
||||
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(`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[] {
|
||||
const embedList: APIEmbed[] = []
|
||||
// DownloadLink and installation
|
||||
@ -67,7 +70,7 @@ export function configureServer(): APIEmbed[] {
|
||||
title: "Server Verbindung",
|
||||
description: "Stelle eine Verbindung zum Hartzarett Jellyfin Server her",
|
||||
fields: [
|
||||
{ name: "Server Adresse", value: "https://media.hartzarett.ruhr" }
|
||||
{ name: "Server Adresse", value: "`https://media.hartzarett.ruhr`" }
|
||||
],
|
||||
image: {
|
||||
url: 'attachment://server_verbindung.png'
|
||||
@ -91,7 +94,7 @@ export function loginInfo(): APIEmbed[] {
|
||||
embedList.push({
|
||||
color,
|
||||
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: {
|
||||
url: 'attachment://login_screen.png'
|
||||
}
|
||||
@ -139,8 +142,7 @@ export function useSyncgroup(): APIEmbed[] {
|
||||
return embedList
|
||||
}
|
||||
|
||||
export function explainRoles(): string {
|
||||
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
|
||||
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
|
||||
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.`
|
||||
}
|
||||
|
||||
|
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)
|
||||
}
|
@ -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 { logger } from "../logger";
|
||||
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 { Configuration, ConfigurationParameters } from "./runtime";
|
||||
|
||||
@ -52,24 +52,46 @@ export class JellyfinHandler {
|
||||
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)
|
||||
logger.info(`New Username for ${discordUser.displayName}: ${newUserName}`, { guildId, requestId })
|
||||
const req: CreateUserByNameOperationRequest = {
|
||||
createUserByNameRequest: {
|
||||
name: newUserName,
|
||||
password: this.generatePasswordForUser(),
|
||||
password: this.generatePasswordForUser()
|
||||
}
|
||||
}
|
||||
logger.debug(JSON.stringify(req), { requestId, guildId })
|
||||
const createResult = await this.userApi.createUserByName(req)
|
||||
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}"!`)
|
||||
return createResult
|
||||
}
|
||||
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> {
|
||||
const jfuser = await this.getUser(discordUser, 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)
|
||||
movies.push(...allMovies.splice(index, 1)) // maybe out of bounds? ?
|
||||
}
|
||||
|
||||
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 }
|
||||
|
@ -29,7 +29,7 @@ export interface ConfigurationParameters {
|
||||
}
|
||||
|
||||
export class Configuration {
|
||||
constructor(private configuration: ConfigurationParameters = {}) {}
|
||||
constructor(private configuration: ConfigurationParameters = {}) { }
|
||||
|
||||
set config(configuration: Configuration) {
|
||||
this.configuration = configuration;
|
||||
@ -393,7 +393,7 @@ export interface ResponseTransformer<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> {
|
||||
return this.transformer(await this.raw.json());
|
||||
@ -401,7 +401,7 @@ export class JSONApiResponse<T> {
|
||||
}
|
||||
|
||||
export class VoidApiResponse {
|
||||
constructor(public raw: Response) {}
|
||||
constructor(public raw: Response) { }
|
||||
|
||||
async value(): Promise<void> {
|
||||
return undefined;
|
||||
@ -409,7 +409,7 @@ export class VoidApiResponse {
|
||||
}
|
||||
|
||||
export class BlobApiResponse {
|
||||
constructor(public raw: Response) {}
|
||||
constructor(public raw: Response) { }
|
||||
|
||||
async value(): Promise<Blob> {
|
||||
return await this.raw.blob();
|
||||
@ -417,7 +417,7 @@ export class BlobApiResponse {
|
||||
}
|
||||
|
||||
export class TextApiResponse {
|
||||
constructor(public raw: Response) {}
|
||||
constructor(public raw: Response) { }
|
||||
|
||||
async value(): Promise<string> {
|
||||
return await this.raw.text();
|
||||
|
@ -8,6 +8,7 @@ import { Maybe } from "../interfaces";
|
||||
import { JellyfinHandler } from "../jellyfin/handler";
|
||||
import { logger } from "../logger";
|
||||
import { CommandType } from "../types/commandTypes";
|
||||
import { checkForPollsToClose } from "../commands/closepoll";
|
||||
|
||||
|
||||
|
||||
@ -16,8 +17,9 @@ export class ExtendedClient extends Client {
|
||||
private commandFilePath = `${__dirname}/../commands`
|
||||
private jellyfin: JellyfinHandler
|
||||
public commands: Collection<string, CommandType> = new Collection()
|
||||
private announcementChannels: Collection<string, TextChannel> = new Collection //guildId to TextChannel
|
||||
private announcementRoleHandlerTask: Collection<string, ScheduledTask> = new Collection //one task per guild
|
||||
private announcementChannels: Collection<string, TextChannel> = new Collection() //guildId to TextChannel
|
||||
private announcementRoleHandlerTask: Collection<string, ScheduledTask> = new Collection() //one task per guild
|
||||
private pollCloseBackgroundTasks: Collection<string, ScheduledTask> = new Collection()
|
||||
public constructor(jf: JellyfinHandler) {
|
||||
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)
|
||||
@ -73,6 +75,7 @@ export class ExtendedClient extends Client {
|
||||
this.cacheUsers(guilds)
|
||||
await this.cacheAnnouncementServer(guilds)
|
||||
this.startAnnouncementRoleBackgroundTask(guilds)
|
||||
this.startPollCloseBackgroundTasks()
|
||||
})
|
||||
} catch (error) {
|
||||
logger.info(`Error refreshing slash commands: ${error}`)
|
||||
@ -127,7 +130,7 @@ export class ExtendedClient extends Client {
|
||||
for (const guild of guilds.values()) {
|
||||
logger.info("Starting background task for announcement role", { guildId: 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 })
|
||||
return
|
||||
}
|
||||
@ -169,4 +172,10 @@ export class ExtendedClient extends Client {
|
||||
}
|
||||
task.stop()
|
||||
}
|
||||
|
||||
private async startPollCloseBackgroundTasks() {
|
||||
for (const guild of this.guilds.cache) {
|
||||
this.pollCloseBackgroundTasks.set(guild[1].id, schedule("0 * * * * *", () => checkForPollsToClose(guild[1])))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user