social-federation tasks 5.1–5.6. Completes the inbound-federation story: a Mastodon follower now receives a trails user's new public activities in their home timeline. Outbox (5.1/5.2): - /users/:username/outbox — paginated OrderedCollection of public activities as Create(Note), newest first; unlisted/private never federate. Private-user 404 enforced at the route layer because Fedify builds collection-level responses from counter/cursors without consulting the page dispatcher. - Note shape: HTML content (escaped name/description/stats + link to the activity page) with structured PropertyValue attachments (distance-m, elevation-gain-m, duration-s) — Mastodon renders the text, trails consumers read the structured fields. Resolves the design open question toward Create(Note). - Authorized Fetch: signed and unsigned outbox fetches deliberately see the same (public-only) content until locked accounts exist. Push delivery (5.3–5.6): - createActivity / updateActivityVisibility(→public) enqueue one deliver-activity job per accepted remote follower; flips away from public and hard deletes enqueue Delete(Tombstone) retractions (enqueued before the row disappears). - deliver-activity job: re-reads the row at delivery time (skips if gone or no longer public), resolves the recipient inbox via the remote_actors cache with actor-document fetch fallback (priming the cache), HTTP-signs via the owner's key, and POSTs. retryLimit 8 + exponential backoff at enqueue time; outbound paced at 1 req/s per remote host. - Actor objects now advertise the outbox IRI. - @js-temporal/polyfill added (same range Fedify uses) for published timestamps; Fedify's types want the global esnext.temporal namespace, bridged with a documented cast. Tests: 9 unit tests for the AS mapping (escaping, stats, attachments, published fallback, stable ids, tombstones), 4 outbox integration tests (collection count, page shape/visibility filtering, private-404, delivery audience query). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
44 lines
1.5 KiB
TypeScript
44 lines
1.5 KiB
TypeScript
// Outbox listing queries (spec 5.1). Offset-paged, public-only —
|
|
// `unlisted` and `private` never federate. Separate from
|
|
// activities.server.ts because the outbox needs raw rows (no geojson
|
|
// batching) in a stable reverse-chronological order keyed on createdAt.
|
|
|
|
import { and, count, desc, eq } from "drizzle-orm";
|
|
import { activities } from "@trails-cool/db/schema/journal";
|
|
import { getDb } from "./db.ts";
|
|
import type { FederatableActivity } from "./federation-objects.server.ts";
|
|
|
|
export const OUTBOX_PAGE_SIZE = 20;
|
|
|
|
export async function listPublicActivitiesPage(
|
|
ownerId: string,
|
|
offset: number,
|
|
limit: number,
|
|
): Promise<FederatableActivity[]> {
|
|
const db = getDb();
|
|
return db
|
|
.select({
|
|
id: activities.id,
|
|
name: activities.name,
|
|
description: activities.description,
|
|
distance: activities.distance,
|
|
elevationGain: activities.elevationGain,
|
|
duration: activities.duration,
|
|
startedAt: activities.startedAt,
|
|
createdAt: activities.createdAt,
|
|
})
|
|
.from(activities)
|
|
.where(and(eq(activities.ownerId, ownerId), eq(activities.visibility, "public")))
|
|
.orderBy(desc(activities.createdAt))
|
|
.offset(offset)
|
|
.limit(limit);
|
|
}
|
|
|
|
export async function countPublicActivities(ownerId: string): Promise<number> {
|
|
const db = getDb();
|
|
const [row] = await db
|
|
.select({ n: count() })
|
|
.from(activities)
|
|
.where(and(eq(activities.ownerId, ownerId), eq(activities.visibility, "public")));
|
|
return row?.n ?? 0;
|
|
}
|