Compare commits
33 Commits
feat/cicd
...
d9d1d74ef9
Author | SHA1 | Date | |
---|---|---|---|
d9d1d74ef9 | |||
331ff89060 | |||
f6476c609b | |||
6220268b14 | |||
b6034d4fb7 | |||
ca0a9e3cb8 | |||
b8a32aab40 | |||
e3e755011d | |||
5a6c66cb3e | |||
0d3c62c6ad | |||
5816db48e6 | |||
66f843b399 | |||
d82a7cffd2 | |||
8a06a661fa | |||
4084f675cd | |||
3bd26a9d6c | |||
e7b21fa658 | |||
2d32f9b680 | |||
5503aa8713 | |||
25bb676fda | |||
9f5abb8a90 | |||
0e67252976 | |||
37b798818c | |||
af414d0bad | |||
c32434a7eb | |||
c133570d8c | |||
65cdee36e9 | |||
6b0e84669a | |||
dd72f8e165 | |||
a6f19ccd2b | |||
c39f9c6ee1 | |||
f41194ba71 | |||
fa49dc0f76 |
7
.editorconfig
Normal file
7
.editorconfig
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
root = true
|
||||||
|
[*]
|
||||||
|
indent_style = tab
|
||||||
|
tab_width = 4
|
||||||
|
[*.ts]
|
||||||
|
indent_style = tab
|
||||||
|
tab_width = 4
|
@ -14,4 +14,4 @@ jobs:
|
|||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
- name: Build Container
|
- name: Build Container
|
||||||
run: docker build .
|
run: docker build --target compile .
|
||||||
|
@ -11,7 +11,6 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
build-docker-image:
|
build-docker-image:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
#if: gitea.ref == 'refs/heads/master'
|
|
||||||
container: catthehacker/ubuntu:act-latest
|
container: catthehacker/ubuntu:act-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@ -22,6 +21,8 @@ jobs:
|
|||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry
|
||||||
run: docker login -u ${{ env.USER }} -p ${{ secrets.TOKEN }} ${{ env.REGISTRY }}
|
run: docker login -u ${{ env.USER }} -p ${{ secrets.TOKEN }} ${{ env.REGISTRY }}
|
||||||
- name: Build Container
|
- name: Build Container
|
||||||
run: docker build -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" .
|
run: docker build --target compile -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}" .
|
||||||
|
env:
|
||||||
|
version: $(cat package.json | awk 'match($0, /version/) {print $2}' | sed 's/[\",]//g') # extracts the version number from the package.json with bash magic
|
||||||
- name: Push Container
|
- name: Push Container
|
||||||
run: docker push --all-tags "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
|
run: docker push --all-tags "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}"
|
||||||
|
18
.gitea/workflows/test.yaml
Normal file
18
.gitea/workflows/test.yaml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
name: Run unit tests
|
||||||
|
on: [pull_request]
|
||||||
|
env:
|
||||||
|
REGISTRY: gitea.brudi.xyz
|
||||||
|
IMAGE_NAME: ${{ gitea.repository }}
|
||||||
|
USER: ${{ gitea.actor }}
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container: catthehacker/ubuntu:act-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Run Tests
|
||||||
|
run: docker build --target test .
|
||||||
|
|
16
Dockerfile
16
Dockerfile
@ -1,11 +1,21 @@
|
|||||||
FROM node:alpine as Build
|
FROM node:alpine as files
|
||||||
ENV NODE_ENV=production
|
ENV TZ="Europe/Berlin"
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY [ "package-lock.json", "package.json", "index.ts", "tsconfig.json", "./" ]
|
COPY [ "package-lock.json", "package.json", "index.ts", "tsconfig.json", "./" ]
|
||||||
COPY server ./server
|
COPY server ./server
|
||||||
|
|
||||||
|
FROM files as proddependencies
|
||||||
|
ENV NODE_ENV=production
|
||||||
RUN npm ci --omit=dev
|
RUN npm ci --omit=dev
|
||||||
|
|
||||||
|
FROM proddependencies as compile
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
CMD ["npm","run","start"]
|
CMD ["npm","run","start"]
|
||||||
|
|
||||||
|
FROM files as dependencies
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
FROM dependencies as test
|
||||||
|
COPY jest.config.js .
|
||||||
|
COPY tests ./tests
|
||||||
|
RUN npm run test
|
||||||
|
18
jest.config.js
Normal file
18
jest.config.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
module.exports = {
|
||||||
|
'roots': [
|
||||||
|
'<rootDir>/tests',
|
||||||
|
'<rootDir>/server'
|
||||||
|
],
|
||||||
|
'transform': {
|
||||||
|
'^.+\\.tsx?$': 'ts-jest'
|
||||||
|
},
|
||||||
|
'testRegex': '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',
|
||||||
|
'moduleFileExtensions': [
|
||||||
|
'ts',
|
||||||
|
'tsx',
|
||||||
|
'js',
|
||||||
|
'jsx',
|
||||||
|
'json',
|
||||||
|
'node'
|
||||||
|
],
|
||||||
|
};
|
27
package-lock.json
generated
27
package-lock.json
generated
@ -30,12 +30,13 @@
|
|||||||
"winston": "^3.8.2"
|
"winston": "^3.8.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^29.5.0",
|
"@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",
|
||||||
"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"
|
||||||
@ -1568,9 +1569,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/jest": {
|
"node_modules/@types/jest": {
|
||||||
"version": "29.5.0",
|
"version": "29.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.2.tgz",
|
||||||
"integrity": "sha512-3Emr5VOl/aoBwnWcH/EFQvlSAmjV+XtV9GGu5mwdYew5vhQh0IUZx/60x0TzHDu09Bi7HMx10t/namdJw5QIcg==",
|
"integrity": "sha512-mSoZVJF5YzGVCk+FsDxzDuH7s+SCkzrgKZzf0Z0T2WudhBUPoF6ktoTPC4R0ZoCPCV5xUvuU6ias5NvxcBcMMg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"expect": "^29.0.0",
|
"expect": "^29.0.0",
|
||||||
@ -4989,6 +4990,12 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mockdate": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/mockdate/-/mockdate-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-iniQP4rj1FhBdBYS/+eQv7j1tadJ9lJtdzgOpvsOHng/GbcDh2Fhdeq+ZRldrPYdXvCyfFUmFeEwEGXZB5I/AQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
@ -8139,9 +8146,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/jest": {
|
"@types/jest": {
|
||||||
"version": "29.5.0",
|
"version": "29.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.2.tgz",
|
||||||
"integrity": "sha512-3Emr5VOl/aoBwnWcH/EFQvlSAmjV+XtV9GGu5mwdYew5vhQh0IUZx/60x0TzHDu09Bi7HMx10t/namdJw5QIcg==",
|
"integrity": "sha512-mSoZVJF5YzGVCk+FsDxzDuH7s+SCkzrgKZzf0Z0T2WudhBUPoF6ktoTPC4R0ZoCPCV5xUvuU6ias5NvxcBcMMg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"expect": "^29.0.0",
|
"expect": "^29.0.0",
|
||||||
@ -10720,6 +10727,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
|
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
|
||||||
},
|
},
|
||||||
|
"mockdate": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/mockdate/-/mockdate-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-iniQP4rj1FhBdBYS/+eQv7j1tadJ9lJtdzgOpvsOHng/GbcDh2Fhdeq+ZRldrPYdXvCyfFUmFeEwEGXZB5I/AQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"ms": {
|
"ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
|
@ -33,15 +33,18 @@
|
|||||||
"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-watch": "jest --watch"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^29.5.0",
|
"@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",
|
||||||
"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"
|
||||||
|
@ -6,6 +6,7 @@ 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 { messageIsInitialAnnouncement } from '../helper/messageIdentifiers'
|
||||||
|
|
||||||
export default new Command({
|
export default new Command({
|
||||||
name: 'announce',
|
name: 'announce',
|
||||||
@ -61,7 +62,7 @@ async function sendInitialAnnouncement(guildId: string, requestId: string): Prom
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentPinnedAnnouncementMessages = (await announcementChannel.messages.fetchPinned()).filter(message => message.cleanContent.includes("[initial]"))
|
const currentPinnedAnnouncementMessages = (await announcementChannel.messages.fetchPinned()).filter(message => messageIsInitialAnnouncement(message))
|
||||||
currentPinnedAnnouncementMessages.forEach(async (message) => await message.unpin())
|
currentPinnedAnnouncementMessages.forEach(async (message) => await message.unpin())
|
||||||
currentPinnedAnnouncementMessages.forEach(message => message.delete())
|
currentPinnedAnnouncementMessages.forEach(message => message.delete())
|
||||||
|
|
||||||
|
@ -3,11 +3,12 @@ import { Guild, GuildScheduledEvent, GuildScheduledEventEditOptions, GuildSchedu
|
|||||||
import { v4 as uuid } from 'uuid'
|
import { v4 as uuid } from 'uuid'
|
||||||
import { client } from '../..'
|
import { client } from '../..'
|
||||||
import { config } from '../configuration'
|
import { config } from '../configuration'
|
||||||
import { Emotes } from '../events/autoCreateVoteByWPEvent'
|
|
||||||
import { Maybe } from '../interfaces'
|
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 { messageIsVoteEndedMessage, messageIsVoteMessage } from '../helper/messageIdentifiers'
|
||||||
|
import { Emotes } from '../constants'
|
||||||
|
|
||||||
export default new Command({
|
export default new Command({
|
||||||
name: 'closepoll',
|
name: 'closepoll',
|
||||||
@ -41,7 +42,7 @@ export async function closePoll(guild: Guild, requestId: string) {
|
|||||||
|
|
||||||
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 => !messageIsVoteEndedMessage(message) && messageIsVoteMessage(message))
|
||||||
.sort((a, b) => b.createdTimestamp - a.createdTimestamp)
|
.sort((a, b) => b.createdTimestamp - a.createdTimestamp)
|
||||||
|
|
||||||
if (!messages || messages.length <= 0) {
|
if (!messages || messages.length <= 0) {
|
||||||
|
3
server/constants.ts
Normal file
3
server/constants.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
export enum Emotes { "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟" }
|
||||||
|
export const NONE_OF_THAT = "❌"
|
@ -6,14 +6,12 @@ import { config } from "../configuration";
|
|||||||
import { createDateStringFromEvent } from "../helper/dateHelper";
|
import { createDateStringFromEvent } from "../helper/dateHelper";
|
||||||
import { Maybe } from "../interfaces";
|
import { Maybe } from "../interfaces";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
|
import { Emotes, NONE_OF_THAT } from "../constants";
|
||||||
|
|
||||||
|
|
||||||
export const name = 'guildScheduledEventCreate'
|
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) {
|
export async function execute(event: GuildScheduledEvent) {
|
||||||
const requestId = uuid()
|
const requestId = uuid()
|
||||||
|
@ -2,6 +2,7 @@ import { Collection, GuildScheduledEvent, GuildScheduledEventStatus, Message } f
|
|||||||
import { v4 as uuid } from "uuid";
|
import { v4 as uuid } from "uuid";
|
||||||
import { client } from "../..";
|
import { client } from "../..";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
|
import { messageIsInitialAnnouncement } from "../helper/messageIdentifiers";
|
||||||
|
|
||||||
|
|
||||||
export const name = 'guildScheduledEventUpdate'
|
export const name = 'guildScheduledEventUpdate'
|
||||||
@ -25,7 +26,7 @@ export async function execute(oldEvent: GuildScheduledEvent, newEvent: GuildSche
|
|||||||
|
|
||||||
const events = await newEvent.guild.scheduledEvents.fetch()
|
const events = await newEvent.guild.scheduledEvents.fetch()
|
||||||
|
|
||||||
const wpAnnouncements = (await announcementChannel.messages.fetch()).filter(message => !message.cleanContent.includes("[initial]"))
|
const wpAnnouncements = (await announcementChannel.messages.fetch()).filter(message => !messageIsInitialAnnouncement(message))
|
||||||
const announcementsWithoutEvent = filterAnnouncementsByPendingWPs(wpAnnouncements, events)
|
const announcementsWithoutEvent = filterAnnouncementsByPendingWPs(wpAnnouncements, events)
|
||||||
logger.info(`Deleting ${announcementsWithoutEvent.length} announcements.`, { guildId, requestId })
|
logger.info(`Deleting ${announcementsWithoutEvent.length} announcements.`, { guildId, requestId })
|
||||||
announcementsWithoutEvent.forEach(message => message.delete())
|
announcementsWithoutEvent.forEach(message => message.delete())
|
||||||
|
45
server/events/handleReactionAdd.ts
Normal file
45
server/events/handleReactionAdd.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
|
||||||
|
import { Message, MessageReaction, User } from "discord.js";
|
||||||
|
import { messageIsVoteMessage } from "../helper/messageIdentifiers";
|
||||||
|
import { logger, newRequestId, noGuildId } from "../logger";
|
||||||
|
import { NONE_OF_THAT } from "../constants";
|
||||||
|
import { client } from "../..";
|
||||||
|
import { getMembersWithRoleFromGuild } from "../helper/roleFilter";
|
||||||
|
import { config } from "../configuration";
|
||||||
|
|
||||||
|
|
||||||
|
export const name = 'messageReactionAdd'
|
||||||
|
|
||||||
|
export async function execute(messageReaction: MessageReaction, user: User) {
|
||||||
|
if (user.id == client.user?.id)
|
||||||
|
logger.info('Skipping bot reaction')
|
||||||
|
|
||||||
|
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) return 'No guild'
|
||||||
|
|
||||||
|
logger.info(`Got reaction on message`, { requestId, guildId })
|
||||||
|
logger.debug(`reactedUponMessage payload: ${JSON.stringify(reactedUponMessage)}`)
|
||||||
|
|
||||||
|
if (messageIsVoteMessage(reactedUponMessage)) {
|
||||||
|
logger.debug(`${reactedUponMessage.id} is vote message`, { requestId, guildId })
|
||||||
|
if (messageReaction.message.reactions.cache.find(reaction => reaction.emoji.toString() == NONE_OF_THAT)) {
|
||||||
|
const watcherRoleMember = await getMembersWithRoleFromGuild(config.bot.announcement_role, messageReaction.message.guild)
|
||||||
|
logger.info("ROLE MEMBERS " + JSON.stringify(watcherRoleMember), { requestId, guildId })
|
||||||
|
const watcherRoleMemberCount = watcherRoleMember.size
|
||||||
|
logger.info(`MEMBER COUNT: ${watcherRoleMemberCount}`, { requestId, guildId })
|
||||||
|
let noneOfThatReactions = messageReaction.message.reactions.cache.get(NONE_OF_THAT)?.users.cache.filter(x => x.id !== client.user?.id).size ?? 0
|
||||||
|
|
||||||
|
const memberThreshold = (watcherRoleMemberCount / 2)
|
||||||
|
logger.info(`Reroll ${noneOfThatReactions} > ${memberThreshold} ?`, { requestId, guildId })
|
||||||
|
if (noneOfThatReactions > memberThreshold) {
|
||||||
|
logger.info('Starting poll reroll', { requestId, guildId })
|
||||||
|
messageReaction.message.edit((messageReaction.message.content ?? "").concat('\nDiese Abstimmung muss wiederholt werden.'))
|
||||||
|
}
|
||||||
|
logger.info(`No reroll`, { requestId, guildId })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
11
server/helper/messageIdentifiers.ts
Normal file
11
server/helper/messageIdentifiers.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Message } from "discord.js";
|
||||||
|
|
||||||
|
export function messageIsVoteMessage(msg: Message): boolean {
|
||||||
|
return msg.cleanContent.includes('[Abstimmung]')
|
||||||
|
}
|
||||||
|
export function messageIsInitialAnnouncement(msg: Message): boolean {
|
||||||
|
return msg.cleanContent.includes("[initial]")
|
||||||
|
}
|
||||||
|
export function messageIsVoteEndedMessage(msg: Message): boolean {
|
||||||
|
return msg.cleanContent.includes("[Abstimmung beendet]")
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import { Collection, GuildMember } from "discord.js"
|
import { Collection, Guild, GuildMember, Role, User } from "discord.js"
|
||||||
import { ChangedRoles, PermissionLevel } from "../interfaces"
|
import { ChangedRoles, Maybe, PermissionLevel } from "../interfaces"
|
||||||
import { logger } from "../logger"
|
import { logger } from "../logger"
|
||||||
import { config } from "../configuration"
|
import { config } from "../configuration"
|
||||||
|
|
||||||
@ -16,6 +16,13 @@ export function filterRolesFromMemberUpdate(oldMember: GuildMember, newMember: G
|
|||||||
return { addedRoles, removedRoles }
|
return { addedRoles, removedRoles }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getMembersWithRoleFromGuild(roleId: string, guild: Guild): Promise<Collection<string, GuildMember>> {
|
||||||
|
const emptyResponse = new Collection<string, GuildMember>
|
||||||
|
const guildRole: Maybe<Role> = guild.roles.resolve(roleId)
|
||||||
|
if (!guildRole) return emptyResponse
|
||||||
|
return guildRole.members
|
||||||
|
}
|
||||||
|
|
||||||
export function getGuildSpecificTriggerRoleId(): Collection<string, PermissionLevel> {
|
export function getGuildSpecificTriggerRoleId(): Collection<string, PermissionLevel> {
|
||||||
const outVal = new Collection<string, PermissionLevel>()
|
const outVal = new Collection<string, PermissionLevel>()
|
||||||
outVal.set(config.bot.watcher_role, "VIEWER")
|
outVal.set(config.bot.watcher_role, "VIEWER")
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import { createLogger, format, transports } from "winston"
|
import { createLogger, format, transports } from "winston"
|
||||||
import { config } from "./configuration"
|
import { config } from "./configuration"
|
||||||
|
import { v4 } from "uuid"
|
||||||
|
export const newRequestId = v4()
|
||||||
|
export const noGuildId = 'NoGuildId'
|
||||||
|
|
||||||
|
|
||||||
const printFn = format.printf(({ guildId, level, message, errorCode, requestId, timestamp: logTimestamp }: { [k: string]: string }) => {
|
const printFn = format.printf(({ guildId, level, message, errorCode, requestId, timestamp: logTimestamp }: { [k: string]: string }) => {
|
||||||
|
@ -9,6 +9,7 @@ import { JellyfinHandler } from "../jellyfin/handler";
|
|||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
import { CommandType } from "../types/commandTypes";
|
import { CommandType } from "../types/commandTypes";
|
||||||
import { checkForPollsToClose } from "../commands/closepoll";
|
import { checkForPollsToClose } from "../commands/closepoll";
|
||||||
|
import { messageIsInitialAnnouncement } from "../helper/messageIdentifiers";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -22,7 +23,7 @@ export class ExtendedClient extends Client {
|
|||||||
private pollCloseBackgroundTasks: Collection<string, ScheduledTask> = new Collection()
|
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.GuildMessageReactions, IntentsBitField.Flags.GuildVoiceStates)
|
||||||
const options: ClientOptions = { intents }
|
const options: ClientOptions = { intents }
|
||||||
super(options)
|
super(options)
|
||||||
this.jellyfin = jf
|
this.jellyfin = jf
|
||||||
@ -74,6 +75,7 @@ export class ExtendedClient extends Client {
|
|||||||
this.registerCommands(slashCommands, guilds)
|
this.registerCommands(slashCommands, guilds)
|
||||||
this.cacheUsers(guilds)
|
this.cacheUsers(guilds)
|
||||||
await this.cacheAnnouncementServer(guilds)
|
await this.cacheAnnouncementServer(guilds)
|
||||||
|
this.fetchAnnouncementChannelMessage(this.announcementChannels)
|
||||||
this.startAnnouncementRoleBackgroundTask(guilds)
|
this.startAnnouncementRoleBackgroundTask(guilds)
|
||||||
this.startPollCloseBackgroundTasks()
|
this.startPollCloseBackgroundTasks()
|
||||||
})
|
})
|
||||||
@ -81,6 +83,11 @@ export class ExtendedClient extends Client {
|
|||||||
logger.info(`Error refreshing slash commands: ${error}`)
|
logger.info(`Error refreshing slash commands: ${error}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private async fetchAnnouncementChannelMessage(channels: Collection<string, TextChannel>): Promise<void> {
|
||||||
|
channels.each(async ch => {
|
||||||
|
ch.messages.fetch()
|
||||||
|
})
|
||||||
|
}
|
||||||
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())
|
||||||
@ -136,7 +143,7 @@ export class ExtendedClient extends Client {
|
|||||||
}
|
}
|
||||||
this.announcementRoleHandlerTask.set(guild.id, schedule("*/10 * * * * *", async () => {
|
this.announcementRoleHandlerTask.set(guild.id, schedule("*/10 * * * * *", async () => {
|
||||||
const requestId = uuid()
|
const requestId = uuid()
|
||||||
const messages = (await textChannel.messages.fetchPinned()).filter(message => message.cleanContent.includes("[initial]"))
|
const messages = (await textChannel.messages.fetchPinned()).filter(message => messageIsInitialAnnouncement(message))
|
||||||
|
|
||||||
if (messages.size > 1) {
|
if (messages.size > 1) {
|
||||||
logger.error("More than one pinned announcement Messages found. Unable to know which one people react to. Please fix!", { guildId: guild.id, requestId })
|
logger.error("More than one pinned announcement Messages found. Unable to know which one people react to. Please fix!", { guildId: guild.id, requestId })
|
||||||
|
16
tests/helpers/date.test.ts
Normal file
16
tests/helpers/date.test.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { GuildScheduledEvent } from "discord.js"
|
||||||
|
import { createDateStringFromEvent } from "../../server/helper/dateHelper"
|
||||||
|
import MockDate from 'mockdate'
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
MockDate.set('01-01-2023')
|
||||||
|
})
|
||||||
|
|
||||||
|
function getTestDate(date: string): GuildScheduledEvent {
|
||||||
|
return <GuildScheduledEvent>{ scheduledStartAt: new Date(date) }
|
||||||
|
}
|
||||||
|
test('createDateStringFromEvent - correct formatting', () => {
|
||||||
|
expect(createDateStringFromEvent(getTestDate('01-01-2023 12:30'), "")).toEqual('heute um 12:30')
|
||||||
|
expect(createDateStringFromEvent(getTestDate('01-02-2023 12:30'), "")).toEqual('am Montag 02.01 um 12:30')
|
||||||
|
expect(createDateStringFromEvent(getTestDate('01-03-2023 12:30'), "")).toEqual('am Dienstag 03.01 um 12:30')
|
||||||
|
})
|
28
tests/helpers/memberRoles.test.ts
Normal file
28
tests/helpers/memberRoles.test.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { Collection, GuildMember, Role } from "discord.js"
|
||||||
|
import { filterRolesFromMemberUpdate } from "../../server/helper/roleFilter"
|
||||||
|
|
||||||
|
function buildFakeRole(id: string, name: string): Role {
|
||||||
|
return <Role>{ id, name }
|
||||||
|
|
||||||
|
}
|
||||||
|
test('filterRolesFromMemberUpdate', () => {
|
||||||
|
const oldMemberRoles: Collection<string, Role> = new Collection<string, Role>()
|
||||||
|
oldMemberRoles.set('1', buildFakeRole('01', 'Role01'))
|
||||||
|
oldMemberRoles.set('2', buildFakeRole('02', 'Role02'))
|
||||||
|
|
||||||
|
const newMemberRoles: Collection<string, Role> = new Collection<string, Role>()
|
||||||
|
newMemberRoles.set('1', buildFakeRole('01', 'Role01'))
|
||||||
|
newMemberRoles.set('2', buildFakeRole('02', 'Role02'))
|
||||||
|
newMemberRoles.set('3', buildFakeRole('03', 'Role03'))
|
||||||
|
|
||||||
|
const oldMember: GuildMember = <GuildMember>{ roles: { cache: oldMemberRoles }, guild: { id: "guildid" } }
|
||||||
|
const newMember: GuildMember = <GuildMember>{ roles: { cache: newMemberRoles }, guild: { id: "guildid" } }
|
||||||
|
const output = filterRolesFromMemberUpdate(oldMember, newMember)
|
||||||
|
|
||||||
|
const expectedAddedRoles: Collection<string, Role> = new Collection<string, Role>()
|
||||||
|
expectedAddedRoles.set('3', buildFakeRole('03', 'Role03'))
|
||||||
|
const expectedRemovedRoles: Collection<string, Role> = new Collection<string, Role>()
|
||||||
|
|
||||||
|
expect(output.addedRoles).toEqual(expectedAddedRoles)
|
||||||
|
expect(output.removedRoles).toEqual(expectedRemovedRoles)
|
||||||
|
})
|
Reference in New Issue
Block a user