From ca632864f8e0deea6b5f69e3f9fbe4839d33e071 Mon Sep 17 00:00:00 2001 From: Matt Isenhower Date: Sat, 31 Jan 2026 20:03:56 -0800 Subject: [PATCH] Add error handling and media validation to social media clients Wrap send() methods in try-catch with descriptive error messages and add early return when media is missing. Co-Authored-By: Claude Opus 4.5 --- app/social/clients/BlueskyClient.mjs | 68 +++++++++++++++------------ app/social/clients/MastodonClient.mjs | 62 ++++++++++++++---------- app/social/clients/ThreadsClient.mjs | 20 ++++++-- app/social/clients/TwitterClient.mjs | 34 +++++++++----- 4 files changed, 112 insertions(+), 72 deletions(-) diff --git a/app/social/clients/BlueskyClient.mjs b/app/social/clients/BlueskyClient.mjs index a0e88f4..f8946c0 100644 --- a/app/social/clients/BlueskyClient.mjs +++ b/app/social/clients/BlueskyClient.mjs @@ -31,40 +31,50 @@ export default class BlueskyClient extends Client } async send(status, generator) { - await this.login(); + if (!status.media?.length) { + console.error(`[${this.name}] No media provided for ${generator.key}`); + return; + } - // Upload images - let images = await Promise.all( - status.media.map(async m => { - // We have to convert the PNG to a JPG for Bluesky because of size limits - let jpeg = sharp(m.file).jpeg(); - let metadata = await jpeg.metadata(); - let buffer = await jpeg.toBuffer(); + try { + await this.login(); - let response = await this.#agent.uploadBlob(buffer, { encoding: 'image/jpeg' }); + // Upload images + let images = await Promise.all( + status.media.map(async m => { + // We have to convert the PNG to a JPG for Bluesky because of size limits + let jpeg = sharp(m.file).jpeg(); + let metadata = await jpeg.metadata(); + let buffer = await jpeg.toBuffer(); - return { - image: response.data.blob, - alt: m.altText || '', - aspectRatio: { width: metadata.width, height: metadata.height }, - }; - }), - ); + let response = await this.#agent.uploadBlob(buffer, { encoding: 'image/jpeg' }); - // Send status - const rt = new RichText({ - text: status.status, - }); + return { + image: response.data.blob, + alt: m.altText || '', + aspectRatio: { width: metadata.width, height: metadata.height }, + }; + }), + ); - await rt.detectFacets(this.#agent); + // Send status + const rt = new RichText({ + text: status.status, + }); - await this.#agent.post({ - text: rt.text, - facets: rt.facets, - embed: { - images, - $type: 'app.bsky.embed.images', - }, - }); + await rt.detectFacets(this.#agent); + + await this.#agent.post({ + text: rt.text, + facets: rt.facets, + embed: { + images, + $type: 'app.bsky.embed.images', + }, + }); + } catch (error) { + console.error(`[${this.name}] Failed to post ${generator.key}:`, error.message); + throw error; + } } } diff --git a/app/social/clients/MastodonClient.mjs b/app/social/clients/MastodonClient.mjs index e4c3101..cf6447f 100644 --- a/app/social/clients/MastodonClient.mjs +++ b/app/social/clients/MastodonClient.mjs @@ -23,35 +23,45 @@ export default class MastodonClient extends Client } async send(status, generator) { - // Mastodon API - const masto = await createRestAPIClient({ - url: this.#url, - accessToken: this.#accessToken, - disableVersionCheck: true, - disableExperimentalWarning: true, - }); + if (!status.media?.length) { + console.error(`[${this.name}] No media provided for ${generator.key}`); + return; + } - // Upload images - let mediaIds = await Promise.all( - status.media.map(async m => { - let request = { file: new Blob([m.file]) }; - if (m.altText) { - request.description = m.altText; - } + try { + // Mastodon API + const masto = await createRestAPIClient({ + url: this.#url, + accessToken: this.#accessToken, + disableVersionCheck: true, + disableExperimentalWarning: true, + }); - let attachment = await masto.v2.media.create(request); + // Upload images + let mediaIds = await Promise.all( + status.media.map(async m => { + let request = { file: new Blob([m.file]) }; + if (m.altText) { + request.description = m.altText; + } - return attachment.id; - }), - ); + let attachment = await masto.v2.media.create(request); - // Send status - await masto.v1.statuses.create({ - status: status.status, - spoilerText: status.contentWrapper, - sensitive: !!status.contentWrapper, // Without the sensitive property the image is still visible - mediaIds, - visibility: this.#visibility, - }); + return attachment.id; + }), + ); + + // Send status + await masto.v1.statuses.create({ + status: status.status, + spoilerText: status.contentWrapper, + sensitive: !!status.contentWrapper, // Without the sensitive property the image is still visible + mediaIds, + visibility: this.#visibility, + }); + } catch (error) { + console.error(`[${this.name}] Failed to post ${generator.key}:`, error.message); + throw error; + } } } diff --git a/app/social/clients/ThreadsClient.mjs b/app/social/clients/ThreadsClient.mjs index 2870a08..f8af2f8 100644 --- a/app/social/clients/ThreadsClient.mjs +++ b/app/social/clients/ThreadsClient.mjs @@ -29,11 +29,21 @@ export default class ThreadsClient extends Client { } async send(status, generator) { - let jpeg = await sharp(status.media[0].file).jpeg().toBuffer(); + if (!status.media?.length) { + console.error(`[${this.name}] No media provided for ${generator.key}`); + return; + } - await this.#api.publish({ - text: status.status, - image: { type: 'image/jpeg', data: jpeg }, - }); + try { + let jpeg = await sharp(status.media[0].file).jpeg().toBuffer(); + + await this.#api.publish({ + text: status.status, + image: { type: 'image/jpeg', data: jpeg }, + }); + } catch (error) { + console.error(`[${this.name}] Failed to post ${generator.key}:`, error.message); + throw error; + } } } diff --git a/app/social/clients/TwitterClient.mjs b/app/social/clients/TwitterClient.mjs index 2ca54e9..cd5c432 100644 --- a/app/social/clients/TwitterClient.mjs +++ b/app/social/clients/TwitterClient.mjs @@ -30,20 +30,30 @@ export default class TwitterClient extends Client } async send(status, generator) { - // Upload images - let mediaIds = await Promise.all( - status.media.map(async m => { - let id = await this.api().v1.uploadMedia(Buffer.from(m.file), { mimeType: m.type }); + if (!status.media?.length) { + console.error(`[${this.name}] No media provided for ${generator.key}`); + return; + } - if (m.altText) { - await this.api().v1.createMediaMetadata(id, { alt_text: { text: m.altText } }); - } + try { + // Upload images + let mediaIds = await Promise.all( + status.media.map(async m => { + let id = await this.api().v1.uploadMedia(Buffer.from(m.file), { mimeType: m.type }); - return id; - }), - ); + if (m.altText) { + await this.api().v1.createMediaMetadata(id, { alt_text: { text: m.altText } }); + } - // Send status - await this.api().v2.tweet(status.status, { media: { media_ids: mediaIds } }); + return id; + }), + ); + + // Send status + await this.api().v2.tweet(status.status, { media: { media_ids: mediaIds } }); + } catch (error) { + console.error(`[${this.name}] Failed to post ${generator.key}:`, error.message); + throw error; + } } }