63 Commits

Author SHA1 Message Date
a60fc2db7e remove unnecessary maybe type union
All checks were successful
Compile the repository / compile (pull_request) Successful in 17s
Run unit tests / test (pull_request) Successful in 15s
2023-11-25 20:35:15 +01:00
a50ac1716f make return value for role non optional, instead throws on error
All checks were successful
Compile the repository / compile (pull_request) Successful in 22s
Run unit tests / test (pull_request) Successful in 19s
2023-11-25 20:05:37 +01:00
ef39c6315d fix interface name and import
All checks were successful
Compile the repository / compile (pull_request) Successful in 19s
Run unit tests / test (pull_request) Successful in 18s
2023-11-25 19:51:02 +01:00
1f372b0aac fix naming for reaction 2023-11-25 19:50:54 +01:00
d1aacbb3d3 fix incorrect parameter passing
All checks were successful
Compile the repository / compile (pull_request) Successful in 16s
Run unit tests / test (pull_request) Successful in 15s
2023-11-22 19:56:20 +01:00
1ae8278fb8 remove unused client
Some checks failed
Compile the repository / compile (pull_request) Failing after 18s
Run unit tests / test (pull_request) Successful in 16s
2023-11-22 19:51:35 +01:00
417b24d408 add function to fetch announcement role id
All checks were successful
Compile the repository / compile (pull_request) Successful in 16s
Run unit tests / test (pull_request) Successful in 14s
intended flow: use id fetch function to get role for current guild, use role id to fetch role from discord role cache,
use role in intended use case
currently the role id is hardcoded, needs to be read from some sort of cache which persists the ids in config file
needs to be differentiated by guild id and role type
could be the target of a user configurable frontend in the future
2023-11-21 23:33:25 +01:00
88061c361c rename announcement role assign function
All checks were successful
Compile the repository / compile (pull_request) Successful in 16s
Run unit tests / test (pull_request) Successful in 15s
2023-11-21 22:42:50 +01:00
f83f54749d move manage announce roles to role controller
All checks were successful
Run unit tests / test (pull_request) Successful in 16s
Compile the repository / compile (pull_request) Successful in 17s
2023-11-21 22:42:00 +01:00
90b0b07080 extend Maybe type to also include void 2023-11-21 22:40:53 +01:00
6d0eaed426 fix incorrect method call
All checks were successful
Compile the repository / compile (pull_request) Successful in 17s
Run unit tests / test (pull_request) Successful in 13s
2023-11-20 00:31:16 +01:00
8f320cee5c implement very basic functionality of role add/rm
Some checks failed
Compile the repository / compile (pull_request) Failing after 16s
Run unit tests / test (pull_request) Successful in 14s
2023-11-20 00:27:23 +01:00
016bb243cc add messageReactionRemove
All checks were successful
Compile the repository / compile (pull_request) Successful in 21s
Run unit tests / test (pull_request) Successful in 1m33s
2023-11-20 00:17:28 +01:00
2c8cd96ac7 call vote controller in messageReactionAdd 2023-11-20 00:17:19 +01:00
ba4aefed8e add a minimal vote controller 2023-11-20 00:17:02 +01:00
8efae12907 add hook 2023-11-20 00:03:45 +01:00
fec0bc31f1 1.1.4
All checks were successful
Build a docker image for node-jellyfin-role-bot / build-docker-image (push) Successful in 56s
2023-11-19 20:25:32 +01:00
1bfcaa95f9 Merge pull request 'feat/40-reroll-on-disinterest' (#54) from feat/40-reroll-on-disinterest into master
Reviewed-on: #54
2023-11-19 20:24:35 +01:00
fb4ab59dc6 rename emotes to validvoteemotes
All checks were successful
Run unit tests / test (pull_request) Successful in 14s
Compile the repository / compile (pull_request) Successful in 16s
2023-11-19 20:22:14 +01:00
6d40930dc1 fix incorrect log regarding update cancellation, fixes return type of function to use Maybe
All checks were successful
Compile the repository / compile (pull_request) Successful in 17s
Run unit tests / test (pull_request) Successful in 14s
2023-11-19 20:17:51 +01:00
4e9fe587b0 rename to getOpenPollEvent
All checks were successful
Compile the repository / compile (pull_request) Successful in 17s
Run unit tests / test (pull_request) Successful in 13s
2023-11-19 20:13:49 +01:00
03b6a30ffa remove unnecessary if
All checks were successful
Compile the repository / compile (pull_request) Successful in 16s
Run unit tests / test (pull_request) Successful in 18s
2023-11-19 20:11:03 +01:00
7d794a8001 refactor voteInfo to include event instead of eventid and startDate
All checks were successful
Run unit tests / test (pull_request) Successful in 16s
Compile the repository / compile (pull_request) Successful in 18s
2023-11-19 20:04:30 +01:00
8df180898e pad logging level to always be 5 characters 2023-11-19 20:04:06 +01:00
976175242b reorder close poll and use message identifier
All checks were successful
Compile the repository / compile (pull_request) Successful in 28s
Run unit tests / test (pull_request) Successful in 16s
2023-11-19 18:56:39 +01:00
68546b0b50 adjust message identifier in test 2023-11-19 18:56:08 +01:00
1348abbd48 make message identifiers actually work properly with LSP 2023-11-19 18:55:51 +01:00
fce9091114 rename message type union to better reflect its intention 2023-11-19 18:24:33 +01:00
081f3c6201 fix incorrect branded type 2023-11-19 18:24:13 +01:00
ca99987a20 clean up variable and function names
Some checks failed
Compile the repository / compile (pull_request) Failing after 16s
Run unit tests / test (pull_request) Failing after 13s
2023-11-19 18:21:51 +01:00
fc64728a78 msg -> message
All checks were successful
Compile the repository / compile (pull_request) Successful in 21s
Run unit tests / test (pull_request) Successful in 13s
2023-11-18 18:26:45 +01:00
20da25f2bf comment filter function
All checks were successful
Compile the repository / compile (pull_request) Successful in 39s
Run unit tests / test (pull_request) Successful in 14s
2023-11-18 18:22:11 +01:00
a455fd8ff7 message -> messageText
All checks were successful
Compile the repository / compile (pull_request) Successful in 16s
Run unit tests / test (pull_request) Successful in 13s
2023-11-18 18:15:27 +01:00
119343c916 fix comment 2023-11-18 18:15:13 +01:00
296a490e93 rename filter function 2023-11-18 18:14:56 +01:00
66507cb08f msg -> message
All checks were successful
Compile the repository / compile (pull_request) Successful in 16s
Run unit tests / test (pull_request) Successful in 13s
2023-11-18 17:40:50 +01:00
4600820889 move preparation of vote Message sending into vote controller
All checks were successful
Compile the repository / compile (pull_request) Successful in 17s
Run unit tests / test (pull_request) Successful in 13s
event only needs to supply information, text creation, sending and pinning happens in the vote controller
2023-11-18 17:28:44 +01:00
4a3e8809be Merge branch 'master' into feat/40-reroll-on-disinterest
All checks were successful
Compile the repository / compile (pull_request) Successful in 15s
Run unit tests / test (pull_request) Successful in 13s
2023-11-18 16:46:28 +01:00
690ba697b6 Merge pull request 'Unit Test Setup' (#58) from feat/unit-test-setup into master
Reviewed-on: #58
2023-11-18 16:45:58 +01:00
71343d6742 update packagelock
All checks were successful
Run unit tests / test (pull_request) Successful in 1m23s
Compile the repository / compile (pull_request) Successful in 59s
2023-11-18 16:42:40 +01:00
3f6e558d39 make logger silent during unit tests, add logging const for more concise requestId/guildid handling 2023-11-18 16:42:27 +01:00
ca259c5f24 update tsconfig
All checks were successful
Compile the repository / compile (pull_request) Successful in 1m10s
Run unit tests / test (pull_request) Successful in 2m0s
2023-11-18 16:38:52 +01:00
b1c581ca6e npm test script
All checks were successful
Compile the repository / compile (pull_request) Successful in 1m40s
Run unit tests / test (pull_request) Successful in 1m21s
2023-11-18 16:28:51 +01:00
96189c2392 adjust docker file to enable better build flow for tests 2023-11-18 16:28:40 +01:00
700353cff4 include a test-env file to setup environment variables for unit tests 2023-11-18 16:28:18 +01:00
f705b97804 move testenv to correct location
All checks were successful
Run unit tests / test (pull_request) Successful in 1m0s
Compile the repository / compile (pull_request) Successful in 20s
2023-10-24 22:42:03 +02:00
9cdc6e1934 add fake env vars for unit tests
Some checks failed
Compile the repository / compile (pull_request) Successful in 39s
Run unit tests / test (pull_request) Failing after 45s
2023-10-24 22:39:57 +02:00
c73cd20ccf add test-relevant fallback values for unit tests
All checks were successful
Compile the repository / compile (pull_request) Successful in 38s
Run unit tests / test (pull_request) Successful in 1m26s
2023-10-21 15:05:25 +02:00
e66aebc88c make top pick retain optional during reroll via env var
Some checks failed
Compile the repository / compile (pull_request) Successful in 32s
Run unit tests / test (pull_request) Failing after 40s
2023-10-21 14:56:33 +02:00
599243990e remove console.logs 2023-10-21 14:56:15 +02:00
eef3a9c358 add missing role to test
Some checks failed
Compile the repository / compile (pull_request) Successful in 2m13s
Run unit tests / test (pull_request) Failing after 1m10s
2023-10-21 14:11:03 +02:00
1e912b20ef formatting for package.json
All checks were successful
Compile the repository / compile (pull_request) Successful in 1m23s
Run unit tests / test (pull_request) Successful in 1m55s
2023-08-13 18:35:48 +02:00
ce4dc81f7d fix incorrect reroll behaviour
now correctly fetches old movies, filters already voted on movies, gets new movies, creates new poll message, deletes old message
2023-08-13 18:35:22 +02:00
b76df79d2a testcases 2023-08-13 18:33:45 +02:00
4e563d57fd fix else branch of memberthreshold 2023-08-13 18:33:45 +02:00
b6a1e06b03 update default movie env var 2023-08-13 18:14:16 +02:00
2ebc7fbdbe restructure docker build a bit
All checks were successful
Compile the repository / compile (pull_request) Successful in 1m12s
Run unit tests / test (pull_request) Successful in 1m39s
2023-08-06 02:37:49 +02:00
8ff5aeff03 logging
All checks were successful
Compile the repository / compile (pull_request) Successful in 1m21s
Run unit tests / test (pull_request) Successful in 1m26s
2023-08-06 02:33:28 +02:00
1101a84501 imports 2023-08-06 02:33:23 +02:00
91ec2ece7e explicit typing 2023-08-06 02:33:17 +02:00
5e58765cf4 also enabled NONE_OF_THAT to be handled 2023-08-06 02:32:44 +02:00
a2adef808f add guildscheduledevents to unit test mock
All checks were successful
Compile the repository / compile (pull_request) Successful in 14m58s
Run unit tests / test (pull_request) Successful in 4m18s
2023-07-17 23:31:00 +02:00
dc66c277b2 big refactoring of none_of_that handler
extracting, better typing, reduction of complexity
2023-07-17 23:30:48 +02:00
25 changed files with 12750 additions and 12365 deletions

View File

@ -2,13 +2,13 @@ FROM node:alpine as files
ENV TZ="Europe/Berlin" ENV TZ="Europe/Berlin"
WORKDIR /app WORKDIR /app
COPY [ "package-lock.json", "package.json", "index.ts", "tsconfig.json", "./" ] COPY [ "package-lock.json", "package.json", "index.ts", "tsconfig.json", "./" ]
COPY server ./server
FROM files as proddependencies FROM files as proddependencies
ENV NODE_ENV=production ENV NODE_ENV=production
RUN npm ci --omit=dev RUN npm ci --omit=dev
FROM proddependencies as compile FROM proddependencies as compile
COPY server ./server
RUN npm run build RUN npm run build
CMD ["npm","run","start"] CMD ["npm","run","start"]
@ -16,6 +16,7 @@ FROM files as dependencies
RUN npm ci RUN npm ci
FROM dependencies as test FROM dependencies as test
COPY server ./server
COPY jest.config.js . COPY jest.config.js .
COPY tests ./tests COPY tests ./tests
RUN npm run test RUN npm run test

View File

@ -1,18 +1,19 @@
module.exports = { module.exports = {
'roots': [ 'roots': [
'<rootDir>/tests', '<rootDir>/tests',
'<rootDir>/server' '<rootDir>/server'
], ],
'transform': { 'transform': {
'^.+\\.tsx?$': 'ts-jest' '^.+\\.tsx?$': 'ts-jest'
}, },
'testRegex': '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$', 'testRegex': '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',
'moduleFileExtensions': [ 'setupFiles': ["<rootDir>/tests/testenv.js"],
'ts', 'moduleFileExtensions': [
'tsx', 'ts',
'js', 'tsx',
'jsx', 'js',
'json', 'jsx',
'node' 'json',
], 'node'
],
}; };

24230
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,52 +1,52 @@
{ {
"name": "node-jellyfin-discord-bot", "name": "node-jellyfin-discord-bot",
"version": "1.1.3", "version": "1.1.4",
"description": "A discord bot to sync jellyfin accounts with discord roles", "description": "A discord bot to sync jellyfin accounts with discord roles",
"main": "index.js", "main": "index.js",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@discordjs/rest": "^1.7.0", "@discordjs/rest": "^1.7.0",
"@tsconfig/recommended": "^1.0.2", "@tsconfig/recommended": "^1.0.2",
"@types/node": "^18.15.11", "@types/node": "^18.15.11",
"@types/node-cron": "^3.0.7", "@types/node-cron": "^3.0.7",
"@types/request": "^2.48.8", "@types/request": "^2.48.8",
"@types/uuid": "^9.0.1", "@types/uuid": "^9.0.1",
"axios": "^1.3.5", "axios": "^1.3.5",
"date-fns": "^2.29.3", "date-fns": "^2.29.3",
"date-fns-tz": "^2.0.0", "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",
"jellyfin-apiclient": "^1.10.0", "jellyfin-apiclient": "^1.10.0",
"node-cron": "^3.0.2", "node-cron": "^3.0.2",
"sqlite3": "^5.1.6", "sqlite3": "^5.1.6",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^5.0.4", "typescript": "^5.0.4",
"uuid": "^9.0.0", "uuid": "^9.0.0",
"winston": "^3.8.2" "winston": "^3.8.2"
}, },
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"buildwatch": "tsc --watch", "buildwatch": "tsc --watch",
"clean": "rimraf build", "clean": "rimraf build",
"start": "node build/index.js", "start": "node build/index.js",
"debuggable": "node build/index.js --inspect-brk", "debuggable": "node build/index.js --inspect-brk",
"monitor": "nodemon build/index.js", "monitor": "nodemon build/index.js",
"lint": "eslint . --ext .ts", "lint": "eslint . --ext .ts",
"lint-fix": "eslint . --ext .ts --fix", "lint-fix": "eslint . --ext .ts --fix",
"test": "jest", "test": "jest --runInBand",
"test-watch": "jest --watch" "test-watch": "jest --watch"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^29.5.2", "@types/jest": "^29.5.2",
"@typescript-eslint/eslint-plugin": "^5.58.0", "@typescript-eslint/eslint-plugin": "^5.58.0",
"@typescript-eslint/parser": "^5.58.0", "@typescript-eslint/parser": "^5.58.0",
"eslint": "^8.38.0", "eslint": "^8.38.0",
"jest": "^29.5.0", "jest": "^29.5.0",
"jest-cli": "^29.5.0", "jest-cli": "^29.5.0",
"mockdate": "^3.0.5", "mockdate": "^3.0.5",
"nodemon": "^2.0.22", "nodemon": "^2.0.22",
"rimraf": "^5.0.0", "rimraf": "^5.0.0",
"ts-jest": "^29.1.0" "ts-jest": "^29.1.0"
} }
} }

View File

@ -82,41 +82,5 @@ Für eine Erklärung wie das alles funktioniert mach einfach /mitgucken für ein
} }
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))
}

View File

@ -1,6 +1,7 @@
import { ApplicationCommandOptionType } from 'discord.js' import { ApplicationCommandOptionType } from 'discord.js'
import { Command } from '../structures/command' import { Command } from '../structures/command'
import { RunOptions } from '../types/commandTypes' import { RunOptions } from '../types/commandTypes'
import { logger } from '../logger'
export default new Command({ export default new Command({
name: 'echo', name: 'echo',
description: 'Echoes a text', description: 'Echoes a text',
@ -13,7 +14,7 @@ export default new Command({
} }
], ],
run: async (interaction: RunOptions) => { run: async (interaction: RunOptions) => {
console.log('echo called') logger.info('echo called')
interaction.interaction.reply(interaction.toString()) interaction.interaction.reply(interaction.toString())
} }
}) })

View File

@ -2,15 +2,16 @@ import { v4 as uuid } from 'uuid'
import { jellyfinHandler } from "../.." import { jellyfinHandler } from "../.."
import { Command } from '../structures/command' import { Command } from '../structures/command'
import { RunOptions } from '../types/commandTypes' import { RunOptions } from '../types/commandTypes'
import { logger } from '../logger'
export default new Command({ export default new Command({
name: 'passwort_reset', name: 'passwort_reset',
description: 'Ich vergebe dir ein neues Passwort und schicke es dir per DM zu. Kostet auch nix! Versprochen! 😉', description: 'Ich vergebe dir ein neues Passwort und schicke es dir per DM zu. Kostet auch nix! Versprochen! 😉',
options: [], options: [],
run: async (interaction: RunOptions) => { run: async (interaction: RunOptions) => {
console.log('PasswortReset called') logger.info('PasswortReset called')
interaction.interaction.followUp('Yo, ich schick dir eins!') interaction.interaction.followUp('Yo, ich schick dir eins!')
console.log(JSON.stringify(interaction.interaction.member, null, 2)) logger.info(JSON.stringify(interaction.interaction.member, null, 2))
jellyfinHandler.resetUserPasswort(interaction.interaction.member, uuid()) jellyfinHandler.resetUserPasswort(interaction.interaction.member, uuid())
} }
}) })

View File

@ -31,6 +31,7 @@ export interface Config {
yavin_jellyfin_token: string yavin_jellyfin_token: string
yavin_jellyfin_collection_user: string yavin_jellyfin_collection_user: string
random_movie_count: number random_movie_count: number
reroll_retains_top_picks: boolean
} }
} }
export const config: Config = { export const config: Config = {
@ -60,17 +61,18 @@ export const config: Config = {
client_id: process.env.CLIENT_ID ?? "", client_id: process.env.CLIENT_ID ?? "",
jellfin_token: process.env.JELLYFIN_TOKEN ?? "", jellfin_token: process.env.JELLYFIN_TOKEN ?? "",
jellyfin_url: process.env.JELLYFIN_URL ?? "", jellyfin_url: process.env.JELLYFIN_URL ?? "",
workaround_token: process.env.TOKEN ?? "", workaround_token: process.env.TOKEN ?? "TOKEN",
watcher_role: process.env.WATCHER_ROLE ?? "", watcher_role: process.env.WATCHER_ROLE ?? "WATCHER_ROLE",
jf_admin_role: process.env.ADMIN_ROLE ?? "", jf_admin_role: process.env.ADMIN_ROLE ?? "ADMIN_ROLE",
announcement_role: process.env.WATCHPARTY_ANNOUNCEMENT_ROLE ?? "", announcement_role: process.env.WATCHPARTY_ANNOUNCEMENT_ROLE ?? "ANNOUNCE_ROLE",
announcement_channel_id: process.env.CHANNEL_ID ?? "", announcement_channel_id: process.env.CHANNEL_ID ?? "ANNOUNCE_CHANNEL",
jf_collection_id: process.env.JELLYFIN_COLLECTION_ID ?? "", jf_collection_id: process.env.JELLYFIN_COLLECTION_ID ?? "",
yavin_collection_id: process.env.YAVIN_COLLECTION_ID ?? "", yavin_collection_id: process.env.YAVIN_COLLECTION_ID ?? "",
yavin_jellyfin_url: process.env.YAVIN_JELLYFIN_URL ?? "", yavin_jellyfin_url: process.env.YAVIN_JELLYFIN_URL ?? "",
yavin_jellyfin_token: process.env.YAVIN_TOKEN ?? "", yavin_jellyfin_token: process.env.YAVIN_TOKEN ?? "",
yavin_jellyfin_collection_user: process.env.YAVIN_COLLECTION_USER ?? "", yavin_jellyfin_collection_user: process.env.YAVIN_COLLECTION_USER ?? "",
jf_user: process.env.JELLYFIN_USER ?? "", jf_user: process.env.JELLYFIN_USER ?? "",
random_movie_count: parseInt(process.env.RANDOM_MOVIE_COUNT ?? "") ?? 5 random_movie_count: parseInt(process.env.RANDOM_MOVIE_COUNT ?? "5") ?? 5,
reroll_retains_top_picks: process.env.REROLL_RETAIN === "true"
} }
} }

View File

@ -1,6 +1,7 @@
export enum Emotes { "1⃣", "2⃣", "3⃣", "4⃣", "5⃣", "6⃣", "7⃣", "8⃣", "9⃣", "🔟" } export enum ValidVoteEmotes { "1⃣", "2⃣", "3⃣", "4⃣", "5⃣", "6⃣", "7⃣", "8⃣", "9⃣", "🔟" }
export const NONE_OF_THAT = "❌" export const NONE_OF_THAT = "❌"
// WIP
export const Emoji = { export const Emoji = {
"one": "\u0031\uFE0F\u20E3", "one": "\u0031\uFE0F\u20E3",
"two": "\u0032\uFE0F\u20E3", "two": "\u0032\uFE0F\u20E3",

View File

@ -4,7 +4,6 @@ import { client, yavinJellyfinHandler } from "../..";
import { Maybe } from "../interfaces"; import { Maybe } from "../interfaces";
import { logger } from "../logger"; import { logger } from "../logger";
export const name = 'guildScheduledEventCreate' export const name = 'guildScheduledEventCreate'
export async function execute(event: GuildScheduledEvent) { export async function execute(event: GuildScheduledEvent) {
@ -25,15 +24,21 @@ export async function execute(event: GuildScheduledEvent) {
return return
} }
logger.debug(`Found channel ${JSON.stringify(announcementChannel, null, 2)}`, { guildId: event.guildId, requestId }) logger.debug(`Found channel ${JSON.stringify(announcementChannel, null, 2)}`, { guildId: event.guildId, requestId })
if (!event.scheduledStartAt) { if (!event.scheduledStartAt) {
logger.info("EVENT DOES NOT HAVE STARTDATE; CANCELLING", { guildId: event.guildId, requestId }) logger.info("Event does not have a start date, cancelling", { guildId: event.guildId, requestId })
return return
} }
const sentMessageText = client.voteController.createVoteMessageText(event.id, event.scheduledStartAt, movies, event.guild?.id ?? "", requestId) const sentMessage = await client.voteController.prepareAndSendVoteMessage({
const sentMessage = await client.voteController.sendVoteMessage(sentMessageText, movies.length, announcementChannel) movies,
startDate: event.scheduledStartAt,
event,
announcementChannel,
pinAfterSending: true
},
event.guildId,
requestId)
sentMessage.pin() logger.debug(JSON.stringify(sentMessage))
} }
} }

View File

@ -1,7 +1,7 @@
import { Message, MessageReaction, User } from "discord.js"; import { Message, MessageReaction, User } from "discord.js";
import { logger, newRequestId, noGuildId } from "../logger"; import { logger, newRequestId, noGuildId } from "../logger";
import { Emoji, Emotes, NONE_OF_THAT } from "../constants"; import { Emoji, ValidVoteEmotes, NONE_OF_THAT } from "../constants";
import { client } from "../.."; import { client } from "../..";
import { isInitialAnnouncement, isVoteMessage } from "../helper/messageIdentifiers"; import { isInitialAnnouncement, isVoteMessage } from "../helper/messageIdentifiers";
@ -22,11 +22,10 @@ export async function execute(messageReaction: MessageReaction, user: User) {
} }
logger.info(`Got reaction on message`, { requestId, guildId }) logger.info(`Got reaction on message`, { requestId, guildId })
//logger.debug(`reactedUponMessage payload: ${JSON.stringify(reactedUponMessage)}`)
logger.info(`emoji: ${messageReaction.emoji.toString()}`) logger.info(`emoji: ${messageReaction.emoji.toString()}`)
if (!Object.values(Emotes).includes(messageReaction.emoji.toString())) { if (!Object.values(ValidVoteEmotes).includes(messageReaction.emoji.toString()) && messageReaction.emoji.toString() !== NONE_OF_THAT) {
logger.info(`${messageReaction.emoji.toString()} currently not handled`) logger.info(`${messageReaction.emoji.toString()} currently not handled`)
return return
} }
@ -34,15 +33,13 @@ export async function execute(messageReaction: MessageReaction, user: User) {
if (isVoteMessage(reactedUponMessage)) { if (isVoteMessage(reactedUponMessage)) {
if (messageReaction.emoji.toString() === NONE_OF_THAT) { if (messageReaction.emoji.toString() === NONE_OF_THAT) {
logger.info(`Reaction is NONE_OF_THAT on a vote message. Handling`, { requestId, guildId }) logger.info(`Reaction is NONE_OF_THAT on a vote message. Handling`, { requestId, guildId })
return client.voteController.handleNoneOfThatVote(messageReaction, user, reactedUponMessage, requestId, guildId) return client.voteController.handleNoneOfThatVote(messageReaction, reactedUponMessage, requestId, guildId)
}
if (messageReaction.emoji.toString() === Emoji.one) {
// do something
} }
} }
else if (isInitialAnnouncement(reactedUponMessage)) { else if (isInitialAnnouncement(reactedUponMessage)) {
if (messageReaction.emoji.toString() === Emoji.ticket) { if (messageReaction.emoji.toString() === Emoji.ticket) {
logger.error(`Got a role emoji. Not implemented yet. ${reactedUponMessage.id}`) logger.error(`Got a role emoji. ${reactedUponMessage.id}`)
return client.roleController.addMediaRoleToUser(user, messageReaction.message.guild, requestId)
} }
return return
} }

View File

@ -0,0 +1,33 @@
import { Message, MessageReaction, User } from "discord.js";
import { logger, newRequestId, noGuildId } from "../logger";
import { Emoji } from "../constants";
import { client } from "../..";
import { isInitialAnnouncement } from "../helper/messageIdentifiers";
export const name = 'messageReactionRemove'
export async function execute(messageReaction: MessageReaction, user: User) {
if (user.id == client.user?.id) {
logger.info('Skipping bot reaction')
return
}
const requestId = newRequestId()
const guildId = messageReaction.message.inGuild() ? messageReaction.message.guildId : noGuildId
const reactedUponMessage: Message = messageReaction.message.partial ? await messageReaction.message.fetch() : messageReaction.message
if (!messageReaction.message.guild) {
logger.warn(`Received messageReactionRemove on non-guild message.`, { requestId })
return
}
logger.info(`Got reaction on message`, { requestId, guildId })
logger.info(`emoji: ${messageReaction.emoji.toString()}`)
if (isInitialAnnouncement(reactedUponMessage)) {
if (messageReaction.emoji.toString() === Emoji.ticket) {
logger.info(`User: ${user.id}, ${user.username} has removed a ticket reaction. Starting role management`, { requestId, guildId })
return client.roleController.removeMediaRoleFromUser(user, messageReaction.message.guild, requestId)
}
}
}

View File

@ -1,6 +1,7 @@
import { Message } from "discord.js" import { Message } from "discord.js"
import { logger } from "../logger"
export const name = 'messageCreate' export const name = 'messageCreate'
export function execute(message: Message) { export function execute(message: Message) {
console.log(`${JSON.stringify(message)} has been created`) logger.info(`${JSON.stringify(message)} has been created`)
} }

View File

@ -1,10 +1,10 @@
import { format, isToday } from "date-fns"; import { format, isToday } from "date-fns";
import { utcToZonedTime } from "date-fns-tz" import { utcToZonedTime } from "date-fns-tz"
import { GuildScheduledEvent } from "discord.js";
import { logger } from "../logger"; import { logger } from "../logger";
import de from "date-fns/locale/de"; import de from "date-fns/locale/de";
import { Maybe } from "../interfaces";
export function createDateStringFromEvent(eventStartDate:Date, requestId: string, guildId?: string): string { export function createDateStringFromEvent(eventStartDate: Maybe<Date>, requestId: string, guildId?: string): string {
if (!eventStartDate) { if (!eventStartDate) {
logger.error("Event has no start. Cannot create dateString.", { guildId, requestId }) logger.error("Event has no start. Cannot create dateString.", { guildId, requestId })
return `"habe keinen Startzeitpunkt ermitteln können"` return `"habe keinen Startzeitpunkt ermitteln können"`

View File

@ -2,19 +2,19 @@ import { Message } from "discord.js";
// branded types to differentiate objects of identical Type but different contents // branded types to differentiate objects of identical Type but different contents
export type VoteEndMessage = Message & { readonly __brand: 'vote' } export type VoteEndMessage = Message<true> & { readonly __brand: 'voteend' }
export type AnnouncementMessage = Message & { readonly __brand: 'announcement' } export type AnnouncementMessage = Message<true> & { readonly __brand: 'announcement' }
export type VoteMessage = Message & { readonly __brand: 'voteend' } export type VoteMessage = Message<true> & { readonly __brand: 'vote' }
export type DiscordMessage = VoteMessage | VoteEndMessage | AnnouncementMessage export type KnownDiscordMessage = VoteMessage | VoteEndMessage | AnnouncementMessage
export function isVoteMessage(msg: Message): msg is VoteMessage { export function isVoteMessage(message: Message): message is VoteMessage {
return msg.cleanContent.includes('[Abstimmung]') return message.cleanContent.includes('[Abstimmung]')
} }
export function isInitialAnnouncement(msg: Message): msg is AnnouncementMessage { export function isInitialAnnouncement(message: Message): message is AnnouncementMessage {
return msg.cleanContent.includes("[initial]") return message.cleanContent.includes("[initial]")
} }
export function isVoteEndedMessage(msg: Message): msg is VoteEndMessage { export function isVoteEndedMessage(message: Message): message is VoteEndMessage {
return msg.cleanContent.includes("[Abstimmung beendet]") return message.cleanContent.includes("[Abstimmung beendet]")
} }

View File

@ -0,0 +1,83 @@
import { Guild, MessageReaction, Role, User } from "discord.js";
import { GuildMember } from "discord.js";
import { logger } from "../logger";
import { config } from "../configuration";
import { Maybe } from "../interfaces";
export default class RoleController {
constructor() { }
private getAnnounceRoleIdForGuild(guildId: string): string {
const role = config.bot.announcement_role
if (!role) throw new Error(`No announcementRole defined for guild ${guildId}`)
return role
}
public async addRoleToUser(member: GuildMember, role: Role, guildId: string, requestId: string) {
logger.info(`Adding Role ${role.id} to user ${member.id}|${member.user.username}`, { requestId, guildId })
return await member.roles.add(role)
}
private async removeRoleFromUser(member: GuildMember, role: Role, guildId: string, requestId: string) {
logger.info(`Removing Role ${role.id} from user ${member.id}|${member.user.username}`, { requestId, guildId })
return await member.roles.remove(role)
}
public async addMediaRoleToUser(user: User, guild: Guild, requestId: string) {
const roleToAdd = await this.getAnnouncementRoleForGuild(guild, requestId)
if (!roleToAdd) throw new Error(`No announcementRole found to add to user`)
const guildMember = await guild.members.fetch(user)
return this.addRoleToUser(guildMember, roleToAdd, guild.id, requestId)
}
public async removeMediaRoleFromUser(user: User, guild: Guild, requestId: string) {
const roleToRemove = await this.getAnnouncementRoleForGuild(guild, requestId)
if (!roleToRemove) throw new Error(`No announcementRole found to remove from user`)
const guildMember = await guild.members.fetch(user)
return this.removeRoleFromUser(guildMember, roleToRemove, guild.id, requestId)
}
public async getAnnouncementRoleForGuild(guild: Guild, requestId: string): Promise<Role> {
const mediaRole = this.getAnnounceRoleIdForGuild(guild.id)
const announcement_role = await guild.roles.fetch()
.then(fetchedRoles => fetchedRoles.find(role => role.id === mediaRole))
.catch(error => {
logger.error(`Could not find announcement_role with id ${config.bot.announcement_role}. Error: ${error}`, { requestId, guildId: guild.id })
throw error
})
if (!announcement_role) throw new Error(`Could not find announcement_role with id ${config.bot.announcement_role}.`)
return announcement_role
}
public async assignAnnouncementRolesFromReaction(guild: Guild, reaction: MessageReaction, requestId: string) {
const guildId = guild.id
logger.info("Managing roles", { guildId, requestId })
const announcementRole = await this.getAnnouncementRoleForGuild(guild, requestId)
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 === announcementRole.id) !== 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 === announcementRole.id) === 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 => this.removeRoleFromUser(user, announcementRole, guild.id, requestId))
usersWhoNeedRole.forEach(user => this.addRoleToUser(user, announcementRole, guild.id, requestId))
}
}

View File

@ -1,10 +1,11 @@
import { CustomError, errorCodes } from "../interfaces" import { CustomError, errorCodes } from "../interfaces"
import { logger } from "../logger"
import { ExtendedClient } from "../structures/client" import { ExtendedClient } from "../structures/client"
export async function sendFailureDM(creatorMessage: string, client: ExtendedClient, creatorId?: string): Promise<void> { export async function sendFailureDM(creatorMessage: string, client: ExtendedClient, creatorId?: string): Promise<void> {
if (!creatorId) throw new CustomError('No creator ID present', errorCodes.no_creator_id) if (!creatorId) throw new CustomError('No creator ID present', errorCodes.no_creator_id)
const creator = await client.users.fetch(creatorId) const creator = await client.users.fetch(creatorId)
console.log(`Creator ${JSON.stringify(creator)}`) logger.info(`Creator ${JSON.stringify(creator)}`)
if (creator) if (creator)
if (!creator.dmChannel) if (!creator.dmChannel)
await creator.createDM() await creator.createDM()

View File

@ -1,11 +1,11 @@
import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildScheduledEventSetStatusArg, GuildScheduledEventStatus, Message, MessageCreateOptions, MessageReaction, PartialMessage, TextChannel, User } from "discord.js" import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildScheduledEventSetStatusArg, GuildScheduledEventStatus, Message, MessageCreateOptions, MessageReaction, TextChannel } from "discord.js"
import { Emoji, Emotes, NONE_OF_THAT } from "../constants" import { ValidVoteEmotes, NONE_OF_THAT } from "../constants"
import { logger, newRequestId } from "../logger" import { logger, newRequestId } from "../logger"
import { getMembersWithRoleFromGuild } from "./roleFilter" import { getMembersWithRoleFromGuild } from "./roleFilter"
import { config } from "../configuration" import { config } from "../configuration"
import { VoteMessage, isVoteEndedMessage, isVoteMessage } from "./messageIdentifiers" import { VoteMessage, isVoteEndedMessage, isVoteMessage } from "./messageIdentifiers"
import { createDateStringFromEvent } from "./dateHelper" import { createDateStringFromEvent } from "./dateHelper"
import { Maybe } from "../interfaces" import { Maybe, prepareVoteMessageInput } from "../interfaces"
import format from "date-fns/format" import format from "date-fns/format"
import toDate from "date-fns/toDate" import toDate from "date-fns/toDate"
import differenceInDays from "date-fns/differenceInDays" import differenceInDays from "date-fns/differenceInDays"
@ -13,7 +13,6 @@ import addDays from "date-fns/addDays"
import isAfter from "date-fns/isAfter" import isAfter from "date-fns/isAfter"
import { ExtendedClient } from "../structures/client" import { ExtendedClient } from "../structures/client"
import { JellyfinHandler } from "../jellyfin/handler" import { JellyfinHandler } from "../jellyfin/handler"
import { ObjectGroupUpdateToJSON } from "../jellyfin"
export type Vote = { export type Vote = {
emote: string, //todo habs nicht hinbekommen hier Emotes zu nutzen emote: string, //todo habs nicht hinbekommen hier Emotes zu nutzen
@ -22,8 +21,7 @@ export type Vote = {
} }
export type VoteMessageInfo = { export type VoteMessageInfo = {
votes: Vote[], votes: Vote[],
eventId: string, event: GuildScheduledEvent,
eventDate: Date
} }
export default class VoteController { export default class VoteController {
private client: ExtendedClient private client: ExtendedClient
@ -34,8 +32,9 @@ export default class VoteController {
this.yavinJellyfinHandler = _yavin this.yavinJellyfinHandler = _yavin
} }
public async handleNoneOfThatVote(messageReaction: MessageReaction, user: User, reactedUponMessage: VoteMessage, requestId: string, guildId: string) { public async handleNoneOfThatVote(messageReaction: MessageReaction, reactedUponMessage: VoteMessage, requestId: string, guildId: string) {
if (!messageReaction.message.guild) return 'No guild' if (!messageReaction.message.guild) return 'No guild'
const guild = messageReaction.message.guild
logger.debug(`${reactedUponMessage.id} is vote message`, { requestId, guildId }) logger.debug(`${reactedUponMessage.id} is vote message`, { requestId, guildId })
const watcherRoleMember = await getMembersWithRoleFromGuild(config.bot.announcement_role, messageReaction.message.guild) const watcherRoleMember = await getMembersWithRoleFromGuild(config.bot.announcement_role, messageReaction.message.guild)
@ -44,52 +43,103 @@ export default class VoteController {
const watcherRoleMemberCount = watcherRoleMember.size const watcherRoleMemberCount = watcherRoleMember.size
logger.info(`MEMBER COUNT: ${watcherRoleMemberCount}`, { requestId, guildId }) logger.info(`MEMBER COUNT: ${watcherRoleMemberCount}`, { requestId, guildId })
const noneOfThatReactions = messageReaction.message.reactions.cache.get(NONE_OF_THAT)?.users.cache.filter(x => x.id !== this.client.user?.id).size ?? 0 const noneOfThatReactions = reactedUponMessage.reactions.cache.get(NONE_OF_THAT)?.users.cache.filter(x => x.id !== this.client.user?.id).size ?? 0
const memberThreshold = (watcherRoleMemberCount / 2) const memberThreshold = (watcherRoleMemberCount / 2)
logger.info(`Reroll ${noneOfThatReactions} > ${memberThreshold} ?`, { requestId, guildId }) logger.info(`Reroll ${noneOfThatReactions} > ${memberThreshold} ?`, { requestId, guildId })
if (noneOfThatReactions > memberThreshold) { if (noneOfThatReactions > memberThreshold)
logger.info(`No reroll`, { requestId, guildId })
else {
logger.info('Starting poll reroll', { requestId, guildId }) logger.info('Starting poll reroll', { requestId, guildId })
messageReaction.message.edit((messageReaction.message.content ?? "").concat('\nDiese Abstimmung muss wiederholt werden.')) await this.handleReroll(reactedUponMessage, guild.id, requestId)
// get movies that _had_ votes logger.info(`Finished handling NONE_OF_THAT vote`, { requestId, guildId })
//const oldMovieNames: Vote[] = this.parseVotesFromVoteMessage(messageReaction.message, requestId)
const parsedIds = this.parseGuildIdAndEventIdFromWholeMessage(messageReaction.message.cleanContent ?? '')
const eventStartDate: Date = this.fetchEventStartDateByEventId(parsedIds.eventId, requestId) //TODO
//
// get movies from jellyfin to fill the remaining slots
const newMovieCount = config.bot.random_movie_count //- oldMovieNames.length
const newMovies = await this.yavinJellyfinHandler.getRandomMovieNames(newMovieCount, guildId, requestId)
// merge
const movies = newMovies
// create new message
await this.closePoll(messageReaction.message.guild, requestId)
const message = this.createVoteMessageText(parsedIds.guildId, eventStartDate, movies, guildId, requestId)
const announcementChannel = this.client.getAnnouncementChannelForGuild(guildId)
if (!announcementChannel) {
logger.error(`No announcementChannel found for ${guildId}, can't post poll`)
return
}
const sentMessage = await this.sendVoteMessage(message, movies.length, announcementChannel)
sentMessage.pin()
} }
logger.info(`No reroll`, { requestId, guildId })
} }
private fetchEventStartDateByEventId(eventId: string, requestId: string): Date {
throw new Error("Method not implemented.") private async removeMessage(message: Message): Promise<Message<boolean>> {
if (message.pinned) {
await message.unpin()
}
return await message.delete()
} }
public parseGuildIdAndEventIdFromWholeMessage(message: string) {
const idmatch = RegExp(/(?:http|https):\/\/discord\.com\/events\/(\d*)\/(\d*)/) /**
const matches = message.match(idmatch) * returns true if a Vote object contains at least one vote
if (matches && matches.length == 3) * @param {Vote} vote
return { guildId: matches[1], eventId: matches[2] } */
throw Error(`Could not find eventId in Vote Message`) private hasAtLeastOneVote(vote: Vote): boolean {
// subtracting the bots initial vote
const overOneVote = (vote.count - 1) >= 1
logger.debug(`${vote.movie} : ${vote.count} -> above: ${overOneVote}`)
return overOneVote
} }
public parseVotesFromVoteMessage(message: VoteMessage, requestId: string): VoteMessageInfo {
public async generateRerollMovieList(voteInfo: VoteMessageInfo, guildId: string, requestId: string) {
if (config.bot.reroll_retains_top_picks) {
const votedOnMovies = voteInfo.votes.filter(this.hasAtLeastOneVote).filter(x => x.emote !== NONE_OF_THAT)
logger.info(`Found ${votedOnMovies.length} with votes`, { requestId, guildId })
const newMovieCount: number = config.bot.random_movie_count - votedOnMovies.length
logger.info(`Fetching ${newMovieCount} from jellyfin`)
const newMovies: string[] = await this.yavinJellyfinHandler.getRandomMovieNames(newMovieCount, guildId, requestId)
// merge
return newMovies.concat(votedOnMovies.map(x => x.movie))
} else {
// get movies from jellyfin to fill the remaining slots
const newMovieCount: number = config.bot.random_movie_count
logger.info(`Fetching ${newMovieCount} from jellyfin`)
return await this.yavinJellyfinHandler.getRandomMovieNames(newMovieCount, guildId, requestId)
}
}
public async handleReroll(voteMessage: VoteMessage, guildId: string, requestId: string) {
// get the movies currently being voted on, their votes, the eventId and its date
const voteInfo: VoteMessageInfo = await this.parseVoteInfoFromVoteMessage(voteMessage, requestId)
if (!voteInfo.event.scheduledStartAt) {
logger.info("Event does not have a start date, cancelling", { guildId: voteInfo.event.guildId, requestId })
return
}
let movies: string[] = await this.generateRerollMovieList(voteInfo, guildId, requestId)
const announcementChannel = this.client.getAnnouncementChannelForGuild(guildId)
if (!announcementChannel) {
logger.error(`No announcementChannel found for ${guildId}, can't post poll`)
return
}
try {
logger.info(`Trying to remove old vote Message`, { requestId, guildId })
this.removeMessage(voteMessage)
} catch (err) {
// TODO: integrate failure DM to media Admin to inform about inability to delete old message
logger.error(`Error during removeMessage: ${err}`)
}
const sentMessage = this.prepareAndSendVoteMessage({
event: voteInfo.event,
movies,
announcementChannel,
startDate: voteInfo.event.scheduledStartAt,
pinAfterSending: true
},
guildId,
requestId)
logger.debug(`Sent reroll message: ${JSON.stringify(sentMessage)}`, { requestId, guildId })
}
private async fetchEventByEventId(guild: Guild, eventId: string, requestId: string): Promise<Maybe<GuildScheduledEvent>> {
const guildEvent: GuildScheduledEvent = await guild.scheduledEvents.fetch(eventId)
if (!guildEvent) logger.error(`GuildScheduledEvent with id${eventId} could not be found`, { requestId, guildId: guild.id })
return guildEvent
}
public async parseVoteInfoFromVoteMessage(message: VoteMessage, requestId: string): Promise<VoteMessageInfo> {
const lines = message.cleanContent.split('\n') const lines = message.cleanContent.split('\n')
let parsedIds = this.parseGuildIdAndEventIdFromWholeMessage(message.cleanContent) let parsedIds = this.parseGuildIdAndEventIdFromWholeMessage(message.cleanContent)
let eventDate: Date = this.parseEventDateFromMessage(message.cleanContent)
if (!message.guild)
throw new Error(`Message ${message.id} not a guild message`)
const event: Maybe<GuildScheduledEvent> = await this.fetchEventByEventId(message.guild, parsedIds.eventId, requestId)
let votes: Vote[] = [] let votes: Vote[] = []
for (const line of lines) { for (const line of lines) {
if (line.slice(0, 5).includes(':')) { if (line.slice(0, 5).includes(':')) {
@ -107,38 +157,55 @@ export default class VoteController {
} }
} }
} }
return <VoteMessageInfo>{ eventId: parsedIds.eventId, eventDate, votes } return <VoteMessageInfo>{ event, votes }
} }
public parseEventDateFromMessage(message: string): Date { public parseEventDateFromMessage(message: string, guildId: string, requestId: string): Date {
logger.warn(`Falling back to RegEx parsing to get Event Date`, { guildId, requestId })
const datematcher = RegExp(/((?:0[1-9]|[12][0-9]|3[01])\.(?:0[1-9]|1[012])\.)(?:\ um\ )((?:(?:[01][0-9]|[2][0-3])\:[0-5][0-9])|(?:[2][4]\:00))!/i) const datematcher = RegExp(/((?:0[1-9]|[12][0-9]|3[01])\.(?:0[1-9]|1[012])\.)(?:\ um\ )((?:(?:[01][0-9]|[2][0-3])\:[0-5][0-9])|(?:[2][4]\:00))!/i)
const result: RegExpMatchArray | null = message.match(datematcher) const result: RegExpMatchArray | null = message.match(datematcher)
const timeFromResult = result?.at(-1) const timeFromResult = result?.at(-1)
const dateFromResult = result?.at(1)?.concat(format(new Date(), 'yyyy')).concat(" " + timeFromResult) ?? "" const dateFromResult = result?.at(1)?.concat(format(new Date(), 'yyyy')).concat(" " + timeFromResult) ?? ""
return new Date(dateFromResult) return new Date(dateFromResult)
} }
public parseGuildIdAndEventIdFromWholeMessage(message: string) {
const idmatch = RegExp(/(?:http|https):\/\/discord\.com\/events\/(\d*)\/(\d*)/)
const matches = message.match(idmatch)
if (matches && matches.length == 3)
return { guildId: matches[1], eventId: matches[2] }
throw Error(`Could not find eventId in Vote Message`)
}
public createVoteMessageText(eventId: string, eventStartDate: Date, movies: string[], guildId: string, requestId: string): string { public async prepareAndSendVoteMessage(inputInfo: prepareVoteMessageInput, guildId: string, requestId: string) {
const messageText = this.createVoteMessageText(inputInfo.event, inputInfo.movies, guildId, requestId)
const sentMessage = await this.sendVoteMessage(messageText, inputInfo.movies.length, inputInfo.announcementChannel)
if (inputInfo.pinAfterSending)
sentMessage.pin()
return sentMessage
}
let message = `[Abstimmung] für https://discord.com/events/${guildId}/${eventId} \n<@&${config.bot.announcement_role}> Es gibt eine neue Abstimmung für die nächste Watchparty ${createDateStringFromEvent(eventStartDate, guildId, requestId)}! Stimme hierunter für den nächsten Film ab!\n` public createVoteMessageText(event: GuildScheduledEvent, movies: string[], guildId: string, requestId: string): string {
let message = `[Abstimmung] für https://discord.com/events/${guildId}/${event.id} \n<@&${config.bot.announcement_role}> Es gibt eine neue Abstimmung für die nächste Watchparty ${createDateStringFromEvent(event.scheduledStartAt, guildId, requestId)}! Stimme hierunter für den nächsten Film ab!\n`
for (let i = 0; i < movies.length; i++) { for (let i = 0; i < movies.length; i++) {
message = message.concat(Emotes[i]).concat(": ").concat(movies[i]).concat("\n") message = message.concat(ValidVoteEmotes[i]).concat(": ").concat(movies[i]).concat("\n")
} }
message = message.concat(NONE_OF_THAT).concat(": Wenn dir nichts davon gefällt.") message = message.concat(NONE_OF_THAT).concat(": Wenn dir nichts davon gefällt.")
return message return message
} }
public async sendVoteMessage(message: string, movieCount: number, announcementChannel: TextChannel) {
// TODO: Refactor into separate message controller
public async sendVoteMessage(messageText: string, movieCount: number, announcementChannel: TextChannel) {
const options: MessageCreateOptions = { const options: MessageCreateOptions = {
allowedMentions: { parse: ["roles"] }, allowedMentions: { parse: ["roles"] },
content: message, content: messageText,
} }
const sentMessage: Message<true> = await (await announcementChannel.fetch()).send(options) const sentMessage: Message<true> = await (await announcementChannel.fetch()).send(options)
for (let i = 0; i < movieCount; i++) { for (let i = 0; i < movieCount; i++) {
sentMessage.react(Emotes[i]) sentMessage.react(ValidVoteEmotes[i])
} }
sentMessage.react(NONE_OF_THAT) sentMessage.react(NONE_OF_THAT)
@ -167,32 +234,35 @@ export default class VoteController {
const lastMessage: Message<true> = messages[0] const lastMessage: Message<true> = messages[0]
logger.debug(`Found messages: ${JSON.stringify(messages, null, 2)}`, { guildId, requestId }) if (!isVoteMessage(lastMessage)) {
logger.error(`Found message that is not a vote message, can't proceed`, { guildId, requestId })
logger.debug(`Last message: ${JSON.stringify(lastMessage, null, 2)}`, { guildId, requestId }) logger.debug(`Found messages: ${JSON.stringify(messages, null, 2)}`, { guildId, requestId })
logger.debug(`Last message: ${JSON.stringify(lastMessage, null, 2)}`, { guildId, requestId })
const votes = (await this.getVotesByEmote(lastMessage, guildId, requestId))
.sort((a, b) => b.count - a.count)
logger.debug(`votes: ${JSON.stringify(votes, null, 2)}`, { guildId, requestId })
logger.info("Deleting vote message")
await lastMessage.delete()
const event = await this.getEvent(guild, guild.id, requestId)
if (event) {
this.updateEvent(event, votes, guild, guildId, requestId)
this.sendVoteClosedMessage(event, votes[0].movie, guildId, requestId)
} }
else {
const votes = (await this.getVotesByEmote(lastMessage, guildId, requestId))
.sort((a, b) => b.count - a.count)
lastMessage.unpin() //todo: uncomment when bot has permission to pin/unpin logger.debug(`votes: ${JSON.stringify(votes, null, 2)}`, { guildId, requestId })
logger.info("Deleting vote message")
lastMessage.unpin()
await lastMessage.delete()
const event = await this.getOpenPollEvent(guild, guild.id, requestId)
if (event && votes?.length > 0) {
this.updateOpenPollEventWithVoteResults(event, votes, guild, guildId, requestId)
this.sendVoteClosedMessage(event, votes[0].movie, guildId, requestId)
}
}
} }
public async getVotesByEmote(message: Message, guildId: string, requestId: string): Promise<Vote[]> { /**
* gets votes for the movies without the NONE_OF_THAT votes
*/
public async getVotesByEmote(message: VoteMessage, guildId: string, requestId: string): Promise<Vote[]> {
const votes: Vote[] = [] const votes: Vote[] = []
logger.debug(`Number of items in emotes: ${Object.values(Emotes).length}`, { guildId, requestId }) logger.debug(`Number of items in emotes: ${Object.values(ValidVoteEmotes).length}`, { guildId, requestId })
for (let i = 0; i < Object.keys(Emotes).length / 2; i++) { for (let i = 0; i < Object.keys(ValidVoteEmotes).length / 2; i++) {
const emote = Emotes[i] const emote = ValidVoteEmotes[i]
logger.debug(`Getting reaction for emote ${emote}`, { guildId, requestId }) logger.debug(`Getting reaction for emote ${emote}`, { guildId, requestId })
const reaction = message.reactions.resolve(emote) const reaction = 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 })
@ -203,19 +273,19 @@ export default class VoteController {
} }
return votes return votes
} }
public async getEvent(guild: Guild, guildId: string, requestId: string): Promise<GuildScheduledEvent | null> { public async getOpenPollEvent(guild: Guild, guildId: string, requestId: string): Promise<Maybe<GuildScheduledEvent>> {
const voteEvents = (await guild.scheduledEvents.fetch()) const voteEvents = (await guild.scheduledEvents.fetch())
.map((value) => value) .map((value) => value)
.filter(event => event.name.toLowerCase().includes("voting offen")) .filter(event => event.name.toLowerCase().includes("voting offen"))
logger.debug(`Found events: ${JSON.stringify(voteEvents, null, 2)}`, { guildId, requestId }) logger.debug(`Found events: ${JSON.stringify(voteEvents, null, 2)}`, { guildId, requestId })
if (!voteEvents || voteEvents.length <= 0) { if (!voteEvents || voteEvents.length <= 0) {
logger.error("Could not find vote event. Cancelling update!", { guildId, requestId }) logger.error("Could not find an open vote event.", { guildId, requestId })
return null return
} }
return voteEvents[0] return voteEvents[0]
} }
public async updateEvent(voteEvent: GuildScheduledEvent, votes: Vote[], guild: Guild, guildId: string, requestId: string) { public async updateOpenPollEventWithVoteResults(voteEvent: GuildScheduledEvent, votes: Vote[], guild: Guild, guildId: string, requestId: string) {
logger.info(`Updating event with movie ${votes[0].movie}.`, { guildId, requestId }) logger.info(`Updating event with movie ${votes[0].movie}.`, { guildId, requestId })
const options: GuildScheduledEventEditOptions<GuildScheduledEventStatus.Scheduled, GuildScheduledEventSetStatusArg<GuildScheduledEventStatus.Scheduled>> = { const options: GuildScheduledEventEditOptions<GuildScheduledEventStatus.Scheduled, GuildScheduledEventSetStatusArg<GuildScheduledEventStatus.Scheduled>> = {
name: votes[0].movie, name: votes[0].movie,
@ -225,10 +295,10 @@ export default class VoteController {
logger.info("Updating event.", { guildId, requestId }) logger.info("Updating event.", { guildId, requestId })
voteEvent.edit(options) voteEvent.edit(options)
} }
public async sendVoteClosedMessage(event: GuildScheduledEvent, movie: string, guildId: string, requestId: string) { public async sendVoteClosedMessage(event: GuildScheduledEvent, movie: string, guildId: string, requestId: string): Promise<Message<boolean>> {
const date = event.scheduledStartAt ? format(event.scheduledStartAt, "dd.MM") : "Fehler, event hatte kein Datum" const date = event.scheduledStartAt ? format(event.scheduledStartAt, "dd.MM.") : "Fehler, Event hatte kein Datum"
const time = event.scheduledStartAt ? format(event.scheduledStartAt, "HH:mm") : "Fehler, event hatte kein Datum" const time = event.scheduledStartAt ? format(event.scheduledStartAt, "HH:mm") : "Fehler, Event hatte keine Uhrzeit"
const body = `[Abstimmung beendet] für https://discord.com/events/${event.guildId}/${event.id}\n<@&${config.bot.announcement_role}> Wir gucken ${movie} am ${date} um ${time}` const body = `[Abstimmung beendet] für https://discord.com/events/${event.guildId}/${event.id}\n<@&${config.bot.announcement_role}> Wir gucken ${movie} am ${date} um ${time}`
const options: MessageCreateOptions = { const options: MessageCreateOptions = {
content: body, content: body,
allowedMentions: { parse: ["roles"] } allowedMentions: { parse: ["roles"] }
@ -236,13 +306,14 @@ export default class VoteController {
const announcementChannel = this.client.getAnnouncementChannelForGuild(guildId) const announcementChannel = this.client.getAnnouncementChannelForGuild(guildId)
logger.info("Sending vote closed message.", { guildId, requestId }) logger.info("Sending vote closed message.", { guildId, requestId })
if (!announcementChannel) { if (!announcementChannel) {
logger.error("Could not find announcement channel. Please fix!", { guildId, requestId }) const errorMessageText = "Could not find announcement channel. Please fix!"
return logger.error(errorMessageText, { guildId, requestId })
throw errorMessageText
} }
announcementChannel.send(options) return announcementChannel.send(options)
} }
private extractMovieFromMessageByEmote(message: Message, emote: string): string { private extractMovieFromMessageByEmote(voteMessage: VoteMessage, emote: string): string {
const lines = message.cleanContent.split("\n") const lines = voteMessage.cleanContent.split("\n")
const emoteLines = lines.filter(line => line.includes(emote)) const emoteLines = lines.filter(line => line.includes(emote))
if (!emoteLines) { if (!emoteLines) {

View File

@ -1,5 +1,5 @@
import { Collection } from "@discordjs/collection" import { Collection } from "@discordjs/collection"
import { Role } from "discord.js" import { GuildScheduledEvent, Role, TextChannel } from "discord.js"
export type Maybe<T> = T | undefined | null export type Maybe<T> = T | undefined | null
export interface Player { export interface Player {
@ -39,3 +39,10 @@ export interface JellyfinConfig {
collectionUser: string collectionUser: string
} }
export type PermissionLevel = "VIEWER" | "ADMIN" | "TEMPORARY" export type PermissionLevel = "VIEWER" | "ADMIN" | "TEMPORARY"
export interface prepareVoteMessageInput {
movies: string[],
startDate: Date,
event: GuildScheduledEvent,
announcementChannel: TextChannel,
pinAfterSending: boolean,
}

View File

@ -253,22 +253,22 @@ function isFormData(value: any): value is FormData {
export class ResponseError extends Error { export class ResponseError extends Error {
override name: "ResponseError" = "ResponseError"; override name: "ResponseError" = "ResponseError";
constructor(public response: Response, msg?: string) { constructor(public response: Response, errorMessage?: string) {
super(msg); super(errorMessage);
} }
} }
export class FetchError extends Error { export class FetchError extends Error {
override name: "FetchError" = "FetchError"; override name: "FetchError" = "FetchError";
constructor(public cause: Error, msg?: string) { constructor(public cause: Error, errorMessage?: string) {
super(msg); super(errorMessage);
} }
} }
export class RequiredError extends Error { export class RequiredError extends Error {
override name: "RequiredError" = "RequiredError"; override name: "RequiredError" = "RequiredError";
constructor(public field: string, msg?: string) { constructor(public field: string, errorMessage?: string) {
super(msg); super(errorMessage);
} }
} }

View File

@ -6,7 +6,7 @@ export const noGuildId = 'NoGuildId'
const printFn = format.printf(({ guildId, level, message, errorCode, requestId, timestamp: logTimestamp }: { [k: string]: string }) => { const printFn = format.printf(({ guildId, level, message, errorCode, requestId, timestamp: logTimestamp }: { [k: string]: string }) => {
return `[${guildId ?? ''}][${level}][${logTimestamp}][${errorCode ?? ''}][${requestId ?? ''}]:${message}` return `[${guildId ?? ''}][${level.padStart(5, " ")}][${logTimestamp}][${errorCode ?? ''}][${requestId ?? ''}]:${message}`
}) })
const logFormat = format.combine( const logFormat = format.combine(
@ -16,7 +16,8 @@ const logFormat = format.combine(
const consoleTransports = [ const consoleTransports = [
new transports.Console({ new transports.Console({
format: logFormat format: logFormat,
silent: process.env.NODE_ENV === 'testing'
}) })
] ]
export const logger = createLogger({ export const logger = createLogger({

View File

@ -2,7 +2,6 @@ import { ApplicationCommandDataResolvable, Client, ClientOptions, Collection, Gu
import fs from 'fs'; import fs from 'fs';
import { ScheduledTask, schedule } from "node-cron"; import { ScheduledTask, schedule } from "node-cron";
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { manageAnnouncementRoles } from "../commands/announce";
import { config } from "../configuration"; import { config } from "../configuration";
import { Maybe } from "../interfaces"; import { Maybe } from "../interfaces";
import { JellyfinHandler } from "../jellyfin/handler"; import { JellyfinHandler } from "../jellyfin/handler";
@ -11,6 +10,7 @@ import { CommandType } from "../types/commandTypes";
import { isInitialAnnouncement } from "../helper/messageIdentifiers"; import { isInitialAnnouncement } from "../helper/messageIdentifiers";
import VoteController from "../helper/vote.controller"; import VoteController from "../helper/vote.controller";
import { yavinJellyfinHandler } from "../.."; import { yavinJellyfinHandler } from "../..";
import RoleController from "../helper/role.controller";
@ -19,6 +19,7 @@ export class ExtendedClient extends Client {
private commandFilePath = `${__dirname}/../commands` private commandFilePath = `${__dirname}/../commands`
private jellyfin: JellyfinHandler private jellyfin: JellyfinHandler
public voteController: VoteController = new VoteController(this, yavinJellyfinHandler) public voteController: VoteController = new VoteController(this, yavinJellyfinHandler)
public roleController: RoleController = new RoleController()
public commands: Collection<string, CommandType> = new Collection() public commands: Collection<string, CommandType> = new Collection()
private announcementChannels: Collection<string, TextChannel> = new Collection() //guildId to TextChannel private announcementChannels: Collection<string, TextChannel> = new Collection() //guildId to TextChannel
private announcementRoleHandlerTask: Collection<string, ScheduledTask> = new Collection() //one task per guild private announcementRoleHandlerTask: Collection<string, ScheduledTask> = new Collection() //one task per guild
@ -172,10 +173,10 @@ export class ExtendedClient extends Client {
} }
//logger.debug(`Message: ${JSON.stringify(message, null, 2)}`, { guildId: guild.id, requestId }) //logger.debug(`Message: ${JSON.stringify(message, null, 2)}`, { guildId: guild.id, requestId })
const reactions = message.reactions.resolve("🎫") const ticketReaction = message.reactions.resolve("🎫")
//logger.debug(`reactions: ${JSON.stringify(reactions, null, 2)}`, { guildId: guild.id, requestId }) //logger.debug(`reactions: ${JSON.stringify(reactions, null, 2)}`, { guildId: guild.id, requestId })
if (reactions) { if (ticketReaction) {
manageAnnouncementRoles(message.guild, reactions, requestId) this.roleController.assignAnnouncementRolesFromReaction(message.guild, ticketReaction, requestId)
} else { } else {
logger.error("Did not get reactions! Aborting!", { guildId: guild.id, requestId }) logger.error("Did not get reactions! Aborting!", { guildId: guild.id, requestId })
} }

View File

@ -0,0 +1,81 @@
import { Guild, GuildScheduledEvent, Message } from "discord.js"
import VoteController from "../../server/helper/vote.controller"
import { JellyfinHandler } from "../../server/jellyfin/handler"
import { ExtendedClient } from "../../server/structures/client"
import { Emoji, NONE_OF_THAT } from "../../server/constants"
import { isVoteMessage } from "../../server/helper/messageIdentifiers"
describe('vote controller - none_of_that functions', () => {
const testEventId = '1234321'
const testEventDate = new Date('2023-01-01')
const testGuildId = "888999888"
const testMovies = [
'Movie1',
'Movie2',
'Movie3',
'Movie4',
'Movie5',
]
const votesList = [
{ emote: Emoji.one, count: 1, movie: testMovies[0] },
{ emote: Emoji.two, count: 2, movie: testMovies[1] },
{ emote: Emoji.three, count: 3, movie: testMovies[2] },
{ emote: Emoji.four, count: 1, movie: testMovies[3] },
{ emote: Emoji.five, count: 1, movie: testMovies[4] },
{ emote: NONE_OF_THAT, count: 2, movie: NONE_OF_THAT },
]
const mockClient: ExtendedClient = <ExtendedClient><unknown>{
user: {
id: 'mockId'
}
}
const mockEvent: GuildScheduledEvent = <GuildScheduledEvent><unknown>{
scheduledStartAt: testEventDate,
id: testEventId,
guild: testGuildId
}
const mockJellyfinHandler: JellyfinHandler = <JellyfinHandler><unknown>{
getRandomMovieNames: jest.fn().mockReturnValue(["movie1"])
}
const votes = new VoteController(mockClient, mockJellyfinHandler)
const mockMessageContent = votes.createVoteMessageText(mockEvent, testMovies, testGuildId, "requestId")
test('sendVoteClosedMessage', async () => {
mockClient.getAnnouncementChannelForGuild = jest.fn().mockReturnValue({
send: jest.fn().mockImplementation((options: any) => {
return new Promise((resolve) => {
resolve(options)
})
})
})
const scheduledEvent: GuildScheduledEvent = <GuildScheduledEvent>{
scheduledStartAt: testEventDate,
guildId: testGuildId,
id: testEventId
}
const res = await votes.sendVoteClosedMessage(scheduledEvent, 'MovieNew', 'guild', 'request')
expect(res).toEqual({
allowedMentions: {
parse: ["roles"]
},
content: `[Abstimmung beendet] für https://discord.com/events/${testGuildId}/${testEventId}\n<@&WATCHPARTY_ANNOUNCEMENT_ROLE> Wir gucken MovieNew am 01.01. um 01:00`
})
})
test('getVotesByEmote', async () => {
const mockMessage: Message = <Message><unknown>{
cleanContent: mockMessageContent,
reactions: {
resolve: jest.fn().mockImplementation((input: any) => {
return votesList.find(e => e.emote === input)
})
}
}
if (isVoteMessage(mockMessage)) {
const result = await votes.getVotesByEmote(mockMessage, 'guildId', 'requestId')
expect(result.length).toEqual(5)
expect(result).toEqual(votesList.filter(x => x.movie != NONE_OF_THAT))
}
})
})

View File

@ -1,9 +1,10 @@
import { Emoji, NONE_OF_THAT } from "../../server/constants" import { Emoji, NONE_OF_THAT } from "../../server/constants"
import VoteController, { Vote, VoteMessageInfo } from "../../server/helper/vote.controller" import VoteController, { VoteMessageInfo } from "../../server/helper/vote.controller"
import { JellyfinHandler } from "../../server/jellyfin/handler" import { JellyfinHandler } from "../../server/jellyfin/handler"
import { ExtendedClient } from "../../server/structures/client" import { ExtendedClient } from "../../server/structures/client"
import { VoteMessage } from "../../server/helper/messageIdentifiers" import { VoteMessage } from "../../server/helper/messageIdentifiers"
test('parse votes from vote message', () => { import { GuildScheduledEvent, MessageReaction } from "discord.js"
test('parse votes from vote message', async () => {
const testMovies = [ const testMovies = [
'Movie1', 'Movie1',
'Movie2', 'Movie2',
@ -15,12 +16,16 @@ test('parse votes from vote message', () => {
const testEventDate = new Date('2023-01-01') const testEventDate = new Date('2023-01-01')
const testGuildId = "888999888" const testGuildId = "888999888"
const voteController: VoteController = new VoteController(<ExtendedClient>{}, <JellyfinHandler>{}) const voteController: VoteController = new VoteController(<ExtendedClient>{}, <JellyfinHandler>{})
const testMessage = voteController.createVoteMessageText(testEventId, testEventDate, testMovies, testGuildId, "requestId") const mockEvent: GuildScheduledEvent = <GuildScheduledEvent><unknown>{
scheduledStartAt: testEventDate,
id: testEventId,
guild: testGuildId
}
const testMessage = voteController.createVoteMessageText(mockEvent, testMovies, testGuildId, "requestId")
const expectedResult: VoteMessageInfo = { const expectedResult: VoteMessageInfo = {
eventId: testEventId, event: mockEvent,
eventDate: testEventDate,
votes: [ votes: [
{ emote: Emoji.one, count: 1, movie: testMovies[0] }, { emote: Emoji.one, count: 1, movie: testMovies[0] },
{ emote: Emoji.two, count: 2, movie: testMovies[1] }, { emote: Emoji.two, count: 2, movie: testMovies[1] },
@ -31,8 +36,21 @@ test('parse votes from vote message', () => {
] ]
} }
const msg: VoteMessage = <VoteMessage><unknown>{ const message: VoteMessage = <VoteMessage><unknown>{
cleanContent: testMessage, cleanContent: testMessage,
guild: {
id: testGuildId,
scheduledEvents: {
fetch: jest.fn().mockImplementation((input: any) => {
if (input === testEventId)
return {
id: testEventId,
guild: testGuildId,
scheduledStartAt: testEventDate
}
})
}
},
reactions: { reactions: {
cache: { cache: {
get: jest.fn().mockImplementation((input: any) => { get: jest.fn().mockImplementation((input: any) => {
@ -46,11 +64,11 @@ test('parse votes from vote message', () => {
} }
} }
const result = voteController.parseVotesFromVoteMessage(msg, 'requestId') const result = await voteController.parseVoteInfoFromVoteMessage(message, 'requestId')
console.log(JSON.stringify(result)) console.log(JSON.stringify(result))
expect(Array.isArray(result)).toBe(false) expect(Array.isArray(result)).toBe(false)
expect(result.eventId).toEqual(testEventId) expect(result.event.id).toEqual(testEventId)
expect(result.eventDate).toEqual(testEventDate) expect(result.event.scheduledStartAt).toEqual(testEventDate)
expect(result.votes.length).toEqual(expectedResult.votes.length) expect(result.votes.length).toEqual(expectedResult.votes.length)
expect(result).toEqual(expectedResult) expect(result).toEqual(expectedResult)
}) })
@ -67,8 +85,108 @@ test('parse votes from vote message', () => {
const testEventDate = new Date('2023-01-01') const testEventDate = new Date('2023-01-01')
const testGuildId = "888999888" const testGuildId = "888999888"
const voteController: VoteController = new VoteController(<ExtendedClient>{}, <JellyfinHandler>{}) const voteController: VoteController = new VoteController(<ExtendedClient>{}, <JellyfinHandler>{})
const testMessage = voteController.createVoteMessageText(testEventId, testEventDate, testMovies, testGuildId, "requestId") const mockEvent: GuildScheduledEvent = <GuildScheduledEvent><unknown>{
scheduledStartAt: testEventDate,
id: testEventId,
guild: testGuildId
}
const testMessage = voteController.createVoteMessageText(mockEvent, testMovies, testGuildId, "requestId")
const result = voteController.parseGuildIdAndEventIdFromWholeMessage(testMessage) const result = voteController.parseGuildIdAndEventIdFromWholeMessage(testMessage)
expect(result).toEqual({ guildId: testGuildId, eventId: testEventId }) expect(result).toEqual({ guildId: testGuildId, eventId: testEventId })
}) })
test.skip('handles complete none_of_that vote', () => {
const mockJellyfinHandler: JellyfinHandler = <JellyfinHandler><unknown>{
getRandomMovieNames: jest.fn().mockReturnValue(["movie1"])
}
const testMovies = [
'Movie1',
'Movie2',
'Movie3',
'Movie4',
'Movie5',
]
const testEventId = '1234321'
const testEventDate = new Date('2023-01-01')
const testGuildId = "888999888"
const mockClient: ExtendedClient = <ExtendedClient><unknown>{
user: {
id: 'mockId'
}
}
const voteController = new VoteController(mockClient, mockJellyfinHandler)
const mockEvent: GuildScheduledEvent = <GuildScheduledEvent><unknown>{
scheduledStartAt: testEventDate,
id: testEventId,
guild: testGuildId
}
const mockMessageContent = voteController.createVoteMessageText(mockEvent, testMovies, testGuildId, "requestId")
const reactedUponMessage: VoteMessage = <VoteMessage><unknown>{
cleanContent: mockMessageContent,
guild: {
id: 'id',
roles: {
resolve: jest.fn().mockReturnValue({
members: [{}, {}, {}, {}, {}]//content does not matter
})
},
scheduledEvents: {
fetch: jest.fn().mockReturnValue([
{
name: 'voting offen'
}
])
}
},
unpin: jest.fn().mockImplementation(() => {
}),
delete: jest.fn().mockImplementation(() => {
}),
reactions: {
resolve: jest.fn().mockImplementation((input: any) => {
console.log(JSON.stringify(input))
}),
cache: {
get: jest.fn().mockReturnValue({
users: {
cache: [
{
id: "mockId"//to filter out
},
{
id: "userId1"
},
{
id: "userId2"
},
{
id: "userId3"
}
]
}
})
}
}
}
const messageReaction: MessageReaction = <MessageReaction><unknown>{
message: reactedUponMessage
}
mockClient.getAnnouncementChannelForGuild = jest.fn().mockReturnValue({
messages: {
fetch: jest.fn().mockReturnValue([
reactedUponMessage
])
}
})
const res = voteController.handleNoneOfThatVote(messageReaction, reactedUponMessage, 'requestId', 'guildId')
})

15
tests/testenv.js Normal file
View File

@ -0,0 +1,15 @@
process.env.CLIENT_ID = "CLIENT_ID"
process.env.SECRET = "SECRET"
process.env.BOT_TOKEN = "BOT_TOKEN"
process.env.WATCHER_ROLE = "WATCHER_ROLE"
process.env.ADMIN_ROLE = "ADMIN_ROLE"
process.env.CHANNEL_ID = "CHANNEL_ID"
process.env.WATCHPARTY_ANNOUNCEMENT_ROLE = "WATCHPARTY_ANNOUNCEMENT_ROLE"
process.env.YAVIN_JELLYFIN_URL = "YAVIN_JELLYFIN_URL"
process.env.YAVIN_COLLECTION_ID = "YAVIN_COLLECTION_ID"
process.env.YAVIN_COLLECTION_USER = "YAVIN_COLLECTION_USER"
process.env.YAVIN_TOKEN = "YAVIN_TOKEN"
process.env.TOKEN = "TOKEN"
process.env.JELLYFIN_USER = "JELLYFIN_USER"
process.env.JELLYFIN_COLLECTION_ID = "JELLYFIN_COLLECTION_ID"
process.env.JELLYFIN_URL = "JELLYFIN_URL"