index.ts 6.2 KB

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