Ver Fonte

added anniversary agent

Pablo Barrera Yaksic há 1 semana atrás
pai
commit
afe32d0e33

+ 5 - 0
.env.example

@@ -8,6 +8,10 @@ MASTODON_ACCESS_TOKEN = "<access-token>"
 IMG_PLACEHOLDER = "https://placehold.co/600x400"
 DEFAULT_TIMEZONE = "America/Santiago"
 DEFAULT_LOCALE = "es-CL"
+MASTODON_DB_HOST = "localhost"
+MASTODON_DB_USER = "mastodon"
+MASTODON_DB_PASSWORD = "password"
+MASTODON_DB_DATABASE = "mastodon"
 
 CHILECULTURA = "https://chilecultura.gob.cl/events/"
 CIPER = "https://www.ciperchile.cl/actualidad/"
@@ -64,6 +68,7 @@ MASTODON_KEY_THECLINIC = ""
 # AGENTS
 MASTODON_KEY_FORTUNE = ""
 MASTODON_KEY_REMINDME = ""
+MASTODON_KEY_ANNIVERSARIES = ""
 
 # Develop
 DEVELOP = true

+ 47 - 0
package-lock.json

@@ -11,9 +11,11 @@
         "axios": "^1.7.9",
         "cheerio": "^1.0.0-rc.12",
         "chrono-node": "^2.9.0",
+        "cron": "^4.4.0",
         "date-fns-tz": "^3.2.0",
         "dotenv": "^16.4.7",
         "masto": "^6.10.1",
+        "postgres": "^3.4.8",
         "redis": "^4.7.0"
       },
       "devDependencies": {
@@ -362,6 +364,12 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@types/luxon": {
+      "version": "3.7.1",
+      "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz",
+      "integrity": "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==",
+      "license": "MIT"
+    },
     "node_modules/@types/node": {
       "version": "22.19.1",
       "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz",
@@ -900,6 +908,23 @@
         "upper-case": "^2.0.2"
       }
     },
+    "node_modules/cron": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/cron/-/cron-4.4.0.tgz",
+      "integrity": "sha512-fkdfq+b+AHI4cKdhZlppHveI/mgz2qpiYxcm+t5E5TsxX7QrLS1VE0+7GENEk9z0EeGPcpSciGv6ez24duWhwQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/luxon": "~3.7.0",
+        "luxon": "~3.7.0"
+      },
+      "engines": {
+        "node": ">=18.x"
+      },
+      "funding": {
+        "type": "ko-fi",
+        "url": "https://ko-fi.com/intcreator"
+      }
+    },
     "node_modules/cross-spawn": {
       "version": "7.0.6",
       "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -1936,6 +1961,15 @@
         "tslib": "^2.0.3"
       }
     },
+    "node_modules/luxon": {
+      "version": "3.7.2",
+      "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz",
+      "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/masto": {
       "version": "6.10.4",
       "resolved": "https://registry.npmjs.org/masto/-/masto-6.10.4.tgz",
@@ -2206,6 +2240,19 @@
         "url": "https://github.com/sponsors/jonschlinkert"
       }
     },
+    "node_modules/postgres": {
+      "version": "3.4.8",
+      "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.8.tgz",
+      "integrity": "sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg==",
+      "license": "Unlicense",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "type": "individual",
+        "url": "https://github.com/sponsors/porsager"
+      }
+    },
     "node_modules/prelude-ls": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",

+ 2 - 0
package.json

@@ -27,9 +27,11 @@
     "axios": "^1.7.9",
     "cheerio": "^1.0.0-rc.12",
     "chrono-node": "^2.9.0",
+    "cron": "^4.4.0",
     "date-fns-tz": "^3.2.0",
     "dotenv": "^16.4.7",
     "masto": "^6.10.1",
+    "postgres": "^3.4.8",
     "redis": "^4.7.0"
   },
   "trustedDependencies": [

+ 112 - 0
src/agents/anniversaries/index.ts

@@ -0,0 +1,112 @@
+import { createStreamingAPIClient, createRestAPIClient, mastodon } from "masto";
+import { CronJob } from "cron";
+
+import PostgresClient from "../../libs/postgres-client";
+
+import config from "../../config";
+import Emojis from "../../enums/emojis";
+import LogLevels from "../../enums/log-levels";
+
+export default class Anniversaries {
+  private readonly _mastodonStreamingClient: mastodon.streaming.Client;
+  private readonly _mastodonRestClient: mastodon.rest.Client;
+  private readonly _postgresClient: PostgresClient;
+
+  constructor(accessToken = config.MASTODON_TEST_ACCESS_TOKEN) {
+    this._mastodonStreamingClient = createStreamingAPIClient({ streamingApiUrl: config.MASTODON_STREAMING_URL, accessToken });
+    this._mastodonRestClient = createRestAPIClient({ url: config.MASTODON_API_URL, accessToken });
+    this._postgresClient = new PostgresClient();
+  }
+
+  private async publish(status: string): Promise<void> {
+    let result: mastodon.v1.Status;
+
+    console.log("Sending status\n", status);
+    result = await this._mastodonRestClient.v1.statuses.create({ status });
+
+    if (config.LOG_LEVEL === LogLevels.DEBUG) {
+      console.log("Result", result);
+    }
+  }
+
+  public async start(): Promise<void> {
+    try {
+      let from = new Date();
+      let to = new Date();
+      to.setDate(from.getDate() + 1)
+
+      if (config.DEVELOP) {
+        from = new Date("2022-11-21");
+        to = new Date("2022-11-22");
+
+        const query = `
+        select a.username, a.display_name, u.created_at
+        from users u
+        join accounts a on u.account_id = a.id
+        where a.actor_type = 'Person'
+        and a.suspended_at is null
+        and u.created_at >= ${from}
+        and u.created_at < ${to}
+        order by created_at asc;
+        `;
+        console.log(query);
+      }
+
+      const anniversaries = await this._postgresClient.sql`
+      select a.username, a.display_name, u.created_at
+      from users u
+      join accounts a on u.account_id = a.id
+      where a.actor_type = 'Person'
+      and a.suspended_at is null
+      and u.created_at >= ${from}
+      and u.created_at < ${to}
+      order by created_at asc;
+      `;
+
+      if (config.LOG_LEVEL == LogLevels.DEBUG) {
+        console.debug(anniversaries);
+      }
+
+      if (anniversaries.length == 0) {
+        console.log("No hay aniversarios");
+        return;
+      }
+
+      let status = `¡Se encienden las velitas y arrancan los festejos! ${Emojis.CAKE}${Emojis.SPARKLES}\n`;
+      status += `Hoy en #MastodonCL ${Emojis.CL_FLAG} celebramos el aniversario de la llegada a nuestra instancia de:\n\n`;
+
+      anniversaries.forEach((user: any) => {
+        const createdAt = new Date(user.created_at);
+        const today = new Date();
+        const years = today.getFullYear() - createdAt.getFullYear();
+
+        status += `- ${Emojis.PERSON} ${user.display_name || user.username} (@${user.username}) quien cumple ${years} año${years > 1 ? "s" : ""} \n`;
+      });
+      status += `\nGracias por acompañarnos ${Emojis.HEART}${Emojis.HUG} en este viaje a través del #Fediverso ${Emojis.ROCKET}. Que sea un muy feliz #Aniversario y que se cumplan muchos más!`;
+
+      this.publish(status);
+
+    } catch (error) {
+      console.error(error);
+    } finally {
+      await this._postgresClient.disconnect();
+    }
+  }
+}
+
+try {
+  const anniversaries = new Anniversaries(config.DEVELOP ? config.MASTODON_TEST_ACCESS_TOKEN : config.MASTODON_KEY_ANNIVERSARIES);
+  new CronJob(
+    "0 0 12 * * *",
+    () => anniversaries.start(),
+    null,
+    true,
+    config.DEFAULT_TIMEZONE
+  );
+
+  if (config.LOG_LEVEL == LogLevels.DEBUG) {
+    anniversaries.start();
+  }
+} catch (error) {
+  console.error(error);
+}

+ 7 - 7
src/agents/remindme/index.ts

@@ -71,8 +71,8 @@ export default class Remindme {
 
               console.log("parsedtime", parsedTime[0].date().getTime());
 
-	      const localDate = parsedTime[0].date();
-	      const utcDate = fromZonedTime(localDate, config.DEFAULT_TIMEZONE);
+              const localDate = parsedTime[0].date();
+              const utcDate = fromZonedTime(localDate, config.DEFAULT_TIMEZONE);
 
               if (utcDate.getTime() <= Date.now()) {
                 throw new Error(`@${event.payload.account.acct} El tiempo ingresado ya pasó ${Emojis.HOURGLASS}`);
@@ -92,11 +92,11 @@ export default class Remindme {
 
               await this.scheduleReminder(utcDate.getTime(), payload);
 
-	      const formattedDate = formatInTimeZone(
-		utcDate,
-		config.DEFAULT_TIMEZONE,
-		"dd-MM-yyyy HH:mm"
-	      );
+              const formattedDate = formatInTimeZone(
+                utcDate,
+                config.DEFAULT_TIMEZONE,
+                "dd-MM-yyyy HH:mm"
+              );
               status = `@${event.payload.account.acct} \nRecordatorio establecido ${Emojis.CALENDAR}${Emojis.CHECK} \nTe avisaré el ${formattedDate}`;
 
             } catch (error: any) {

+ 5 - 0
src/config.ts

@@ -10,6 +10,10 @@ const config = {
   IMG_PLACEHOLDER: process.env.IMG_PLACEHOLDER ?? "https://placehold.co/600x400",
   DEFAULT_TIMEZONE: process.env.DEFAULT_TIMEZONE ?? "America/Santiago",
   DEFAULT_LOCALE: process.env.DEFAULT_LOCALE ?? "es-CL",
+  MASTODON_DB_HOST: process.env.MASTODON_DB_HOST,
+  MASTODON_DB_USER: process.env.MASTODON_DB_USER,
+  MASTODON_DB_PASSWORD: process.env.MASTODON_DB_PASSWORD,
+  MASTODON_DB_DATABASE: process.env.MASTODON_DB_DATABASE,
   // PORTALES
   CHILECULTURA: process.env.CHILECULTURA ?? "https://chilecultura.gob.cl/",
   CIPER: process.env.CIPER ?? "https://www.ciperchile.cl/actualidad/",
@@ -54,6 +58,7 @@ const config = {
   // AGENTS
   MASTODON_KEY_FORTUNE: process.env.MASTODON_KEY_FORTUNE ?? "",
   MASTODON_KEY_REMINDME: process.env.MASTODON_KEY_REMINDME ?? "",
+  MASTODON_KEY_ANNIVERSARIES: process.env.MASTODON_KEY_ANNIVERSARIES ?? "",
   // Develop
   DEVELOP: !(process.env.DEVELOP === "false"),
   DEV_ACTIVE_PORTALS: process.env.DEV_ACTIVE_PORTALS?.split(";") ?? [],

+ 6 - 0
src/enums/emojis.ts

@@ -1,7 +1,9 @@
 enum Emojis {
   ALARM_CLOCK = "⏰",
   BELL = "🔔",
+  CL_FLAG = "🇨🇱",
   CALENDAR = "📆",
+  CAKE = "🎂",
   CHECK = "✔️",
   CRISTAL_BALL = "🔮",
   DEPTH = "🕳️",
@@ -9,7 +11,9 @@ enum Emojis {
   FAIRY = "🧚",
   FORTUNE_COOKIE = "🥠",
   GENIE = "🧞‍♂️",
+  HEART = "❤️",
   HOURGLASS = "⏳",
+  HUG = "🫂",
   LINK = "🔗",
   LOCATION = "📍",
   MAGIC_SHELL = "🐚",
@@ -18,8 +22,10 @@ enum Emojis {
   MAGNITUDE = "🎚️",
   NEWS = "📰",
   PADLOCK = "🔓",
+  PERSON = "👤",
   PIN = "📍",
   RAINBOW = "🌈",
+  ROCKET = "🚀",
   SAD_FACE = "😞",
   SCROLL = "📜",
   SIREN = "🚨",

+ 40 - 0
src/libs/postgres-client.ts

@@ -0,0 +1,40 @@
+import postgres from "postgres";
+
+import config from "../config";
+import LogLevels from "../enums/log-levels";
+
+export default class PostgresClient {
+  private _client: any = null;
+
+  constructor() {
+    void this.connect().then(() => {
+      if (config.LOG_LEVEL === LogLevels.DEBUG) {
+        console.debug("Postgres connetion stablished");
+      }
+    });
+  }
+
+  private async connect(): Promise<void> {
+    this._client = postgres({
+      host: config.MASTODON_DB_HOST,
+      database: config.MASTODON_DB_DATABASE,
+      username: config.MASTODON_DB_USER,
+      password: config.MASTODON_DB_PASSWORD
+    });
+  }
+
+  public async disconnect(): Promise<void> {
+    if (this._client !== null) {
+      await this._client.end();
+      this._client = null;
+    }
+  }
+
+  get sql() {
+    if (this._client == null) {
+      throw new Error("Postgres not connected");
+    }
+
+    return this._client;
+  }
+}

+ 10 - 4
tsconfig.json

@@ -4,16 +4,22 @@
     "outDir": "dist",
     "target": "ES6",
     "sourceMap": false,
-    "module":"commonjs",
+    "module": "commonjs",
     "forceConsistentCasingInFileNames": true,
     "isolatedModules": true,
-    "types": ["node"],
+    "types": [
+      "node"
+    ],
     "strictNullChecks": true,
-    "skipLibCheck": true
+    "skipLibCheck": true,
+    "esModuleInterop": true
   },
   "include": [
     "./src/**/*",
     "./node_modules/@types/node/index.d.ts"
   ],
-  "exclude": ["node_modules", "**/*.test.ts"]
+  "exclude": [
+    "node_modules",
+    "**/*.test.ts"
+  ]
 }