Przeglądaj źródła

added video-streams agent

Pablo 1 tydzień temu
rodzic
commit
46f529f259

+ 7 - 0
.env.example

@@ -78,10 +78,17 @@ MASTODON_KEY_REMINDME = ""
 MASTODON_KEY_ANNIVERSARIES = ""
 MASTODON_KEY_ANONYBOT = ""
 MASTODON_KEY_COUNTDOWN = ""
+MASTODON_KEY_VIDEO_STREAMS = ""
 
 # COUNTDOWN
 COUNTDOWN_EVENTS = ""
 
+# VIDEO STREAMS
+# Format: "Location|URL|hashtag1,hashtag2;Location2|URL2|hashtag3"
+VIDEO_STREAMS = ""
+VIDEO_STREAMS_SCREENSHOT_DELAY = 10000
+VIDEO_STREAMS_CLICK_SELECTOR = ".ytp-large-play-button"
+
 # OLLAMA
 OLLAMA_URL = "http://localhost:11434/api/generate"
 OLLAMA_MODEL = "llama3.2"

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "bot-noticias",
-  "version": "0.0.27",
+  "version": "0.0.28",
   "description": "Bot que busca noticias y las replica en mastodon.cl",
   "main": "dist/index.js",
   "scripts": {

+ 1 - 0
serverless.yml

@@ -24,3 +24,4 @@ functions:
   - ${file(./src/portales/resumen/definition.yml)}
   - ${file(./src/portales/theclinic/definition.yml)}
   - ${file(./src/agents/countdown/definition.yml)}
+  - ${file(./src/agents/video-streams/definition.yml)}

+ 4 - 0
src/agents/video-streams/definition.yml

@@ -0,0 +1,4 @@
+video-streams:
+  handler: ./src/agents/video-streams/index.handler
+  events:
+    - schedule: rate(1 hour)

+ 159 - 0
src/agents/video-streams/index.ts

@@ -0,0 +1,159 @@
+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[];
+}
+
+export default class VideoStreams {
+  private readonly _name: string = "Video Streams";
+  private readonly _mastodonClient: mastodon.rest.Client;
+  private readonly _scraper: Scraper;
+  private readonly _selector: string = "#movie_player";
+  private readonly _clickSelector: string = ".ytp-large-play-button";
+
+  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 [];
+    }
+
+    const streams: IStreamConfig[] = [];
+    const streamPairs = streamsString.split(";");
+
+    streamPairs.forEach((streamPair) => {
+      const [location, url, hashTags] = streamPair.split("|");
+      if (location && url) {
+        streams.push({
+          location: location.trim(),
+          url: url.trim(),
+          hashtags: hashTags?.split(",").map((h) => h.trim()).filter((h) => h !== "") ?? []
+        });
+      }
+    });
+
+    return streams;
+  }
+
+  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<void> {
+    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<void> {
+    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 response = await this._scraper.scrape({
+          url: stream.url,
+          scraperMethod: ScraperMethods.PUPPETEER,
+          screenshotSelector: this._selector,
+          screenshotDelay: config.VIDEO_STREAMS_SCREENSHOT_DELAY,
+          clickSelector: config.VIDEO_STREAMS_CLICK_SELECTOR
+        });
+
+        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);
+}

+ 4 - 0
src/config.ts

@@ -80,7 +80,11 @@ const config = {
   MASTODON_KEY_ANNIVERSARIES: process.env.MASTODON_KEY_ANNIVERSARIES ?? "",
   MASTODON_KEY_ANONYBOT: process.env.MASTODON_KEY_ANONYBOT ?? "",
   MASTODON_KEY_COUNTDOWN: process.env.MASTODON_KEY_COUNTDOWN ?? "",
+  MASTODON_KEY_VIDEO_STREAMS: process.env.MASTODON_KEY_VIDEO_STREAMS ?? "",
   COUNTDOWN_EVENTS: process.env.COUNTDOWN_EVENTS ?? "",
+  VIDEO_STREAMS: process.env.VIDEO_STREAMS ?? "",
+  VIDEO_STREAMS_SCREENSHOT_DELAY: Number(process.env.VIDEO_STREAMS_SCREENSHOT_DELAY ?? 10000),
+  VIDEO_STREAMS_CLICK_SELECTOR: process.env.VIDEO_STREAMS_CLICK_SELECTOR ?? ".ytp-large-play-button",
   // ANONYBOT Settings
   ANONYBOT_MAX_MESSAGE_LENGTH: process.env.ANONYBOT_MAX_MESSAGE_LENGTH ?? 500,
   ANONYBOT_RATE_LIMIT_HOURS: process.env.ANONYBOT_RATE_LIMIT_HOURS ?? 1,

+ 1 - 0
src/enums/emojis.ts

@@ -38,6 +38,7 @@ enum Emojis {
   TAGS = "🏷️",
   TADA = "🎊",
   UNICORN = "🦄",
+  VIDEO_CAMERA = "📹",
   WAVE = "🌊",
   WIZARD = "🧙🏼‍♂️💭",
 };

+ 3 - 0
src/interfaces/scraper-options.ts

@@ -5,4 +5,7 @@ export interface IScraperOptions {
   userAgent?: string
   incognito?: boolean
   scraperMethod?: ScraperMethods
+  screenshotSelector?: string
+  screenshotDelay?: number
+  clickSelector?: string
 }

+ 17 - 3
src/utils/scraper.ts

@@ -34,11 +34,25 @@ export default class Scraper {
 
     try {
       if (this._options.scraperMethod == ScraperMethods.PUPPETEER) {
-        response = await axios.post(config.PUPPETEER_URL, {
+        const payload: any = {
           url: this._options.url,
-          screenshot: config.LOG_LEVEL === LogLevels.DEBUG,
+          screenshot: config.LOG_LEVEL === LogLevels.DEBUG || this._options.screenshotSelector !== undefined,
           incognito: this._options.incognito ?? false
-        }, { headers });
+        };
+
+        if (this._options.screenshotSelector !== undefined) {
+          payload.screenshotSelector = this._options.screenshotSelector;
+        }
+
+        if (this._options.screenshotDelay !== undefined) {
+          payload.screenshotDelay = this._options.screenshotDelay;
+        }
+
+        if (this._options.clickSelector !== undefined) {
+          payload.clickSelector = this._options.clickSelector;
+        }
+
+        response = await axios.post(config.PUPPETEER_URL, payload, { headers });
       } else {
         response = await axios.get(this._options.url, { headers });
       }