[READ-ONLY] Mirror of https://github.com/FoxxMD/tautulli-notification-digest. Consolidate Tautuilli Notification agent events for discord
digest discord newsletter notification plex tautulli tautulli-api webhook
0

Configure Feed

Select the types of activity you want to include in your feed.

Implement sanity test for one event

FoxxMD (Aug 14, 2023, 11:20 AM EDT) 015f8c2e 2d337f42

+90 -41
+10 -10
src/discord/builder.ts
··· 1 1 import {AppLogger} from "../common/logging.js"; 2 2 import {DigestData, DiscordOptions} from "../common/infrastructure/OperatorConfig.js"; 3 - import {TautulliRequestData, TautulliRequestFileData} from "../common/infrastructure/Atomic.js"; 3 + import {FileData, TautulliRequestData, TautulliRequestFileData} from "../common/infrastructure/Atomic.js"; 4 4 import {mergeArr} from "../utils/index.js"; 5 5 import {APIEmbed, AttachmentBuilder, BaseMessageOptions} from "discord.js"; 6 6 import {ErrorWithCause} from "pony-cause"; ··· 23 23 } = digest; 24 24 25 25 let currEmbeds: (APIEmbed)[] = []; 26 - let currImages: [Buffer, string][] = []; 26 + let currImages: FileData[] = []; 27 27 28 28 for (const req of pending) { 29 29 const requestEmbeds = req.content.embeds as APIEmbed[]; ··· 33 33 if (embed.image !== undefined) { 34 34 const imageData = allImages.filter(x => x.tautulliRequestId === req.id && embed.image.url.includes(x.filename)); 35 35 for (const i of imageData) { 36 - if (!currImages.some(([b, n]) => n === i.filename)) { 37 - currImages.push([i.content, i.filename]); 36 + if (!currImages.some((ci) => ci.filename === i.filename)) { 37 + currImages.push(i); 38 38 } 39 39 } 40 40 } 41 41 if (embed.thumbnail !== undefined) { 42 42 const imageData = allImages.filter(x => x.tautulliRequestId === req.id && embed.thumbnail.url.includes(x.filename)); 43 43 for (const i of imageData) { 44 - if (!currImages.some(([b, n]) => n === i.filename)) { 45 - currImages.push([i.content, i.filename]); 44 + if (!currImages.some((ci) => ci.filename === i.filename)) { 45 + currImages.push(i); 46 46 } 47 47 } 48 48 } ··· 66 66 return [messages, events]; 67 67 } 68 68 69 - export const buildMessage = (options: DiscordOptions, currEmbeds: (APIEmbed)[], currImages: [Buffer, string][]): BaseMessageOptions => { 69 + export const buildMessage = (options: DiscordOptions = {}, currEmbeds: (APIEmbed)[], currImages: FileData[]): BaseMessageOptions => { 70 70 71 71 const { 72 72 defaultImageFormat = 'image', ··· 89 89 } 90 90 91 91 // remove duplicate images 92 - const dedupedImages = currImages.reduce((acc, curr) => { 93 - if (!acc.some(x => x[1] === curr[1])) { 92 + const dedupedImages = currImages.reduce((acc: FileData[], curr) => { 93 + if (!acc.some(x => x.filename === curr.filename)) { 94 94 return acc.concat(curr); 95 95 } 96 96 }, []); 97 97 const attachments: AttachmentBuilder[] = []; 98 98 for (const f of dedupedImages) { 99 - const file = new AttachmentBuilder(f[0], {name: f[1]}); 99 + const file = new AttachmentBuilder(f.content, {name: f.filename}); 100 100 attachments.push(file); 101 101 } 102 102
+15
src/utils/index.ts
··· 112 112 Array.from({length: Math.ceil(arr.length / size)}, (v, i) => 113 113 arr.slice(i * size, i * size + size) 114 114 ); 115 + 116 + export const randomNumber = (max: number = 100) => { 117 + return Math.floor(Math.random() * max); 118 + } 119 + 120 + export const uniqueRandomNumber = (max: number = 100) => { 121 + const existing: number[] = []; 122 + return () => { 123 + const num = randomNumber(max); 124 + if (!existing.includes(num)) { 125 + existing.push(num); 126 + return num; 127 + } 128 + } 129 + }
-18
tests/collapse.test.ts
··· 1 - import {describe, it} from 'mocha'; 2 - import {assert} from 'chai'; 3 - import {OperatorConfig} from "../src/common/infrastructure/OperatorConfig"; 4 - import dotenv from 'dotenv'; 5 - import {getDummyEvent} from "./testFactory"; 6 - 7 - describe('Collapse Behavior', function () { 8 - let config: OperatorConfig; 9 - 10 - before(async () => { 11 - dotenv.config({path: '../'}); 12 - }); 13 - 14 - it('Generates embed with image', async function () { 15 - const [msg] = await getDummyEvent(); 16 - assert.isTrue(true, 'is true'); 17 - }); 18 - });
+33
tests/embed.test.ts
··· 1 + import {describe, it} from 'mocha'; 2 + import {assert} from 'chai'; 3 + import {OperatorConfig} from "../src/common/infrastructure/OperatorConfig"; 4 + import dotenv from 'dotenv'; 5 + import {defaultTestDigest, getDummyEvent, getDummyRequest} from "./testFactory"; 6 + import {buildMessages} from "../src/discord/builder"; 7 + import {uniqueRandomNumber} from "../src/utils"; 8 + 9 + describe('Basic Message Building', function () { 10 + let config: OperatorConfig; 11 + 12 + before(async () => { 13 + dotenv.config({path: '../'}); 14 + }); 15 + 16 + it('Generates embed with image url', async function () { 17 + const rand = uniqueRandomNumber(); 18 + const [event, image] = await getDummyRequest({id: rand()}); 19 + const images = image !== undefined ? [image] : []; 20 + 21 + const [messages, eventCount] = buildMessages(defaultTestDigest, [event], images); 22 + assert.isTrue(true, 'is true'); 23 + }); 24 + 25 + it('Generates embed with image file', async function () { 26 + const rand = uniqueRandomNumber(); 27 + const [event, image] = await getDummyRequest({id: rand(), image: 'file'}); 28 + const images = image !== undefined ? [image] : []; 29 + 30 + const [messages, eventCount] = buildMessages(defaultTestDigest, [event], images); 31 + assert.isTrue(true, 'is true'); 32 + }); 33 + });
+32 -13
tests/testFactory.ts
··· 1 - import {OperatorConfig} from "../src/common/infrastructure/OperatorConfig.js"; 1 + import {DigestData, OperatorConfig} from "../src/common/infrastructure/OperatorConfig.js"; 2 2 import dayjs, {Dayjs} from "dayjs"; 3 3 import {LoremIpsum} from "lorem-ipsum"; 4 4 import {promises} from "fs"; ··· 6 6 import {pickRandom, readFile} from "../src/utils/io.js"; 7 7 import {APIEmbedImage, BaseMessageOptions} from "discord.js"; 8 8 import crypto from 'crypto'; 9 - import {FileData} from "../src/common/infrastructure/Atomic.js"; 9 + import {FileData, TautulliRequestData, TautulliRequestFileData} from "../src/common/infrastructure/Atomic.js"; 10 + import {randomNumber} from "../src/utils/index.js"; 11 + import {projectDir} from "../src/common/index.js"; 10 12 13 + const imageDir = path.join(projectDir, 'tests/assets/images'); 11 14 let posterFiles: string[] = []; 12 15 13 16 const posterColors = [ ··· 35 38 } 36 39 }); 37 40 38 - const memoryConfig: OperatorConfig = { 41 + export const defaultTestDigest: DigestData = { 42 + slug: 'test', 43 + // every day at 12:00 pm 44 + cron: '0 12 * * *', 45 + discord: { 46 + webhook: process.env.DISCORD_WEBHOOK ?? 'MY_WEBHOOK', 47 + } 48 + } 49 + 50 + 51 + export const memoryConfig: OperatorConfig = { 39 52 digests: [ 40 - { 41 - slug: 'test', 42 - // every day at 12:00 pm 43 - cron: '0 12 * * *', 44 - discord: { 45 - webhook: process.env.DISCORD_WEBHOOK ?? 'MY_WEBHOOK', 46 - } 47 - } 53 + defaultTestDigest 48 54 ] 49 55 } 50 56 ··· 54 60 thumbnail?: 'url' | 'file' 55 61 title?: string 56 62 content?: string 63 + } 64 + 65 + export const getDummyRequest = async (opts: DummyEventOptions & { 66 + id?: number 67 + } = {}): Promise<[TautulliRequestData, TautulliRequestFileData?]> => { 68 + const [requestData, imageData] = await getDummyEvent(opts); 69 + const id = opts.id ?? randomNumber(); 70 + return [ 71 + { 72 + id, 73 + content: requestData 74 + }, imageData !== undefined ? {tautulliRequestId: id, ...imageData} : undefined 75 + ]; 57 76 } 58 77 59 78 export const getDummyEvent = async (opts: DummyEventOptions = {}): Promise<[BaseMessageOptions, FileData?]> => { ··· 113 132 114 133 const getRandomPosterFile = async () => { 115 134 if (posterFiles.length === 0) { 116 - posterFiles = await promises.readdir(path.join('../assets')); 135 + posterFiles = await promises.readdir(imageDir); 117 136 } 118 - return await readFile(pickRandom(posterFiles)); 137 + return await readFile(path.join(imageDir, pickRandom(posterFiles))); 119 138 } 120 139 121 140 export const getRandomPosterUrl = (text: string = dummyTitle.generateWords(2)) => {