Merge branch 'master' into feat/cicd
This commit is contained in:
commit
2edd0312dc
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-ot
|
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 }}
|
||||||
@ -8,6 +11,7 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
build-docker-image:
|
build-docker-image:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
#if: gitea.ref == 'refs/heads/master'
|
||||||
container: catthehacker/ubuntu:act-latest
|
container: catthehacker/ubuntu:act-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@ -20,4 +24,4 @@ jobs:
|
|||||||
- 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" .
|
||||||
- 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 }}"
|
||||||
|
3
index.ts
3
index.ts
@ -5,7 +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(config)
|
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)
|
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": "0.0.1",
|
"version": "1.1.3",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "node-jellyfin-discord-bot",
|
"name": "node-jellyfin-discord-bot",
|
||||||
"version": "0.0.1",
|
"version": "1.1.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordjs/rest": "^1.7.0",
|
"@discordjs/rest": "^1.7.0",
|
||||||
@ -17,6 +17,7 @@
|
|||||||
"@types/uuid": "^9.0.1",
|
"@types/uuid": "^9.0.1",
|
||||||
"axios": "^1.3.5",
|
"axios": "^1.3.5",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
|
"date-fns-tz": "^2.0.0",
|
||||||
"discord-api-types": "^0.37.38",
|
"discord-api-types": "^0.37.38",
|
||||||
"discord.js": "^14.9.0",
|
"discord.js": "^14.9.0",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
@ -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": "0.0.1",
|
"version": "1.1.3",
|
||||||
"description": "A discord bot to sync jellyfin accounts with discord roles",
|
"description": "A discord bot to sync jellyfin accounts with discord roles",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -13,6 +13,7 @@
|
|||||||
"@types/uuid": "^9.0.1",
|
"@types/uuid": "^9.0.1",
|
||||||
"axios": "^1.3.5",
|
"axios": "^1.3.5",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
|
"date-fns-tz": "^2.0.0",
|
||||||
"discord-api-types": "^0.37.38",
|
"discord-api-types": "^0.37.38",
|
||||||
"discord.js": "^14.9.0",
|
"discord.js": "^14.9.0",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
|
121
server/commands/announce.ts
Normal file
121
server/commands/announce.ts
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import { ApplicationCommandOptionType, Guild, GuildMember, Message, MessageCreateOptions, MessageReaction, Role, TextChannel, User } from 'discord.js'
|
||||||
|
import { v4 as uuid } from 'uuid'
|
||||||
|
import { client } from '../..'
|
||||||
|
import { config } from '../configuration'
|
||||||
|
import { Maybe } from '../interfaces'
|
||||||
|
import { logger } from '../logger'
|
||||||
|
import { Command } from '../structures/command'
|
||||||
|
import { RunOptions } from '../types/commandTypes'
|
||||||
|
|
||||||
|
export default new Command({
|
||||||
|
name: 'announce',
|
||||||
|
description: 'Neues announcement im announcement Channel an alle senden.',
|
||||||
|
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"}],
|
||||||
|
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})
|
||||||
|
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`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function isAdmin(member: GuildMember): boolean {
|
||||||
|
return member.roles.cache.find((role) => role.id === config.bot.jf_admin_role) !== undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendInitialAnnouncement(guildId: string, requestId: string): Promise<void> {
|
||||||
|
logger.info("Sending initial announcement")
|
||||||
|
const announcementChannel: Maybe<TextChannel> = client.getAnnouncementChannelForGuild(guildId)
|
||||||
|
if(!announcementChannel) {
|
||||||
|
logger.error("Could not find announcement channel. Aborting", { guildId, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPinnedAnnouncementMessages = (await announcementChannel.messages.fetchPinned()).filter(message => message.cleanContent.includes("[initial]"))
|
||||||
|
currentPinnedAnnouncementMessages.forEach(async (message) => await message.unpin())
|
||||||
|
currentPinnedAnnouncementMessages.forEach(message => message.delete())
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
allowedMentions: { parse: ['everyone'] },
|
||||||
|
content: body
|
||||||
|
}
|
||||||
|
const message: Message<true> = await announcementChannel.send(options)
|
||||||
|
await message.react("🎫")
|
||||||
|
await message.pin()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function manageAnnouncementRoles(guild: Guild, reaction: MessageReaction, requestId: string) {
|
||||||
|
const guildId = guild.id
|
||||||
|
logger.info("Managing roles", { guildId, requestId })
|
||||||
|
|
||||||
|
const announcementRole: Role | undefined = (await guild.roles.fetch()).find(role => role.id === config.bot.announcement_role)
|
||||||
|
if (!announcementRole) {
|
||||||
|
logger.error(`Could not find announcement role! Aborting! Was looking for role with id: ${config.bot.announcement_role}`, { guildId, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const usersWhoWantRole: User[] = (await reaction.users.fetch()).filter(user => !user.bot).map(user => user)
|
||||||
|
|
||||||
|
const allUsers = (await guild.members.fetch())
|
||||||
|
|
||||||
|
const usersWhoHaveRole: GuildMember[] = allUsers
|
||||||
|
.filter(member=> member.roles.cache
|
||||||
|
.find(role => role.id === config.bot.announcement_role) !== undefined)
|
||||||
|
.map(member => member)
|
||||||
|
|
||||||
|
const usersWhoNeedRoleRevoked: GuildMember[] = usersWhoHaveRole
|
||||||
|
.filter(userWhoHas => !usersWhoWantRole.map(wanter => wanter.id).includes(userWhoHas.id))
|
||||||
|
|
||||||
|
const usersWhoDontHaveRole: GuildMember[] = allUsers
|
||||||
|
.filter(member => member.roles.cache
|
||||||
|
.find(role=> role.id === config.bot.announcement_role) === undefined)
|
||||||
|
.map(member => member)
|
||||||
|
|
||||||
|
const usersWhoNeedRole: GuildMember[] = usersWhoDontHaveRole
|
||||||
|
.filter(userWhoNeeds => usersWhoWantRole.map(wanter => wanter.id).includes(userWhoNeeds.id))
|
||||||
|
|
||||||
|
|
||||||
|
logger.debug(`Theses users will get the role removed: ${JSON.stringify(usersWhoNeedRoleRevoked)}`, {guildId, requestId})
|
||||||
|
logger.debug(`Theses users will get the role added: ${JSON.stringify(usersWhoNeedRole)}`, {guildId, requestId})
|
||||||
|
|
||||||
|
usersWhoNeedRoleRevoked.forEach(user => user.roles.remove(announcementRole))
|
||||||
|
usersWhoNeedRole.forEach(user => user.roles.add(announcementRole))
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,13 @@
|
|||||||
import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildScheduledEventSetStatusArg, GuildScheduledEventStatus, Message, MessageEditOptions, TextChannel } from 'discord.js'
|
import { addDays, differenceInDays, format, isAfter, toDate } from 'date-fns'
|
||||||
|
import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildScheduledEventSetStatusArg, GuildScheduledEventStatus, Message, MessageCreateOptions, TextChannel } from 'discord.js'
|
||||||
import { v4 as uuid } from 'uuid'
|
import { v4 as uuid } from 'uuid'
|
||||||
|
import { client } from '../..'
|
||||||
import { 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 { client } from '../..'
|
|
||||||
|
|
||||||
export default new Command({
|
export default new Command({
|
||||||
name: 'closepoll',
|
name: 'closepoll',
|
||||||
@ -14,13 +16,13 @@ export default new Command({
|
|||||||
run: async (interaction: RunOptions) => {
|
run: async (interaction: RunOptions) => {
|
||||||
const command = interaction.interaction
|
const command = interaction.interaction
|
||||||
const requestId = uuid()
|
const requestId = uuid()
|
||||||
const guildId = command.guildId!
|
|
||||||
logger.info("Got command for closing poll!", { guildId, requestId })
|
|
||||||
if (!command.guild) {
|
if (!command.guild) {
|
||||||
logger.error("No guild found in interaction. Cancelling closing request", { guildId, 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 :)")
|
command.followUp("Alles klar, beende die Umfrage :)")
|
||||||
closePoll(command.guild, requestId)
|
closePoll(command.guild, requestId)
|
||||||
@ -31,10 +33,14 @@ 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: TextChannel = client.getAnnouncementChannelForGuild(guildId)
|
const announcementChannel: Maybe<TextChannel> = client.getAnnouncementChannelForGuild(guildId)
|
||||||
|
if (!announcementChannel) {
|
||||||
|
logger.error("Could not find the textchannel. Unable to close poll.", { guildId, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const messages: Message<true>[] = (await announcementChannel.messages.fetch()) //todo: fetch only pinned messages
|
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)
|
||||||
|
|
||||||
@ -49,42 +55,42 @@ export async function closePoll(guild: Guild, requestId: string) {
|
|||||||
|
|
||||||
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.debug(`votes: ${JSON.stringify(votes, null, 2)}`, { guildId, requestId })
|
||||||
|
|
||||||
updateEvent(votes, guild!, guildId, requestId)
|
logger.info("Deleting vote message")
|
||||||
updateMessage(votes[0].movie, lastMessage, guildId, requestId)
|
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
|
//lastMessage.unpin() //todo: uncomment when bot has permission to pin/unpin
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateMessage(movie: string, message: Message, guildId: string, requestId: string) {
|
async function sendVoteClosedMessage(event: GuildScheduledEvent, movie: string, guildId: string, requestId: string) {
|
||||||
const body = `[Abstimmung beendet] Gewonnen hat: ${movie}`
|
const date = event.scheduledStartAt ? format(event.scheduledStartAt, "dd.MM") : "Fehler, event hatte kein Datum"
|
||||||
.concat(message.cleanContent.substring("[Abstimmung]".length))
|
const time = event.scheduledStartAt ? format(event.scheduledStartAt, "HH:mm") : "Fehler, event hatte kein Datum"
|
||||||
|
const body = `[Abstimmung beendet] für https://discord.com/events/${event.guildId}/${event.id}\n<@&${config.bot.announcement_role}> Wir gucken ${movie} am ${date} um ${time}`
|
||||||
const options: MessageEditOptions = {
|
const options: MessageCreateOptions = {
|
||||||
content: body,
|
content: body,
|
||||||
|
allowedMentions: { parse: ["roles"] }
|
||||||
}
|
}
|
||||||
logger.info("Updating message.", { guildId, requestId })
|
const announcementChannel = client.getAnnouncementChannelForGuild(guildId)
|
||||||
message.edit(options)
|
logger.info("Sending vote closed message.", { guildId, requestId })
|
||||||
|
if (!announcementChannel) {
|
||||||
}
|
logger.error("Could not find announcement channel. Please fix!", { guildId, requestId })
|
||||||
|
|
||||||
async function updateEvent(votes: Vote[], guild: Guild, guildId: string, requestId: string) {
|
|
||||||
logger.info(`Updating event with movie ${votes[0].movie}.`, { guildId, requestId })
|
|
||||||
const voteEvents = (await guild.scheduledEvents.fetch())
|
|
||||||
.map((value, _) => value)
|
|
||||||
.filter(event => event.name.toLowerCase().includes("voting offen"))
|
|
||||||
logger.debug(`Found events: ${JSON.stringify(voteEvents, null, 2)}`, { guildId, requestId })
|
|
||||||
|
|
||||||
if (!voteEvents || voteEvents.length <= 0) {
|
|
||||||
logger.error("Could not find vote event. Cancelling update!", { guildId, requestId })
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
announcementChannel.send(options)
|
||||||
|
}
|
||||||
|
|
||||||
const voteEvent: GuildScheduledEvent<GuildScheduledEventStatus> = voteEvents[0]
|
async function updateEvent(voteEvent: GuildScheduledEvent, votes: Vote[], guild: Guild, guildId: string, requestId: string) {
|
||||||
|
logger.info(`Updating event with movie ${votes[0].movie}.`, { guildId, requestId })
|
||||||
const options: GuildScheduledEventEditOptions<GuildScheduledEventStatus.Scheduled, GuildScheduledEventSetStatusArg<GuildScheduledEventStatus.Scheduled>> = {
|
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`
|
||||||
@ -94,6 +100,19 @@ async function updateEvent(votes: Vote[], guild: Guild, guildId: string, request
|
|||||||
voteEvent.edit(options)
|
voteEvent.edit(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getEvent(guild: Guild, guildId: string, requestId: string): Promise<GuildScheduledEvent | null> {
|
||||||
|
const voteEvents = (await guild.scheduledEvents.fetch())
|
||||||
|
.map((value) => value)
|
||||||
|
.filter(event => event.name.toLowerCase().includes("voting offen"))
|
||||||
|
logger.debug(`Found events: ${JSON.stringify(voteEvents, null, 2)}`, { guildId, requestId })
|
||||||
|
|
||||||
|
if (!voteEvents || voteEvents.length <= 0) {
|
||||||
|
logger.error("Could not find vote event. Cancelling update!", { guildId, requestId })
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return voteEvents[0]
|
||||||
|
}
|
||||||
|
|
||||||
type Vote = {
|
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,
|
||||||
@ -109,14 +128,14 @@ async function getVotesByEmote(message: Message, guildId: string, requestId: str
|
|||||||
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, guildId, requestId) }
|
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, guildId: string, requestId: 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))
|
||||||
|
|
||||||
@ -127,3 +146,39 @@ function extractMovieFromMessageByEmote(message: Message, emote: string, guildId
|
|||||||
|
|
||||||
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 })
|
||||||
|
}
|
||||||
|
}
|
@ -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.`
|
||||||
}
|
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { ApplicationCommandOptionType, BurstHandlerMajorIdKey } from 'discord.js'
|
import { v4 as uuid } from 'uuid'
|
||||||
|
import { jellyfinHandler } from "../.."
|
||||||
import { Command } from '../structures/command'
|
import { Command } from '../structures/command'
|
||||||
import { RunOptions } from '../types/commandTypes'
|
import { RunOptions } from '../types/commandTypes'
|
||||||
import { jellyfinHandler } from "../.."
|
|
||||||
import { v4 as uuid } from 'uuid'
|
|
||||||
|
|
||||||
export default new Command({
|
export default new Command({
|
||||||
name: 'passwort_reset',
|
name: 'passwort_reset',
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import dotenv from "dotenv"
|
import dotenv from "dotenv"
|
||||||
import { AddListingProviderRequestToJSON } from "./jellyfin"
|
|
||||||
dotenv.config()
|
dotenv.config()
|
||||||
|
|
||||||
interface options {
|
interface options {
|
||||||
@ -23,8 +22,14 @@ export interface Config {
|
|||||||
workaround_token: string
|
workaround_token: string
|
||||||
watcher_role: string
|
watcher_role: string
|
||||||
jf_admin_role: string
|
jf_admin_role: string
|
||||||
|
announcement_role: string
|
||||||
announcement_channel_id: string
|
announcement_channel_id: string
|
||||||
jf_collection_id: string
|
jf_collection_id: string
|
||||||
|
jf_user: string
|
||||||
|
yavin_collection_id: string
|
||||||
|
yavin_jellyfin_url: string
|
||||||
|
yavin_jellyfin_token: string
|
||||||
|
yavin_jellyfin_collection_user: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export const config: Config = {
|
export const config: Config = {
|
||||||
@ -57,7 +62,13 @@ export const config: Config = {
|
|||||||
workaround_token: process.env.TOKEN ?? "",
|
workaround_token: process.env.TOKEN ?? "",
|
||||||
watcher_role: process.env.WATCHER_ROLE ?? "",
|
watcher_role: process.env.WATCHER_ROLE ?? "",
|
||||||
jf_admin_role: process.env.ADMIN_ROLE ?? "",
|
jf_admin_role: process.env.ADMIN_ROLE ?? "",
|
||||||
|
announcement_role: process.env.WATCHPARTY_ANNOUNCEMENT_ROLE ?? "",
|
||||||
announcement_channel_id: process.env.CHANNEL_ID ?? "",
|
announcement_channel_id: process.env.CHANNEL_ID ?? "",
|
||||||
jf_collection_id: process.env.JELLYFIN_COLLECTION_ID ?? ""
|
jf_collection_id: process.env.JELLYFIN_COLLECTION_ID ?? "",
|
||||||
|
yavin_collection_id: process.env.YAVIN_COLLECTION_ID ?? "",
|
||||||
|
yavin_jellyfin_url: process.env.YAVIN_JELLYFIN_URL ?? "",
|
||||||
|
yavin_jellyfin_token: process.env.YAVIN_TOKEN ?? "",
|
||||||
|
yavin_jellyfin_collection_user: process.env.YAVIN_COLLECTION_USER ?? "",
|
||||||
|
jf_user: process.env.JELLYFIN_USER ?? ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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,88 +0,0 @@
|
|||||||
import { GuildScheduledEvent, Message, TextChannel } from "discord.js";
|
|
||||||
import { ScheduledTask, schedule } from "node-cron";
|
|
||||||
import { v4 as uuid } from "uuid";
|
|
||||||
import { client, jellyfinHandler } from "../..";
|
|
||||||
import { closePoll } from "../commands/closepoll";
|
|
||||||
import { config } from "../configuration";
|
|
||||||
import { logger } from "../logger";
|
|
||||||
import toDate from "date-fns/fp/toDate";
|
|
||||||
import { addDays, isAfter, isBefore } from "date-fns";
|
|
||||||
|
|
||||||
|
|
||||||
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 jellyfinHandler.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: TextChannel = client.getAnnouncementChannelForGuild(event.guildId)
|
|
||||||
logger.debug(`Found channel ${JSON.stringify(announcementChannel, null, 2)}`, { guildId: event.guildId, requestId })
|
|
||||||
|
|
||||||
let message = "[Abstimmung]\nEs gibt eine neue Abstimmung für die nächste Watchparty! 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!).concat("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
const sentMessage: Message<true> = await (await announcementChannel.fetch()).send(message)
|
|
||||||
|
|
||||||
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 })
|
|
||||||
}
|
|
||||||
}
|
|
@ -9,7 +9,7 @@ export async function execute(oldMember: GuildMember, newMember: GuildMember) {
|
|||||||
try {
|
try {
|
||||||
const requestId = uuid()
|
const requestId = uuid()
|
||||||
const changedRoles: ChangedRoles = filterRolesFromMemberUpdate(oldMember, newMember)
|
const changedRoles: ChangedRoles = filterRolesFromMemberUpdate(oldMember, newMember)
|
||||||
const triggerRoleIds: Collection<string, PermissionLevel> = getGuildSpecificTriggerRoleId(oldMember.guild.id)
|
const triggerRoleIds: Collection<string, PermissionLevel> = getGuildSpecificTriggerRoleId()
|
||||||
|
|
||||||
triggerRoleIds.forEach((level, key) => {
|
triggerRoleIds.forEach((level, key) => {
|
||||||
const addedRoleMatches = changedRoles.addedRoles.find(aRole => aRole.id === key)
|
const addedRoleMatches = changedRoles.addedRoles.find(aRole => aRole.id === key)
|
@ -18,8 +18,8 @@ export async function execute(oldState: VoiceState, newState: VoiceState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const scheduledEvents = (await newState.guild.scheduledEvents.fetch())
|
const scheduledEvents = (await newState.guild.scheduledEvents.fetch())
|
||||||
.filter((key, value) => key.description?.toLowerCase().includes("!wp") && key.isActive())
|
.filter((key) => key.description?.toLowerCase().includes("!wp") && key.isActive())
|
||||||
.map((key, value) => key)
|
.map((key) => key)
|
||||||
|
|
||||||
const scheduledEventUsers = (await Promise.all(scheduledEvents.map(event => event.fetchSubscribers({withMember: true}))))
|
const scheduledEventUsers = (await Promise.all(scheduledEvents.map(event => event.fetchSubscribers({withMember: true}))))
|
||||||
|
|
@ -10,12 +10,16 @@ export const name = 'guildScheduledEventUpdate'
|
|||||||
export async function execute(oldEvent: GuildScheduledEvent, newEvent: GuildScheduledEvent) {
|
export async function execute(oldEvent: GuildScheduledEvent, newEvent: GuildScheduledEvent) {
|
||||||
try {
|
try {
|
||||||
const requestId = uuid()
|
const requestId = uuid()
|
||||||
logger.debug(`Got scheduledEvent update. New Event: ${JSON.stringify(newEvent, null, 2)}`,{guildId: newEvent.guildId, requestId})
|
// 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)) {
|
if (newEvent.description?.toLowerCase().includes("!wp") && [GuildScheduledEventStatus.Active, GuildScheduledEventStatus.Completed].includes(newEvent.status)) {
|
||||||
const roles = getGuildSpecificTriggerRoleId(newEvent.guildId).map((key, value)=> value)
|
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 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 channelMembers = newEvent.channel?.members.filter(member => !member.roles.cache.hasAny(...roles)).map((value) => value)
|
||||||
const allMembers = eventMembers.concat(channelMembers ?? [])
|
const allMembers = eventMembers.concat(channelMembers ?? [])
|
||||||
|
|
||||||
const members: GuildMember[] = []
|
const members: GuildMember[] = []
|
||||||
@ -24,9 +28,11 @@ export async function execute(oldEvent: GuildScheduledEvent, newEvent: GuildSche
|
|||||||
members.push(member)
|
members.push(member)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (newEvent.status === GuildScheduledEventStatus.Active)
|
if (newEvent.status === GuildScheduledEventStatus.Active)
|
||||||
createJFUsers(members, newEvent.name, requestId)
|
createJFUsers(members, newEvent.name, requestId)
|
||||||
else {
|
else {
|
||||||
|
|
||||||
members.forEach(member => {
|
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.`))
|
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.`))
|
||||||
})
|
})
|
@ -1,4 +0,0 @@
|
|||||||
export const name = 'ready'
|
|
||||||
export function execute(client: any) {
|
|
||||||
//console.log(`Processing ready: ${JSON.stringify(client)} has been created.`)
|
|
||||||
}
|
|
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}`
|
||||||
|
}
|
@ -16,7 +16,7 @@ export function filterRolesFromMemberUpdate(oldMember: GuildMember, newMember: G
|
|||||||
return { addedRoles, removedRoles }
|
return { addedRoles, removedRoles }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getGuildSpecificTriggerRoleId(guildId: string): Collection<string, PermissionLevel> {
|
export function getGuildSpecificTriggerRoleId(): Collection<string, PermissionLevel> {
|
||||||
const outVal = new Collection<string, PermissionLevel>()
|
const outVal = new Collection<string, PermissionLevel>()
|
||||||
outVal.set(config.bot.watcher_role, "VIEWER")
|
outVal.set(config.bot.watcher_role, "VIEWER")
|
||||||
outVal.set(config.bot.jf_admin_role, "ADMIN")
|
outVal.set(config.bot.jf_admin_role, "ADMIN")
|
||||||
|
@ -32,4 +32,10 @@ export interface ChangedRoles {
|
|||||||
addedRoles: Collection<string, Role>
|
addedRoles: Collection<string, Role>
|
||||||
removedRoles: Collection<string, Role>
|
removedRoles: Collection<string, Role>
|
||||||
}
|
}
|
||||||
|
export interface JellyfinConfig {
|
||||||
|
jellyfinUrl: string,
|
||||||
|
jellyfinToken: string,
|
||||||
|
movieCollectionId: string,
|
||||||
|
collectionUser: string
|
||||||
|
}
|
||||||
export type PermissionLevel = "VIEWER" | "ADMIN" | "TEMPORARY"
|
export type PermissionLevel = "VIEWER" | "ADMIN" | "TEMPORARY"
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { GuildMember } from "discord.js";
|
import { GuildMember } from "discord.js";
|
||||||
import { Config } from "../configuration";
|
import { JellyfinConfig, Maybe, PermissionLevel } from "../interfaces";
|
||||||
import { Maybe, PermissionLevel } from "../interfaces";
|
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
import { CreateUserByNameOperationRequest, DeleteUserRequest, GetItemsRequest, GetMovieRemoteSearchResultsOperationRequest, ItemLookupApi, ItemsApi, LibraryApi, 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";
|
||||||
|
|
||||||
@ -15,34 +14,27 @@ export class JellyfinHandler {
|
|||||||
private moviesApi: ItemsApi
|
private moviesApi: ItemsApi
|
||||||
private token: string
|
private token: string
|
||||||
private authHeader: { headers: { 'X-Emby-Authorization': string } }
|
private authHeader: { headers: { 'X-Emby-Authorization': string } }
|
||||||
private config: Config
|
private config: JellyfinConfig
|
||||||
private serverName = "";
|
private serverName = "";
|
||||||
|
|
||||||
public async ServerName(): Promise<string> {
|
constructor(_config: JellyfinConfig, _userApi?: UserApi, _systemApi?: SystemApi, _itemsApi?: ItemsApi) {
|
||||||
if (this.serverName === "") {
|
|
||||||
const info = await this.systemApi.getSystemInfo(this.authHeader)
|
|
||||||
this.serverName = info.serverName ?? this.config.bot.jellyfin_url
|
|
||||||
}
|
|
||||||
return this.serverName
|
|
||||||
}
|
|
||||||
constructor(_config: Config, _userApi?: UserApi, _systemApi?: SystemApi, _itemsApi?: ItemsApi) {
|
|
||||||
this.config = _config
|
this.config = _config
|
||||||
this.token = this.config.bot.jellfin_token
|
this.token = this.config.jellyfinToken
|
||||||
this.authHeader = {
|
this.authHeader = {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Emby-Authorization": this.config.bot.workaround_token
|
"X-Emby-Authorization": this.config.jellyfinToken
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const userApiConfigurationParams: ConfigurationParameters = {
|
const userApiConfigurationParams: ConfigurationParameters = {
|
||||||
basePath: this.config.bot.jellyfin_url,
|
basePath: this.config.jellyfinUrl,
|
||||||
headers: this.authHeader.headers
|
headers: this.authHeader.headers
|
||||||
}
|
}
|
||||||
const systemApiConfigurationParams: ConfigurationParameters = {
|
const systemApiConfigurationParams: ConfigurationParameters = {
|
||||||
basePath: this.config.bot.jellyfin_url,
|
basePath: this.config.jellyfinUrl,
|
||||||
headers: this.authHeader.headers
|
headers: this.authHeader.headers
|
||||||
}
|
}
|
||||||
const libraryApiConfigurationParams: ConfigurationParameters = {
|
const libraryApiConfigurationParams: ConfigurationParameters = {
|
||||||
basePath: this.config.bot.jellyfin_url,
|
basePath: this.config.jellyfinUrl,
|
||||||
headers: this.authHeader.headers
|
headers: this.authHeader.headers
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,32 +48,50 @@ export class JellyfinHandler {
|
|||||||
return `${discordUser.displayName}${level == "TEMPORARY" ? "_tmp" : ""}`
|
return `${discordUser.displayName}${level == "TEMPORARY" ? "_tmp" : ""}`
|
||||||
}
|
}
|
||||||
|
|
||||||
public async addPermissionsToUserAccount(jfUserAccount: UserDto, guildId: string, requestId: string): Promise<UserDto> {
|
|
||||||
throw new Error("Method not implemented.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private generatePasswordForUser(): string {
|
private generatePasswordForUser(): string {
|
||||||
return (Math.random() * 10000 + 10000).toFixed(0)
|
return (Math.random() * 10000 + 10000).toFixed(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createUserAccountForDiscordUser(discordUser: GuildMember, level: PermissionLevel, guildId?: string, requestId?: string): Promise<UserDto> {
|
public async createUserAccountForDiscordUser(discordUser: GuildMember, level: PermissionLevel, requestId: string, guildId?: string): Promise<UserDto> {
|
||||||
const newUserName = this.generateJFUserName(discordUser, level)
|
const newUserName = this.generateJFUserName(discordUser, level)
|
||||||
logger.info(`New Username for ${discordUser.displayName}: ${newUserName}`, { guildId, requestId })
|
logger.info(`New Username for ${discordUser.displayName}: ${newUserName}`, { guildId, requestId })
|
||||||
const req: CreateUserByNameOperationRequest = {
|
const req: CreateUserByNameOperationRequest = {
|
||||||
createUserByNameRequest: {
|
createUserByNameRequest: {
|
||||||
name: newUserName,
|
name: newUserName,
|
||||||
password: this.generatePasswordForUser(),
|
password: this.generatePasswordForUser()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.debug(JSON.stringify(req), { requestId, guildId })
|
logger.debug(JSON.stringify(req), { requestId, guildId })
|
||||||
const createResult = await this.userApi.createUserByName(req)
|
const createResult = await this.userApi.createUserByName(req)
|
||||||
if (createResult) {
|
if (createResult) {
|
||||||
|
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 })
|
||||||
@ -232,11 +242,9 @@ export class JellyfinHandler {
|
|||||||
public async getAllMovies(guildId: string, requestId: string): Promise<BaseItemDto[]> {
|
public async getAllMovies(guildId: string, requestId: string): Promise<BaseItemDto[]> {
|
||||||
logger.info("requesting all movies from jellyfin", { guildId, requestId })
|
logger.info("requesting all movies from jellyfin", { guildId, requestId })
|
||||||
|
|
||||||
const liloJfUser = await this.getUser(<GuildMember>{ guild: { id: guildId }, displayName: "lilo" }, requestId)
|
|
||||||
|
|
||||||
const searchParams: GetItemsRequest = {
|
const searchParams: GetItemsRequest = {
|
||||||
userId: liloJfUser?.id,
|
userId: this.config.collectionUser,
|
||||||
parentId: this.config.bot.jf_collection_id // collection ID for all movies
|
parentId: this.config.movieCollectionId // collection ID for all movies
|
||||||
}
|
}
|
||||||
const movies = (await (this.moviesApi.getItems(searchParams))).items?.filter(item => !item.isFolder)
|
const movies = (await (this.moviesApi.getItems(searchParams))).items?.filter(item => !item.isFolder)
|
||||||
// logger.debug(JSON.stringify(movies, null, 2), { guildId: guildId, requestId })
|
// logger.debug(JSON.stringify(movies, null, 2), { guildId: guildId, requestId })
|
||||||
@ -253,13 +261,24 @@ export class JellyfinHandler {
|
|||||||
}
|
}
|
||||||
const movies: BaseItemDto[] = []
|
const movies: BaseItemDto[] = []
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
const index = 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 }
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
import { ApplicationCommandDataResolvable, Client, ClientOptions, Collection, GatewayIntentBits, Guild, IntentsBitField, Snowflake, TextChannel } from "discord.js";
|
import { ApplicationCommandDataResolvable, Client, ClientOptions, Collection, Guild, IntentsBitField, Snowflake, TextChannel } from "discord.js";
|
||||||
import { CommandType } from "../types/commandTypes";
|
import fs from 'fs';
|
||||||
import fs from 'fs'
|
import { ScheduledTask, schedule } from "node-cron";
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import { manageAnnouncementRoles } from "../commands/announce";
|
||||||
import { config } from "../configuration";
|
import { config } from "../configuration";
|
||||||
import { logger } from "../logger";
|
import { Maybe } from "../interfaces";
|
||||||
import { JellyfinHandler } from "../jellyfin/handler";
|
import { JellyfinHandler } from "../jellyfin/handler";
|
||||||
|
import { logger } from "../logger";
|
||||||
|
import { CommandType } from "../types/commandTypes";
|
||||||
|
import { checkForPollsToClose } from "../commands/closepoll";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -12,7 +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 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)
|
||||||
@ -28,7 +35,6 @@ export class ExtendedClient extends Client {
|
|||||||
Promise.all(promises).then(() => {
|
Promise.all(promises).then(() => {
|
||||||
this.login(config.bot.token)
|
this.login(config.bot.token)
|
||||||
})
|
})
|
||||||
logger.info(`Connected with ${await this.jellyfin.ServerName()}`)
|
|
||||||
}
|
}
|
||||||
private async importFile(filepath: string): Promise<any> {
|
private async importFile(filepath: string): Promise<any> {
|
||||||
logger.debug(`Importing ${filepath}`)
|
logger.debug(`Importing ${filepath}`)
|
||||||
@ -60,14 +66,16 @@ export class ExtendedClient extends Client {
|
|||||||
this.commands.set(command.name, command)
|
this.commands.set(command.name, command)
|
||||||
slashCommands.push(command)
|
slashCommands.push(command)
|
||||||
}
|
}
|
||||||
this.on("ready", (client: Client) => {
|
this.on("ready", async (client: Client) => {
|
||||||
//logger.info(`Ready processing ${JSON.stringify(client)}`)
|
//logger.info(`Ready processing ${JSON.stringify(client)}`)
|
||||||
logger.info(`SlashCommands: ${JSON.stringify(slashCommands)}`)
|
logger.info(`SlashCommands: ${JSON.stringify(slashCommands)}`)
|
||||||
const guilds = client.guilds.cache
|
const guilds = client.guilds.cache
|
||||||
|
|
||||||
this.registerCommands(slashCommands, guilds)
|
this.registerCommands(slashCommands, guilds)
|
||||||
this.cacheUsers(guilds)
|
this.cacheUsers(guilds)
|
||||||
this.cacheAnnouncementServer(guilds)
|
await this.cacheAnnouncementServer(guilds)
|
||||||
|
this.startAnnouncementRoleBackgroundTask(guilds)
|
||||||
|
this.startPollCloseBackgroundTasks()
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.info(`Error refreshing slash commands: ${error}`)
|
logger.info(`Error refreshing slash commands: ${error}`)
|
||||||
@ -76,8 +84,8 @@ export class ExtendedClient extends Client {
|
|||||||
private async cacheAnnouncementServer(guilds: Collection<Snowflake, Guild>) {
|
private async cacheAnnouncementServer(guilds: Collection<Snowflake, Guild>) {
|
||||||
for (const guild of guilds.values()) {
|
for (const guild of guilds.values()) {
|
||||||
const channels: TextChannel[] = <TextChannel[]>(await guild.channels.fetch())
|
const channels: TextChannel[] = <TextChannel[]>(await guild.channels.fetch())
|
||||||
?.filter(channel => channel!.id === config.bot.announcement_channel_id)
|
?.filter(channel => channel?.id === config.bot.announcement_channel_id)
|
||||||
.map((value, _) => value)
|
.map((value) => value)
|
||||||
|
|
||||||
if (!channels || channels.length != 1) {
|
if (!channels || channels.length != 1) {
|
||||||
logger.error(`Could not find announcement channel for guild ${guild.name} with guildId ${guild.id}. Found ${channels}`)
|
logger.error(`Could not find announcement channel for guild ${guild.name} with guildId ${guild.id}. Found ${channels}`)
|
||||||
@ -87,8 +95,8 @@ export class ExtendedClient extends Client {
|
|||||||
this.announcementChannels.set(guild.id, channels[0])
|
this.announcementChannels.set(guild.id, channels[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public getAnnouncementChannelForGuild(guildId: string): TextChannel {
|
public getAnnouncementChannelForGuild(guildId: string): Maybe<TextChannel> {
|
||||||
return this.announcementChannels.get(guildId)! //we set the channel by ourselves only if we find one, I think this is sage (mark my words)
|
return this.announcementChannels.get(guildId)
|
||||||
}
|
}
|
||||||
public async cacheUsers(guilds: Collection<Snowflake, Guild>) {
|
public async cacheUsers(guilds: Collection<Snowflake, Guild>) {
|
||||||
guilds.forEach((guild: Guild, id: Snowflake) => {
|
guilds.forEach((guild: Guild, id: Snowflake) => {
|
||||||
@ -117,4 +125,57 @@ export class ExtendedClient extends Client {
|
|||||||
logger.error(error)
|
logger.error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async startAnnouncementRoleBackgroundTask(guilds: Collection<string, Guild>) {
|
||||||
|
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) {
|
||||||
|
logger.error("Could not find announcement channel. Aborting", { guildId: guild.id })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.announcementRoleHandlerTask.set(guild.id, schedule("*/10 * * * * *", async () => {
|
||||||
|
const requestId = uuid()
|
||||||
|
const messages = (await textChannel.messages.fetchPinned()).filter(message => message.cleanContent.includes("[initial]"))
|
||||||
|
|
||||||
|
if (messages.size > 1) {
|
||||||
|
logger.error("More than one pinned announcement Messages found. Unable to know which one people react to. Please fix!", { guildId: guild.id, requestId })
|
||||||
|
return
|
||||||
|
} else if (messages.size == 0) {
|
||||||
|
logger.error("Could not find any pinned announcement messages. Unable to manage roles!", { guildId: guild.id, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = await messages.at(0)?.fetch()
|
||||||
|
if (!message) {
|
||||||
|
logger.error(`No pinned message found`, { guildId: guild.id, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//logger.debug(`Message: ${JSON.stringify(message, null, 2)}`, { guildId: guild.id, requestId })
|
||||||
|
|
||||||
|
const reactions = message.reactions.resolve("🎫")
|
||||||
|
//logger.debug(`reactions: ${JSON.stringify(reactions, null, 2)}`, { guildId: guild.id, requestId })
|
||||||
|
if (reactions) {
|
||||||
|
manageAnnouncementRoles(message.guild, reactions, requestId)
|
||||||
|
} else {
|
||||||
|
logger.error("Did not get reactions! Aborting!", { guildId: guild.id, requestId })
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public stopAnnouncementRoleBackgroundTask(guildId: string, requestId: string) {
|
||||||
|
const task: Maybe<ScheduledTask> = this.announcementRoleHandlerTask.get(guildId)
|
||||||
|
if (!task) {
|
||||||
|
logger.error(`No task found for guildID ${guildId}.`, { guildId, requestId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
task.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
private async startPollCloseBackgroundTasks() {
|
||||||
|
for(const guild of this.guilds.cache) {
|
||||||
|
this.pollCloseBackgroundTasks.set(guild[1].id, schedule("0 * * * * *", () => checkForPollsToClose(guild[1])))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user