Initial toot-worker implementation
This commit is contained in:
@@ -0,0 +1,532 @@
|
||||
import {
|
||||
deleteActorFromCache,
|
||||
exportUserPublicKeyPem,
|
||||
findFavourite,
|
||||
findReblog,
|
||||
getStatus,
|
||||
getStatusByObjectId,
|
||||
getUserByUsername,
|
||||
recordNotification,
|
||||
upsertActorCache
|
||||
} from "./db";
|
||||
import {
|
||||
deliverToInboxes,
|
||||
isDuplicateActivity,
|
||||
notifyForLocalStatus,
|
||||
objectAsJson,
|
||||
objectIdString,
|
||||
parseActivity,
|
||||
recordRemoteActivity,
|
||||
resolveDeliveryInboxes,
|
||||
resolveRemoteActor,
|
||||
sendSignedActivity,
|
||||
verifyInboundSignature
|
||||
} from "./federation";
|
||||
import { activityJson, json } from "./http";
|
||||
import {
|
||||
ACTIVITY_CONTEXT,
|
||||
AVATAR_SVG,
|
||||
HEADER_SVG,
|
||||
PUBLIC_COLLECTION,
|
||||
SECURITY_CONTEXT
|
||||
} from "./types";
|
||||
import type { ActorCache, Json, RemoteActor, Status, User } from "./types";
|
||||
import {
|
||||
actorUrl,
|
||||
activityUrl,
|
||||
baseUrl,
|
||||
clampLimit,
|
||||
hostFromBaseUrl,
|
||||
id,
|
||||
objectUrl
|
||||
} from "./util";
|
||||
|
||||
export async function webFinger(request: Request, env: Env): Promise<Response> {
|
||||
const resource = new URL(request.url).searchParams.get("resource") ?? "";
|
||||
const match = resource.match(/^acct:([^@]+)@(.+)$/);
|
||||
if (!match) return json({ error: "not_found" }, 404);
|
||||
if (match[2].toLowerCase() !== hostFromBaseUrl(env).toLowerCase()) return json({ error: "not_found" }, 404);
|
||||
const user = await getUserByUsername(env, match[1]);
|
||||
if (!user) return json({ error: "not_found" }, 404);
|
||||
|
||||
return json({
|
||||
subject: `acct:${user.username}@${hostFromBaseUrl(env)}`,
|
||||
aliases: [actorUrl(env, user)],
|
||||
links: [
|
||||
{ rel: "self", type: "application/activity+json", href: actorUrl(env, user) },
|
||||
{ rel: "http://webfinger.net/rel/profile-page", type: "text/html", href: actorUrl(env, user) }
|
||||
]
|
||||
}, 200, { "content-type": "application/jrd+json; charset=utf-8" });
|
||||
}
|
||||
|
||||
export async function hostMeta(env: Env): Promise<Response> {
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
|
||||
<Link rel="lrdd" template="${baseUrl(env)}/.well-known/webfinger?resource={uri}"/>
|
||||
</XRD>`;
|
||||
return new Response(xml, { headers: { "content-type": "application/xrd+xml; charset=utf-8" } });
|
||||
}
|
||||
|
||||
export function nodeInfoLinks(env: Env): Response {
|
||||
return json({ links: [{ rel: "http://nodeinfo.diaspora.software/ns/schema/2.0", href: `${baseUrl(env)}/nodeinfo/2.0` }] });
|
||||
}
|
||||
|
||||
export async function nodeInfo(env: Env): Promise<Response> {
|
||||
const users = await env.DB.prepare("SELECT COUNT(*) AS count FROM users").first<{ count: number }>();
|
||||
const posts = await env.DB.prepare("SELECT COUNT(*) AS count FROM statuses").first<{ count: number }>();
|
||||
return json({
|
||||
version: "2.0",
|
||||
software: { name: "toot-worker", version: "0.3.0" },
|
||||
protocols: ["activitypub"],
|
||||
services: { inbound: [], outbound: [] },
|
||||
usage: { users: { total: users?.count ?? 0 }, localPosts: posts?.count ?? 0 },
|
||||
openRegistrations: false,
|
||||
metadata: { nodeName: env.INSTANCE_NAME, singleUserMode: true }
|
||||
});
|
||||
}
|
||||
|
||||
export async function actor(env: Env, username: string): Promise<Response> {
|
||||
const user = await getUserByUsername(env, username);
|
||||
if (!user) return json({ error: "not_found" }, 404);
|
||||
return activityJson(await actorDocument(env, user));
|
||||
}
|
||||
|
||||
export async function actorDocument(env: Env, user: User): Promise<Json> {
|
||||
const url = actorUrl(env, user);
|
||||
return {
|
||||
"@context": [ACTIVITY_CONTEXT, SECURITY_CONTEXT, { manuallyApprovesFollowers: "as:manuallyApprovesFollowers" }],
|
||||
id: url,
|
||||
type: "Person",
|
||||
preferredUsername: user.username,
|
||||
name: user.display_name,
|
||||
summary: user.note,
|
||||
url,
|
||||
inbox: `${url}/inbox`,
|
||||
outbox: `${url}/outbox`,
|
||||
followers: `${url}/followers`,
|
||||
following: `${url}/following`,
|
||||
endpoints: { sharedInbox: `${baseUrl(env)}/inbox` },
|
||||
icon: { type: "Image", mediaType: "image/svg+xml", url: `${baseUrl(env)}/avatar.png` },
|
||||
image: { type: "Image", mediaType: "image/svg+xml", url: `${baseUrl(env)}/header.png` },
|
||||
manuallyApprovesFollowers: false,
|
||||
discoverable: true,
|
||||
publicKey: {
|
||||
id: `${url}#main-key`,
|
||||
owner: url,
|
||||
publicKeyPem: await exportUserPublicKeyPem(user)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export async function outbox(request: Request, env: Env, username: string): Promise<Response> {
|
||||
const user = await getUserByUsername(env, username);
|
||||
if (!user) return json({ error: "not_found" }, 404);
|
||||
const url = new URL(request.url);
|
||||
const wantsPage = url.searchParams.has("page");
|
||||
const totalRow = await env.DB.prepare("SELECT COUNT(*) AS count FROM statuses WHERE user_id = ?").bind(user.id).first<{ count: number }>();
|
||||
const totalItems = totalRow?.count ?? 0;
|
||||
const base = `${actorUrl(env, user)}/outbox`;
|
||||
|
||||
if (!wantsPage) {
|
||||
return activityJson({
|
||||
"@context": ACTIVITY_CONTEXT,
|
||||
id: base,
|
||||
type: "OrderedCollection",
|
||||
totalItems,
|
||||
first: `${base}?page=true`
|
||||
});
|
||||
}
|
||||
|
||||
const limit = clampLimit(url.searchParams.get("limit"), 20, 40);
|
||||
const rows = await env.DB.prepare("SELECT * FROM statuses WHERE user_id = ? ORDER BY created_at DESC LIMIT ?").bind(user.id, limit).all<Status>();
|
||||
const items = rows.results.map((status) => createActivity(env, user, status));
|
||||
return activityJson({
|
||||
"@context": ACTIVITY_CONTEXT,
|
||||
id: `${base}?page=true`,
|
||||
partOf: base,
|
||||
type: "OrderedCollectionPage",
|
||||
orderedItems: items,
|
||||
totalItems
|
||||
});
|
||||
}
|
||||
|
||||
export async function followersCollection(env: Env, username: string): Promise<Response> {
|
||||
const user = await getUserByUsername(env, username);
|
||||
if (!user) return json({ error: "not_found" }, 404);
|
||||
const row = await env.DB.prepare("SELECT COUNT(*) AS count FROM follows WHERE local_user_id = ? AND accepted = 1").bind(user.id).first<{ count: number }>();
|
||||
return activityJson({
|
||||
"@context": ACTIVITY_CONTEXT,
|
||||
id: `${actorUrl(env, user)}/followers`,
|
||||
type: "Collection",
|
||||
totalItems: row?.count ?? 0
|
||||
});
|
||||
}
|
||||
|
||||
export async function followingCollection(env: Env, username: string): Promise<Response> {
|
||||
const user = await getUserByUsername(env, username);
|
||||
if (!user) return json({ error: "not_found" }, 404);
|
||||
const row = await env.DB.prepare("SELECT COUNT(*) AS count FROM outgoing_follows WHERE local_user_id = ? AND accepted = 1").bind(user.id).first<{ count: number }>();
|
||||
return activityJson({
|
||||
"@context": ACTIVITY_CONTEXT,
|
||||
id: `${actorUrl(env, user)}/following`,
|
||||
type: "Collection",
|
||||
totalItems: row?.count ?? 0
|
||||
});
|
||||
}
|
||||
|
||||
export async function activityObject(env: Env, objectId: string): Promise<Response> {
|
||||
const status = await getStatus(env, objectId);
|
||||
if (status) {
|
||||
const user = await env.DB.prepare("SELECT * FROM users WHERE id = ?").bind(status.user_id).first<User>();
|
||||
if (!user) return json({ error: "not_found" }, 404);
|
||||
return activityJson(noteObject(env, user, status));
|
||||
}
|
||||
const tomb = await env.DB.prepare("SELECT * FROM deleted_statuses WHERE id = ?").bind(objectId).first<{ id: string; deleted_at: string }>();
|
||||
if (tomb) {
|
||||
return activityJson({
|
||||
"@context": ACTIVITY_CONTEXT,
|
||||
id: `${baseUrl(env)}/objects/${tomb.id}`,
|
||||
type: "Tombstone",
|
||||
deleted: tomb.deleted_at
|
||||
}, 410);
|
||||
}
|
||||
return json({ error: "not_found" }, 404);
|
||||
}
|
||||
|
||||
export async function inboxHandler(request: Request, env: Env, username: string | null): Promise<Response> {
|
||||
const localUser = username ? await getUserByUsername(env, username) : null;
|
||||
if (username && !localUser) return json({ error: "not_found" }, 404);
|
||||
|
||||
const bodyText = await request.text();
|
||||
const activity = parseActivity(bodyText);
|
||||
if (!activity || !activity.type) return json({ error: "invalid_activity" }, 400);
|
||||
if (activity.activityId && await isDuplicateActivity(env, activity.activityId)) {
|
||||
return new Response(null, { status: 202 });
|
||||
}
|
||||
|
||||
const verified = await verifyInboundSignature(request, bodyText, env);
|
||||
if (!verified) return json({ error: "invalid_signature" }, 401);
|
||||
if (activity.actor && verified.actor.id !== activity.actor) {
|
||||
return json({ error: "actor_signature_mismatch" }, 401);
|
||||
}
|
||||
await recordRemoteActivity(env, activity, true);
|
||||
|
||||
const ctx: InboxContext = {
|
||||
env,
|
||||
activity,
|
||||
actorId: verified.actor.id,
|
||||
actorCache: verified.actor,
|
||||
localUser
|
||||
};
|
||||
|
||||
switch (activity.type) {
|
||||
case "Follow":
|
||||
return handleFollow(ctx);
|
||||
case "Undo":
|
||||
return handleUndo(ctx);
|
||||
case "Accept":
|
||||
return handleAccept(ctx);
|
||||
case "Reject":
|
||||
return handleReject(ctx);
|
||||
case "Like":
|
||||
return handleLike(ctx);
|
||||
case "Announce":
|
||||
return handleAnnounce(ctx);
|
||||
case "Delete":
|
||||
return handleDelete(ctx);
|
||||
case "Update":
|
||||
return handleUpdate(ctx);
|
||||
case "Create":
|
||||
return handleCreate(ctx);
|
||||
default:
|
||||
return new Response(null, { status: 202 });
|
||||
}
|
||||
}
|
||||
|
||||
type InboxContext = {
|
||||
env: Env;
|
||||
activity: NonNullable<ReturnType<typeof parseActivity>>;
|
||||
actorId: string;
|
||||
actorCache: ActorCache;
|
||||
localUser: User | null;
|
||||
};
|
||||
|
||||
async function handleFollow(ctx: InboxContext): Promise<Response> {
|
||||
const { env, activity, actorId, actorCache } = ctx;
|
||||
const object = objectIdString(activity.body.object);
|
||||
const localUser = await localUserFromTarget(env, object) ?? ctx.localUser;
|
||||
if (!localUser) return json({ error: "unknown_local_user" }, 404);
|
||||
|
||||
const inbox = actorCache.shared_inbox ?? actorCache.inbox;
|
||||
await env.DB.prepare(
|
||||
"INSERT OR REPLACE INTO follows (id, follower_actor, local_user_id, inbox, accepted, created_at) VALUES (?, ?, ?, ?, 1, ?)"
|
||||
).bind(id(), actorId, localUser.id, inbox, new Date().toISOString()).run();
|
||||
|
||||
await recordNotification(env, localUser.id, "follow", actorId, null);
|
||||
|
||||
await sendSignedActivity(env, localUser, actorCache.inbox, {
|
||||
"@context": [ACTIVITY_CONTEXT, SECURITY_CONTEXT],
|
||||
id: activityUrl(env, id()),
|
||||
type: "Accept",
|
||||
actor: actorUrl(env, localUser),
|
||||
object: activity.body
|
||||
});
|
||||
return new Response(null, { status: 202 });
|
||||
}
|
||||
|
||||
async function handleUndo(ctx: InboxContext): Promise<Response> {
|
||||
const { env, activity, actorId } = ctx;
|
||||
const inner = objectAsJson(activity.body.object);
|
||||
if (!inner) return new Response(null, { status: 202 });
|
||||
const innerType = String(inner.type ?? "");
|
||||
const localUser = ctx.localUser ?? await localUserFromTarget(env, objectIdString(inner.object));
|
||||
if (!localUser) return new Response(null, { status: 202 });
|
||||
|
||||
if (innerType === "Follow") {
|
||||
await env.DB.prepare("DELETE FROM follows WHERE follower_actor = ? AND local_user_id = ?").bind(actorId, localUser.id).run();
|
||||
} else if (innerType === "Like") {
|
||||
const target = objectIdString(inner.object);
|
||||
if (target) {
|
||||
const status = await getStatusByObjectId(env, target);
|
||||
if (status) await env.DB.prepare("DELETE FROM favourites WHERE status_id = ? AND actor = ?").bind(status.id, actorId).run();
|
||||
}
|
||||
} else if (innerType === "Announce") {
|
||||
const target = objectIdString(inner.object);
|
||||
if (target) {
|
||||
const status = await getStatusByObjectId(env, target);
|
||||
if (status) await env.DB.prepare("DELETE FROM reblogs WHERE status_id = ? AND actor = ?").bind(status.id, actorId).run();
|
||||
}
|
||||
}
|
||||
return new Response(null, { status: 202 });
|
||||
}
|
||||
|
||||
async function handleAccept(ctx: InboxContext): Promise<Response> {
|
||||
const { env, activity, actorId } = ctx;
|
||||
const inner = objectAsJson(activity.body.object);
|
||||
if (!inner || String(inner.type ?? "") !== "Follow") return new Response(null, { status: 202 });
|
||||
const innerActor = typeof inner.actor === "string" ? inner.actor : String((inner.actor as Json | undefined)?.id ?? "");
|
||||
const localUser = await localUserFromTarget(env, innerActor);
|
||||
if (!localUser) return new Response(null, { status: 202 });
|
||||
await env.DB.prepare(
|
||||
"UPDATE outgoing_follows SET accepted = 1 WHERE local_user_id = ? AND target_actor = ?"
|
||||
).bind(localUser.id, actorId).run();
|
||||
return new Response(null, { status: 202 });
|
||||
}
|
||||
|
||||
async function handleReject(ctx: InboxContext): Promise<Response> {
|
||||
const { env, actorId } = ctx;
|
||||
await env.DB.prepare("DELETE FROM outgoing_follows WHERE target_actor = ?").bind(actorId).run();
|
||||
return new Response(null, { status: 202 });
|
||||
}
|
||||
|
||||
async function handleLike(ctx: InboxContext): Promise<Response> {
|
||||
const { env, activity, actorId } = ctx;
|
||||
const target = objectIdString(activity.body.object);
|
||||
if (!target) return new Response(null, { status: 202 });
|
||||
const status = await getStatusByObjectId(env, target);
|
||||
if (!status) return new Response(null, { status: 202 });
|
||||
const existing = await findFavourite(env, status.id, actorId);
|
||||
if (existing) return new Response(null, { status: 202 });
|
||||
await env.DB.prepare(
|
||||
"INSERT INTO favourites (id, status_id, actor, activity_id, created_at) VALUES (?, ?, ?, ?, ?)"
|
||||
).bind(id(), status.id, actorId, activity.activityId || id(), new Date().toISOString()).run();
|
||||
await notifyForLocalStatus(env, status, "favourite", actorId);
|
||||
return new Response(null, { status: 202 });
|
||||
}
|
||||
|
||||
async function handleAnnounce(ctx: InboxContext): Promise<Response> {
|
||||
const { env, activity, actorId } = ctx;
|
||||
const target = objectIdString(activity.body.object);
|
||||
if (!target) return new Response(null, { status: 202 });
|
||||
const status = await getStatusByObjectId(env, target);
|
||||
if (!status) return new Response(null, { status: 202 });
|
||||
const existing = await findReblog(env, status.id, actorId);
|
||||
if (existing) return new Response(null, { status: 202 });
|
||||
await env.DB.prepare(
|
||||
"INSERT INTO reblogs (id, status_id, actor, activity_id, created_at) VALUES (?, ?, ?, ?, ?)"
|
||||
).bind(id(), status.id, actorId, activity.activityId || id(), new Date().toISOString()).run();
|
||||
await notifyForLocalStatus(env, status, "reblog", actorId);
|
||||
return new Response(null, { status: 202 });
|
||||
}
|
||||
|
||||
async function handleDelete(ctx: InboxContext): Promise<Response> {
|
||||
const { env, activity, actorId } = ctx;
|
||||
const target = objectIdString(activity.body.object);
|
||||
if (!target) return new Response(null, { status: 202 });
|
||||
|
||||
if (target === actorId) {
|
||||
await env.DB.prepare("DELETE FROM follows WHERE follower_actor = ?").bind(actorId).run();
|
||||
await env.DB.prepare("DELETE FROM outgoing_follows WHERE target_actor = ?").bind(actorId).run();
|
||||
await env.DB.prepare("DELETE FROM favourites WHERE actor = ?").bind(actorId).run();
|
||||
await env.DB.prepare("DELETE FROM reblogs WHERE actor = ?").bind(actorId).run();
|
||||
await env.DB.prepare("DELETE FROM notifications WHERE actor = ?").bind(actorId).run();
|
||||
await deleteActorFromCache(env, actorId);
|
||||
return new Response(null, { status: 202 });
|
||||
}
|
||||
|
||||
await env.DB.prepare("DELETE FROM favourites WHERE actor = ? AND activity_id LIKE ?").bind(actorId, `%${target}%`).run();
|
||||
return new Response(null, { status: 202 });
|
||||
}
|
||||
|
||||
async function handleUpdate(ctx: InboxContext): Promise<Response> {
|
||||
const { env, activity, actorId } = ctx;
|
||||
const obj = objectAsJson(activity.body.object);
|
||||
if (!obj) return new Response(null, { status: 202 });
|
||||
if (String(obj.type ?? "") === "Person" && obj.id === actorId) {
|
||||
await upsertActorCache(env, obj as unknown as RemoteActor);
|
||||
}
|
||||
return new Response(null, { status: 202 });
|
||||
}
|
||||
|
||||
async function handleCreate(ctx: InboxContext): Promise<Response> {
|
||||
const { env, activity, actorId } = ctx;
|
||||
const obj = objectAsJson(activity.body.object);
|
||||
if (!obj) return new Response(null, { status: 202 });
|
||||
|
||||
const recipients = collectRecipients(activity.body, obj);
|
||||
const localActorIds = new Set<string>();
|
||||
for (const target of recipients) {
|
||||
if (!target.startsWith(baseUrl(env))) continue;
|
||||
const m = target.match(/\/users\/([^/?#]+)$/);
|
||||
if (m) localActorIds.add(m[1]);
|
||||
}
|
||||
|
||||
for (const username of localActorIds) {
|
||||
const localUser = await getUserByUsername(env, username);
|
||||
if (!localUser) continue;
|
||||
const inReplyTo = typeof obj.inReplyTo === "string" ? obj.inReplyTo : null;
|
||||
let statusId: string | null = null;
|
||||
if (inReplyTo) {
|
||||
const parent = await getStatusByObjectId(env, inReplyTo);
|
||||
if (parent) statusId = parent.id;
|
||||
}
|
||||
await recordNotification(env, localUser.id, "mention", actorId, statusId);
|
||||
}
|
||||
return new Response(null, { status: 202 });
|
||||
}
|
||||
|
||||
function collectRecipients(activity: Json, object: Json): string[] {
|
||||
const fields: unknown[] = [activity.to, activity.cc, activity.bto, activity.bcc, object.to, object.cc];
|
||||
const out = new Set<string>();
|
||||
for (const field of fields) {
|
||||
if (Array.isArray(field)) {
|
||||
for (const value of field) {
|
||||
if (typeof value === "string") out.add(value);
|
||||
}
|
||||
} else if (typeof field === "string") {
|
||||
out.add(field);
|
||||
}
|
||||
}
|
||||
return [...out];
|
||||
}
|
||||
|
||||
async function localUserFromTarget(env: Env, actorId: string | null): Promise<User | null> {
|
||||
if (!actorId) return null;
|
||||
if (!actorId.startsWith(baseUrl(env))) return null;
|
||||
const match = actorId.match(/\/users\/([^/?#]+)$/);
|
||||
if (!match) return null;
|
||||
return getUserByUsername(env, match[1]);
|
||||
}
|
||||
|
||||
export function createActivity(env: Env, user: User, status: Status, extra: { to?: string[]; cc?: string[] } = {}): Json {
|
||||
const to = extra.to ?? [PUBLIC_COLLECTION];
|
||||
const cc = extra.cc ?? [`${actorUrl(env, user)}/followers`];
|
||||
return {
|
||||
"@context": [ACTIVITY_CONTEXT, SECURITY_CONTEXT],
|
||||
id: status.activity_id,
|
||||
type: "Create",
|
||||
actor: actorUrl(env, user),
|
||||
published: status.created_at,
|
||||
to,
|
||||
cc,
|
||||
object: noteObject(env, user, status, { to, cc })
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteActivity(env: Env, user: User, status: Status): Json {
|
||||
return {
|
||||
"@context": [ACTIVITY_CONTEXT, SECURITY_CONTEXT],
|
||||
id: activityUrl(env, id()),
|
||||
type: "Delete",
|
||||
actor: actorUrl(env, user),
|
||||
to: [PUBLIC_COLLECTION],
|
||||
object: {
|
||||
id: status.object_id,
|
||||
type: "Tombstone"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function followActivity(env: Env, user: User, target: string, activityId: string): Json {
|
||||
return {
|
||||
"@context": [ACTIVITY_CONTEXT, SECURITY_CONTEXT],
|
||||
id: activityId,
|
||||
type: "Follow",
|
||||
actor: actorUrl(env, user),
|
||||
object: target
|
||||
};
|
||||
}
|
||||
|
||||
export function undoActivity(env: Env, user: User, inner: Json): Json {
|
||||
return {
|
||||
"@context": [ACTIVITY_CONTEXT, SECURITY_CONTEXT],
|
||||
id: activityUrl(env, id()),
|
||||
type: "Undo",
|
||||
actor: actorUrl(env, user),
|
||||
object: inner
|
||||
};
|
||||
}
|
||||
|
||||
export function likeActivity(env: Env, user: User, target: string, activityId: string): Json {
|
||||
return {
|
||||
"@context": [ACTIVITY_CONTEXT, SECURITY_CONTEXT],
|
||||
id: activityId,
|
||||
type: "Like",
|
||||
actor: actorUrl(env, user),
|
||||
object: target
|
||||
};
|
||||
}
|
||||
|
||||
export function announceActivity(env: Env, user: User, target: string, activityId: string): Json {
|
||||
return {
|
||||
"@context": [ACTIVITY_CONTEXT, SECURITY_CONTEXT],
|
||||
id: activityId,
|
||||
type: "Announce",
|
||||
actor: actorUrl(env, user),
|
||||
published: new Date().toISOString(),
|
||||
to: [PUBLIC_COLLECTION],
|
||||
cc: [`${actorUrl(env, user)}/followers`],
|
||||
object: target
|
||||
};
|
||||
}
|
||||
|
||||
export function updatePersonActivity(env: Env, user: User, doc: Json): Json {
|
||||
return {
|
||||
"@context": [ACTIVITY_CONTEXT, SECURITY_CONTEXT],
|
||||
id: activityUrl(env, id()),
|
||||
type: "Update",
|
||||
actor: actorUrl(env, user),
|
||||
to: [PUBLIC_COLLECTION],
|
||||
object: doc
|
||||
};
|
||||
}
|
||||
|
||||
export function noteObject(env: Env, user: User, status: Status, opts: { to?: string[]; cc?: string[]; attachments?: Json[]; tag?: Json[] } = {}): Json {
|
||||
return {
|
||||
id: status.object_id,
|
||||
type: "Note",
|
||||
summary: status.summary || null,
|
||||
sensitive: Boolean(status.sensitive),
|
||||
inReplyTo: status.in_reply_to_id ? objectUrl(env, status.in_reply_to_id) : null,
|
||||
attributedTo: actorUrl(env, user),
|
||||
content: status.content,
|
||||
published: status.created_at,
|
||||
url: status.url,
|
||||
to: opts.to ?? [PUBLIC_COLLECTION],
|
||||
cc: opts.cc ?? [`${actorUrl(env, user)}/followers`],
|
||||
attachment: opts.attachments ?? [],
|
||||
tag: opts.tag ?? []
|
||||
};
|
||||
}
|
||||
|
||||
export { AVATAR_SVG, HEADER_SVG };
|
||||
Reference in New Issue
Block a user