fedipod 1.29.2 → 1.32.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.
@@ -9700,6 +9700,7 @@ __export(wire_exports, {
9700
9700
  DEFAULT_ROOT: () => DEFAULT_ROOT,
9701
9701
  FOLLOWERS_PAGE_SIZE: () => FOLLOWERS_PAGE_SIZE,
9702
9702
  HASHTAG_RE: () => HASHTAG_RE,
9703
+ LEMMY_CTX: () => LEMMY_CTX,
9703
9704
  MENTION_RE: () => MENTION_RE,
9704
9705
  OUTBOX_PAGE_SIZE: () => OUTBOX_PAGE_SIZE,
9705
9706
  POLICY_CTX: () => POLICY_CTX,
@@ -9812,7 +9813,8 @@ function actorDoc({
9812
9813
  inbox = null,
9813
9814
  outbox = null,
9814
9815
  oauthAuthorize = null,
9815
- oauthToken = null
9816
+ oauthToken = null,
9817
+ postingRestrictedToMods = null
9816
9818
  }) {
9817
9819
  const context = [AS_CTX, SEC_CTX];
9818
9820
  context.push({ toot: "http://joinmastodon.org/ns#", featured: { "@id": "toot:featured", "@type": "@id" } });
@@ -9835,6 +9837,7 @@ function actorDoc({
9835
9837
  });
9836
9838
  }
9837
9839
  if (webId || aliases.length) context.push({ alsoKnownAs: { "@id": "as:alsoKnownAs", "@type": "@id" } });
9840
+ if (kind === "group" && postingRestrictedToMods !== null) context.push(LEMMY_CTX);
9838
9841
  if (fields.length) {
9839
9842
  context.push({ schema: "http://schema.org#", PropertyValue: "schema:PropertyValue", value: "schema:value" });
9840
9843
  }
@@ -9853,6 +9856,7 @@ function actorDoc({
9853
9856
  }] } : {},
9854
9857
  ...movedTo ? { movedTo } : {},
9855
9858
  ...webId || aliases.length ? { alsoKnownAs: [...webId ? [webId] : [], ...aliases] } : {},
9859
+ ...kind === "group" && postingRestrictedToMods !== null ? { postingRestrictedToMods: !!postingRestrictedToMods } : {},
9856
9860
  preferredUsername: handle7,
9857
9861
  name: name || handle7,
9858
9862
  // The bio, and the avatar. For a group, `summary` is where what the group
@@ -10375,7 +10379,7 @@ function addRemoveActivity({ urls, type, object, target, serial }) {
10375
10379
  target
10376
10380
  };
10377
10381
  }
10378
- var import_sanitize_html, AS_CTX, SEC_CTX, PUBLIC, DEFAULT_ROOT, assertionKeyId, OUTBOX_PAGE_SIZE, outboxPageId, outboxPageCount, outboxItemId, outboxWireItem, outboxLocalItem, FOLLOWERS_PAGE_SIZE, followersPageId, followersPageCount, ALLOWED_TAGS, ALLOWED_ATTRS, MAX_ATTACHMENTS, MAX_ATTACHMENT_URL, attachmentUrl, HTML_ESCAPES2, MENTION_RE, HASHTAG_RE, QUOTE_CTX, POLICY_CTX;
10382
+ var import_sanitize_html, AS_CTX, LEMMY_CTX, SEC_CTX, PUBLIC, DEFAULT_ROOT, assertionKeyId, OUTBOX_PAGE_SIZE, outboxPageId, outboxPageCount, outboxItemId, outboxWireItem, outboxLocalItem, FOLLOWERS_PAGE_SIZE, followersPageId, followersPageCount, ALLOWED_TAGS, ALLOWED_ATTRS, MAX_ATTACHMENTS, MAX_ATTACHMENT_URL, attachmentUrl, HTML_ESCAPES2, MENTION_RE, HASHTAG_RE, QUOTE_CTX, POLICY_CTX;
10379
10383
  var init_wire = __esm({
10380
10384
  "lib/core/wire.mjs"() {
10381
10385
  init_urls();
@@ -10383,6 +10387,7 @@ var init_wire = __esm({
10383
10387
  init_profile_page();
10384
10388
  import_sanitize_html = __toESM(require_sanitize_html(), 1);
10385
10389
  AS_CTX = "https://www.w3.org/ns/activitystreams";
10390
+ LEMMY_CTX = "https://join-lemmy.org/context.json";
10386
10391
  SEC_CTX = "https://w3id.org/security/v1";
10387
10392
  PUBLIC = "https://www.w3.org/ns/activitystreams#Public";
10388
10393
  DEFAULT_ROOT = "fedipod/";
@@ -57180,6 +57185,9 @@ var Publisher = class {
57180
57185
  fields: this.config.fields || [],
57181
57186
  webId: this.remote.webId || null,
57182
57187
  aliases: this.config.aliases || [],
57188
+ // A group says whether only its moderators may open posts (Lemmy's
57189
+ // term); a person's actor carries nothing of the kind.
57190
+ postingRestrictedToMods: this.config.kind === "group" ? !!this.config.postingRestrictedToMods : null,
57183
57191
  moderators,
57184
57192
  pendingFollowers: priv ? urls.pendingFollowers : null,
57185
57193
  pendingFollowing: priv ? urls.pendingFollowing : null,
@@ -69911,6 +69919,8 @@ var ADMIN_PATHS = /* @__PURE__ */ new Set([
69911
69919
  "/park",
69912
69920
  "/revive",
69913
69921
  "/takeover",
69922
+ "/gateway/pause",
69923
+ "/gateway/close",
69914
69924
  "/fediacct/connect",
69915
69925
  "/fediacct/disconnect",
69916
69926
  "/fediacct/callback"
@@ -70015,7 +70025,11 @@ var AdminFacade = class {
70015
70025
  frontActor: g?.frontActor || null,
70016
70026
  mode: g?.mode || "off",
70017
70027
  hasSecret: !!g?.hmacSecret,
70018
- stats: a.store.read("gateway-stats.json", { verified: 0, unverified: 0, lastAt: null })
70028
+ stats: a.store.read("gateway-stats.json", { verified: 0, unverified: 0, lastAt: null }),
70029
+ // The account's standing at the gateway — paused, closed — as the
70030
+ // gateway last said it (agent.mjs tellGateway). The record page
70031
+ // shows its pause and close controls only when this is here.
70032
+ standing: a.gatewayStanding || null
70019
70033
  });
70020
70034
  }
70021
70035
  case "/deadletter":
@@ -70159,6 +70173,24 @@ var AdminFacade = class {
70159
70173
  if (!await a.requestTakeover?.()) return json2(503, { error: "the lease could not be taken \u2014 the pod refused the write" });
70160
70174
  return json2(200, { ok: true, mode: a.status().mode });
70161
70175
  }
70176
+ // The account's standing at its gateway: paused by its owner, or closed
70177
+ // for good (front-core: accounts that go quiet). Both go to the gateway
70178
+ // with the pod session as proof, and its answer is the new standing.
70179
+ case "/gateway/pause": {
70180
+ if (typeof body.paused !== "boolean") return json2(400, { error: "paused must be true or false" });
70181
+ if (!a.pauseAtGateway || !a.gatewayApi) return json2(501, { error: "this account is not at a gateway" });
70182
+ const r = await a.pauseAtGateway(body.paused);
70183
+ if (!r) return json2(502, { error: "the gateway could not be reached" });
70184
+ return json2(r.status === 200 ? 200 : r.status, r);
70185
+ }
70186
+ case "/gateway/close": {
70187
+ const handle7 = String(a.store.getConfig()?.handle || "").toLowerCase();
70188
+ if (!handle7 || String(body.confirm || "").toLowerCase() !== handle7) return json2(400, { error: "type the handle to confirm" });
70189
+ if (!a.closeAtGateway || !a.gatewayApi) return json2(501, { error: "this account is not at a gateway" });
70190
+ const r = await a.closeAtGateway();
70191
+ if (!r) return json2(502, { error: "the gateway could not be reached" });
70192
+ return json2(r.status === 200 ? 200 : r.status, r);
70193
+ }
70162
70194
  // Going quiet, and coming back. The record page's active/parked select.
70163
70195
  case "/park": {
70164
70196
  if (!await a.requestTakeover?.()) return json2(503, { error: "another device is active for this pod \u2014 park from there" });
@@ -71700,10 +71732,46 @@ var BrowserAgent = class _BrowserAgent {
71700
71732
  this.log("took over from the other device (action here)");
71701
71733
  return true;
71702
71734
  }
71735
+ // One call to the gateway's owner API, proved with the pod session. What
71736
+ // comes back on 200 or 410 is the account's standing there, kept for the
71737
+ // manage page; anything else is logged and forgotten.
71738
+ async tellGateway(what, body) {
71739
+ if (!this.gatewayApi) return null;
71740
+ let res;
71741
+ try {
71742
+ res = await this.sessionFetch(`${this.gatewayApi}/${what}`, {
71743
+ method: "POST",
71744
+ headers: { "content-type": "application/json" },
71745
+ body: JSON.stringify(body)
71746
+ });
71747
+ } catch (e) {
71748
+ this.log(`gateway ${what}: ${e.message}`);
71749
+ return null;
71750
+ }
71751
+ const json2 = await res.json().catch(() => ({}));
71752
+ const out = { status: res.status, ...json2 };
71753
+ if (res.status === 200 || res.status === 410) this.gatewayStanding = out;
71754
+ else this.log(`gateway ${what}: ${res.status} ${json2.error || ""}`);
71755
+ return out;
71756
+ }
71757
+ openAtGateway() {
71758
+ return this.tellGateway("open", { handle: this.doorKey });
71759
+ }
71760
+ pauseAtGateway(paused) {
71761
+ return this.tellGateway("pause", { handle: this.doorKey, paused: !!paused });
71762
+ }
71763
+ closeAtGateway() {
71764
+ return this.tellGateway("close", { handle: this.doorKey, confirm: true });
71765
+ }
71766
+ static OPEN_EVERY_MS = 60 * 6e4;
71703
71767
  // Become the active agent: renew the lease, then start what a viewer skips —
71704
71768
  // publish the face, drain the inbox, run the mirrors.
71705
71769
  async goActive() {
71706
71770
  this.viewer = false;
71771
+ clearInterval(this._openTimer);
71772
+ this._openTimer = setInterval(() => {
71773
+ this.openAtGateway();
71774
+ }, _BrowserAgent.OPEN_EVERY_MS);
71707
71775
  this.lease.onLost = () => this.demote();
71708
71776
  this.lease.startRenewal();
71709
71777
  try {
@@ -71730,6 +71798,8 @@ var BrowserAgent = class _BrowserAgent {
71730
71798
  this.viewer = true;
71731
71799
  this.log("another device took over \u2014 read-only here");
71732
71800
  this.lease.stopRenewal();
71801
+ clearInterval(this._openTimer);
71802
+ this._openTimer = null;
71733
71803
  this.intake?.stop?.();
71734
71804
  this.deliverer?.stop?.();
71735
71805
  this.importer?.stop?.();
@@ -71843,6 +71913,29 @@ var BrowserAgent = class _BrowserAgent {
71843
71913
  sessionFetch: session.fetch,
71844
71914
  onGone: () => this.publisher.publishCollections({ followers: true })
71845
71915
  });
71916
+ this.gatewayApi = null;
71917
+ try {
71918
+ if (config.gateway?.url) this.gatewayApi = `${new URL(config.gateway.url).origin}/api`;
71919
+ } catch {
71920
+ this.gatewayApi = null;
71921
+ }
71922
+ this.doorKey = doorKeyOf(config.gateway?.url) || config.handle;
71923
+ this.gatewayStanding = null;
71924
+ const standing = await this.openAtGateway();
71925
+ if (standing?.status === 410) {
71926
+ const host = (() => {
71927
+ try {
71928
+ return new URL(this.gatewayApi).host;
71929
+ } catch {
71930
+ return "the gateway";
71931
+ }
71932
+ })();
71933
+ const address = this.doorKey.includes("@") ? `@${this.doorKey}` : `@${this.doorKey}@${host}`;
71934
+ const why = standing.closedBy === "owner" ? "you closed it" : "nothing opened it for six months";
71935
+ const e = new Error(`${address} is closed at ${host}: ${why}, and a closed address does not come back. Everything on your pod is untouched. The sign-in used here was ${webId}.`);
71936
+ e.code = "address-closed";
71937
+ throw e;
71938
+ }
71846
71939
  this.publisher = new Publisher({
71847
71940
  config: this.store.getConfig(),
71848
71941
  remote: this.remote,