diff --git a/app/screenshots/HttpServer.mjs b/app/screenshots/HttpServer.mjs index 54ece7a..2aabb26 100644 --- a/app/screenshots/HttpServer.mjs +++ b/app/screenshots/HttpServer.mjs @@ -4,29 +4,29 @@ import sirv from 'sirv'; export default class HttpServer { /** @member {http.Server} */ - #server = null; + _server = null; get port() { - return this.#server.address().port; + return this._server.address().port; } open() { return new Promise((resolve, reject) => { - if (this.#server) { + if (this._server) { return resolve(); } const handler = sirv('./dist'); - this.#server = http.createServer(handler); - this.#server.on('listening', () => resolve()); - this.#server.listen(); + this._server = http.createServer(handler); + this._server.on('listening', () => resolve()); + this._server.listen(); }); } async close() { - if (this.#server) { - await this.#server.close(); - this.#server = null; + if (this._server) { + await this._server.close(); + this._server = null; } } } diff --git a/app/screenshots/ScreenshotHelper.mjs b/app/screenshots/ScreenshotHelper.mjs index ba7e195..710ff2d 100644 --- a/app/screenshots/ScreenshotHelper.mjs +++ b/app/screenshots/ScreenshotHelper.mjs @@ -12,43 +12,43 @@ const defaultViewport = { export default class ScreenshotHelper { /** @type {HttpServer} */ - #httpServer = null; + _httpServer = null; /** @type {puppeteer.Browser} */ - #browser = null; + _browser = null; /** @type {puppeteer.Page} */ - #page = null; + _page = null; defaultParams = null; get isOpen() { - return !!this.#browser; + return !!this._browser; } /** @type {puppeteer.Page} */ get page() { - return this.#page; + return this._page; } async open() { await this.close(); // Start the HTTP server - this.#httpServer = new HttpServer; - await this.#httpServer.open(); + this._httpServer = new HttpServer; + await this._httpServer.open(); // Connect to Browserless - this.#browser = await puppeteer.connect({ + this._browser = await puppeteer.connect({ browserWSEndpoint: process.env.BROWSERLESS_ENDPOINT, }); // Create a new page and set the viewport - this.#page = await this.#browser.newPage(); + this._page = await this._browser.newPage(); await this.applyViewport(); } async applyViewport(viewport = {}) { - if (this.#page) { - await this.#page.setViewport({ + if (this._page) { + await this._page.setViewport({ ...defaultViewport, ...viewport, }); @@ -64,7 +64,7 @@ export default class ScreenshotHelper // Navigate to the URL let host = process.env.SCREENSHOT_HOST || 'localhost'; - let url = new URL(`http://${host}:${this.#httpServer.port}/screenshots/`); + let url = new URL(`http://${host}:${this._httpServer.port}/screenshots/`); url.hash = path; let params = { @@ -80,31 +80,31 @@ export default class ScreenshotHelper .join('&'); } - await this.#page.goto(url, { + await this._page.goto(url, { waitUntil: 'networkidle0', // Wait until the network is idle }); // Wait an additional 1000ms - await this.#page.waitForNetworkIdle({ idleTime: 1000 }); + await this._page.waitForNetworkIdle({ idleTime: 1000 }); // Take the screenshot - return await this.#page.screenshot(); + return await this._page.screenshot(); } async close() { - if (this.#httpServer) { - await this.#httpServer.close(); + if (this._httpServer) { + await this._httpServer.close(); } - this.#httpServer = null; + this._httpServer = null; - if (this.#page) { - await this.#page.close(); + if (this._page) { + await this._page.close(); } - this.#page = null; + this._page = null; - if (this.#browser) { - await this.#browser.close(); + if (this._browser) { + await this._browser.close(); } - this.#browser = null; + this._browser = null; } } diff --git a/app/social/StatusGeneratorManager.mjs b/app/social/StatusGeneratorManager.mjs index 3f418dc..2e6a0c4 100644 --- a/app/social/StatusGeneratorManager.mjs +++ b/app/social/StatusGeneratorManager.mjs @@ -23,16 +23,16 @@ export default class StatusGeneratorManager } async sendStatuses(force = false) { - let availableClients = await this.#getAvailableClients(); + let availableClients = await this._getAvailableClients(); // Create screenshots in parallel (via Browserless) - let statusPromises = this.#getStatuses(availableClients, force); + let statusPromises = this._getStatuses(availableClients, force); // Process each client in parallel (while maintaining post order) - await this.#sendStatusesToClients(statusPromises, availableClients); + await this._sendStatusesToClients(statusPromises, availableClients); } - async #getAvailableClients() { + async _getAvailableClients() { let clients = []; for (let client of this.clients) { @@ -47,11 +47,11 @@ export default class StatusGeneratorManager return clients; } - #getStatuses(availableClients, force) { - return this.generators.map(generator => this.#getStatus(availableClients, generator, force)); + _getStatuses(availableClients, force) { + return this.generators.map(generator => this._getStatus(availableClients, generator, force)); } - async #getStatus(availableClients, generator, force) { + async _getStatus(availableClients, generator, force) { let screenshotHelper = new ScreenshotHelper; try { let clients = []; @@ -82,23 +82,23 @@ export default class StatusGeneratorManager return null; } - #sendStatusesToClients(statusPromises, availableClients) { - return Promise.allSettled(availableClients.map(client => this.#sendStatusesToClient(statusPromises, client))); + _sendStatusesToClients(statusPromises, availableClients) { + return Promise.allSettled(availableClients.map(client => this._sendStatusesToClient(statusPromises, client))); } - async #sendStatusesToClient(statusPromises, client) { + async _sendStatusesToClient(statusPromises, client) { for (let promise of statusPromises) { let statusDetails = await promise; if (statusDetails && statusDetails.clients.includes(client)) { let { generator, status } = statusDetails; - await this.#sendToClient(generator, status, client); + await this._sendToClient(generator, status, client); } } } - async #sendToClient(generator, status, client) { + async _sendToClient(generator, status, client) { this.console(generator, client).info('Posting...'); try { await client.send(status, generator); diff --git a/app/social/clients/BlueskyClient.mjs b/app/social/clients/BlueskyClient.mjs index 8919d59..147680a 100644 --- a/app/social/clients/BlueskyClient.mjs +++ b/app/social/clients/BlueskyClient.mjs @@ -9,7 +9,7 @@ export default class BlueskyClient extends Client key = 'bluesky'; name = 'Bluesky'; - #agent; + _agent; async canSend() { return process.env.BLUESKY_SERVICE @@ -18,12 +18,12 @@ export default class BlueskyClient extends Client } async login() { - if (!this.#agent) { - this.#agent = new BskyAgent({ + if (!this._agent) { + this._agent = new BskyAgent({ service: process.env.BLUESKY_SERVICE, }); - await this.#agent.login({ + await this._agent.login({ identifier: process.env.BLUESKY_IDENTIFIER, password: process.env.BLUESKY_PASSWORD, }); @@ -34,9 +34,9 @@ export default class BlueskyClient extends Client await this.login(); let jpeg = await sharp(avatarBuffer).jpeg().toBuffer(); - let uploadResponse = await this.#agent.uploadBlob(jpeg, { encoding: 'image/jpeg' }); + let uploadResponse = await this._agent.uploadBlob(jpeg, { encoding: 'image/jpeg' }); - await this.#agent.upsertProfile((existing) => { + await this._agent.upsertProfile((existing) => { return { ...existing, avatar: uploadResponse.data.blob, @@ -62,7 +62,7 @@ export default class BlueskyClient extends Client let metadata = await jpeg.metadata(); let buffer = await jpeg.toBuffer(); - let response = await this.#agent.uploadBlob(buffer, { encoding: 'image/jpeg' }); + let response = await this._agent.uploadBlob(buffer, { encoding: 'image/jpeg' }); return { image: response.data.blob, @@ -77,9 +77,9 @@ export default class BlueskyClient extends Client text: status.status, }); - await rt.detectFacets(this.#agent); + await rt.detectFacets(this._agent); - await this.#agent.post({ + await this._agent.post({ text: rt.text, facets: rt.facets, embed: { diff --git a/app/social/clients/MastodonClient.mjs b/app/social/clients/MastodonClient.mjs index 2a770a2..db290e4 100644 --- a/app/social/clients/MastodonClient.mjs +++ b/app/social/clients/MastodonClient.mjs @@ -6,26 +6,26 @@ export default class MastodonClient extends Client key = 'mastodon'; name = 'Mastodon'; - #url; - #accessToken; - #visibility; + _url; + _accessToken; + _visibility; constructor() { super(); - this.#url = process.env.MASTODON_URL; - this.#accessToken = process.env.MASTODON_ACCESS_TOKEN; - this.#visibility = process.env.MASTODON_VISIBILITY || 'public'; + this._url = process.env.MASTODON_URL; + this._accessToken = process.env.MASTODON_ACCESS_TOKEN; + this._visibility = process.env.MASTODON_VISIBILITY || 'public'; } async canSend() { - return this.#url && this.#accessToken; + return this._url && this._accessToken; } async updateProfile(avatarBuffer, displayName) { const masto = await createRestAPIClient({ - url: this.#url, - accessToken: this.#accessToken, + url: this._url, + accessToken: this._accessToken, disableVersionCheck: true, disableExperimentalWarning: true, }); @@ -45,8 +45,8 @@ export default class MastodonClient extends Client try { // Mastodon API const masto = await createRestAPIClient({ - url: this.#url, - accessToken: this.#accessToken, + url: this._url, + accessToken: this._accessToken, disableVersionCheck: true, disableExperimentalWarning: true, }); @@ -71,7 +71,7 @@ export default class MastodonClient extends Client spoilerText: status.contentWrapper, sensitive: !!status.contentWrapper, // Without the sensitive property the image is still visible mediaIds, - visibility: this.#visibility, + visibility: this._visibility, }); } catch (error) { console.error(`[${this.name}] Failed to post ${generator.key}:`, error.message); diff --git a/app/social/clients/ThreadsClient.mjs b/app/social/clients/ThreadsClient.mjs index 90b9946..8a2cd7c 100644 --- a/app/social/clients/ThreadsClient.mjs +++ b/app/social/clients/ThreadsClient.mjs @@ -8,9 +8,9 @@ export default class ThreadsClient extends Client { key = 'threads'; name = 'Threads'; - #baseUrl = 'https://graph.threads.net/v1.0'; - #tokenCache = new ValueCache('threads.token'); - #accessToken; + _baseUrl = 'https://graph.threads.net/v1.0'; + _tokenCache = new ValueCache('threads.token'); + _accessToken; get console() { return this._console ??= prefixedConsole('Social', this.name); @@ -23,32 +23,32 @@ export default class ThreadsClient extends Client { && process.env.AWS_S3_ENDPOINT; } - async #getAccessToken() { - if (this.#accessToken) { - return this.#accessToken; + async _getAccessToken() { + if (this._accessToken) { + return this._accessToken; } // Try to use a previously refreshed token from cache - let cached = await this.#tokenCache.getData(); + let cached = await this._tokenCache.getData(); if (cached) { - this.#accessToken = cached; - return this.#accessToken; + this._accessToken = cached; + return this._accessToken; } // Fall back to the .env token - this.#accessToken = process.env.THREADS_ACCESS_TOKEN; - return this.#accessToken; + this._accessToken = process.env.THREADS_ACCESS_TOKEN; + return this._accessToken; } - async #refreshToken() { - let currentToken = await this.#getAccessToken(); + async _refreshToken() { + let currentToken = await this._getAccessToken(); let params = new URLSearchParams({ grant_type: 'th_refresh_token', access_token: currentToken, }); - let response = await fetch(`${this.#baseUrl}/refresh_access_token?${params}`); + let response = await fetch(`${this._baseUrl}/refresh_access_token?${params}`); let data = await response.json(); if (data.error) { @@ -56,11 +56,11 @@ export default class ThreadsClient extends Client { return; } - this.#accessToken = data.access_token; + this._accessToken = data.access_token; // Cache the token with its expiry let expires = new Date(Date.now() + data.expires_in * 1000); - await this.#tokenCache.setData(data.access_token, expires); + await this._tokenCache.setData(data.access_token, expires); this.console.log(`Token refreshed, expires ${expires.toISOString()}`); } @@ -73,30 +73,30 @@ export default class ThreadsClient extends Client { try { // Refresh the token if it hasn't been refreshed in the last 24 hours - let lastRefresh = await this.#tokenCache.getCachedAt(); + let lastRefresh = await this._tokenCache.getCachedAt(); let oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000); if (!lastRefresh || lastRefresh < oneDayAgo) { - await this.#refreshToken(); + await this._refreshToken(); } - let accessToken = await this.#getAccessToken(); + let accessToken = await this._getAccessToken(); let jpeg = await sharp(status.media[0].file).jpeg().toBuffer(); // Upload image to S3 so it's publicly accessible - let { imageUrl, s3Key } = await this.#uploadImage(jpeg, generator.key); + let { imageUrl, s3Key } = await this._uploadImage(jpeg, generator.key); try { // Create a media container - let containerId = await this.#createContainer(status.status, imageUrl, accessToken); + let containerId = await this._createContainer(status.status, imageUrl, accessToken); // Wait for the container to finish processing - await this.#waitForContainer(containerId, accessToken); + await this._waitForContainer(containerId, accessToken); // Publish the container - await this.#publish(containerId, accessToken); + await this._publish(containerId, accessToken); } finally { // Clean up the temporary image from S3 - await this.#deleteImage(s3Key); + await this._deleteImage(s3Key); } } catch (error) { this.console.error(`Failed to post ${generator.key}:`, error.message); @@ -104,7 +104,7 @@ export default class ThreadsClient extends Client { } } - async #uploadImage(buffer, key) { + async _uploadImage(buffer, key) { let s3 = new S3Client({ endpoint: process.env.AWS_S3_ENDPOINT, region: process.env.AWS_REGION, @@ -130,7 +130,7 @@ export default class ThreadsClient extends Client { return { imageUrl, s3Key }; } - async #deleteImage(s3Key) { + async _deleteImage(s3Key) { try { let s3 = new S3Client({ endpoint: process.env.AWS_S3_ENDPOINT, @@ -150,7 +150,7 @@ export default class ThreadsClient extends Client { } } - async #createContainer(text, imageUrl, accessToken) { + async _createContainer(text, imageUrl, accessToken) { let userId = process.env.THREADS_USER_ID; let params = new URLSearchParams({ media_type: 'IMAGE', @@ -159,7 +159,7 @@ export default class ThreadsClient extends Client { access_token: accessToken, }); - let response = await fetch(`${this.#baseUrl}/${userId}/threads`, { + let response = await fetch(`${this._baseUrl}/${userId}/threads`, { method: 'POST', body: params, }); @@ -177,7 +177,7 @@ export default class ThreadsClient extends Client { return data.id; } - async #waitForContainer(containerId, accessToken) { + async _waitForContainer(containerId, accessToken) { // Poll container status until it's ready (or timeout after 60s) let maxAttempts = 12; @@ -187,7 +187,7 @@ export default class ThreadsClient extends Client { access_token: accessToken, }); - let response = await fetch(`${this.#baseUrl}/${containerId}?${params}`); + let response = await fetch(`${this._baseUrl}/${containerId}?${params}`); let data = await response.json(); if (data.status === 'FINISHED') { @@ -205,14 +205,14 @@ export default class ThreadsClient extends Client { throw new Error('Container processing timed out'); } - async #publish(containerId, accessToken) { + async _publish(containerId, accessToken) { let userId = process.env.THREADS_USER_ID; let params = new URLSearchParams({ creation_id: containerId, access_token: accessToken, }); - let response = await fetch(`${this.#baseUrl}/${userId}/threads_publish`, { + let response = await fetch(`${this._baseUrl}/${userId}/threads_publish`, { method: 'POST', body: params, }); diff --git a/app/social/clients/TwitterClient.mjs b/app/social/clients/TwitterClient.mjs index cd5c432..a3544ff 100644 --- a/app/social/clients/TwitterClient.mjs +++ b/app/social/clients/TwitterClient.mjs @@ -7,7 +7,7 @@ export default class TwitterClient extends Client name = 'Twitter'; /** @member {TwitterApi} */ - #api; + _api; async canSend() { return process.env.TWITTER_CONSUMER_KEY @@ -17,8 +17,8 @@ export default class TwitterClient extends Client } api() { - if (!this.#api) { - this.#api = new TwitterApi({ + if (!this._api) { + this._api = new TwitterApi({ appKey: process.env.TWITTER_CONSUMER_KEY, appSecret: process.env.TWITTER_CONSUMER_SECRET, accessToken: process.env.TWITTER_ACCESS_TOKEN_KEY, @@ -26,7 +26,7 @@ export default class TwitterClient extends Client }); } - return this.#api; + return this._api; } async send(status, generator) {