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 <noreply@anthropic.com>
This commit is contained in:
Matt Isenhower 2026-01-31 20:03:56 -08:00
parent c1a290e7bc
commit ca632864f8
4 changed files with 112 additions and 72 deletions

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}