this repo has no description
at main 5.8 kB view raw
1import logger from "./logger"; 2 3import { and, eq } from "drizzle-orm"; 4import { nanoid } from "nanoid/non-secure"; 5 6import { read } from "bun:ffi"; 7import config from "../config"; 8import db from "../db"; 9import { schema } from "../db"; 10import { generateKeyPair, signData } from "./crypto"; 11import type { 12 AcceptActivity, 13 ActivityPubActivity, 14 CreateNoteActivity, 15 FederatedActor, 16 FollowActivity, 17} from "./types"; 18 19export function getBaseUrl(req?: Request): string { 20 if (req) { 21 const host = req.headers.get("host") || "localhost"; 22 const proto = req.headers.get("x-forwarded-proto") || "http"; 23 return `${proto}://${host}`; 24 } 25 26 return `${config.server.secure ? "https" : "http"}://${config.server.hostName || "localhost"}`; 27} 28 29export async function ensureActorExists( 30 userId: number, 31 baseUrl: string, 32): Promise<string> { 33 const existingActor = db 34 .select() 35 .from(schema.federatedActors) 36 .where( 37 and( 38 eq(schema.federatedActors.userId, userId), 39 eq(schema.federatedActors.actorType, "user"), 40 ), 41 ) 42 .get(); 43 44 if (existingActor) return existingActor.id; 45 const user = db 46 .select() 47 .from(schema.users) 48 .where(eq(schema.users.id, userId)) 49 .get(); 50 51 if (!user) throw new Error("User not found"); 52 const keys = await generateKeyPair(); 53 const actorUrl = `${baseUrl}/ap/users/${user.username}`; 54 try { 55 const actor = await db 56 .insert(schema.federatedActors) 57 .values({ 58 id: nanoid(), 59 userId, 60 communityId: null, 61 actorType: "user", 62 actorUrl, 63 inbox: `${actorUrl}/inbox`, 64 outbox: `${actorUrl}/outbox`, 65 followers: `${actorUrl}/followers`, 66 following: `${actorUrl}/following`, 67 publicKey: keys.publicKey, 68 privateKey: keys.privateKey, 69 sharedInbox: `${baseUrl}/ap/shared/inbox`, 70 }) 71 .returning(); 72 73 return actor[0].id; 74 } catch (error) { 75 console.error(error); 76 throw new Error("Failed to create actor"); 77 } 78} 79 80export async function ensureCommunityActorExists( 81 communityId: string, 82 baseUrl: string, 83): Promise<string> { 84 const existingActor = db 85 .select() 86 .from(schema.federatedActors) 87 .where( 88 and( 89 eq(schema.federatedActors.communityId, communityId), 90 eq(schema.federatedActors.actorType, "community"), 91 ), 92 ) 93 .get(); 94 95 if (existingActor) return existingActor.id; 96 97 const community = db 98 .select() 99 .from(schema.communities) 100 .where(eq(schema.communities.id, communityId)) 101 .get(); 102 103 if (!community) throw new Error("community not found"); 104 105 const keys = await generateKeyPair(); 106 const actorUrl = `${baseUrl}/ap/communities/${communityId}`; 107 108 const actor = await db 109 .insert(schema.federatedActors) 110 .values({ 111 id: nanoid(), 112 userId: null, 113 communityId, 114 actorType: "community", 115 actorUrl, 116 inbox: `${actorUrl}/inbox`, 117 outbox: `${actorUrl}/outbox`, 118 followers: `${actorUrl}/followers`, 119 following: `${actorUrl}/following`, 120 publicKey: keys.publicKey, 121 privateKey: keys.privateKey, 122 sharedInbox: `${baseUrl}/ap/shared/inbox`, 123 }) 124 .returning(); 125 126 return actor[0].id; 127} 128 129export function createMessageActivity( 130 actorUrl: string, 131 messageId: string, 132 content: string, 133 channelUrl: string, 134): CreateNoteActivity { 135 return { 136 "@context": "https://www.w3.org/ns/activitystreams", 137 id: `${actorUrl}/activities/${nanoid()}`, 138 type: "Create", 139 actor: actorUrl, 140 published: new Date().toISOString(), 141 to: [channelUrl], 142 object: { 143 id: `${actorUrl}/notes/${messageId}`, 144 type: "Note", 145 attributedTo: actorUrl, 146 content, 147 published: new Date().toISOString(), 148 to: [channelUrl], 149 }, 150 }; 151} 152 153export function createFollowActivity( 154 actorUrl: string, 155 targetActorUrl: string, 156): FollowActivity { 157 return { 158 "@context": "https://www.w3.org/ns/activitystreams", 159 id: `${actorUrl}/activities/${nanoid()}`, 160 type: "Follow", 161 actor: actorUrl, 162 object: targetActorUrl, 163 published: new Date().toISOString(), 164 }; 165} 166 167export function createAcceptActivity( 168 actorUrl: string, 169 followActivity: FollowActivity, 170): AcceptActivity { 171 return { 172 "@context": "https://www.w3.org/ns/activitystreams", 173 id: `${actorUrl}/activities/${nanoid()}`, 174 type: "Accept", 175 actor: actorUrl, 176 object: followActivity, 177 published: new Date().toISOString(), 178 }; 179} 180 181export async function deliverActivity( 182 activity: ActivityPubActivity, 183 targetInbox: string, 184 privateKey: string, 185 keyId: string, 186): Promise<boolean> { 187 try { 188 const activityJson = JSON.stringify(activity); 189 190 const date = new Date().toUTCString(); 191 const parsedUrl = new URL(targetInbox); 192 const path = parsedUrl.pathname; 193 194 const stringToSign = `(request-target): post ${path}\nhost: ${parsedUrl.host}\ndate: ${date}\ndigest: SHA-256=${Buffer.from(activityJson).toString("base64")}`; 195 const signature = await signData(stringToSign, privateKey); 196 const signatureHeader = `keyId="${keyId}",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="${signature}"`; 197 198 const response = await fetch(targetInbox, { 199 method: "POST", 200 headers: { 201 "Content-Type": "application/activity+json", 202 Host: parsedUrl.host, 203 Date: date, 204 Digest: `SHA-256=${Buffer.from(activityJson).toString("base64")}`, 205 Signature: signatureHeader, 206 }, 207 body: activityJson, 208 }); 209 210 if (!response.ok) { 211 logger.error( 212 `failed to deliver activity to ${targetInbox}: ${response.status} ${response.statusText}`, 213 ); 214 return false; 215 } 216 217 return true; 218 } catch (error) { 219 logger.error(`error delivering activity to ${targetInbox}:`, error); 220 return false; 221 } 222}