Unverified Commit 811b623e authored by Hong Minhee's avatar Hong Minhee
Browse files

Publish posts to the fediverse

parent 9ec5aff3
Loading
Loading
Loading
Loading
+12 −13
Original line number Diff line number Diff line
import { Federation } from "fedify/federation/middleware.ts";
import { isActor } from "fedify/vocab/actor.ts";
import { getActorTypeName, isActor } from "fedify/vocab/actor.ts";
import {
  Accept,
  Activity,
  Create,
  Follow,
  Note,
  Link,
  Person,
  Undo,
} from "fedify/vocab/mod.ts";
import { getBlog } from "../models/blog.ts";
import { addFollower, removeFollower } from "../models/follower.ts";
import { openKv } from "../models/kv.ts";
import { countPosts, getPosts } from "../models/post.ts";
import { addFollower } from "fedify/examples/blog/models/follower.ts";
import { Link } from "fedify/vocab/mod.ts";
import { removeFollower } from "fedify/examples/blog/models/follower.ts";
import { countPosts, getPosts, toNote } from "../models/post.ts";

// The `Federation<TContextData>` object is a registry that registers
// federation-related callbacks:
@@ -69,13 +67,10 @@ federation.setOutboxDispatcher(
    );
    for await (const post of posts) {
      const activity = new Create({
        id: new URL(`/posts/${post.uuid}#activity`, ctx.request.url),
        actor: ctx.getActorUri(handle),
        object: new Note({
          attributedTo: ctx.getActorUri(handle),
          content: post.content,
          published: post.published,
          url: new URL(`/posts/${post.uuid}`, ctx.request.url),
        }),
        to: new URL("https://www.w3.org/ns/activitystreams#Public"),
        object: toNote(ctx, blog, post),
      });
      activities.push(activity);
    }
@@ -115,7 +110,8 @@ federation.setInboxListeners("/users/{handle}/inbox")
    const recipient = await follow.getActor(ctx);
    if (
      !isActor(recipient) || recipient.id == null ||
      recipient.preferredUsername == null
      recipient.preferredUsername == null ||
      recipient.inboxId == null
    ) return;
    const handle =
      `@${recipient.preferredUsername.toString()}@${recipient.id.host}`;
@@ -129,6 +125,9 @@ federation.setInboxListeners("/users/{handle}/inbox")
        ? (recipient.url.href ?? recipient.id).href
        : recipient.url.href,
      handle,
      inbox: recipient.inboxId.href,
      sharedInbox: recipient.endpoints?.sharedInbox?.href,
      typeName: getActorTypeName(recipient),
    });
    await ctx.sendActivity(
      { handle: blog.handle },
+28 −0
Original line number Diff line number Diff line
import {
  Actor,
  ActorTypeName,
  getActorClassByTypeName,
} from "fedify/vocab/actor.ts";
import { Endpoints } from "fedify/vocab/mod.ts";
import { openKv } from "./kv.ts";

export interface Follower {
@@ -6,6 +12,9 @@ export interface Follower {
  name: string;
  url: string;
  handle: string;
  inbox: string;
  sharedInbox?: string;
  typeName: ActorTypeName;
}

export async function addFollower(follower: Follower): Promise<Follower> {
@@ -56,3 +65,22 @@ export async function countFollowers(): Promise<bigint> {
  const record = await kv.get(["followers"]);
  return (record?.value as bigint | null) ?? 0n;
}

export async function getFollowersAsActors(): Promise<Actor[]> {
  const kv = await openKv();
  const actors: Actor[] = [];
  for await (const f of kv.list<Follower>({ prefix: ["follower"] })) {
    const cls = getActorClassByTypeName(f.value.typeName);
    const actor = new cls({
      id: new URL(f.value.id),
      inbox: new URL(f.value.inbox),
      endpoints: new Endpoints({
        sharedInbox: f.value.sharedInbox
          ? new URL(f.value.sharedInbox)
          : undefined,
      }),
    });
    actors.push(actor);
  }
  return actors;
}
+20 −0
Original line number Diff line number Diff line
import { RequestContext } from "fedify/federation/context.ts";
import { Note } from "fedify/vocab/mod.ts";
import markdownIt from "markdown-it";
import { uuidv7 } from "uuidv7";
import { Blog } from "./blog.ts";
import { openKv } from "./kv.ts";

export interface Post {
@@ -65,3 +68,20 @@ export function getContentHtml(post: Post): string {
  const md = markdownIt();
  return md.render(post.content);
}

export function toNote(
  context: RequestContext<void>,
  blog: Blog,
  post: Post,
): Note {
  const url = new URL(`/posts/${post.uuid}`, context.request.url);
  return new Note({
    id: url,
    attributedTo: context.getActorUri(blog.handle),
    to: new URL("https://www.w3.org/ns/activitystreams#Public"),
    summary: post.title,
    content: post.content,
    published: post.published,
    url,
  });
}
+21 −1
Original line number Diff line number Diff line
import { Handler, PageProps } from "$fresh/server.ts";
import { Head } from "$fresh/runtime.ts";
import { accepts } from "$std/http/mod.ts";
import Post from "../../components/Post.tsx";
import { federation } from "../../federation/mod.ts";
import { Blog, getBlog } from "../../models/blog.ts";
import { getPost, type Post as PostModel } from "../../models/post.ts";
import { getPost, type Post as PostModel, toNote } from "../../models/post.ts";

export interface PostPageData {
  domain: string;
@@ -15,6 +17,24 @@ export const handler: Handler<PostPageData> = async (req, ctx) => {
  if (blog == null) return await ctx.renderNotFound();
  const post = await getPost(ctx.params.uuid);
  if (post == null) return await ctx.renderNotFound();
  const accept = accepts(
    req,
    "application/ld+json",
    "application/json",
    "text/html",
    "application/xhtml+xml",
  );
  if (accept == "application/ld+json" || accept == "application/json") {
    const fedCtx = federation.createContext(req);
    const note = toNote(fedCtx, blog, post);
    const jsonLd = await note.toJsonLd(fedCtx);
    return new Response(JSON.stringify(jsonLd), {
      headers: {
        "Content-Type": "application/ld+json",
        Vary: "Accept",
      },
    });
  }
  const data: PostPageData = { blog, post, domain: ctx.url.host };
  return ctx.render(data);
};
+28 −4
Original line number Diff line number Diff line
import { Handlers, PageProps } from "$fresh/server.ts";
import { Blog, getBlog, verifyPassword } from "../../models/blog.ts";
import { countFollowers } from "../../models/follower.ts";
import { addPost, countPosts, getPosts, Post } from "../../models/post.ts";
import { getFollowersAsActors } from "fedify/examples/blog/models/follower.ts";
import { Create, Note } from "fedify/vocab/mod.ts";
import { PostFormProps } from "../../components/PostForm.tsx";
import PostList from "../../components/PostList.tsx";
import { federation } from "../../federation/mod.ts";
import { Blog, getBlog, verifyPassword } from "../../models/blog.ts";
import { countFollowers } from "../../models/follower.ts";
import {
  addPost,
  countPosts,
  getPosts,
  Post,
  toNote,
} from "../../models/post.ts";

interface PostsData extends PostFormProps {
  blog: Blog;
@@ -67,7 +76,22 @@ export const handler: Handlers<PostsData> = {
        defaultValues: { title, content },
      });
    }
    await addPost({ title, content, published: Temporal.Now.instant() });
    const post = await addPost({
      title,
      content,
      published: Temporal.Now.instant(),
    });
    const fedCtx = await federation.createContext(req);
    await fedCtx.sendActivity(
      { handle: blog.handle },
      await getFollowersAsActors(),
      new Create({
        id: new URL(`/posts/${post.uuid}#activity`, req.url),
        actor: fedCtx.getActorUri(blog.handle),
        to: new URL("https://www.w3.org/ns/activitystreams#Public"),
        object: toNote(fedCtx, blog, post),
      }),
    );

    const { posts, nextCursor } = await getPosts();
    const total = await countPosts();
Loading