index.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. import { type Handler } from "aws-lambda";
  2. import { createRestAPIClient, mastodon } from "masto";
  3. import { CronJob } from "cron";
  4. import Scraper from "../../utils/scraper";
  5. import ScraperMethods from "../../enums/scraper-methods";
  6. import Emojis from "../../enums/emojis";
  7. import LogLevels from "../../enums/log-levels";
  8. import config from "../../config";
  9. interface IStreamConfig {
  10. location: string;
  11. url: string;
  12. hashtags: string[];
  13. screenshotSelector?: string;
  14. clickSelector?: string;
  15. screenshotDelay?: number;
  16. }
  17. export default class VideoStreams {
  18. private readonly _name: string = "Video Streams";
  19. private readonly _mastodonClient: mastodon.rest.Client;
  20. private readonly _scraper: Scraper;
  21. constructor(accessToken = config.MASTODON_TEST_ACCESS_TOKEN) {
  22. accessToken = config.DEVELOP ? config.MASTODON_TEST_ACCESS_TOKEN : accessToken;
  23. this._mastodonClient = createRestAPIClient({ url: config.MASTODON_URL, accessToken });
  24. this._scraper = new Scraper();
  25. }
  26. private parseStreams(): IStreamConfig[] {
  27. const streamsString = config.VIDEO_STREAMS;
  28. if (!streamsString || streamsString.trim() === "") {
  29. return [];
  30. }
  31. try {
  32. const parsed = JSON.parse(streamsString);
  33. if (!Array.isArray(parsed)) {
  34. throw new Error("VIDEO_STREAMS must be a JSON array");
  35. }
  36. return parsed.map((item: any) => ({
  37. location: String(item.location ?? "").trim(),
  38. url: String(item.url ?? "").trim(),
  39. hashtags: Array.isArray(item.hashtags)
  40. ? item.hashtags.map((h: unknown) => String(h).trim()).filter((h: string) => h !== "")
  41. : [],
  42. screenshotSelector: item.screenshotSelector !== undefined ? String(item.screenshotSelector).trim() : undefined,
  43. clickSelector: item.clickSelector !== undefined ? String(item.clickSelector).trim() : undefined,
  44. screenshotDelay: item.screenshotDelay !== undefined ? Number(item.screenshotDelay) : undefined
  45. })).filter((stream) => stream.location !== "" && stream.url !== "");
  46. } catch (err: any) {
  47. console.error(`${this._name} | Error parsing VIDEO_STREAMS: ${err.message}`);
  48. throw err;
  49. }
  50. }
  51. private base64ToFile(base64: string, fileName: string): File {
  52. const buffer = Buffer.from(base64, "base64");
  53. return new File([buffer], fileName, { type: "image/png" });
  54. }
  55. private buildMessage(stream: IStreamConfig): string {
  56. let message = `${Emojis.VIDEO_CAMERA} Transmisión en vivo desde ${stream.location}`;
  57. message += `\n\n${Emojis.LINK} ${stream.url}`;
  58. if (stream.hashtags.length > 0) {
  59. message += `\n\n${stream.hashtags.map((hashtag) => `#${hashtag}`).join(" ")}`;
  60. }
  61. return message;
  62. }
  63. private async publish(stream: IStreamConfig, screenshotBase64: string): Promise<void> {
  64. try {
  65. const media = await this._mastodonClient.v2.media.create({
  66. file: this.base64ToFile(screenshotBase64, `${stream.location.replace(/\s+/g, "_")}.png`),
  67. description: `Transmisión en vivo desde ${stream.location}`
  68. });
  69. const message = this.buildMessage(stream);
  70. console.log(`\n${this._name} | Sending\n`, message);
  71. await this._mastodonClient.v1.statuses.create({ status: message, mediaIds: [media.id] });
  72. } catch (err: any) {
  73. console.error(`${this._name} | Error publishing stream ${stream.location}`);
  74. console.error(err.message);
  75. }
  76. }
  77. public async run(event?: any, context?: any): Promise<void> {
  78. try {
  79. const streams = this.parseStreams();
  80. if (streams.length === 0) {
  81. console.log(`${this._name} | No streams configured`);
  82. return;
  83. }
  84. console.log(`${this._name} | Found ${streams.length} stream(s)`);
  85. for (const stream of streams) {
  86. console.log(`${this._name} | Capturing ${stream.location}`);
  87. const scrapeOptions: any = {
  88. url: stream.url,
  89. scraperMethod: ScraperMethods.PUPPETEER,
  90. screenshotDelay: stream.screenshotDelay ?? config.VIDEO_STREAMS_SCREENSHOT_DELAY
  91. };
  92. if (stream.screenshotSelector !== undefined && stream.screenshotSelector !== "") {
  93. scrapeOptions.screenshot = true;
  94. scrapeOptions.screenshotSelector = stream.screenshotSelector;
  95. }
  96. if (stream.clickSelector !== undefined && stream.clickSelector !== "") {
  97. scrapeOptions.clickSelector = stream.clickSelector;
  98. }
  99. const response = await this._scraper.scrape(scrapeOptions);
  100. const screenshotBase64 = response?.data?.data?.screenshot;
  101. if (!screenshotBase64 || typeof screenshotBase64 !== "string") {
  102. console.log(`${this._name} | No screenshot obtained for ${stream.location}`);
  103. continue;
  104. }
  105. await this.publish(stream, screenshotBase64);
  106. }
  107. console.log(`${this._name} | Finished`);
  108. } catch (err: any) {
  109. console.log(`${this._name} | An error has occurred\n`);
  110. console.error(err.message);
  111. if (config.LOG_LEVEL === LogLevels.DEBUG) {
  112. if (event !== undefined) {
  113. console.debug("\nEvent\n");
  114. console.debug(event);
  115. }
  116. if (context !== undefined) {
  117. console.debug("\nContext\n");
  118. console.debug(context);
  119. }
  120. }
  121. }
  122. }
  123. public getHandler(): Handler {
  124. return async (event, context) => {
  125. await this.run(event, context);
  126. };
  127. }
  128. }
  129. try {
  130. const videoStreams = new VideoStreams(config.DEVELOP ? config.MASTODON_TEST_ACCESS_TOKEN : config.MASTODON_KEY_VIDEO_STREAMS);
  131. new CronJob(
  132. "0 0 * * * *",
  133. () => videoStreams.run(),
  134. null,
  135. true,
  136. config.DEFAULT_TIMEZONE
  137. );
  138. if (config.DEVELOP) {
  139. videoStreams.run();
  140. }
  141. } catch (error) {
  142. console.error(error);
  143. }