Commit 6b4cc9e8 authored by ChanHaeng Lee's avatar ChanHaeng Lee
Browse files

Implement posting

parent cc89eba5
Loading
Loading
Loading
Loading
+40 −3
Original line number Diff line number Diff line
@@ -6,7 +6,10 @@ import {
  generateCryptoKeyPair,
  Image,
  MemoryKvStore,
  Note,
  Person,
  PUBLIC_COLLECTION,
  type Recipient,
  Undo,
} from "@fedify/fedify";
import { keyPairsStore, relationStore } from "./store";
@@ -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(
@@ -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);
@@ -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;
+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;
+0 −3
Original line number Diff line number Diff line
@@ -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" },
+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;
+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