mirror of
https://github.com/misenhower/splatoon3.ink.git
synced 2026-03-21 09:44:09 -05:00
Auto-update social media avatars
Some checks failed
Some checks failed
This commit is contained in:
parent
00f0ee4cf1
commit
c1664590b5
|
|
@ -3,6 +3,7 @@ import { update } from './data/index.mjs';
|
|||
import { warmCaches } from './splatnet/index.mjs';
|
||||
import { sendStatuses } from './social/index.mjs';
|
||||
import { archiveData } from './data/DataArchiver.mjs';
|
||||
import { updateAvatars } from './social/updateAvatars.mjs';
|
||||
|
||||
let updating = false;
|
||||
|
||||
|
|
@ -35,4 +36,6 @@ export default function() {
|
|||
new CronJob('20 * * * *', () => {
|
||||
return updateIfNotUpdating('all');
|
||||
}, null, true);
|
||||
|
||||
new CronJob('30 * * * *', updateAvatars, null, true);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import ThreadsClient from './social/clients/ThreadsClient.mjs';
|
|||
import { archiveData } from './data/DataArchiver.mjs';
|
||||
import { sentryInit } from './common/sentry.mjs';
|
||||
import { sync, syncUpload, syncDownload } from './sync/index.mjs';
|
||||
import { updateAvatars } from './social/updateAvatars.mjs';
|
||||
|
||||
consoleStamp(console);
|
||||
dotenv.config({ quiet: true });
|
||||
|
|
@ -30,6 +31,7 @@ const actions = {
|
|||
sync,
|
||||
syncUpload,
|
||||
syncDownload,
|
||||
updateAvatars,
|
||||
};
|
||||
|
||||
const command = process.argv[2];
|
||||
|
|
|
|||
45
app/social/avatarVariants.mjs
Normal file
45
app/social/avatarVariants.mjs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
const defaultVariant = {
|
||||
key: 'default',
|
||||
displayName: 'Splatoon3.ink',
|
||||
avatar: 'default.png',
|
||||
};
|
||||
|
||||
const variants = [
|
||||
{
|
||||
key: 'pride',
|
||||
displayName: 'Splatoon3.ink',
|
||||
avatar: 'pride.png',
|
||||
start: { month: 6, day: 1 },
|
||||
end: { month: 7, day: 1 },
|
||||
},
|
||||
{
|
||||
key: 'halloween',
|
||||
displayName: 'Splatoon3.eek \u{1F383}',
|
||||
avatar: 'halloween.png',
|
||||
start: { month: 10, day: 1 },
|
||||
end: { month: 11, day: 1 },
|
||||
},
|
||||
];
|
||||
|
||||
function isDateInRange(date, start, end) {
|
||||
let month = date.getMonth() + 1;
|
||||
let day = date.getDate();
|
||||
let value = month * 100 + day;
|
||||
|
||||
return value >= start.month * 100 + start.day
|
||||
&& value <= end.month * 100 + end.day;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the avatar variant for the given date (or now).
|
||||
* @param {Date} [date]
|
||||
*/
|
||||
export function getCurrentVariant(date = new Date()) {
|
||||
for (let variant of variants) {
|
||||
if (isDateInRange(date, variant.start, variant.end)) {
|
||||
return variant;
|
||||
}
|
||||
}
|
||||
|
||||
return defaultVariant;
|
||||
}
|
||||
62
app/social/avatarVariants.test.mjs
Normal file
62
app/social/avatarVariants.test.mjs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { getCurrentVariant } from './avatarVariants.mjs';
|
||||
|
||||
// Date-only strings (YYYY-MM-DD) are parsed as UTC, which can shift the
|
||||
// local date. Adding T00:00 makes them parse as local time instead.
|
||||
function date(str) {
|
||||
return new Date(`${str}T00:00`);
|
||||
}
|
||||
|
||||
describe('getCurrentVariant', () => {
|
||||
it('returns default for most of the year', () => {
|
||||
expect(getCurrentVariant(date('2026-01-15')).key).toBe('default');
|
||||
expect(getCurrentVariant(date('2026-03-01')).key).toBe('default');
|
||||
expect(getCurrentVariant(date('2026-07-04')).key).toBe('default');
|
||||
expect(getCurrentVariant(date('2026-12-25')).key).toBe('default');
|
||||
});
|
||||
|
||||
it('returns pride for June', () => {
|
||||
let variant = getCurrentVariant(date('2026-06-15'));
|
||||
expect(variant.key).toBe('pride');
|
||||
expect(variant.displayName).toBe('Splatoon3.ink');
|
||||
});
|
||||
|
||||
it('returns pride on boundary days', () => {
|
||||
expect(getCurrentVariant(date('2026-06-01')).key).toBe('pride');
|
||||
expect(getCurrentVariant(date('2026-07-01')).key).toBe('pride'); // padded for US timezones
|
||||
});
|
||||
|
||||
it('returns default just outside pride range', () => {
|
||||
expect(getCurrentVariant(date('2026-05-31')).key).toBe('default');
|
||||
expect(getCurrentVariant(date('2026-07-02')).key).toBe('default');
|
||||
});
|
||||
|
||||
it('returns halloween for October', () => {
|
||||
let variant = getCurrentVariant(date('2026-10-15'));
|
||||
expect(variant.key).toBe('halloween');
|
||||
expect(variant.displayName).toBe('Splatoon3.eek \u{1F383}');
|
||||
});
|
||||
|
||||
it('returns halloween on boundary days', () => {
|
||||
expect(getCurrentVariant(date('2026-10-01')).key).toBe('halloween');
|
||||
expect(getCurrentVariant(date('2026-11-01')).key).toBe('halloween'); // padded for US timezones
|
||||
});
|
||||
|
||||
it('returns default just outside halloween range', () => {
|
||||
expect(getCurrentVariant(date('2026-09-30')).key).toBe('default');
|
||||
expect(getCurrentVariant(date('2026-11-02')).key).toBe('default');
|
||||
});
|
||||
|
||||
it('returns the correct avatar filename', () => {
|
||||
expect(getCurrentVariant(date('2026-01-01')).avatar).toBe('default.png');
|
||||
expect(getCurrentVariant(date('2026-06-01')).avatar).toBe('pride.png');
|
||||
expect(getCurrentVariant(date('2026-10-01')).avatar).toBe('halloween.png');
|
||||
});
|
||||
|
||||
it('defaults to now when no date is provided', () => {
|
||||
let variant = getCurrentVariant();
|
||||
expect(variant).toHaveProperty('key');
|
||||
expect(variant).toHaveProperty('displayName');
|
||||
expect(variant).toHaveProperty('avatar');
|
||||
});
|
||||
});
|
||||
BIN
app/social/avatars/default.png
Normal file
BIN
app/social/avatars/default.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
BIN
app/social/avatars/halloween.png
Normal file
BIN
app/social/avatars/halloween.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
BIN
app/social/avatars/pride.png
Normal file
BIN
app/social/avatars/pride.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
|
|
@ -30,6 +30,21 @@ export default class BlueskyClient extends Client
|
|||
}
|
||||
}
|
||||
|
||||
async updateProfile(avatarBuffer, displayName) {
|
||||
await this.login();
|
||||
|
||||
let jpeg = await sharp(avatarBuffer).jpeg().toBuffer();
|
||||
let uploadResponse = await this.#agent.uploadBlob(jpeg, { encoding: 'image/jpeg' });
|
||||
|
||||
await this.#agent.upsertProfile((existing) => {
|
||||
return {
|
||||
...existing,
|
||||
avatar: uploadResponse.data.blob,
|
||||
displayName,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async send(status, generator) {
|
||||
if (!status.media?.length) {
|
||||
console.error(`[${this.name}] No media provided for ${generator.key}`);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,20 @@ export default class MastodonClient extends Client
|
|||
return this.#url && this.#accessToken;
|
||||
}
|
||||
|
||||
async updateProfile(avatarBuffer, displayName) {
|
||||
const masto = await createRestAPIClient({
|
||||
url: this.#url,
|
||||
accessToken: this.#accessToken,
|
||||
disableVersionCheck: true,
|
||||
disableExperimentalWarning: true,
|
||||
});
|
||||
|
||||
await masto.v1.accounts.updateCredentials({
|
||||
avatar: new Blob([avatarBuffer]),
|
||||
displayName,
|
||||
});
|
||||
}
|
||||
|
||||
async send(status, generator) {
|
||||
if (!status.media?.length) {
|
||||
console.error(`[${this.name}] No media provided for ${generator.key}`);
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ function defaultStatusGenerators() {
|
|||
];
|
||||
}
|
||||
|
||||
function defaultClients() {
|
||||
export function defaultClients() {
|
||||
return [
|
||||
// new TwitterClient,
|
||||
new MastodonClient,
|
||||
|
|
|
|||
55
app/social/updateAvatars.mjs
Normal file
55
app/social/updateAvatars.mjs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import prefixedConsole from '../common/prefixedConsole.mjs';
|
||||
import ValueCache from '../common/ValueCache.mjs';
|
||||
import { defaultClients } from './index.mjs';
|
||||
import { getCurrentVariant } from './avatarVariants.mjs';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const avatarsDir = path.join(__dirname, 'avatars');
|
||||
|
||||
const console = prefixedConsole('Social', 'Avatar');
|
||||
|
||||
function cacheForClient(client) {
|
||||
return new ValueCache(`social.${client.key}.avatar`);
|
||||
}
|
||||
|
||||
export async function updateAvatars() {
|
||||
let variant = getCurrentVariant();
|
||||
let avatarBuffer = null;
|
||||
|
||||
for (let client of defaultClients()) {
|
||||
if (!client.updateProfile || !(await client.canSend())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let cache = cacheForClient(client);
|
||||
let cachedVariant = await cache.getData();
|
||||
|
||||
if (cachedVariant === variant.key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`[${client.name}] Avatar update needed: ${cachedVariant || '(none)'} -> ${variant.key}`);
|
||||
|
||||
// Read avatar file on first client that needs an update
|
||||
if (!avatarBuffer) {
|
||||
try {
|
||||
let avatarPath = path.join(avatarsDir, variant.avatar);
|
||||
avatarBuffer = await fs.readFile(avatarPath);
|
||||
} catch (error) {
|
||||
console.error(`Failed to read avatar file ${variant.avatar}:`, error.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await client.updateProfile(avatarBuffer, variant.displayName);
|
||||
await cache.setData(variant.key);
|
||||
console.log(`[${client.name}] Profile updated successfully`);
|
||||
} catch (error) {
|
||||
console.error(`[${client.name}] Failed to update profile:`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
"social:test:mastodon": "node app/index.mjs socialTestMastodon",
|
||||
"social:test:bluesky": "node app/index.mjs socialTestBluesky",
|
||||
"social:test:threads": "node app/index.mjs socialTestThreads",
|
||||
"social:avatars": "node app/index.mjs updateAvatars",
|
||||
"splatnet:quick": "node app/index.mjs splatnet quick",
|
||||
"splatnet": "node app/index.mjs splatnet default",
|
||||
"splatnet:all": "node app/index.mjs splatnet all",
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user