mirror of
https://github.com/misenhower/splatoon2.ink.git
synced 2026-09-29 04:37:00 -05:00
Report social posting failures and retain successful post checkpoints
This commit is contained in:
@@ -6,6 +6,10 @@ console.info('Starting periodic tasks...');
|
||||
|
||||
// Run every hour at 10sec after the hour
|
||||
new CronJob('10 0 * * * *', async () => {
|
||||
await updateAllLocally();
|
||||
await postLocally();
|
||||
try {
|
||||
await updateAllLocally();
|
||||
await postLocally();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, null, true);
|
||||
|
||||
@@ -13,13 +13,17 @@ export function filesystemStorage() {
|
||||
}
|
||||
|
||||
export async function updateAllLocally() {
|
||||
await updateAll(filesystemStorage());
|
||||
|
||||
let updaters = await updateAll(filesystemStorage());
|
||||
if (updaters.some(updater => !updater.ok))
|
||||
throw new Error('One or more updaters failed; social posting skipped.');
|
||||
return 'Done';
|
||||
}
|
||||
|
||||
export function postLocally() {
|
||||
return sendStatuses(filesystemStorage());
|
||||
export async function postLocally() {
|
||||
let result = await sendStatuses(filesystemStorage());
|
||||
if (!result.ok)
|
||||
throw new Error('One or more social posts failed.');
|
||||
return result;
|
||||
}
|
||||
|
||||
export function testScreenshotsLocally() {
|
||||
|
||||
@@ -15,29 +15,27 @@ export default class SocialPostBase {
|
||||
}
|
||||
|
||||
async maybePost() {
|
||||
// Make sure we have data to post
|
||||
if (!await this.getData()) {
|
||||
this.info('No data to post');
|
||||
return false;
|
||||
try {
|
||||
// Read once: Salmon Run's getData also remembers the current shift.
|
||||
let data = await this.getData();
|
||||
if (!data)
|
||||
return false;
|
||||
|
||||
let due = [];
|
||||
for (let client of this.clients)
|
||||
if (await client.canSend() && await this.shouldPostForCurrentTime(client))
|
||||
due.push(client);
|
||||
|
||||
// Without credentials, retain the public-image-only shadow mode.
|
||||
let enabled = await this.canPost();
|
||||
if (!due.length && (enabled || !this.getPublicImageFilename()))
|
||||
return false;
|
||||
|
||||
return await this.post(data, due);
|
||||
} catch (error) {
|
||||
this.error(`Could not prepare post: ${error.message}`);
|
||||
return { ok: false, error: error.message, clients: [] };
|
||||
}
|
||||
|
||||
let due = [];
|
||||
for (let client of this.clients)
|
||||
if (await this.shouldPostForCurrentTime(client))
|
||||
due.push(client);
|
||||
|
||||
if (!due.length && this.clients.length) {
|
||||
this.info('Already posted for this time');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make sure we can post or save to a file
|
||||
if (!await this.canPost() && !this.getPublicImageFilename()) {
|
||||
this.error('Social API parameters not specified');
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.post();
|
||||
}
|
||||
|
||||
async canPost() {
|
||||
@@ -47,55 +45,34 @@ export default class SocialPostBase {
|
||||
return false;
|
||||
}
|
||||
|
||||
async post() {
|
||||
try {
|
||||
// Get the Post's text and image
|
||||
let data = await this.getData();
|
||||
let text = await this.getText(data);
|
||||
let image = await this.getMedia(data, 'image/png');
|
||||
async post(data, clients) {
|
||||
let results = [];
|
||||
let text = await this.getText(data);
|
||||
let time = await this.getDataTime();
|
||||
let image = await this.getMedia(data, 'image/png');
|
||||
await this.maybeSavePublicImage(data, image.file);
|
||||
let media = { 'image/png': image };
|
||||
|
||||
// Maybe save the image
|
||||
await this.maybeSavePublicImage(data, image.file);
|
||||
|
||||
// Some clients want a different format (Bluesky limits image size, so it gets JPEG)
|
||||
let media = { 'image/png': image };
|
||||
|
||||
for (let client of this.clients) {
|
||||
if (!await client.canSend()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!await this.shouldPostForCurrentTime(client)) {
|
||||
this.info(`Already posted to ${client.name}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
let mediaType = client.mediaType ?? 'image/png';
|
||||
media[mediaType] ??= await this.getMedia(data, mediaType);
|
||||
let status = {
|
||||
status: text,
|
||||
media: [media[mediaType]],
|
||||
};
|
||||
|
||||
await client.send(status);
|
||||
await this.updateLastPostTime(client);
|
||||
this.info(`Posted to ${client.name}`);
|
||||
} catch (e) {
|
||||
this.error(`Couldn't post to ${client.name}`);
|
||||
console.error(e);
|
||||
}
|
||||
for (let client of clients) {
|
||||
try {
|
||||
let mediaType = client.mediaType ?? 'image/png';
|
||||
media[mediaType] ??= await this.getMedia(data, mediaType);
|
||||
await client.send({ status: text, media: [media[mediaType]] });
|
||||
await this.updateLastPostTime(client, time);
|
||||
results.push({ client: client.key, ok: true });
|
||||
this.info(`Posted to ${client.name}`);
|
||||
} catch (error) {
|
||||
results.push({ client: client.key, ok: false, error: error.message });
|
||||
this.error(`Could not post to ${client.name}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
this.error('Couldn\'t post Post');
|
||||
console.error(e);
|
||||
}
|
||||
return { ok: results.every(result => result.ok), clients: results };
|
||||
}
|
||||
|
||||
async maybeSavePublicImage(data, image) {
|
||||
let filename = this.getPublicImageFilename();
|
||||
if (filename) {
|
||||
// Keep the established public URLs; removing X does not require breaking image links.
|
||||
await this.publicStorage.writeBytes(`twitter-images/${filename}`, image);
|
||||
this.info(`Saved public image as ${filename}`);
|
||||
}
|
||||
@@ -117,7 +94,7 @@ export default class SocialPostBase {
|
||||
}
|
||||
catch (e) {
|
||||
this.error('Couldn\'t save screenshot');
|
||||
console.error(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,9 +133,8 @@ export default class SocialPostBase {
|
||||
return (await this.getLastPostTimes(client))[key] || 0;
|
||||
}
|
||||
|
||||
async updateLastPostTime(client) {
|
||||
async updateLastPostTime(client, time) {
|
||||
let key = this.getKey();
|
||||
let time = await this.getDataTime();
|
||||
let lastPostTimes = await this.getLastPostTimes(client);
|
||||
|
||||
lastPostTimes[key] = time;
|
||||
|
||||
@@ -11,8 +11,12 @@ export function createClients() {
|
||||
* @param {object[]} [clients]
|
||||
*/
|
||||
export async function sendStatuses(storage, clients = createClients()) {
|
||||
for (let post of createPosts(storage, clients))
|
||||
await post.maybePost();
|
||||
let posts = [];
|
||||
for (let post of createPosts(storage, clients)) {
|
||||
let result = await post.maybePost();
|
||||
posts.push({ key: post.getKey(), ...(result || { ok: true, skipped: true }) });
|
||||
}
|
||||
return { ok: posts.every(post => post.ok), posts };
|
||||
}
|
||||
|
||||
export async function testScreenshots(storage, clients = createClients()) {
|
||||
|
||||
@@ -24,7 +24,7 @@ beforeEach(() => { s = storage(); bluesky = fakeClient('bluesky'); other = fakeC
|
||||
|
||||
test('posts to every client, saves the public image, and records the time per client', async () => {
|
||||
const post = new HourlyPost(s, [bluesky, other]);
|
||||
assert.equal(await post.maybePost(), undefined); // post ran
|
||||
assert.equal((await post.maybePost()).ok, true);
|
||||
|
||||
assert.deepEqual(bluesky.sent, [{ status: 'Post 1', media: [{ file: IMAGE, type: 'image/png' }] }]);
|
||||
assert.deepEqual(other.sent, bluesky.sent);
|
||||
@@ -56,7 +56,9 @@ test('a client that already posted is skipped while another still posts', async
|
||||
|
||||
test('a failing client does not record a post time and does not block the other client', async () => {
|
||||
const broken = fakeClient('bluesky', { fail: true });
|
||||
await new HourlyPost(s, [broken, other]).maybePost();
|
||||
const result = await new HourlyPost(s, [broken, other]).maybePost();
|
||||
assert.equal(result.ok, false);
|
||||
assert.deepEqual(result.clients.map(c => c.ok), [false, true]);
|
||||
assert.equal(other.sent.length, 1);
|
||||
assert.equal(await json(s.privateBucket, 'bluesky-lastPostTimes.json'), null);
|
||||
assert.deepEqual(await json(s.privateBucket, 'other-lastPostTimes.json'), { hourly: 3600 });
|
||||
@@ -70,7 +72,7 @@ test('renders once per media type a client needs, with size from the screenshot'
|
||||
const post = new ScreenshotPost(s, [jpegClient, other]);
|
||||
await post.maybePost();
|
||||
|
||||
assert.equal(post.images, 2); // one PNG (public copy + Other), one JPEG (Bluesky)
|
||||
assert.equal(post.images, 2); // one PNG public copy, one JPEG for Bluesky
|
||||
assert.deepEqual(jpegClient.sent[0].media, [{ file: new Uint8Array([2]), type: 'image/jpeg', width: 2432, height: 1368 }]);
|
||||
assert.deepEqual(other.sent[0].media, [{ file: new Uint8Array([1]), type: 'image/png', width: 2432, height: 1368 }]);
|
||||
assert.deepEqual(new Uint8Array(await (await s.publicBucket.get('twitter-images/hourly.png')).arrayBuffer()), new Uint8Array([1]));
|
||||
@@ -100,3 +102,24 @@ test('test screenshots go to public storage', async () => {
|
||||
await new HourlyPost(s, []).saveTestScreenshot();
|
||||
assert.deepEqual(new Uint8Array(await (await s.publicBucket.get('test-screenshots/hourly.png')).arrayBuffer()), IMAGE);
|
||||
});
|
||||
|
||||
test('screenshot failure is reported and does not advance post state', async () => {
|
||||
const post = new HourlyPost(s, [bluesky]);
|
||||
post.getImage = async () => { throw new Error('render failed'); };
|
||||
const result = await post.maybePost();
|
||||
assert.equal(result.ok, false);
|
||||
assert.match(result.error, /render failed/);
|
||||
assert.equal(await json(s.privateBucket, 'bluesky-lastPostTimes.json'), null);
|
||||
});
|
||||
|
||||
test('retry only sends the previously failed client, using a fresh storage view', async () => {
|
||||
const broken = fakeClient('other', { fail: true });
|
||||
const first = await new HourlyPost(s, [bluesky, broken]).maybePost();
|
||||
assert.equal(first.ok, false);
|
||||
const recovered = fakeClient('other');
|
||||
const fresh = { ...s, privateStorage: new s.privateStorage.constructor(s.privateBucket) };
|
||||
const second = await new HourlyPost(fresh, [bluesky, recovered]).maybePost();
|
||||
assert.equal(second.ok, true);
|
||||
assert.equal(bluesky.sent.length, 1);
|
||||
assert.equal(recovered.sent.length, 1);
|
||||
});
|
||||
|
||||
@@ -23,9 +23,9 @@ export async function runPosters(env) {
|
||||
if (await client.canSend())
|
||||
enabled.push(client.key);
|
||||
|
||||
await sendStatuses(bucketStorage(env), clients);
|
||||
let result = await sendStatuses(bucketStorage(env), clients);
|
||||
|
||||
let summary = { ok: true, ms: Date.now() - started, clients: enabled };
|
||||
log.info('Posters finished', summary);
|
||||
let summary = { ...result, ms: Date.now() - started, clients: enabled };
|
||||
log[summary.ok ? 'info' : 'error']('Posters finished', summary);
|
||||
return summary;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user