fedipod-server 0.19.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9664,11 +9664,14 @@ function hostMeta(base) {
9664
9664
  </XRD>
9665
9665
  `;
9666
9666
  }
9667
- function jrd({ handle: handle7, host, actor, aliases = [] }) {
9667
+ function jrd({ handle: handle7, host, actor, aliases = [], page: page2 = null }) {
9668
9668
  return {
9669
9669
  subject: `acct:${handle7}@${host}`,
9670
9670
  ...aliases.length ? { aliases } : {},
9671
- links: [{ rel: "self", type: "application/activity+json", href: actor }]
9671
+ links: [
9672
+ { rel: "self", type: "application/activity+json", href: actor },
9673
+ ...page2 ? [{ rel: "http://webfinger.net/rel/profile-page", type: "text/html", href: page2 }] : []
9674
+ ]
9672
9675
  };
9673
9676
  }
9674
9677
  function followsNeedApproval(config = {}) {
@@ -9794,9 +9797,30 @@ function actorDoc({
9794
9797
  publicKey: { id: urls.actor + "#main-key", owner: urls.actor, publicKeyPem }
9795
9798
  };
9796
9799
  }
9797
- function profilePageHtml({ name, address, summary = null, icon = null, kind = "person" }) {
9800
+ function profilePageHtml({
9801
+ name,
9802
+ address,
9803
+ summary = null,
9804
+ icon = null,
9805
+ image = null,
9806
+ fields = [],
9807
+ joined = null,
9808
+ pinned = [],
9809
+ kind = "person"
9810
+ }) {
9798
9811
  const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ ...HTML_ESCAPES, '"': "&quot;" })[c]);
9799
9812
  const what = kind === "group" ? "a group" : "an account";
9813
+ const month = (iso) => {
9814
+ const d = new Date(iso);
9815
+ return Number.isNaN(d.getTime()) ? null : d.toLocaleDateString("en", { month: "long", year: "numeric", timeZone: "UTC" });
9816
+ };
9817
+ const day = (iso) => {
9818
+ const d = new Date(iso);
9819
+ return Number.isNaN(d.getTime()) ? "" : d.toISOString().slice(0, 10);
9820
+ };
9821
+ const fieldRows = fields.filter((f) => f?.name && f?.value !== void 0 && f?.value !== null && String(f.value) !== "");
9822
+ const joinedText = joined ? month(joined) : null;
9823
+ const pins = pinned.filter((x) => x && typeof x.content === "string");
9800
9824
  return `<!doctype html>
9801
9825
  <html lang="en">
9802
9826
  <head>
@@ -9806,14 +9830,23 @@ function profilePageHtml({ name, address, summary = null, icon = null, kind = "p
9806
9830
  <style>
9807
9831
  :root { color-scheme: light dark; }
9808
9832
  body { font: 112.5%/1.5 system-ui, sans-serif; max-width: 34rem; margin: 3rem auto; padding: 0 1rem; }
9809
- img.avatar { width: 6rem; height: 6rem; border-radius: 1rem; object-fit: cover; }
9833
+ img.header { width: 100%; max-height: 12rem; object-fit: cover; border-radius: 1rem; }
9834
+ img.avatar { width: 6rem; height: 6rem; border-radius: 1rem; object-fit: cover; margin-top: 1rem; }
9810
9835
  h1 { margin: .5rem 0 0; }
9811
9836
  .address { font-size: 1.1rem; user-select: all; }
9837
+ dl.fields { display: grid; grid-template-columns: max-content 1fr; gap: .4rem 1rem; margin: 1.5rem 0; }
9838
+ dl.fields dt { font-weight: 600; }
9839
+ dl.fields dd { margin: 0; overflow-wrap: anywhere; }
9840
+ .joined { margin: 1rem 0; }
9841
+ section.pinned h2 { font-size: 1.1rem; margin: 2rem 0 .5rem; }
9842
+ article.pin { border: 1px solid #8884; border-radius: 1rem; padding: 1rem 1.2rem; margin: 1rem 0; }
9843
+ article.pin p:last-child { margin-bottom: 0; }
9844
+ article.pin .when { font-size: 1rem; }
9812
9845
  form { margin-top: 2rem; }
9813
9846
  label { display: block; margin-bottom: .3rem; }
9814
9847
  input { font: inherit; padding: .5rem; width: 14rem; max-width: 100%; }
9815
9848
  button { font: inherit; padding: .5rem 1rem; }
9816
- .hint { color: #777; font-size: .9rem; }
9849
+ .hint { color: #666; font-size: 1rem; }
9817
9850
  .err { color: #b00020; }
9818
9851
  .err:empty { display: none; }
9819
9852
  @media (prefers-color-scheme: dark) {
@@ -9824,10 +9857,22 @@ button { font: inherit; padding: .5rem 1rem; }
9824
9857
  </head>
9825
9858
  <body>
9826
9859
  <main>
9860
+ ${image ? `<img class="header" src="${esc(image)}" alt="">` : ""}
9827
9861
  ${icon ? `<img class="avatar" src="${esc(icon)}" alt="">` : ""}
9828
9862
  <h1>${esc(name)}</h1>
9829
9863
  <p class="address">${esc(address)}</p>
9830
9864
  ${summary ? `<div>${summary}</div>` : ""}
9865
+ ${fieldRows.length ? `<dl class="fields">
9866
+ ${fieldRows.map((f) => `<dt>${esc(f.name)}</dt><dd>${esc(f.value)}</dd>`).join("\n")}
9867
+ </dl>` : ""}
9868
+ ${joinedText ? `<p class="joined">Joined ${esc(joinedText)}</p>` : ""}
9869
+ ${pins.length ? `<section class="pinned">
9870
+ <h2>Pinned</h2>
9871
+ ${pins.map((x) => `<article class="pin">
9872
+ ${x.content}
9873
+ <p class="when">${x.url ? `<a href="${esc(x.url)}">${esc(day(x.published))}</a>` : esc(day(x.published))}</p>
9874
+ </article>`).join("\n")}
9875
+ </section>` : ""}
9831
9876
  <p>This is ${what} on the Fediverse. To follow it, paste the address above
9832
9877
  into the search box of Mastodon or any Fediverse app \u2014 or use the form.</p>
9833
9878
  <form id="follow">
@@ -56761,6 +56806,40 @@ var Publisher = class {
56761
56806
  oauthToken: this.clientOrigin ? `${this.clientOrigin}oauth/token` : null
56762
56807
  });
56763
56808
  }
56809
+ // The human half: a page a browser can open and follow from. The actor
56810
+ // document is for servers; this is the address you hand to a person. It
56811
+ // shows who the account is, its fields, when it joined and what it pinned;
56812
+ // the counts and the posts live in the collections. Written only when its
56813
+ // content changed.
56814
+ async publishProfilePage({ force = false } = {}) {
56815
+ const { urls } = this;
56816
+ const host = new URL(urls.base).host;
56817
+ if (!this.config.createdAt) {
56818
+ const oldest = this.store.getStatuses().filter((s) => s.kind === "post" && s.published).map((s) => s.published).sort()[0];
56819
+ this.config.createdAt = oldest || (/* @__PURE__ */ new Date()).toISOString();
56820
+ this.store.setConfig?.({ ...this.store.getConfig?.(), createdAt: this.config.createdAt });
56821
+ }
56822
+ const pinned = this.store.getStatuses().filter((s) => s.kind === "post" && s.pinned && s.visibility !== "private" && s.visibility !== "direct").sort((a, b) => String(b.published).localeCompare(String(a.published))).slice(0, 5).map((s) => ({ content: s.content, published: s.published, url: s.noteId }));
56823
+ const frontActor = this.config.gateway?.frontActor;
56824
+ const address = frontActor ? `@${publicHandle(this.config)}@${new URL(frontActor).host}` : webfingerHost(urls.base) ? `@${this.config.handle}@${host}` : urls.actor;
56825
+ const html = profilePageHtml({
56826
+ name: this.config.name || this.config.handle,
56827
+ address,
56828
+ summary: this.config.summary ? contentHtml(this.config.summary) : null,
56829
+ icon: this.config.icon || null,
56830
+ image: this.config.image || null,
56831
+ fields: this.config.fields || [],
56832
+ joined: this.config.createdAt,
56833
+ pinned,
56834
+ kind: this.config.kind
56835
+ });
56836
+ const digest = node_crypto_default.createHash("sha256").update(html).digest("hex").slice(0, 32);
56837
+ const seen = this.store.read("published.json", {});
56838
+ if (!force && seen.pageDigest === digest) return false;
56839
+ await writeProfilePage(this.remote, urls, html);
56840
+ this.store.write("published.json", { ...this.store.read("published.json", {}), pageDigest: digest });
56841
+ return true;
56842
+ }
56764
56843
  async publishProfile({ force = false } = {}) {
56765
56844
  const { urls } = this;
56766
56845
  const host = new URL(urls.base).host;
@@ -56777,6 +56856,7 @@ var Publisher = class {
56777
56856
  version: AGENT_VERSION,
56778
56857
  moderators: this.config.moderators || []
56779
56858
  })).digest("hex").slice(0, 32);
56859
+ await this.publishProfilePage({ force });
56780
56860
  if (!force && this.store.read("published.json", {}).surfaceDigest === surface) {
56781
56861
  this.log("profile unchanged \u2014 nothing republished");
56782
56862
  return { unreachable: [], updated: 0, skipped: true };
@@ -56784,7 +56864,7 @@ var Publisher = class {
56784
56864
  await writeWebfinger(
56785
56865
  this.remote,
56786
56866
  urls,
56787
- jrd({ handle: this.config.handle, host, actor: urls.actor })
56867
+ jrd({ handle: this.config.handle, host, actor: urls.actor, page: urls.profileHtml })
56788
56868
  );
56789
56869
  await writeHostMeta(this.remote, urls, hostMeta(urls.base));
56790
56870
  const nodeinfoDocUrl = urls.home + "ap/nodeinfo-2.0";
@@ -56803,13 +56883,6 @@ var Publisher = class {
56803
56883
  );
56804
56884
  }
56805
56885
  if (gwActive) await this.publishGatewayPolicy();
56806
- await writeProfilePage(this.remote, urls, profilePageHtml({
56807
- name: this.config.name || this.config.handle,
56808
- address: webfingerHost(urls.base) ? `@${this.config.handle}@${host}` : urls.actor,
56809
- summary: this.config.summary ? contentHtml(this.config.summary) : null,
56810
- icon: this.config.icon || null,
56811
- kind: this.config.kind
56812
- }));
56813
56886
  try {
56814
56887
  const wrote = await linkInWebIdProfile(this.remote, {
56815
56888
  actorUrl: urls.actor,
@@ -64145,6 +64218,9 @@ async function pinStatus(agent2, s, pinned) {
64145
64218
  if (s.actor !== agent2.publisher.urls.actor) throw new Error("not your status");
64146
64219
  const updated = agent2.store.updateStatus(s.noteId, { pinned: !!pinned });
64147
64220
  await agent2.publisher.publishFeatured().catch((e) => agent2.log?.(`featured: ${e.message}`));
64221
+ if (agent2.publisher.publishProfilePage) {
64222
+ await agent2.publisher.publishProfilePage().catch((e) => agent2.log?.(`profile page: ${e.message}`));
64223
+ }
64148
64224
  agent2.publisher.publishProfile?.().catch(() => {
64149
64225
  });
64150
64226
  return updated || s;
@@ -65613,7 +65689,7 @@ function account(api, actorUrl, { selfAcct } = {}) {
65613
65689
  bot: false,
65614
65690
  discoverable: true,
65615
65691
  group: self2 ? api.store.getConfig()?.kind === "group" : cached.type === "Group",
65616
- created_at: "2026-01-01T00:00:00.000Z",
65692
+ created_at: self2 && api.store.getConfig()?.createdAt || "2026-01-01T00:00:00.000Z",
65617
65693
  note: (self2 ? api.store.getConfig()?.summary : cached.summary) || "",
65618
65694
  url: actorUrl,
65619
65695
  uri: actorUrl,
@@ -70538,6 +70614,7 @@ var BrowserAgent = class _BrowserAgent {
70538
70614
  try {
70539
70615
  await this.store.load({ force: true }).catch((e) => this.log(`re-reading state: ${e.message}`));
70540
70616
  await this.publisher.healStatuses().catch((e) => this.log(`healing the timeline index: ${e.message}`));
70617
+ await this.publisher.publishProfilePage().catch((e) => this.log(`profile page: ${e.message}`));
70541
70618
  this.deliverer?.startQueue?.();
70542
70619
  await this.publisher.publishProfile();
70543
70620
  await this.store.flush?.();