diff --git a/src/configuration/configuration.ts b/src/configuration/configuration.ts index 0989c2c..6ee5a20 100644 --- a/src/configuration/configuration.ts +++ b/src/configuration/configuration.ts @@ -5,6 +5,7 @@ import Counter from "@pm2/io/build/main/utils/metrics/counter.js"; import Gauge from "@pm2/io/build/main/utils/metrics/gauge.js"; import { Scraper } from "@the-convocation/twitter-scraper"; import { createRestAPIClient, mastodon } from "masto"; +import ora from "ora"; import { BLUESKY_IDENTIFIER, @@ -18,7 +19,9 @@ import { } from "../constants.js"; import { handleTwitterAuth } from "../helpers/auth/auth.js"; import { createCacheFile, getCache } from "../helpers/cache/index.js"; +import { runMigrations } from "../helpers/cache/run-migrations.js"; import { TouitomamoutError } from "../helpers/error.js"; +import { oraPrefixer } from "../helpers/logs/ora-prefixer.js"; import { buildConfigurationRules } from "./build-configuration-rules.js"; export const configuration = async (): Promise<{ @@ -45,6 +48,7 @@ export const configuration = async (): Promise<{ // Init configuration await createCacheFile(); + await runMigrations(); const synchronizedPostsCountThisRun = pm2.counter({ name: "Synced posts this run", @@ -69,22 +73,58 @@ export const configuration = async (): Promise<{ let mastodonClient = null; if (SYNC_MASTODON) { + const log = ora({ + color: "gray", + prefixText: oraPrefixer("🦣 client"), + }).start("connecting to mastodon..."); + mastodonClient = createRestAPIClient({ url: `https://${MASTODON_INSTANCE}`, accessToken: MASTODON_ACCESS_TOKEN, }); - console.log("🦣 client: ✔ connected"); + + await mastodonClient.v1.accounts + .verifyCredentials() + .then(() => log.succeed("connected")) + .catch(() => { + log.fail("authentication failure"); + throw new Error( + TouitomamoutError( + "Touitomamout was unable to connect to mastodon with the given credentials", + ["Please check your .env settings."], + ), + ); + }); } let blueskyClient = null; if (SYNC_BLUESKY) { + const log = ora({ + color: "gray", + prefixText: oraPrefixer("☁️ client"), + }).start("connecting to bluesky..."); + blueskyClient = new bsky.BskyAgent({ service: `https://${BLUESKY_INSTANCE}`, }); await blueskyClient - .login({ identifier: BLUESKY_IDENTIFIER, password: BLUESKY_PASSWORD }) + .login({ + identifier: BLUESKY_IDENTIFIER, + password: BLUESKY_PASSWORD, + }) + .then(() => { + log.succeed("connected"); + }) .catch(({ error }) => { + log.fail("authentication failure"); switch (error) { + case ResponseTypeNames[ResponseType.AuthRequired]: + throw new Error( + TouitomamoutError( + "Touitomamout was unable to connect to bluesky with the given credentials", + ["Please check your .env settings."], + ), + ); case ResponseTypeNames[ResponseType.XRPCNotSupported]: throw new Error( TouitomamoutError( @@ -103,10 +143,9 @@ export const configuration = async (): Promise<{ ), ); default: - console.log({ error }); + console.log(error); } }); - console.log("☁️ client: ✔ connected"); } return { diff --git a/src/helpers/auth/auth.ts b/src/helpers/auth/auth.ts index 50821d5..0a6504a 100644 --- a/src/helpers/auth/auth.ts +++ b/src/helpers/auth/auth.ts @@ -1,9 +1,11 @@ import { Scraper } from "@the-convocation/twitter-scraper"; +import ora from "ora"; import { TWITTER_PASSWORD, TWITTER_USERNAME } from "../../constants.js"; import { getCookies } from "../cookies/get-cookies.js"; import { saveCookies } from "../cookies/save-cookies.js"; import { TouitomamoutError } from "../error.js"; +import { oraPrefixer } from "../logs/ora-prefixer.js"; const restorePreviousSession = async (client: Scraper): Promise => { try { @@ -23,10 +25,13 @@ const restorePreviousSession = async (client: Scraper): Promise => { }; export const handleTwitterAuth = async (client: Scraper) => { + const log = ora({ + color: "gray", + prefixText: oraPrefixer("🦤 client"), + }).start("connecting to twitter..."); + if (!TWITTER_USERNAME || !TWITTER_PASSWORD) { - console.log( - "🦤 client: ✔ connected as guest | replies will not be synced", - ); + log.succeed("connected as guest | replies will not be synced"); return; } @@ -34,12 +39,13 @@ export const handleTwitterAuth = async (client: Scraper) => { await restorePreviousSession(client); if (await client.isLoggedIn()) { - console.log("🦤 client: ✔ connected (session restored)"); + log.succeed("connected (session restored)"); } else { // Handle restoration failure await client.login(TWITTER_USERNAME, TWITTER_PASSWORD); - console.log("🦤 client: ✔ connected (using credentials)"); + log.succeed("connected (using credentials)"); } + log.stop(); // Save session if (await client.isLoggedIn()) { diff --git a/src/helpers/cache/migrations/__tests__/cache-migration-0.0.spec.ts b/src/helpers/cache/migrations/__tests__/cache-migration-0.0.spec.ts new file mode 100644 index 0000000..469496d --- /dev/null +++ b/src/helpers/cache/migrations/__tests__/cache-migration-0.0.spec.ts @@ -0,0 +1,21 @@ +import { migration } from "../cache-migration-0.0.js"; + +describe("cache-migration-0.0", () => { + it("should migration initial cache", async () => { + const cache = {}; + const migrationResult = await migration(cache); + expect(migrationResult).toStrictEqual({ + version: "0.0", + }); + }); + + describe("when the cache has a version", () => { + it("should not run the migration", async () => { + const cache = { + version: "x.x", + }; + const migrationResult = await migration(cache); + expect(migrationResult).toStrictEqual(cache); + }); + }); +}); diff --git a/src/helpers/cache/migrations/cache-migration-0.0.ts b/src/helpers/cache/migrations/cache-migration-0.0.ts new file mode 100644 index 0000000..4c833e8 --- /dev/null +++ b/src/helpers/cache/migrations/cache-migration-0.0.ts @@ -0,0 +1,9 @@ +export const migration = async (outdatedCache: NonNullable) => { + if (Object.hasOwn(outdatedCache, "version")) { + return outdatedCache; + } + return { + ...outdatedCache, + version: "0.0", + }; +}; diff --git a/src/helpers/cache/migrations/index.ts b/src/helpers/cache/migrations/index.ts new file mode 100644 index 0000000..73d1db6 --- /dev/null +++ b/src/helpers/cache/migrations/index.ts @@ -0,0 +1,3 @@ +import { migration as cacheMigration_0_0 } from "./cache-migration-0.0.js"; + +export default [cacheMigration_0_0]; diff --git a/src/helpers/cache/run-migrations.ts b/src/helpers/cache/run-migrations.ts new file mode 100644 index 0000000..85b7831 --- /dev/null +++ b/src/helpers/cache/run-migrations.ts @@ -0,0 +1,37 @@ +import fs from "fs"; +import ora from "ora"; + +import { CACHE_PATH } from "../../constants.js"; +import { oraPrefixer } from "../logs/ora-prefixer.js"; +import { oraProgress } from "../logs/ora-progress.js"; +import { getCompleteCache } from "./get-cache.js"; +import migrations from "./migrations/index.js"; + +export const runMigrations = async () => { + const log = ora({ + color: "gray", + prefixText: oraPrefixer("⚙️ cache"), + }).start(); + oraProgress(log, `running migrations`, 0, migrations.length); + + let migrationCounter = 0; + for (const migration of migrations) { + const outdatedCache = await getCompleteCache(); + await migration(outdatedCache) + .then(async (updatedCache) => { + try { + await fs.promises.writeFile(CACHE_PATH, JSON.stringify(updatedCache)); + } catch (err) { + log.fail("Error updating cache file:" + err); + } + }) + .catch((err) => { + throw new Error(`Error running migration ${migration.name}: ${err}`); + }); + + migrationCounter++; + oraProgress(log, `running migrations`, migrationCounter, migrations.length); + } + + log.succeed("task finished"); +}; diff --git a/src/helpers/logs/ora-progress.ts b/src/helpers/logs/ora-progress.ts new file mode 100644 index 0000000..d870df6 --- /dev/null +++ b/src/helpers/logs/ora-progress.ts @@ -0,0 +1,17 @@ +import { Ora } from "ora"; + +const SEGMENT_DONE = "█"; +const SEGMENT_UNDONE = "░"; +export const oraProgress = ( + ora: Ora, + text: string, + index: number, + maximum: number, +) => { + const progress = Math.round((index / maximum) * 100); + const segments = Math.round(progress / 5); + const bar = `${SEGMENT_DONE.repeat(segments)}${SEGMENT_UNDONE.repeat( + 20 - segments, + )}`; + ora.text = `${bar} ${progress}% ${text}`; +};