fedipod-server 0.20.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.
@@ -9797,9 +9797,30 @@ function actorDoc({
9797
9797
  publicKey: { id: urls.actor + "#main-key", owner: urls.actor, publicKeyPem }
9798
9798
  };
9799
9799
  }
9800
- 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
+ }) {
9801
9811
  const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ ...HTML_ESCAPES, '"': "&quot;" })[c]);
9802
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");
9803
9824
  return `<!doctype html>
9804
9825
  <html lang="en">
9805
9826
  <head>
@@ -9809,14 +9830,23 @@ function profilePageHtml({ name, address, summary = null, icon = null, kind = "p
9809
9830
  <style>
9810
9831
  :root { color-scheme: light dark; }
9811
9832
  body { font: 112.5%/1.5 system-ui, sans-serif; max-width: 34rem; margin: 3rem auto; padding: 0 1rem; }
9812
- 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; }
9813
9835
  h1 { margin: .5rem 0 0; }
9814
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; }
9815
9845
  form { margin-top: 2rem; }
9816
9846
  label { display: block; margin-bottom: .3rem; }
9817
9847
  input { font: inherit; padding: .5rem; width: 14rem; max-width: 100%; }
9818
9848
  button { font: inherit; padding: .5rem 1rem; }
9819
- .hint { color: #777; font-size: .9rem; }
9849
+ .hint { color: #666; font-size: 1rem; }
9820
9850
  .err { color: #b00020; }
9821
9851
  .err:empty { display: none; }
9822
9852
  @media (prefers-color-scheme: dark) {
@@ -9827,10 +9857,22 @@ button { font: inherit; padding: .5rem 1rem; }
9827
9857
  </head>
9828
9858
  <body>
9829
9859
  <main>
9860
+ ${image ? `<img class="header" src="${esc(image)}" alt="">` : ""}
9830
9861
  ${icon ? `<img class="avatar" src="${esc(icon)}" alt="">` : ""}
9831
9862
  <h1>${esc(name)}</h1>
9832
9863
  <p class="address">${esc(address)}</p>
9833
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>` : ""}
9834
9876
  <p>This is ${what} on the Fediverse. To follow it, paste the address above
9835
9877
  into the search box of Mastodon or any Fediverse app \u2014 or use the form.</p>
9836
9878
  <form id="follow">
@@ -56764,6 +56806,40 @@ var Publisher = class {
56764
56806
  oauthToken: this.clientOrigin ? `${this.clientOrigin}oauth/token` : null
56765
56807
  });
56766
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
+ }
56767
56843
  async publishProfile({ force = false } = {}) {
56768
56844
  const { urls } = this;
56769
56845
  const host = new URL(urls.base).host;
@@ -56780,6 +56856,7 @@ var Publisher = class {
56780
56856
  version: AGENT_VERSION,
56781
56857
  moderators: this.config.moderators || []
56782
56858
  })).digest("hex").slice(0, 32);
56859
+ await this.publishProfilePage({ force });
56783
56860
  if (!force && this.store.read("published.json", {}).surfaceDigest === surface) {
56784
56861
  this.log("profile unchanged \u2014 nothing republished");
56785
56862
  return { unreachable: [], updated: 0, skipped: true };
@@ -56806,13 +56883,6 @@ var Publisher = class {
56806
56883
  );
56807
56884
  }
56808
56885
  if (gwActive) await this.publishGatewayPolicy();
56809
- await writeProfilePage(this.remote, urls, profilePageHtml({
56810
- name: this.config.name || this.config.handle,
56811
- address: webfingerHost(urls.base) ? `@${this.config.handle}@${host}` : urls.actor,
56812
- summary: this.config.summary ? contentHtml(this.config.summary) : null,
56813
- icon: this.config.icon || null,
56814
- kind: this.config.kind
56815
- }));
56816
56886
  try {
56817
56887
  const wrote = await linkInWebIdProfile(this.remote, {
56818
56888
  actorUrl: urls.actor,
@@ -64148,6 +64218,9 @@ async function pinStatus(agent2, s, pinned) {
64148
64218
  if (s.actor !== agent2.publisher.urls.actor) throw new Error("not your status");
64149
64219
  const updated = agent2.store.updateStatus(s.noteId, { pinned: !!pinned });
64150
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
+ }
64151
64224
  agent2.publisher.publishProfile?.().catch(() => {
64152
64225
  });
64153
64226
  return updated || s;
@@ -65616,7 +65689,7 @@ function account(api, actorUrl, { selfAcct } = {}) {
65616
65689
  bot: false,
65617
65690
  discoverable: true,
65618
65691
  group: self2 ? api.store.getConfig()?.kind === "group" : cached.type === "Group",
65619
- created_at: "2026-01-01T00:00:00.000Z",
65692
+ created_at: self2 && api.store.getConfig()?.createdAt || "2026-01-01T00:00:00.000Z",
65620
65693
  note: (self2 ? api.store.getConfig()?.summary : cached.summary) || "",
65621
65694
  url: actorUrl,
65622
65695
  uri: actorUrl,
@@ -70541,6 +70614,7 @@ var BrowserAgent = class _BrowserAgent {
70541
70614
  try {
70542
70615
  await this.store.load({ force: true }).catch((e) => this.log(`re-reading state: ${e.message}`));
70543
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}`));
70544
70618
  this.deliverer?.startQueue?.();
70545
70619
  await this.publisher.publishProfile();
70546
70620
  await this.store.flush?.();