diff --git a/docs/cli.md b/docs/cli.md index a52cbd7..1255cb0 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -664,9 +664,44 @@ curl http://[::1]:12345/api/presence/0123456789abcdef # Fetch presence data for a specific user including Splatoon 3 presence using curl curl http://[::1]:12345/api/presence/0123456789abcdef?include-splatoon3=1 +# Fetch Splatoon 3 fest voting history for a specific user using curl +curl http://[::1]:12345/api/presence/0123456789abcdef/splatoon3-fest-votes +# Fetch Splatoon 3 fest voting history including all prevotes for a specific user using curl +curl http://[::1]:12345/api/presence/0123456789abcdef/splatoon3-fest-votes?include-all=1 + # Watch for presence events curl --no-buffer http://[::1]:12345/api/presence/0123456789abcdef/events curl --no-buffer http://[::1]:12345/api/presence/0123456789abcdef/events?include-splatoon3=1 + +# Save a user's current picture +curl -L http://[::1]:12345/api/presence/0123456789abcdef/image > image.jpeg + +# Show the Nintendo eShop page for a user's current title +# http://[::1]:12345/api/presence/0123456789abcdef/title/redirect +# Redirect to a friend code URL if not playing +# http://[::1]:12345/api/presence/0123456789abcdef/title/redirect?friend-code=0000-0000-0000&friend-code-hash=0000000000 +# Redirect to another URL if not playing +# http://[::1]:12345/api/presence/0123456789abcdef/title/redirect?fallback-url=https://example.com +# Signal to the browser to cancel navigation if not playing +# http://[::1]:12345/api/presence/0123456789abcdef/title/redirect?fallback-prevent-navigation=1 + +# Generate an SVG showing a user's presence +curl http://[::1]:12345/api/presence/0123456789abcdef/embed > embed.svg +# Generate a PNG/JPEG/WEBP showing a user's presence +curl http://[::1]:12345/api/presence/0123456789abcdef/embed.png > embed.png +curl http://[::1]:12345/api/presence/0123456789abcdef/embed.jpeg > embed.jpeg +curl http://[::1]:12345/api/presence/0123456789abcdef/embed.webp > embed.webp +# ... using a specific theme +curl http://[::1]:12345/api/presence/0123456789abcdef/embed?theme=light > embed.svg +curl http://[::1]:12345/api/presence/0123456789abcdef/embed?theme=dark > embed.svg +# ... including a friend code +curl http://[::1]:12345/api/presence/0123456789abcdef/embed?friend-code=0000-0000-0000 > embed.svg +# ... without a background and border +curl http://[::1]:12345/api/presence/0123456789abcdef/embed?transparent=1 > embed.svg +# ... with a custom width (500 to 1500, or 440 to 1440 with transparency) +curl http://[::1]:12345/api/presence/0123456789abcdef/embed?width=800 > embed.svg +# ... with Splatoon 3 presence +curl http://[::1]:12345/api/presence/0123456789abcdef/embed?include-splatoon3=1 > embed.svg ``` Example EventStream use: diff --git a/src/cli/presence-server.ts b/src/cli/presence-server.ts index 897485f..51a9aff 100644 --- a/src/cli/presence-server.ts +++ b/src/cli/presence-server.ts @@ -1189,13 +1189,14 @@ class Server extends HttpServer { const result = await this.handlePresenceRequest(req, null, presence_user_nsaid); - const {theme, friend_code, transparent} = getUserEmbedOptionsFromRequest(req); + const {theme, friend_code, transparent, width} = getUserEmbedOptionsFromRequest(req); const etag = createHash('sha256').update(JSON.stringify({ result, theme, friend_code, transparent, + width, v: version + '-' + git?.revision, })).digest('base64url'); @@ -1207,10 +1208,11 @@ class Server extends HttpServer { const url_map = await this.downloadImages(result, this.getResourceBaseUrls(req)); - const svg = renderUserEmbedSvg(result, url_map, theme, friend_code); + const svg = renderUserEmbedSvg(result, url_map, theme, friend_code, 1, transparent, width); const [image, type] = await renderUserEmbedImage(svg, format); res.setHeader('Content-Type', type); + res.setHeader('Cache-Control', 'public, no-cache'); // no-cache means store but revalidate res.setHeader('Etag', '"' + etag + '"'); res.end(image); } diff --git a/src/cli/util/presence-embed-render.ts b/src/cli/util/presence-embed-render.ts index 26b88ea..acd9fc4 100644 --- a/src/cli/util/presence-embed-render.ts +++ b/src/cli/util/presence-embed-render.ts @@ -34,6 +34,9 @@ export function builder(yargs: Argv) { describe: 'Remove border and use transparent background', type: 'boolean', default: false, + }).option('width', { + describe: 'Image width', + type: 'number', }); } @@ -51,6 +54,10 @@ export async function handler(argv: ArgumentsCamelCase) { throw new TypeError('Invalid friend code'); } + const width = argv.width ? + argv.transparent ? argv.width + 60 : argv.width : + 500; + const [presence, user, data] = await getPresenceFromUrl(argv.url); const result = data as PresenceResponse; @@ -70,7 +77,7 @@ export async function handler(argv: ArgumentsCamelCase) { url_map[url] = [url, data, 'image/jpeg']; })); - const svg = renderUserEmbedSvg(result, url_map, theme, argv.friendCode, argv.scale, argv.transparent); + const svg = renderUserEmbedSvg(result, url_map, theme, argv.friendCode, argv.scale, argv.transparent, width); const [image, type] = await renderUserEmbedImage(svg, format); console.warn('output type', type); diff --git a/src/cli/util/presence-embed-server.ts b/src/cli/util/presence-embed-server.ts index 83b299d..44973da 100644 --- a/src/cli/util/presence-embed-server.ts +++ b/src/cli/util/presence-embed-server.ts @@ -100,7 +100,7 @@ class Server extends HttpServer { const [presence, user, data] = await getPresenceFromUrl(this.base_url + '/' + presence_user_nsaid + qs); const result = data as PresenceResponse; - const {theme, friend_code, transparent} = getUserEmbedOptionsFromRequest(req); + const {theme, friend_code, transparent, width} = getUserEmbedOptionsFromRequest(req); const etag = createHash('sha256').update(JSON.stringify({ data, @@ -108,6 +108,7 @@ class Server extends HttpServer { theme, friend_code, transparent, + width, v: version + '-' + git?.revision, })).digest('base64url'); @@ -131,10 +132,11 @@ class Server extends HttpServer { url_map[url] = [url, data, 'image/jpeg']; })); - const svg = renderUserEmbedSvg(result, url_map, theme, friend_code, 1, transparent); + const svg = renderUserEmbedSvg(result, url_map, theme, friend_code, 1, transparent, width); const [image, type] = await renderUserEmbedImage(svg, format); res.setHeader('Content-Type', type); + res.setHeader('Cache-Control', 'public, no-cache'); // no-cache means store but revalidate res.setHeader('Etag', '"' + etag + '"'); res.end(image); } @@ -168,7 +170,7 @@ class Server extends HttpServer { url_map[url] = [url, data, 'image/jpeg']; })); - const svg = renderUserEmbedSvg(result, url_map, PresenceEmbedTheme.LIGHT, undefined, 1, true); + const svg = renderUserEmbedSvg(result, url_map, PresenceEmbedTheme.LIGHT, undefined, 1, true, 800); res.setHeader('Content-Type', 'text/html'); res.write(``); diff --git a/src/cli/util/presence-embed.ts b/src/cli/util/presence-embed.ts index 3c4dd3c..fc4637c 100644 --- a/src/cli/util/presence-embed.ts +++ b/src/cli/util/presence-embed.ts @@ -65,8 +65,16 @@ export function getUserEmbedOptionsFromRequest(req: Request) { const theme = url.searchParams.get('theme') === 'dark' ? PresenceEmbedTheme.DARK : PresenceEmbedTheme.LIGHT; const friend_code = url.searchParams.getAll('friend-code').find(c => c.match(/^\d{4}-\d{4}-\d{4}$/)); const transparent = url.searchParams.get('transparent') === '1'; + + let width = url.searchParams.getAll('width') + .map(w => parseInt(w)) + .map(w => transparent ? w + 60 : w) + .find(w => !isNaN(w) && w >= 500); - return {theme, friend_code, transparent}; + if (!width) width = 500; + if (width > 1500) width = 1500; + + return {theme, friend_code, transparent, width}; } export async function renderUserEmbedImage( @@ -106,8 +114,9 @@ export function renderUserEmbedSvg( friend_code?: string, scale = 1, transparent = false, + width = 500, ) { - let width = 500; + if (width < 500) width = 500; let height = 180; if (friend_code) height += 40; @@ -155,9 +164,9 @@ export function renderUserEmbedSvg( - ${result.friend.name} + ${result.friend.name} - + ${{[RawValueSymbol]: game && (state === PresenceState.ONLINE || state === PresenceState.PLAYING) ? htmlentities`