Update documentation, add width option

This commit is contained in:
Samuel Elliott 2023-12-27 19:27:01 +00:00
parent 01fdfb1fec
commit b7afcfb175
No known key found for this signature in database
GPG Key ID: 8420C7CDE43DC4D6
5 changed files with 65 additions and 10 deletions

View File

@ -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:

View File

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

View File

@ -34,6 +34,9 @@ export function builder(yargs: Argv<ParentArguments>) {
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<Arguments>) {
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<Arguments>) {
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);

View File

@ -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(`<!DOCTYPE html><html><head><meta name="viewport" content="width=device-width,initial-scale=1"/><style>body{margin:0;min-height:100vh;width:100vw;min-width:fit-content;display:flex;align-items:center;justify-content:center}</style></head>`);

View File

@ -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(
<image x="30" y="30" width="120" height="120"
href="${image(result.friend.imageUri) ?? result.friend.imageUri}" />
<text x="180" y="57" fill="${colours.text}" font-size="26" font-family="${font_family}" font-weight="500">${result.friend.name}</text>
<text x="180" y="57" fill="${colours.text}" font-size="26" font-family="${font_family}" font-weight="500" mask="url(#mask-out)">${result.friend.name}</text>
<line x1="180" y1="73" x2="470" y2="73" stroke="${colours.separator}" />
<line x1="180" y1="73" x2="${width - 30}" y2="73" stroke="${colours.separator}" />
${{[RawValueSymbol]: game && (state === PresenceState.ONLINE || state === PresenceState.PLAYING) ? htmlentities`
<image x="180" y="87" width="60" height="60"