← back to CelebritySignatures

scripts/fetch-declaration.mjs

117 lines

#!/usr/bin/env node
// Declaration of Independence signers — the safest possible signature set:
// all 56 signers, deceased since the 1700s-1800s (no publicity rights), every
// signature public domain. Built entirely on MediaWiki/Commons/Wikidata-entity
// APIs (NOT the SPARQL endpoint), so it works even during a WDQS outage.

import { writeFile, mkdir } from 'node:fs/promises';

const UA = 'CelebritySignatures-research/1.0 (steve@designerwallcoverings.com)';
const EN = 'https://en.wikipedia.org/w/api.php';
const WD = 'https://www.wikidata.org/w/api.php';
const COMMONS = 'https://commons.wikimedia.org/w/api.php';
const CAT = 'Category:Signatories of the United States Declaration of Independence';
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function api(base, params) {
  const url = base + '?' + new URLSearchParams({ format: 'json', formatversion: '2', ...params });
  const res = await fetch(url, { headers: { 'User-Agent': UA } });
  if (!res.ok) throw new Error(`${base} ${res.status}`);
  return res.json();
}
const chunk = (a, n) => Array.from({ length: Math.ceil(a.length / n) }, (_, i) => a.slice(i * n, i * n + n));

async function main() {
  await mkdir('data', { recursive: true });

  // 1. all 56 signer article titles
  const cm = await api(EN, { action: 'query', list: 'categorymembers', cmtitle: CAT, cmlimit: '100', cmtype: 'page' });
  const titles = cm.query.categorymembers.map((m) => m.title);
  console.log(`signers in category: ${titles.length}`);

  // 2. enwiki title -> Wikidata QID
  const titleToQid = {};
  for (const grp of chunk(titles, 50)) {
    const r = await api(EN, { action: 'query', prop: 'pageprops', titles: grp.join('|'), ppprop: 'wikibase_item' });
    for (const p of r.query.pages) if (p.pageprops?.wikibase_item) titleToQid[p.title] = p.pageprops.wikibase_item;
    await sleep(200);
  }
  const qids = Object.values(titleToQid);
  console.log(`resolved QIDs: ${qids.length}`);

  // 3. Wikidata entities: P109 signature file, P570 death, sitelink count, label
  const ent = {};
  for (const grp of chunk(qids, 50)) {
    const r = await api(WD, { action: 'wbgetentities', ids: grp.join('|'), props: 'claims|sitelinks|labels', languages: 'en' });
    Object.assign(ent, r.entities);
    await sleep(200);
  }

  // collect signature filenames
  const rows = [];
  const fileTitles = new Set();
  for (const [title, qid] of Object.entries(titleToQid)) {
    const e = ent[qid] || {};
    const sigClaim = e.claims?.P109?.[0]?.mainsnak?.datavalue?.value || null;
    const dod = e.claims?.P570?.[0]?.mainsnak?.datavalue?.value?.time || null;
    const links = e.sitelinks ? Object.keys(e.sitelinks).length : 0;
    const label = e.labels?.en?.value || title;
    const fileTitle = sigClaim ? 'File:' + sigClaim : null;
    if (fileTitle) fileTitles.add(fileTitle);
    rows.push({ title, qid, label, sigClaim, fileTitle, dod, links });
  }

  // 4. Commons license + direct URL per signature file
  const lic = {};
  for (const grp of chunk([...fileTitles], 50)) {
    const r = await api(COMMONS, { action: 'query', prop: 'imageinfo', iiprop: 'extmetadata|url', titles: grp.join('|') });
    const norm = {}; (r.query?.normalized || []).forEach((n) => (norm[n.to] = n.from));
    for (const p of r.query?.pages || []) {
      const ii = p.imageinfo?.[0]; const ext = ii?.extmetadata || {};
      lic[norm[p.title] || p.title] = {
        license: ext.LicenseShortName?.value || 'Public domain (signature, pre-1830)',
        directUrl: ii?.url || null, filePage: ii?.descriptionurl || null,
      };
    }
    await sleep(200);
  }

  rows.sort((a, b) => b.links - a.links);
  const out = rows.map((r, i) => {
    const meta = r.fileTitle ? lic[r.fileTitle] : null;
    const hasFile = !!meta?.directUrl;
    const deathYear = r.dod ? r.dod.slice(1, 5) : '';
    return {
      category: 'Declaration of Independence',
      rank: i + 1,
      full_name: r.label,
      wikidata: `https://www.wikidata.org/wiki/${r.qid}`,
      reason_for_ranking: `Signer of the US Declaration of Independence (1776); cross-wiki notability: ${r.links} language Wikipedias`,
      ranking_source_urls: 'https://www.archives.gov/founding-docs/declaration | https://founders.archives.gov/',
      signature_image_url: hasFile ? meta.directUrl
        : 'https://www.archives.gov/founding-docs/downloads',
      signature_source_type: hasFile
        ? 'Wikimedia Commons (Wikidata P109 signature file)'
        : 'US National Archives — engrossed Declaration of Independence (PD)',
      image_license: hasFile ? meta.license : 'Public domain (US founding document, 1776)',
      image_author: hasFile ? '' : 'Engrossed by Timothy Matlack; signed 1776',
      deceased: 'yes',
      death_date: deathYear,
      risk_level: 'low',
      usable_in_commercial_collage: 'yes',
      notes: hasFile
        ? 'Public-domain signature; signer deceased since the founding era — no copyright or publicity-rights exposure'
        : 'No standalone Commons signature file; signature appears on the engrossed Declaration (NARA, public domain)',
      backup_source: hasFile ? meta.filePage : 'https://catalog.archives.gov/id/1419123',
    };
  });

  await writeFile('data/declaration.json', JSON.stringify(out, null, 2));
  const withFile = out.filter((r) => r.signature_source_type.startsWith('Wikimedia')).length;
  console.log(`\n${out.length} signers written -> data/declaration.json`);
  console.log(`  ${withFile} have a standalone Commons signature file; ${out.length - withFile} sourced from the engrossed Declaration (NARA)`);
  console.log(`  ALL ${out.length}: risk_level=low, usable_in_commercial_collage=yes`);
  console.log(`  Top 5 by notability: ${out.slice(0, 5).map((r) => r.full_name).join(', ')}`);
}
main().catch((e) => { console.error(e); process.exit(1); });