Back to Pickle Rustlers
SOURCE / PINNED RELEASE

Made of little things.

Pickle Rustlers

Release
039e51f55e34…
Author-recorded commit
40608586a8a1…
License
LICENSE
Author’s source reference
nostr://npub12lm3rxsva8kfempyjkxupt09e02n3fezurkyadyqfejdc5e0r2kscqty6r/wss%3A%2F%2Fgit.napplet.soy%2F/n-bad44e7ab35

Archive hash verified: f38e99afa0992052…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

src/share/post.ts
// Upload the card through NAP-UPLOAD, then publish a kind 1 note through NAP-OUTBOX.
import { outbox, upload, type UploadResult } from '@napplet/sdk';
import type { ShareResult } from './types.js';

type Uploaded = UploadResult & { url: string };

// A failed publish can be retried without uploading the same card again.
const uploads = new WeakMap<Blob, Uploaded>();

const reason = (error: unknown) => (error instanceof Error ? error.message : String(error ?? 'unknown error'));

/** Resolve once the upload reaches a terminal state. */
async function uploadCard(png: Blob, onStatus: (text: string) => void): Promise<UploadResult> {
  let id: string | null = null;
  const early: UploadResult[] = [];
  let settle: ((r: UploadResult) => void) | null = null;
  const terminal = (s: UploadResult) => s.status === 'complete' || s.status === 'failed' || s.status === 'cancelled';
  // Listen before uploading so a fast completion is not missed.
  const listener = upload.onStatus((status) => {
    if (id === null) early.push(status);
    else if (status.uploadId === id && terminal(status)) settle?.(status);
  });
  try {
    onStatus('Waiting for you to approve the upload…');
    const initial = await upload.upload({
      data: png,
      mimeType: 'image/png',
      filename: 'pickle-rustlers-scorecard.png',
      caption: 'Pickle Rustlers score card',
      noTransform: true,
    });
    if (terminal(initial)) return initial;
    id = initial.uploadId;
    const seen = early.find((s) => s.uploadId === id && terminal(s));
    if (seen) return seen;
    onStatus('Uploading your score card…');
    return await new Promise<UploadResult>((resolve) => (settle = resolve));
  } finally {
    listener.close();
  }
}

/** NIP-92 imeta from the upload's NIP-94 tags, falling back to the result fields. */
function imeta(result: Uploaded, width: number, height: number): string[] {
  const fields = new Map<string, string[]>();
  const add = (key: string, value: string | number | undefined) => {
    if (value === undefined || value === '') return;
    fields.set(key, [...(fields.get(key) ?? []), String(value)]);
  };
  for (const tag of result.nip94 ?? []) {
    const [key, value] = tag;
    if (key && value && key !== 'url') add(key, value);
  }
  if (!fields.has('m')) add('m', result.mimeType ?? 'image/png');
  if (!fields.has('x')) add('x', result.sha256);
  if (!fields.has('size')) add('size', result.size);
  if (!fields.has('dim')) add('dim', result.dimensions ? `${result.dimensions.width}x${result.dimensions.height}` : `${width}x${height}`);
  if (!fields.has('fallback')) for (const url of result.fallbackUrls ?? []) add('fallback', url);
  const entries = [`url ${result.url}`];
  for (const [key, values] of fields) for (const value of values) entries.push(`${key} ${value}`);
  return ['imeta', ...entries];
}

export async function postScoreCard(
  png: Blob,
  text: string,
  size: { width: number; height: number },
  onStatus: (text: string) => void,
): Promise<ShareResult> {
  let uploaded = uploads.get(png);
  if (!uploaded) {
    let result: UploadResult;
    try {
      result = await uploadCard(png, onStatus);
    } catch (error) {
      return { ok: false, done: false, message: `Upload failed: ${reason(error)}` };
    }
    if (result.status !== 'complete' || !result.url) {
      const what = result.status === 'cancelled' ? 'Upload cancelled' : 'Upload failed';
      return { ok: false, done: false, message: result.error ? `${what}: ${result.error}` : `${what}.` };
    }
    uploaded = result as Uploaded;
    uploads.set(png, uploaded);
  }

  onStatus('Waiting for you to approve the note…');
  try {
    const result = await outbox.publish({
      kind: 1,
      content: `${text.trim()}\n\n${uploaded.url}`,
      tags: [imeta(uploaded, size.width, size.height), ['t', 'picklerustlers']],
      created_at: Math.floor(Date.now() / 1000),
    });
    if (result.ok) return { ok: true, done: true, message: 'Posted! Your score card is on your feed.' };
    if (result.eventId) {
      // Some relays may already hold the signed note; a second post would duplicate it.
      return { ok: false, done: true, message: `Posted to some relays only${result.error ? ` (${result.error})` : ''}.` };
    }
    return { ok: false, done: false, message: `Couldn't post: ${result.error ?? 'the host declined'}. Card: ${uploaded.url}` };
  } catch (error) {
    return { ok: false, done: false, message: `Couldn't post: ${reason(error)}. Card: ${uploaded.url}` };
  }
}