fedbox 0.0.1 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/actions.js ADDED
@@ -0,0 +1,312 @@
1
+ /**
2
+ * Fedbox Actions
3
+ * User actions: post, follow, reply, timeline
4
+ */
5
+
6
+ import { readFileSync } from 'fs'
7
+ import { outbox, webfinger } from 'microfed'
8
+ import {
9
+ initStore,
10
+ savePost,
11
+ getPosts,
12
+ getPost,
13
+ getFollowers,
14
+ getFollowing,
15
+ addFollowing,
16
+ getActivities,
17
+ cacheActor,
18
+ getCachedActor
19
+ } from './store.js'
20
+
21
+ let config = null
22
+
23
+ /**
24
+ * Load config
25
+ */
26
+ function loadConfig() {
27
+ config = JSON.parse(readFileSync('fedbox.json', 'utf8'))
28
+ return config
29
+ }
30
+
31
+ /**
32
+ * Get base URL
33
+ */
34
+ function getBaseUrl() {
35
+ const domain = config.domain || `localhost:${config.port}`
36
+ const protocol = config.domain ? 'https' : 'http'
37
+ return `${protocol}://${domain}`
38
+ }
39
+
40
+ /**
41
+ * Get profile URL (the document)
42
+ */
43
+ function getProfileUrl() {
44
+ return `${getBaseUrl()}/${config.username}`
45
+ }
46
+
47
+ /**
48
+ * Get actor URL (WebID with #me fragment)
49
+ */
50
+ function getActorUrl() {
51
+ return `${getProfileUrl()}#me`
52
+ }
53
+
54
+ /**
55
+ * Fetch remote actor
56
+ */
57
+ async function fetchActor(id) {
58
+ const cached = getCachedActor(id)
59
+ if (cached) return cached
60
+
61
+ try {
62
+ const response = await fetch(id, {
63
+ headers: { 'Accept': 'application/activity+json' }
64
+ })
65
+ if (!response.ok) return null
66
+ const actor = await response.json()
67
+ cacheActor(actor)
68
+ return actor
69
+ } catch {
70
+ return null
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Post a note to followers
76
+ */
77
+ export async function post(content, inReplyTo = null) {
78
+ loadConfig()
79
+ initStore()
80
+
81
+ const actorUrl = getActorUrl()
82
+ const profileUrl = getProfileUrl()
83
+ const noteId = `${profileUrl}/posts/${Date.now()}`
84
+
85
+ // Create the Note
86
+ const note = outbox.createNote(actorUrl, content, {
87
+ id: noteId,
88
+ inReplyTo,
89
+ to: ['https://www.w3.org/ns/activitystreams#Public'],
90
+ cc: [`${profileUrl}/followers`]
91
+ })
92
+
93
+ // Wrap in Create activity
94
+ const create = {
95
+ '@context': 'https://www.w3.org/ns/activitystreams',
96
+ type: 'Create',
97
+ id: `${noteId}/activity`,
98
+ actor: actorUrl,
99
+ published: new Date().toISOString(),
100
+ to: note.to,
101
+ cc: note.cc,
102
+ object: note
103
+ }
104
+
105
+ // Save to local posts
106
+ savePost(noteId, content, inReplyTo)
107
+
108
+ // Deliver to all followers
109
+ const followers = getFollowers()
110
+ const results = { success: 0, failed: 0 }
111
+
112
+ for (const follower of followers) {
113
+ if (!follower.inbox) continue
114
+ try {
115
+ await outbox.send({
116
+ activity: create,
117
+ inbox: follower.inbox,
118
+ privateKey: config.privateKey,
119
+ keyId: `${profileUrl}#main-key`
120
+ })
121
+ results.success++
122
+ } catch (err) {
123
+ results.failed++
124
+ }
125
+ }
126
+
127
+ return { noteId, note, delivered: results }
128
+ }
129
+
130
+ /**
131
+ * Follow a remote user
132
+ */
133
+ export async function follow(handle) {
134
+ loadConfig()
135
+ initStore()
136
+
137
+ // Parse handle (@user@domain or user@domain)
138
+ const cleanHandle = handle.replace(/^@/, '')
139
+ const [username, domain] = cleanHandle.split('@')
140
+
141
+ if (!username || !domain) {
142
+ throw new Error('Invalid handle. Use format: @user@domain or user@domain')
143
+ }
144
+
145
+ // Resolve via WebFinger
146
+ console.log(`🔍 Looking up ${cleanHandle}...`)
147
+ const resolved = await webfinger.resolve(username, domain)
148
+
149
+ if (!resolved) {
150
+ throw new Error(`Could not find ${cleanHandle}`)
151
+ }
152
+
153
+ // Fetch the actor
154
+ console.log(`📥 Fetching actor...`)
155
+ const remoteActor = await fetchActor(resolved.actorId)
156
+
157
+ if (!remoteActor) {
158
+ throw new Error(`Could not fetch actor: ${resolved.actorId}`)
159
+ }
160
+
161
+ const actorUrl = getActorUrl()
162
+ const profileUrl = getProfileUrl()
163
+ const inbox = remoteActor.inbox
164
+
165
+ // Create Follow activity
166
+ const followActivity = outbox.createFollow(actorUrl, remoteActor.id)
167
+
168
+ // Send Follow
169
+ console.log(`📤 Sending Follow to ${inbox}...`)
170
+ await outbox.send({
171
+ activity: followActivity,
172
+ inbox,
173
+ privateKey: config.privateKey,
174
+ keyId: `${profileUrl}#main-key`
175
+ })
176
+
177
+ // Save to following (pending acceptance)
178
+ addFollowing(remoteActor.id, false)
179
+
180
+ return {
181
+ actor: remoteActor,
182
+ followActivity
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Reply to a post
188
+ */
189
+ export async function reply(postUrl, content) {
190
+ loadConfig()
191
+ initStore()
192
+
193
+ // Fetch the original post to get the author
194
+ let originalAuthor = null
195
+ try {
196
+ const response = await fetch(postUrl, {
197
+ headers: { 'Accept': 'application/activity+json' }
198
+ })
199
+ if (response.ok) {
200
+ const post = await response.json()
201
+ originalAuthor = post.attributedTo
202
+ }
203
+ } catch {
204
+ // Continue without original author
205
+ }
206
+
207
+ const actorUrl = getActorUrl()
208
+ const profileUrl = getProfileUrl()
209
+ const noteId = `${profileUrl}/posts/${Date.now()}`
210
+
211
+ // Create the reply Note
212
+ const note = outbox.createNote(actorUrl, content, {
213
+ id: noteId,
214
+ inReplyTo: postUrl,
215
+ to: ['https://www.w3.org/ns/activitystreams#Public'],
216
+ cc: [`${profileUrl}/followers`, originalAuthor].filter(Boolean)
217
+ })
218
+
219
+ // Wrap in Create activity
220
+ const create = {
221
+ '@context': 'https://www.w3.org/ns/activitystreams',
222
+ type: 'Create',
223
+ id: `${noteId}/activity`,
224
+ actor: actorUrl,
225
+ published: new Date().toISOString(),
226
+ to: note.to,
227
+ cc: note.cc,
228
+ object: note
229
+ }
230
+
231
+ // Save locally
232
+ savePost(noteId, content, postUrl)
233
+
234
+ // Collect inboxes to deliver to
235
+ const inboxes = new Set()
236
+
237
+ // Add all followers
238
+ const followers = getFollowers()
239
+ for (const f of followers) {
240
+ if (f.inbox) inboxes.add(f.inbox)
241
+ }
242
+
243
+ // Add original author's inbox
244
+ if (originalAuthor) {
245
+ const authorActor = await fetchActor(originalAuthor)
246
+ if (authorActor?.inbox) {
247
+ inboxes.add(authorActor.inbox)
248
+ }
249
+ }
250
+
251
+ // Deliver
252
+ const results = { success: 0, failed: 0 }
253
+ for (const inbox of inboxes) {
254
+ try {
255
+ await outbox.send({
256
+ activity: create,
257
+ inbox,
258
+ privateKey: config.privateKey,
259
+ keyId: `${profileUrl}#main-key`
260
+ })
261
+ results.success++
262
+ } catch {
263
+ results.failed++
264
+ }
265
+ }
266
+
267
+ return { noteId, note, delivered: results }
268
+ }
269
+
270
+ /**
271
+ * Get timeline (posts from people we follow + mentions)
272
+ */
273
+ export function timeline(limit = 20) {
274
+ loadConfig()
275
+ initStore()
276
+
277
+ // Get Create activities from inbox
278
+ const activities = getActivities(100)
279
+
280
+ const posts = activities
281
+ .filter(a => a.type === 'Create' && a.raw?.object)
282
+ .map(a => {
283
+ const obj = a.raw.object
284
+ return {
285
+ id: obj.id || a.id,
286
+ author: typeof a.raw.actor === 'string' ? a.raw.actor : a.raw.actor?.id,
287
+ content: obj.content || '',
288
+ published: obj.published || a.created_at,
289
+ inReplyTo: obj.inReplyTo
290
+ }
291
+ })
292
+ .slice(0, limit)
293
+
294
+ return posts
295
+ }
296
+
297
+ /**
298
+ * Get our own posts
299
+ */
300
+ export function myPosts(limit = 20) {
301
+ loadConfig()
302
+ initStore()
303
+ return getPosts(limit)
304
+ }
305
+
306
+ export default {
307
+ post,
308
+ follow,
309
+ reply,
310
+ timeline,
311
+ myPosts
312
+ }