Loading examples/sveltekit-sample/src/lib/federation.ts +40 −3 Original line number Diff line number Diff line Loading @@ -6,7 +6,10 @@ import { generateCryptoKeyPair, Image, MemoryKvStore, Note, Person, PUBLIC_COLLECTION, type Recipient, Undo, } from "@fedify/fedify"; import { keyPairsStore, relationStore } from "./store"; Loading Loading @@ -63,8 +66,8 @@ federation if (result?.type !== "actor" || result.identifier !== IDENTIFIER) { return; } const follower = await follow.getActor(context); if (follower?.id == null) { const follower = await follow.getActor(context) as Person; if (!follower?.id || follower.id === null) { throw new Error("follower is null"); } await context.sendActivity( Loading @@ -79,7 +82,7 @@ federation object: follow, }), ); relationStore.set(follower.id.href, follow.objectId.href); relationStore.set(follower.id.href, follower); }) .on(Undo, async (context, undo) => { const activity = await undo.getObject(context); Loading @@ -96,4 +99,38 @@ federation } }); federation.setObjectDispatcher( Note, "/users/{identifier}/posts/{id}", (ctx, values) => { const id = ctx.getObjectUri(Note, values); const post = postStore.get(id); if (post == null) return null; return new Note({ id, attribution: ctx.getActorUri(values.identifier), to: PUBLIC_COLLECTION, cc: ctx.getFollowersUri(values.identifier), content: post.content, mediaType: "text/html", published: post.published, url: id, }); }, ); federation .setFollowersDispatcher( "/users/{identifier}/followers", () => { const followers = Array.from(relationStore.values()); const items: Recipient[] = followers.map((f) => ({ id: f.id, inboxId: f.inboxId, endpoints: f.endpoints, })); return { items }; }, ); export default federation; examples/sveltekit-sample/src/lib/store.ts +44 −8 Original line number Diff line number Diff line import type { Note, Person } from "@fedify/fedify"; declare global { var keyPairsStore: Map<string, Array<CryptoKeyPair>>; var relationStore: Map<string, string>; var relationStore: Map<string, Person>; var postStore: PostStore; } class PostStore { #map: Map<string, Note> = new Map(); #timeline: URL[] = []; constructor() {} #append(posts: Note[]) { posts.filter((p) => p.id && !this.#map.has(p.id.toString())) .forEach((p) => { this.#map.set(p.id!.toString(), p); this.#timeline.push(p.id!); }); } append = this.#append.bind(this); #get(id: URL) { return this.#map.get(id.toString()); } get = this.#get.bind(this); async #getAll() { return await Array.fromAsync( this.#timeline.reverse() .map((id) => id.toString()) .map((id) => this.#map.get(id)!) .filter((p) => p) .map((p) => p.toJsonLd()), ); } getAll = this.#getAll.bind(this); #delete(id: URL) { const existed = this.#map.delete(id.toString()); if (existed) { this.#timeline = this.#timeline.filter((i) => i !== id); } } delete = this.#delete.bind(this); } export const keyPairsStore: Map< string, Array<CryptoKeyPair> > = globalThis.keyPairsStore ?? new Map(); export const relationStore: Map<string, string> = globalThis.relationStore ?? new Map(); export const keyPairsStore = globalThis.keyPairsStore ?? new Map(); export const relationStore = globalThis.relationStore ?? new Map(); export const postStore = globalThis.postStore ?? new PostStore(); // this is just a hack to demo nextjs // this is just a hack to demo svelte // never do this in production, use safe and secure storage globalThis.keyPairsStore = keyPairsStore; globalThis.relationStore = relationStore; globalThis.postStore = postStore; examples/sveltekit-sample/src/routes/users/[identifier]/+page.svelte +0 −3 Original line number Diff line number Diff line Loading @@ -5,9 +5,6 @@ let { params }: PageProps = $props(); const { identifier } = params; $effect(() => { console.log(identifier); }); const data = browser ? fetch(`/users/${identifier}`, { headers: { Accept: "application/activity+json" }, Loading examples/sveltekit-sample/src/routes/users/[identifier]/posts/+page.server.ts 0 → 100644 +46 −0 Original line number Diff line number Diff line import type { Action, Actions } from "./$types"; import { error, redirect } from "@sveltejs/kit"; import { postStore } from "$lib/store"; import { Create, Note } from "@fedify/fedify"; import federation from "$lib/federation"; const post: Action = async (event) => { const data = await event.request.formData(); const content = data.get("content") as string; const identifier = data.get("identifier") as string; if (typeof content !== "string" && typeof identifier !== "string") { error(400, "Title and content are required"); } const ctx = federation.createContext(event.request, undefined); const id = crypto.randomUUID(); const attribution = ctx.getActorUri(identifier); const url = new URL(`/users/${identifier}/posts/${id}`, attribution); const post = new Note({ id: url, attribution, content, url, }); try { postStore.append([post!]); const note = await ctx.getObject(Note, { identifier, id }); await ctx.sendActivity( { identifier }, "followers", new Create({ id: new URL("#activity", attribution), object: note, actors: note?.attributionIds, tos: note?.toIds, ccs: note?.ccIds, }), ); // await getPosts().refresh(); } catch { postStore.delete(url); } redirect(303, `/users/${identifier}/posts`); }; export const actions = { post } satisfies Actions; examples/sveltekit-sample/src/routes/users/[identifier]/posts/+page.svelte 0 → 100644 +29 −0 Original line number Diff line number Diff line <script lang="ts"> import type { PageProps } from "./$types"; import { getPosts } from "./data.remote"; let { params }: PageProps = $props(); const { identifier } = params; const query = getPosts(); </script> <form method="POST" action="?/post"> <input name="identifier" type="hidden" value={identifier} /> <label> Content <input name="content" type="text" /> </label> <button>Post</button> </form> {#if query.error} <p>oops!</p> {:else if query.loading} <p>loading...</p> {:else if query.current} <ul> {#each query.current as note} <pre>{JSON.stringify(note, null, 2)}</pre> {/each} </ul> {/if} Loading
examples/sveltekit-sample/src/lib/federation.ts +40 −3 Original line number Diff line number Diff line Loading @@ -6,7 +6,10 @@ import { generateCryptoKeyPair, Image, MemoryKvStore, Note, Person, PUBLIC_COLLECTION, type Recipient, Undo, } from "@fedify/fedify"; import { keyPairsStore, relationStore } from "./store"; Loading Loading @@ -63,8 +66,8 @@ federation if (result?.type !== "actor" || result.identifier !== IDENTIFIER) { return; } const follower = await follow.getActor(context); if (follower?.id == null) { const follower = await follow.getActor(context) as Person; if (!follower?.id || follower.id === null) { throw new Error("follower is null"); } await context.sendActivity( Loading @@ -79,7 +82,7 @@ federation object: follow, }), ); relationStore.set(follower.id.href, follow.objectId.href); relationStore.set(follower.id.href, follower); }) .on(Undo, async (context, undo) => { const activity = await undo.getObject(context); Loading @@ -96,4 +99,38 @@ federation } }); federation.setObjectDispatcher( Note, "/users/{identifier}/posts/{id}", (ctx, values) => { const id = ctx.getObjectUri(Note, values); const post = postStore.get(id); if (post == null) return null; return new Note({ id, attribution: ctx.getActorUri(values.identifier), to: PUBLIC_COLLECTION, cc: ctx.getFollowersUri(values.identifier), content: post.content, mediaType: "text/html", published: post.published, url: id, }); }, ); federation .setFollowersDispatcher( "/users/{identifier}/followers", () => { const followers = Array.from(relationStore.values()); const items: Recipient[] = followers.map((f) => ({ id: f.id, inboxId: f.inboxId, endpoints: f.endpoints, })); return { items }; }, ); export default federation;
examples/sveltekit-sample/src/lib/store.ts +44 −8 Original line number Diff line number Diff line import type { Note, Person } from "@fedify/fedify"; declare global { var keyPairsStore: Map<string, Array<CryptoKeyPair>>; var relationStore: Map<string, string>; var relationStore: Map<string, Person>; var postStore: PostStore; } class PostStore { #map: Map<string, Note> = new Map(); #timeline: URL[] = []; constructor() {} #append(posts: Note[]) { posts.filter((p) => p.id && !this.#map.has(p.id.toString())) .forEach((p) => { this.#map.set(p.id!.toString(), p); this.#timeline.push(p.id!); }); } append = this.#append.bind(this); #get(id: URL) { return this.#map.get(id.toString()); } get = this.#get.bind(this); async #getAll() { return await Array.fromAsync( this.#timeline.reverse() .map((id) => id.toString()) .map((id) => this.#map.get(id)!) .filter((p) => p) .map((p) => p.toJsonLd()), ); } getAll = this.#getAll.bind(this); #delete(id: URL) { const existed = this.#map.delete(id.toString()); if (existed) { this.#timeline = this.#timeline.filter((i) => i !== id); } } delete = this.#delete.bind(this); } export const keyPairsStore: Map< string, Array<CryptoKeyPair> > = globalThis.keyPairsStore ?? new Map(); export const relationStore: Map<string, string> = globalThis.relationStore ?? new Map(); export const keyPairsStore = globalThis.keyPairsStore ?? new Map(); export const relationStore = globalThis.relationStore ?? new Map(); export const postStore = globalThis.postStore ?? new PostStore(); // this is just a hack to demo nextjs // this is just a hack to demo svelte // never do this in production, use safe and secure storage globalThis.keyPairsStore = keyPairsStore; globalThis.relationStore = relationStore; globalThis.postStore = postStore;
examples/sveltekit-sample/src/routes/users/[identifier]/+page.svelte +0 −3 Original line number Diff line number Diff line Loading @@ -5,9 +5,6 @@ let { params }: PageProps = $props(); const { identifier } = params; $effect(() => { console.log(identifier); }); const data = browser ? fetch(`/users/${identifier}`, { headers: { Accept: "application/activity+json" }, Loading
examples/sveltekit-sample/src/routes/users/[identifier]/posts/+page.server.ts 0 → 100644 +46 −0 Original line number Diff line number Diff line import type { Action, Actions } from "./$types"; import { error, redirect } from "@sveltejs/kit"; import { postStore } from "$lib/store"; import { Create, Note } from "@fedify/fedify"; import federation from "$lib/federation"; const post: Action = async (event) => { const data = await event.request.formData(); const content = data.get("content") as string; const identifier = data.get("identifier") as string; if (typeof content !== "string" && typeof identifier !== "string") { error(400, "Title and content are required"); } const ctx = federation.createContext(event.request, undefined); const id = crypto.randomUUID(); const attribution = ctx.getActorUri(identifier); const url = new URL(`/users/${identifier}/posts/${id}`, attribution); const post = new Note({ id: url, attribution, content, url, }); try { postStore.append([post!]); const note = await ctx.getObject(Note, { identifier, id }); await ctx.sendActivity( { identifier }, "followers", new Create({ id: new URL("#activity", attribution), object: note, actors: note?.attributionIds, tos: note?.toIds, ccs: note?.ccIds, }), ); // await getPosts().refresh(); } catch { postStore.delete(url); } redirect(303, `/users/${identifier}/posts`); }; export const actions = { post } satisfies Actions;
examples/sveltekit-sample/src/routes/users/[identifier]/posts/+page.svelte 0 → 100644 +29 −0 Original line number Diff line number Diff line <script lang="ts"> import type { PageProps } from "./$types"; import { getPosts } from "./data.remote"; let { params }: PageProps = $props(); const { identifier } = params; const query = getPosts(); </script> <form method="POST" action="?/post"> <input name="identifier" type="hidden" value={identifier} /> <label> Content <input name="content" type="text" /> </label> <button>Post</button> </form> {#if query.error} <p>oops!</p> {:else if query.loading} <p>loading...</p> {:else if query.current} <ul> {#each query.current as note} <pre>{JSON.stringify(note, null, 2)}</pre> {/each} </ul> {/if}