fedbox 0.0.1

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/README.md ADDED
@@ -0,0 +1,99 @@
1
+ # 📦 Fedbox
2
+
3
+ **Zero to Fediverse in 60 seconds.**
4
+
5
+ Fedbox is the fastest way to get your own identity on the Fediverse. Run your own ActivityPub server, federate with Mastodon, and own your social presence.
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ # Install
11
+ npm install -g fedbox
12
+
13
+ # Set up your identity
14
+ fedbox init
15
+
16
+ # Start your server
17
+ fedbox start
18
+ ```
19
+
20
+ That's it. You're on the Fediverse.
21
+
22
+ ## Federation (so Mastodon can find you)
23
+
24
+ To federate with the wider Fediverse, you need a public HTTPS URL. The easiest way:
25
+
26
+ ```bash
27
+ # In another terminal
28
+ ngrok http 3000
29
+ ```
30
+
31
+ Copy your ngrok URL (e.g., `abc123.ngrok.io`) and add it to `fedbox.json`:
32
+
33
+ ```json
34
+ {
35
+ "domain": "abc123.ngrok.io",
36
+ ...
37
+ }
38
+ ```
39
+
40
+ Restart your server, and you're federated! Search for `@yourname@abc123.ngrok.io` on Mastodon.
41
+
42
+ ## What You Get
43
+
44
+ - **Your own identity** — `@you@yourdomain.com`
45
+ - **ActivityPub compatible** — Works with Mastodon, Pleroma, Pixelfed, etc.
46
+ - **Persistent storage** — SQLite database for followers, posts, activities
47
+ - **Beautiful profile page** — Dark theme, looks great
48
+ - **Zero config** — Just answer a few questions
49
+
50
+ ## Commands
51
+
52
+ ```bash
53
+ fedbox init # Set up your identity
54
+ fedbox start # Start the server
55
+ fedbox status # Show current config
56
+ fedbox help # Show help
57
+ ```
58
+
59
+ ## How It Works
60
+
61
+ Fedbox uses [microfed](https://github.com/micro-fed/microfed.org) for ActivityPub primitives:
62
+
63
+ - **Profile** — Your actor/identity
64
+ - **Inbox** — Receive follows, likes, boosts
65
+ - **Outbox** — Your posts
66
+ - **WebFinger** — So others can find you
67
+
68
+ Data is stored in SQLite (`data/fedbox.db`).
69
+
70
+ ## Configuration
71
+
72
+ After `fedbox init`, you'll have a `fedbox.json`:
73
+
74
+ ```json
75
+ {
76
+ "username": "alice",
77
+ "displayName": "Alice",
78
+ "summary": "Hello, Fediverse!",
79
+ "port": 3000,
80
+ "domain": null,
81
+ "publicKey": "...",
82
+ "privateKey": "..."
83
+ }
84
+ ```
85
+
86
+ Add `"domain"` for federation with the wider Fediverse.
87
+
88
+ ## Requirements
89
+
90
+ - Node.js 18+
91
+ - For federation: ngrok or a public server with HTTPS
92
+
93
+ ## License
94
+
95
+ MIT
96
+
97
+ ---
98
+
99
+ **Built with [microfed](https://github.com/micro-fed/microfed.org). Happy federating! 📦**
package/bin/cli.js ADDED
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Pubcrawl CLI
5
+ * Zero to Fediverse in 60 seconds
6
+ */
7
+
8
+ import { createInterface } from 'readline'
9
+ import { existsSync, writeFileSync, mkdirSync } from 'fs'
10
+ import { join } from 'path'
11
+ import { generateKeypair } from 'microfed/auth'
12
+
13
+ const rl = createInterface({
14
+ input: process.stdin,
15
+ output: process.stdout
16
+ })
17
+
18
+ const ask = (q) => new Promise(resolve => rl.question(q, resolve))
19
+
20
+ const BANNER = `
21
+ ╔═══════════════════════════════════════════╗
22
+ ║ ║
23
+ ║ 📦 FEDBOX ║
24
+ ║ Zero to Fediverse in 60 seconds ║
25
+ ║ ║
26
+ ╚═══════════════════════════════════════════╝
27
+ `
28
+
29
+ const COMMANDS = {
30
+ init: runInit,
31
+ start: runStart,
32
+ status: runStatus,
33
+ help: runHelp
34
+ }
35
+
36
+ async function main() {
37
+ const command = process.argv[2] || 'help'
38
+ const handler = COMMANDS[command]
39
+
40
+ if (!handler) {
41
+ console.log(`Unknown command: ${command}`)
42
+ runHelp()
43
+ process.exit(1)
44
+ }
45
+
46
+ await handler()
47
+ }
48
+
49
+ async function runInit() {
50
+ console.log(BANNER)
51
+
52
+ // Check if already initialized
53
+ if (existsSync('fedbox.json')) {
54
+ console.log('⚠️ Already initialized. Delete fedbox.json to start over.\n')
55
+ process.exit(1)
56
+ }
57
+
58
+ console.log('Let\'s get you on the Fediverse!\n')
59
+
60
+ // Gather info
61
+ const username = await ask('👤 Username (e.g., alice): ')
62
+ const displayName = await ask('📛 Display name (e.g., Alice): ') || username
63
+ const summary = await ask('📝 Bio (optional): ') || ''
64
+ const port = await ask('🔌 Port (default 3000): ') || '3000'
65
+
66
+ console.log('\n🔐 Generating keypair...')
67
+ const { publicKey, privateKey } = generateKeypair()
68
+
69
+ // Create config
70
+ const config = {
71
+ username: username.toLowerCase().replace(/[^a-z0-9]/g, ''),
72
+ displayName,
73
+ summary,
74
+ port: parseInt(port),
75
+ publicKey,
76
+ privateKey,
77
+ createdAt: new Date().toISOString()
78
+ }
79
+
80
+ // Create data directory
81
+ if (!existsSync('data')) {
82
+ mkdirSync('data')
83
+ }
84
+
85
+ // Save config
86
+ writeFileSync('fedbox.json', JSON.stringify(config, null, 2))
87
+ console.log('✅ Config saved to fedbox.json')
88
+
89
+ console.log(`
90
+ ╔═══════════════════════════════════════════╗
91
+ ║ ✅ READY! ║
92
+ ╚═══════════════════════════════════════════╝
93
+
94
+ Next steps:
95
+
96
+ 1. Start your server:
97
+ $ fedbox start
98
+
99
+ 2. Expose with ngrok (for federation):
100
+ $ ngrok http ${port}
101
+
102
+ 3. Update fedbox.json with your ngrok domain
103
+
104
+ 4. Visit your profile:
105
+ http://localhost:${port}/@${config.username}
106
+
107
+ Happy federating! 📦
108
+ `)
109
+
110
+ rl.close()
111
+ }
112
+
113
+ async function runStart() {
114
+ if (!existsSync('fedbox.json')) {
115
+ console.log('❌ Not initialized. Run: fedbox init\n')
116
+ process.exit(1)
117
+ }
118
+
119
+ console.log('🚀 Starting server...\n')
120
+
121
+ // Dynamic import to avoid loading before init
122
+ const { startServer } = await import('../lib/server.js')
123
+ await startServer()
124
+ }
125
+
126
+ async function runStatus() {
127
+ if (!existsSync('fedbox.json')) {
128
+ console.log('❌ Not initialized. Run: fedbox init\n')
129
+ process.exit(1)
130
+ }
131
+
132
+ const config = JSON.parse(await import('fs').then(fs =>
133
+ fs.readFileSync('fedbox.json', 'utf8')
134
+ ))
135
+
136
+ console.log(`
137
+ ╔═══════════════════════════════════════════╗
138
+ ║ 📊 FEDBOX STATUS ║
139
+ ╚═══════════════════════════════════════════╝
140
+
141
+ Username: @${config.username}
142
+ Name: ${config.displayName}
143
+ Port: ${config.port}
144
+ Domain: ${config.domain || '(not set - run with ngrok)'}
145
+ Created: ${config.createdAt}
146
+ `)
147
+
148
+ rl.close()
149
+ }
150
+
151
+ function runHelp() {
152
+ console.log(`
153
+ ${BANNER}
154
+ Usage: fedbox <command>
155
+
156
+ Commands:
157
+ init Set up a new Fediverse identity
158
+ start Start the server
159
+ status Show current configuration
160
+ help Show this help
161
+
162
+ Quick start:
163
+ $ fedbox init
164
+ $ fedbox start
165
+
166
+ For federation (so Mastodon can find you):
167
+ $ ngrok http 3000
168
+ Then update fedbox.json with your ngrok domain
169
+ `)
170
+
171
+ rl.close()
172
+ }
173
+
174
+ main().catch(err => {
175
+ console.error('Error:', err.message)
176
+ process.exit(1)
177
+ })
package/lib/server.js ADDED
@@ -0,0 +1,464 @@
1
+ /**
2
+ * Fedbox Server
3
+ * ActivityPub server using microfed
4
+ */
5
+
6
+ import { createServer } from 'http'
7
+ import { readFileSync, existsSync } from 'fs'
8
+ import { profile, auth, webfinger, outbox } from 'microfed'
9
+ import {
10
+ initStore,
11
+ addFollower,
12
+ removeFollower,
13
+ getFollowers,
14
+ getFollowerCount,
15
+ addFollowing,
16
+ acceptFollowing,
17
+ getFollowingCount,
18
+ saveActivity,
19
+ getActivities,
20
+ savePost,
21
+ getPosts,
22
+ cacheActor,
23
+ getCachedActor
24
+ } from './store.js'
25
+
26
+ let config = null
27
+ let actor = null
28
+
29
+ /**
30
+ * Load configuration
31
+ */
32
+ function loadConfig() {
33
+ if (!existsSync('fedbox.json')) {
34
+ throw new Error('Not initialized. Run: fedbox init')
35
+ }
36
+ config = JSON.parse(readFileSync('fedbox.json', 'utf8'))
37
+ return config
38
+ }
39
+
40
+ /**
41
+ * Get the domain (with ngrok support)
42
+ */
43
+ function getDomain() {
44
+ return config.domain || `localhost:${config.port}`
45
+ }
46
+
47
+ /**
48
+ * Get protocol
49
+ */
50
+ function getProtocol() {
51
+ return config.domain ? 'https' : 'http'
52
+ }
53
+
54
+ /**
55
+ * Build actor object
56
+ */
57
+ function buildActor() {
58
+ const domain = getDomain()
59
+ const protocol = getProtocol()
60
+ const baseUrl = `${protocol}://${domain}`
61
+
62
+ return profile.createActor({
63
+ id: `${baseUrl}/users/${config.username}`,
64
+ username: config.username,
65
+ name: config.displayName,
66
+ summary: config.summary ? `<p>${config.summary}</p>` : '',
67
+ publicKey: config.publicKey,
68
+ sharedInbox: `${baseUrl}/inbox`
69
+ })
70
+ }
71
+
72
+ /**
73
+ * Fetch remote actor (with caching)
74
+ */
75
+ async function fetchActor(id) {
76
+ const cached = getCachedActor(id)
77
+ if (cached) return cached
78
+
79
+ try {
80
+ const response = await fetch(id, {
81
+ headers: { 'Accept': 'application/activity+json' }
82
+ })
83
+ if (!response.ok) return null
84
+
85
+ const actorData = await response.json()
86
+ cacheActor(actorData)
87
+ return actorData
88
+ } catch {
89
+ return null
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Handle incoming activities
95
+ */
96
+ async function handleActivity(activity) {
97
+ console.log(`📥 ${activity.type} from ${activity.actor}`)
98
+ saveActivity(activity)
99
+
100
+ switch (activity.type) {
101
+ case 'Follow':
102
+ await handleFollow(activity)
103
+ break
104
+ case 'Undo':
105
+ await handleUndo(activity)
106
+ break
107
+ case 'Accept':
108
+ handleAccept(activity)
109
+ break
110
+ case 'Create':
111
+ console.log(` New post: ${activity.object?.content?.slice(0, 50)}...`)
112
+ break
113
+ case 'Like':
114
+ console.log(` ❤️ Liked: ${activity.object}`)
115
+ break
116
+ case 'Announce':
117
+ console.log(` 🔁 Boosted: ${activity.object}`)
118
+ break
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Handle Follow activity
124
+ */
125
+ async function handleFollow(activity) {
126
+ const followerActor = await fetchActor(activity.actor)
127
+ if (!followerActor) {
128
+ console.log(' Could not fetch follower actor')
129
+ return
130
+ }
131
+
132
+ // Add to followers
133
+ addFollower(activity.actor, followerActor.inbox)
134
+ console.log(` ✅ New follower: ${followerActor.preferredUsername}`)
135
+
136
+ // Send Accept
137
+ const accept = outbox.createAccept(actor.id, activity)
138
+
139
+ try {
140
+ await outbox.send({
141
+ activity: accept,
142
+ inbox: followerActor.inbox,
143
+ privateKey: config.privateKey,
144
+ keyId: `${actor.id}#main-key`
145
+ })
146
+ console.log(` 📤 Sent Accept to ${followerActor.inbox}`)
147
+ } catch (err) {
148
+ console.log(` ❌ Failed to send Accept: ${err.message}`)
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Handle Undo activity
154
+ */
155
+ async function handleUndo(activity) {
156
+ if (activity.object?.type === 'Follow') {
157
+ removeFollower(activity.actor)
158
+ console.log(` 👋 Unfollowed by ${activity.actor}`)
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Handle Accept activity (our follow was accepted)
164
+ */
165
+ function handleAccept(activity) {
166
+ if (activity.object?.type === 'Follow') {
167
+ acceptFollowing(activity.object.object)
168
+ console.log(` ✅ Follow accepted!`)
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Request handler
174
+ */
175
+ async function handleRequest(req, res) {
176
+ const url = new URL(req.url, `${getProtocol()}://${getDomain()}`)
177
+ const path = url.pathname
178
+ const accept = req.headers.accept || ''
179
+ const isAP = accept.includes('activity+json') || accept.includes('ld+json')
180
+
181
+ console.log(`${req.method} ${path}`)
182
+
183
+ // CORS
184
+ res.setHeader('Access-Control-Allow-Origin', '*')
185
+ res.setHeader('Access-Control-Allow-Headers', '*')
186
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
187
+
188
+ if (req.method === 'OPTIONS') {
189
+ res.writeHead(204)
190
+ return res.end()
191
+ }
192
+
193
+ // WebFinger
194
+ if (path === '/.well-known/webfinger') {
195
+ const resource = url.searchParams.get('resource')
196
+ const parsed = webfinger.parseResource(resource)
197
+
198
+ if (!parsed || parsed.username !== config.username) {
199
+ res.writeHead(404)
200
+ return res.end('Not found')
201
+ }
202
+
203
+ const response = webfinger.createResponse(
204
+ `${config.username}@${getDomain()}`,
205
+ actor.id,
206
+ { profileUrl: `${getProtocol()}://${getDomain()}/@${config.username}` }
207
+ )
208
+
209
+ res.setHeader('Content-Type', 'application/jrd+json')
210
+ return res.end(JSON.stringify(response, null, 2))
211
+ }
212
+
213
+ // Actor
214
+ if (path === `/users/${config.username}`) {
215
+ res.setHeader('Content-Type', 'application/activity+json')
216
+ return res.end(JSON.stringify(actor, null, 2))
217
+ }
218
+
219
+ // Inbox
220
+ if (path === `/users/${config.username}/inbox` || path === '/inbox') {
221
+ if (req.method !== 'POST') {
222
+ res.writeHead(405)
223
+ return res.end('Method not allowed')
224
+ }
225
+
226
+ const chunks = []
227
+ for await (const chunk of req) chunks.push(chunk)
228
+ const body = Buffer.concat(chunks).toString()
229
+
230
+ try {
231
+ const activity = JSON.parse(body)
232
+ await handleActivity(activity)
233
+ res.writeHead(202)
234
+ return res.end()
235
+ } catch (err) {
236
+ console.error('Inbox error:', err)
237
+ res.writeHead(400)
238
+ return res.end('Bad request')
239
+ }
240
+ }
241
+
242
+ // Outbox
243
+ if (path === `/users/${config.username}/outbox`) {
244
+ const posts = getPosts(20)
245
+ const collection = {
246
+ '@context': 'https://www.w3.org/ns/activitystreams',
247
+ type: 'OrderedCollection',
248
+ id: `${actor.id}/outbox`,
249
+ totalItems: posts.length,
250
+ orderedItems: posts.map(p => ({
251
+ type: 'Create',
252
+ actor: actor.id,
253
+ object: {
254
+ type: 'Note',
255
+ id: p.id,
256
+ content: p.content,
257
+ published: p.published,
258
+ attributedTo: actor.id
259
+ }
260
+ }))
261
+ }
262
+ res.setHeader('Content-Type', 'application/activity+json')
263
+ return res.end(JSON.stringify(collection, null, 2))
264
+ }
265
+
266
+ // Followers
267
+ if (path === `/users/${config.username}/followers`) {
268
+ const followers = getFollowers()
269
+ const collection = {
270
+ '@context': 'https://www.w3.org/ns/activitystreams',
271
+ type: 'OrderedCollection',
272
+ id: `${actor.id}/followers`,
273
+ totalItems: followers.length,
274
+ orderedItems: followers.map(f => f.actor)
275
+ }
276
+ res.setHeader('Content-Type', 'application/activity+json')
277
+ return res.end(JSON.stringify(collection, null, 2))
278
+ }
279
+
280
+ // Following
281
+ if (path === `/users/${config.username}/following`) {
282
+ const collection = {
283
+ '@context': 'https://www.w3.org/ns/activitystreams',
284
+ type: 'OrderedCollection',
285
+ id: `${actor.id}/following`,
286
+ totalItems: getFollowingCount(),
287
+ orderedItems: []
288
+ }
289
+ res.setHeader('Content-Type', 'application/activity+json')
290
+ return res.end(JSON.stringify(collection, null, 2))
291
+ }
292
+
293
+ // HTML Profile
294
+ if (path === `/@${config.username}` || (path === `/users/${config.username}` && !isAP)) {
295
+ res.setHeader('Content-Type', 'text/html')
296
+ return res.end(renderProfile())
297
+ }
298
+
299
+ // Home
300
+ if (path === '/') {
301
+ res.setHeader('Content-Type', 'text/html')
302
+ return res.end(renderHome())
303
+ }
304
+
305
+ res.writeHead(404)
306
+ res.end('Not found')
307
+ }
308
+
309
+ /**
310
+ * Render HTML profile
311
+ */
312
+ function renderProfile() {
313
+ const followers = getFollowerCount()
314
+ const following = getFollowingCount()
315
+
316
+ return `<!DOCTYPE html>
317
+ <html>
318
+ <head>
319
+ <meta charset="utf-8">
320
+ <title>${config.displayName} (@${config.username}@${getDomain()})</title>
321
+ <meta name="viewport" content="width=device-width, initial-scale=1">
322
+ <style>
323
+ * { box-sizing: border-box; }
324
+ body {
325
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
326
+ max-width: 600px;
327
+ margin: 0 auto;
328
+ padding: 2rem;
329
+ background: #1a1a2e;
330
+ color: #eee;
331
+ min-height: 100vh;
332
+ }
333
+ .card {
334
+ background: #16213e;
335
+ border-radius: 16px;
336
+ padding: 2rem;
337
+ text-align: center;
338
+ }
339
+ .avatar {
340
+ width: 120px;
341
+ height: 120px;
342
+ border-radius: 50%;
343
+ background: linear-gradient(135deg, #667eea, #764ba2);
344
+ margin: 0 auto 1rem;
345
+ display: flex;
346
+ align-items: center;
347
+ justify-content: center;
348
+ font-size: 3rem;
349
+ }
350
+ h1 { margin: 0 0 0.5rem; }
351
+ .handle { color: #888; margin-bottom: 1rem; }
352
+ .bio { color: #aaa; margin-bottom: 1.5rem; }
353
+ .stats { display: flex; justify-content: center; gap: 2rem; }
354
+ .stat { text-align: center; }
355
+ .stat-num { font-size: 1.5rem; font-weight: bold; }
356
+ .stat-label { color: #888; font-size: 0.9rem; }
357
+ .badge {
358
+ display: inline-block;
359
+ background: #667eea;
360
+ color: white;
361
+ padding: 0.5rem 1rem;
362
+ border-radius: 20px;
363
+ margin-top: 1.5rem;
364
+ font-size: 0.9rem;
365
+ }
366
+ </style>
367
+ </head>
368
+ <body>
369
+ <div class="card">
370
+ <div class="avatar">🍺</div>
371
+ <h1>${config.displayName}</h1>
372
+ <p class="handle">@${config.username}@${getDomain()}</p>
373
+ ${config.summary ? `<p class="bio">${config.summary}</p>` : ''}
374
+ <div class="stats">
375
+ <div class="stat">
376
+ <div class="stat-num">${followers}</div>
377
+ <div class="stat-label">Followers</div>
378
+ </div>
379
+ <div class="stat">
380
+ <div class="stat-num">${following}</div>
381
+ <div class="stat-label">Following</div>
382
+ </div>
383
+ </div>
384
+ <div class="badge">📦 Powered by Fedbox</div>
385
+ </div>
386
+ </body>
387
+ </html>`
388
+ }
389
+
390
+ /**
391
+ * Render home page
392
+ */
393
+ function renderHome() {
394
+ return `<!DOCTYPE html>
395
+ <html>
396
+ <head>
397
+ <meta charset="utf-8">
398
+ <title>Fedbox</title>
399
+ <meta name="viewport" content="width=device-width, initial-scale=1">
400
+ <style>
401
+ body {
402
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
403
+ max-width: 600px;
404
+ margin: 0 auto;
405
+ padding: 2rem;
406
+ background: #1a1a2e;
407
+ color: #eee;
408
+ }
409
+ h1 { color: #667eea; }
410
+ a { color: #667eea; }
411
+ code { background: #16213e; padding: 0.2rem 0.4rem; border-radius: 4px; }
412
+ .endpoints { background: #16213e; padding: 1rem; border-radius: 8px; }
413
+ .endpoints li { margin: 0.5rem 0; }
414
+ </style>
415
+ </head>
416
+ <body>
417
+ <h1>📦 Fedbox</h1>
418
+ <p>Your Fediverse server is running!</p>
419
+
420
+ <h2>Your Profile</h2>
421
+ <p><a href="/@${config.username}">@${config.username}@${getDomain()}</a></p>
422
+
423
+ <h2>Endpoints</h2>
424
+ <ul class="endpoints">
425
+ <li><code>/.well-known/webfinger</code> - Discovery</li>
426
+ <li><code>/users/${config.username}</code> - Actor</li>
427
+ <li><code>/users/${config.username}/inbox</code> - Inbox</li>
428
+ <li><code>/users/${config.username}/outbox</code> - Outbox</li>
429
+ <li><code>/users/${config.username}/followers</code> - Followers</li>
430
+ </ul>
431
+
432
+ <h2>Federation</h2>
433
+ <p>To federate with Mastodon, expose this server via ngrok:</p>
434
+ <code>ngrok http ${config.port}</code>
435
+ <p>Then update <code>fedbox.json</code> with your ngrok domain.</p>
436
+ </body>
437
+ </html>`
438
+ }
439
+
440
+ /**
441
+ * Start the server
442
+ */
443
+ export async function startServer() {
444
+ loadConfig()
445
+ initStore()
446
+ actor = buildActor()
447
+
448
+ const server = createServer(handleRequest)
449
+
450
+ server.listen(config.port, () => {
451
+ console.log(`
452
+ 📦 Fedbox is running!
453
+
454
+ Profile: http://localhost:${config.port}/@${config.username}
455
+ Actor: http://localhost:${config.port}/users/${config.username}
456
+
457
+ ${config.domain ? ` Federated: https://${config.domain}/@${config.username}` : ' ⚠️ Set "domain" in fedbox.json for federation'}
458
+
459
+ Press Ctrl+C to stop
460
+ `)
461
+ })
462
+ }
463
+
464
+ export default { startServer }
package/lib/store.js ADDED
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Fedbox Store
3
+ * SQLite persistence layer
4
+ */
5
+
6
+ import Database from 'better-sqlite3'
7
+ import { existsSync } from 'fs'
8
+
9
+ let db = null
10
+
11
+ /**
12
+ * Initialize the database
13
+ * @param {string} path - Path to SQLite file
14
+ */
15
+ export function initStore(path = 'data/fedbox.db') {
16
+ db = new Database(path)
17
+
18
+ // Create tables
19
+ db.exec(`
20
+ -- Followers (people following us)
21
+ CREATE TABLE IF NOT EXISTS followers (
22
+ id TEXT PRIMARY KEY,
23
+ actor TEXT NOT NULL,
24
+ inbox TEXT,
25
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
26
+ );
27
+
28
+ -- Following (people we follow)
29
+ CREATE TABLE IF NOT EXISTS following (
30
+ id TEXT PRIMARY KEY,
31
+ actor TEXT NOT NULL,
32
+ accepted INTEGER DEFAULT 0,
33
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
34
+ );
35
+
36
+ -- Activities (inbox)
37
+ CREATE TABLE IF NOT EXISTS activities (
38
+ id TEXT PRIMARY KEY,
39
+ type TEXT NOT NULL,
40
+ actor TEXT,
41
+ object TEXT,
42
+ raw TEXT NOT NULL,
43
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
44
+ );
45
+
46
+ -- Posts (our outbox)
47
+ CREATE TABLE IF NOT EXISTS posts (
48
+ id TEXT PRIMARY KEY,
49
+ content TEXT NOT NULL,
50
+ in_reply_to TEXT,
51
+ published TEXT DEFAULT CURRENT_TIMESTAMP
52
+ );
53
+
54
+ -- Known actors (cache)
55
+ CREATE TABLE IF NOT EXISTS actors (
56
+ id TEXT PRIMARY KEY,
57
+ data TEXT NOT NULL,
58
+ fetched_at TEXT DEFAULT CURRENT_TIMESTAMP
59
+ );
60
+ `)
61
+
62
+ return db
63
+ }
64
+
65
+ /**
66
+ * Get database instance
67
+ */
68
+ export function getStore() {
69
+ if (!db) {
70
+ throw new Error('Store not initialized. Call initStore() first.')
71
+ }
72
+ return db
73
+ }
74
+
75
+ // Followers
76
+
77
+ export function addFollower(actorId, inbox) {
78
+ const stmt = db.prepare(`
79
+ INSERT OR REPLACE INTO followers (id, actor, inbox)
80
+ VALUES (?, ?, ?)
81
+ `)
82
+ stmt.run(actorId, actorId, inbox)
83
+ }
84
+
85
+ export function removeFollower(actorId) {
86
+ const stmt = db.prepare('DELETE FROM followers WHERE id = ?')
87
+ stmt.run(actorId)
88
+ }
89
+
90
+ export function getFollowers() {
91
+ const stmt = db.prepare('SELECT * FROM followers ORDER BY created_at DESC')
92
+ return stmt.all()
93
+ }
94
+
95
+ export function getFollowerCount() {
96
+ const stmt = db.prepare('SELECT COUNT(*) as count FROM followers')
97
+ return stmt.get().count
98
+ }
99
+
100
+ // Following
101
+
102
+ export function addFollowing(actorId, accepted = false) {
103
+ const stmt = db.prepare(`
104
+ INSERT OR REPLACE INTO following (id, actor, accepted)
105
+ VALUES (?, ?, ?)
106
+ `)
107
+ stmt.run(actorId, actorId, accepted ? 1 : 0)
108
+ }
109
+
110
+ export function acceptFollowing(actorId) {
111
+ const stmt = db.prepare('UPDATE following SET accepted = 1 WHERE id = ?')
112
+ stmt.run(actorId)
113
+ }
114
+
115
+ export function removeFollowing(actorId) {
116
+ const stmt = db.prepare('DELETE FROM following WHERE id = ?')
117
+ stmt.run(actorId)
118
+ }
119
+
120
+ export function getFollowing() {
121
+ const stmt = db.prepare('SELECT * FROM following WHERE accepted = 1 ORDER BY created_at DESC')
122
+ return stmt.all()
123
+ }
124
+
125
+ export function getFollowingCount() {
126
+ const stmt = db.prepare('SELECT COUNT(*) as count FROM following WHERE accepted = 1')
127
+ return stmt.get().count
128
+ }
129
+
130
+ // Activities
131
+
132
+ export function saveActivity(activity) {
133
+ const stmt = db.prepare(`
134
+ INSERT OR REPLACE INTO activities (id, type, actor, object, raw)
135
+ VALUES (?, ?, ?, ?, ?)
136
+ `)
137
+ stmt.run(
138
+ activity.id,
139
+ activity.type,
140
+ typeof activity.actor === 'string' ? activity.actor : activity.actor?.id,
141
+ typeof activity.object === 'string' ? activity.object : JSON.stringify(activity.object),
142
+ JSON.stringify(activity)
143
+ )
144
+ }
145
+
146
+ export function getActivities(limit = 20) {
147
+ const stmt = db.prepare('SELECT * FROM activities ORDER BY created_at DESC LIMIT ?')
148
+ return stmt.all(limit).map(row => ({
149
+ ...row,
150
+ raw: JSON.parse(row.raw)
151
+ }))
152
+ }
153
+
154
+ // Posts
155
+
156
+ export function savePost(id, content, inReplyTo = null) {
157
+ const stmt = db.prepare(`
158
+ INSERT INTO posts (id, content, in_reply_to)
159
+ VALUES (?, ?, ?)
160
+ `)
161
+ stmt.run(id, content, inReplyTo)
162
+ }
163
+
164
+ export function getPosts(limit = 20) {
165
+ const stmt = db.prepare('SELECT * FROM posts ORDER BY published DESC LIMIT ?')
166
+ return stmt.all(limit)
167
+ }
168
+
169
+ export function getPost(id) {
170
+ const stmt = db.prepare('SELECT * FROM posts WHERE id = ?')
171
+ return stmt.get(id)
172
+ }
173
+
174
+ // Actor cache
175
+
176
+ export function cacheActor(actor) {
177
+ const stmt = db.prepare(`
178
+ INSERT OR REPLACE INTO actors (id, data, fetched_at)
179
+ VALUES (?, ?, CURRENT_TIMESTAMP)
180
+ `)
181
+ stmt.run(actor.id, JSON.stringify(actor))
182
+ }
183
+
184
+ export function getCachedActor(id) {
185
+ const stmt = db.prepare('SELECT * FROM actors WHERE id = ?')
186
+ const row = stmt.get(id)
187
+ return row ? JSON.parse(row.data) : null
188
+ }
189
+
190
+ export default {
191
+ initStore,
192
+ getStore,
193
+ addFollower,
194
+ removeFollower,
195
+ getFollowers,
196
+ getFollowerCount,
197
+ addFollowing,
198
+ acceptFollowing,
199
+ removeFollowing,
200
+ getFollowing,
201
+ getFollowingCount,
202
+ saveActivity,
203
+ getActivities,
204
+ savePost,
205
+ getPosts,
206
+ getPost,
207
+ cacheActor,
208
+ getCachedActor
209
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "fedbox",
3
+ "version": "0.0.1",
4
+ "description": "Zero to Fediverse in 60 seconds",
5
+ "type": "module",
6
+ "main": "lib/server.js",
7
+ "bin": {
8
+ "fedbox": "./bin/cli.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node lib/server.js",
12
+ "test": "node --test test/"
13
+ },
14
+ "dependencies": {
15
+ "microfed": "^0.0.13",
16
+ "better-sqlite3": "^11.0.0"
17
+ },
18
+ "keywords": [
19
+ "activitypub",
20
+ "fediverse",
21
+ "mastodon",
22
+ "server",
23
+ "federation"
24
+ ],
25
+ "author": "Melvin Carvalho",
26
+ "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/micro-fed/fedbox"
30
+ }
31
+ }