import { type Handler } from "aws-lambda"; import { createRestAPIClient, mastodon } from "masto"; import { CronJob } from "cron"; import Scraper from "../../utils/scraper"; import ScraperMethods from "../../enums/scraper-methods"; import Emojis from "../../enums/emojis"; import LogLevels from "../../enums/log-levels"; import config from "../../config"; interface IStreamConfig { location: string; url: string; hashtags: string[]; screenshotSelector?: string; clickSelector?: string; screenshotDelay?: number; } export default class VideoStreams { private readonly _name: string = "Video Streams"; private readonly _mastodonClient: mastodon.rest.Client; private readonly _scraper: Scraper; constructor(accessToken = config.MASTODON_TEST_ACCESS_TOKEN) { accessToken = config.DEVELOP ? config.MASTODON_TEST_ACCESS_TOKEN : accessToken; this._mastodonClient = createRestAPIClient({ url: config.MASTODON_URL, accessToken }); this._scraper = new Scraper(); } private parseStreams(): IStreamConfig[] { const streamsString = config.VIDEO_STREAMS; if (!streamsString || streamsString.trim() === "") { return []; } try { const parsed = JSON.parse(streamsString); if (!Array.isArray(parsed)) { throw new Error("VIDEO_STREAMS must be a JSON array"); } return parsed.map((item: any) => ({ location: String(item.location ?? "").trim(), url: String(item.url ?? "").trim(), hashtags: Array.isArray(item.hashtags) ? item.hashtags.map((h: unknown) => String(h).trim()).filter((h: string) => h !== "") : [], screenshotSelector: item.screenshotSelector !== undefined ? String(item.screenshotSelector).trim() : undefined, clickSelector: item.clickSelector !== undefined ? String(item.clickSelector).trim() : undefined, screenshotDelay: item.screenshotDelay !== undefined ? Number(item.screenshotDelay) : undefined })).filter((stream) => stream.location !== "" && stream.url !== ""); } catch (err: any) { console.error(`${this._name} | Error parsing VIDEO_STREAMS: ${err.message}`); throw err; } } private base64ToFile(base64: string, fileName: string): File { const buffer = Buffer.from(base64, "base64"); return new File([buffer], fileName, { type: "image/png" }); } private buildMessage(stream: IStreamConfig): string { let message = `${Emojis.VIDEO_CAMERA} Transmisión en vivo desde ${stream.location}`; message += `\n\n${Emojis.LINK} ${stream.url}`; if (stream.hashtags.length > 0) { message += `\n\n${stream.hashtags.map((hashtag) => `#${hashtag}`).join(" ")}`; } return message; } private async publish(stream: IStreamConfig, screenshotBase64: string): Promise { try { const media = await this._mastodonClient.v2.media.create({ file: this.base64ToFile(screenshotBase64, `${stream.location.replace(/\s+/g, "_")}.png`), description: `Transmisión en vivo desde ${stream.location}` }); const message = this.buildMessage(stream); console.log(`\n${this._name} | Sending\n`, message); await this._mastodonClient.v1.statuses.create({ status: message, mediaIds: [media.id] }); } catch (err: any) { console.error(`${this._name} | Error publishing stream ${stream.location}`); console.error(err.message); } } public async run(event?: any, context?: any): Promise { try { const streams = this.parseStreams(); if (streams.length === 0) { console.log(`${this._name} | No streams configured`); return; } console.log(`${this._name} | Found ${streams.length} stream(s)`); for (const stream of streams) { console.log(`${this._name} | Capturing ${stream.location}`); const scrapeOptions: any = { url: stream.url, scraperMethod: ScraperMethods.PUPPETEER, screenshotDelay: stream.screenshotDelay ?? config.VIDEO_STREAMS_SCREENSHOT_DELAY }; if (stream.screenshotSelector !== undefined && stream.screenshotSelector !== "") { scrapeOptions.screenshot = true; scrapeOptions.screenshotSelector = stream.screenshotSelector; } if (stream.clickSelector !== undefined && stream.clickSelector !== "") { scrapeOptions.clickSelector = stream.clickSelector; } const response = await this._scraper.scrape(scrapeOptions); const screenshotBase64 = response?.data?.data?.screenshot; if (!screenshotBase64 || typeof screenshotBase64 !== "string") { console.log(`${this._name} | No screenshot obtained for ${stream.location}`); continue; } await this.publish(stream, screenshotBase64); } console.log(`${this._name} | Finished`); } catch (err: any) { console.log(`${this._name} | An error has occurred\n`); console.error(err.message); if (config.LOG_LEVEL === LogLevels.DEBUG) { if (event !== undefined) { console.debug("\nEvent\n"); console.debug(event); } if (context !== undefined) { console.debug("\nContext\n"); console.debug(context); } } } } public getHandler(): Handler { return async (event, context) => { await this.run(event, context); }; } } try { const videoStreams = new VideoStreams(config.DEVELOP ? config.MASTODON_TEST_ACCESS_TOKEN : config.MASTODON_KEY_VIDEO_STREAMS); new CronJob( "0 0 * * * *", () => videoStreams.run(), null, true, config.DEFAULT_TIMEZONE ); if (config.DEVELOP) { videoStreams.run(); } } catch (error) { console.error(error); }