const express = require('express'); const bodyParser = require('body-parser'); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const { ActivityPubRouter, createActor } = require('activitypub-express'); const uuid = require('uuid'); const db = require('quick.db'); // Import quick.db const fetch = require('node-fetch'); const app = express(); const port = 3000; const SECRET_KEY = 'your_secret_key'; // Replace with a secure key in a real app // Middleware to parse JSON app.use(bodyParser.json()); // Helper function to create an actor URL const actorURL = (username) => `http://localhost:${port}/users/${username}`; // Helper function to generate JWT token const generateToken = (user) => { return jwt.sign({ id: user.username }, SECRET_KEY, { expiresIn: '1h' }); }; // Middleware to protect routes const authenticate = (req, res, next) => { const token = req.headers['authorization']?.split(' ')[1]; if (!token) { return res.status(401).json({ error: 'Unauthorized' }); } jwt.verify(token, SECRET_KEY, (err, decoded) => { if (err) { return res.status(403).json({ error: 'Invalid or expired token' }); } req.user = decoded; next(); }); }; // Route to create a new user (actor with password) app.post('/users', async (req, res) => { const { username, displayName, password } = req.body; // Check if user already exists const existingUser = db.get(`user_${username}`); if (existingUser) { return res.status(400).json({ error: 'User already exists' }); } // Hash the password before saving const hashedPassword = await bcrypt.hash(password, 10); // Create the actor and store it in the DB const actor = createActor({ id: actorURL(username), username, displayName, inbox: `http://localhost:${port}/users/${username}/inbox`, outbox: `http://localhost:${port}/users/${username}/outbox`, followers: `http://localhost:${port}/users/${username}/followers`, following: `http://localhost:${port}/users/${username}/following`, }); // Store actor info and password in the database db.set(`user_${username}`, { ...actor, password: hashedPassword }); res.status(201).json(actor); }); // Route to log in a user and get a token app.post('/login', async (req, res) => { const { username, password } = req.body; // Retrieve user from the database const user = db.get(`user_${username}`); if (!user) { return res.status(404).json({ error: 'User not found' }); } // Compare hashed password const isPasswordValid = await bcrypt.compare(password, user.password); if (!isPasswordValid) { return res.status(401).json({ error: 'Invalid password' }); } // Generate and return JWT token const token = generateToken(user); res.json({ token }); }); // Route to create a post (status update) with authentication app.post('/users/:username/posts', authenticate, (req, res) => { const { username } = req.params; const { content } = req.body; if (req.user.id !== username) { return res.status(403).json({ error: 'You can only post as yourself' }); } // Retrieve the user from the database const user = db.get(`user_${username}`); if (!user) { return res.status(404).json({ error: 'User not found' }); } // Create a post and store it in the database const post = { id: `http://localhost:${port}/posts/${uuid.v4()}`, type: 'Note', content, author: user, published: new Date().toISOString(), }; // Store the post in the database db.set(`post_${post.id}`, post); res.status(201).json(post); }); // Route to get a user's posts app.get('/users/:username/posts', authenticate, (req, res) => { const { username } = req.params; if (req.user.id !== username) { return res.status(403).json({ error: 'You can only view your own posts' }); } // Retrieve all posts from the database const userPosts = Object.values(db.get("posts") || {}).filter(post => post.author.username === username); res.json(userPosts); }); // ActivityPub setup const activityPubRouter = new ActivityPubRouter(); // Handle incoming Follow activity activityPubRouter.post('/users/:username/inbox', authenticate, async (req, res) => { const { username } = req.params; const activity = req.body; if (!db.get(`user_${username}`)) { return res.status(404).json({ error: 'User not found' }); } if (activity.type === 'Follow') { const actor = activity.actor; // Retrieve followers list and add new follower const followersList = db.get(`followers_${username}`) || []; followersList.push(actor.id); // Store the updated followers list in the database db.set(`followers_${username}`, followersList); res.status(200).json({ status: 'Followed successfully' }); // Optionally, notify the actor that the follow was accepted await sendFollowResponse(actor, username); } else { res.status(400).json({ error: 'Unsupported activity type' }); } }); // Federated actor info endpoint activityPubRouter.get('/users/:username', (req, res) => { const { username } = req.params; const user = db.get(`user_${username}`); if (!user) { return res.status(404).json({ error: 'User not found' }); } res.json(user); }); // Outbox endpoint to send posts activityPubRouter.get('/users/:username/outbox', (req, res) => { const { username } = req.params; const user = db.get(`user_${username}`); if (!user) { return res.status(404).json({ error: 'User not found' }); } // Fetch posts from this user's outbox (from the DB) const userPosts = Object.values(db.get("posts") || {}).filter(post => post.author.username === username); res.json(userPosts); }); app.use(activityPubRouter.router); // Start the server app.listen(port, () => { console.log(`Federated social network with SQLite (quick.db) running at http://localhost:${port}`); });