-
-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #42 from wmurphyrd/apex
Port to activitypub-express
- Loading branch information
Showing
65 changed files
with
23,059 additions
and
11,550 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
node_modules | ||
web/node_modules | ||
npm-debug.log | ||
certs | ||
.vscode | ||
web/dist | ||
.env |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
DB_NAME=guppe | ||
NODE_ENV=production |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,3 +5,4 @@ certs/ | |
.vscode | ||
dump/ | ||
logs/ | ||
.env |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
FROM node:16 | ||
|
||
# Web clinet installs | ||
WORKDIR /usr/src/guppe/web | ||
COPY ./web/package*.json ./ | ||
RUN npm ci | ||
|
||
# server installs | ||
WORKDIR /usr/src/guppe | ||
COPY package*.json ./ | ||
RUN npm ci | ||
|
||
# source | ||
COPY . . | ||
|
||
# web client build | ||
WORKDIR /usr/src/guppe/web | ||
RUN npm run build | ||
|
||
# entry | ||
WORKDIR /usr/src/guppe | ||
EXPOSE 443 80 | ||
CMD [ "node", "index.js" ] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1,34 @@ | ||
# Current state of guppe | ||
|
||
Guppe is a tech demo. It is missing a lot of features, and I am in the process of rewriting it's core code from scratch in the fully-implemented, [modular ActivityPub library activitypub-express](https://github.com/immers-space/activitypub-express). I'll try to keep up with major bugfixes, but I won't be adding any features to guppe until i finish apex and can port guppe over to the new engine. | ||
|
||
# Gup.pe | ||
|
||
Social groups for the fediverse - making it easy to connect and meet new people based on shared interests without the manipulation of your attention to maximize ad revenue nor the walled garden lock-in of capitalist social media. | ||
|
||
This server-2-server ActivityPub implementation adds decentralized, federaded "groups" support across all ActivityPub compliant social media networks. Users join groups by following group-type actors on Guppe servers and contribute to groups by mentioning those same actors in a post. Guppe group actors will automatically forward posts they receive to all group members so that everyone in the group sees any post made to the group. Guppe group actors' profiles (e.g. outboxes) also serve as a group discussion history. | ||
Creation of new groups is automatic, users simply search for or mention a new group and it will be created. | ||
|
||
|
||
## Tech stack | ||
|
||
MEVN: MongoDB, ExpressJS, Vue, NodeJS | ||
Mostly powered by [activitypub-express](https://github.com/immers-space/activitypub-express) | ||
from [Immers Space](https://web.immers.space). | ||
The gup.pe server app, `index.js` is 200 lines of code, | ||
just adding the auto-create actor, auto-accept follow, and auto-boost from inbox behaviors | ||
to the base apex setup. | ||
|
||
There's also an HTML front-end using Vue (`/web`) to display popular groups and provide | ||
fallback user profile discovery. | ||
|
||
## Installation | ||
|
||
Instructions coming soon. | ||
`Dockerfile` and `docker-compose.yml` are provided for easy install | ||
|
||
``` | ||
git clone https://github.com/wmurphyrd/guppe.git | ||
cd guppe | ||
cp .env.defaults .env | ||
echo DOMAIN=yourdomain.com >> .env | ||
docker-compose up -d | ||
``` | ||
|
||
## License | ||
|
||
Copyright (c) 2019 William Murphy. Licensed under the AGPL-3 | ||
Copyright (c) 2021 William Murphy. Licensed under the AGPL-3 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
version: "3.8" | ||
|
||
services: | ||
guppe: | ||
build: . | ||
restart: unless-stopped | ||
ports: | ||
- 443:443 | ||
- 80:80 | ||
env_file: '.env' | ||
environment: | ||
DB_URL: 'mongodb://mongodb:27017' | ||
PORT_HTTPS: 443 | ||
depends_on: | ||
- mongodb | ||
volumes: | ||
- certs:/root/.small-tech.org/auto-encrypt | ||
# localdev certs | ||
- ./certs:/usr/src/guppe/certs | ||
logging: | ||
driver: local | ||
options: | ||
max-size: '10m' | ||
|
||
mongodb: | ||
image: mongo:4.2 | ||
restart: unless-stopped | ||
volumes: | ||
- mongo-data:/data/db | ||
logging: | ||
driver: local | ||
options: | ||
max-size: '10m' | ||
|
||
volumes: | ||
mongo-data: | ||
certs: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,100 +1,201 @@ | ||
require('dotenv').config() | ||
const path = require('path') | ||
const express = require('express') | ||
const MongoClient = require('mongodb').MongoClient | ||
const fs = require('fs') | ||
const bodyParser = require('body-parser') | ||
const cors = require('cors') | ||
const AutoEncrypt = require('@small-tech/auto-encrypt') | ||
const https = require('https') | ||
const morgan = require('morgan') | ||
const history = require('connect-history-api-fallback') | ||
const { onShutdown } = require('node-graceful-shutdown') | ||
const ActivitypubExpress = require('activitypub-express') | ||
|
||
const routes = require('./routes') | ||
const pub = require('./pub') | ||
const store = require('./store') | ||
const net = require('./net') | ||
const { DOMAIN, KEY_PATH, CERT_PATH, CA_PATH, PORT, PORT_HTTPS, DB_URL, DB_NAME } = require('./config.json') | ||
const { DOMAIN, KEY_PATH, CERT_PATH, CA_PATH, PORT_HTTPS, DB_URL, DB_NAME } = process.env | ||
|
||
const app = express() | ||
|
||
const client = new MongoClient(DB_URL, { useUnifiedTopology: true, useNewUrlParser: true }) | ||
|
||
const sslOptions = { | ||
key: fs.readFileSync(path.join(__dirname, KEY_PATH)), | ||
cert: fs.readFileSync(path.join(__dirname, CERT_PATH)), | ||
ca: CA_PATH ? fs.readFileSync(path.join(__dirname, CA_PATH)) : undefined | ||
key: KEY_PATH && fs.readFileSync(path.join(__dirname, KEY_PATH)), | ||
cert: CERT_PATH && fs.readFileSync(path.join(__dirname, CERT_PATH)), | ||
ca: CA_PATH && fs.readFileSync(path.join(__dirname, CA_PATH)) | ||
} | ||
const icon = { | ||
type: 'Image', | ||
mediaType: 'image/jpeg', | ||
url: `https://${DOMAIN}/f/guppe.png` | ||
} | ||
|
||
const routes = { | ||
actor: '/u/:actor', | ||
object: '/o/:id', | ||
activity: '/s/:id', | ||
inbox: '/u/:actor/inbox', | ||
outbox: '/u/:actor/outbox', | ||
followers: '/u/:actor/followers', | ||
following: '/u/:actor/following', | ||
liked: '/u/:actor/liked', | ||
collections: '/u/:actor/c/:id', | ||
blocked: '/u/:actor/blocked', | ||
rejections: '/u/:actor/rejections', | ||
rejected: '/u/:actor/rejected', | ||
shares: '/s/:id/shares', | ||
likes: '/s/:id/likes' | ||
} | ||
const apex = ActivitypubExpress({ | ||
domain: DOMAIN, | ||
actorParam: 'actor', | ||
objectParam: 'id', | ||
itemsPerPage: 100, | ||
routes | ||
}) | ||
|
||
app.set('domain', DOMAIN) | ||
app.set('port', process.env.PORT || PORT) | ||
app.set('port-https', process.env.PORT_HTTPS || PORT_HTTPS) | ||
app.use(morgan(':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status Accepts ":req[accept]" ":referrer" ":user-agent"')) | ||
app.use(express.json({ type: apex.consts.jsonldTypes }), apex) | ||
|
||
// Create new groups on demand whenever someone tries to access one | ||
async function actorOnDemand (req, res, next) { | ||
const actor = req.params.actor | ||
if (!actor) { | ||
return next() | ||
} | ||
const actorIRI = apex.utils.usernameToIRI(actor) | ||
try { | ||
if (!(await apex.store.getObject(actorIRI)) && actor.length <= 255) { | ||
console.log(`Creating group: ${actor}`) | ||
const summary = `I'm a group about ${actor}. Follow me to get all the group posts. Tag me to share with the group. Create other groups by searching for or tagging @yourGroupName@${DOMAIN}` | ||
const actorObj = await apex.createActor(actor, `${actor} group`, summary, icon, 'Group') | ||
await apex.store.saveObject(actorObj) | ||
} | ||
} catch (err) { return next(err) } | ||
next() | ||
} | ||
// define routes using prepacakged middleware collections | ||
app.route(routes.inbox) | ||
.post(actorOnDemand, apex.net.inbox.post) | ||
.get(actorOnDemand, apex.net.inbox.get) | ||
app.route(routes.outbox) | ||
.get(actorOnDemand, apex.net.outbox.get) | ||
// no C2S at present | ||
// .post(apex.net.outbox.post) | ||
|
||
// replace apex's target actor validator with our create on demand method | ||
app.get(routes.actor, actorOnDemand, apex.net.actor.get) | ||
app.get(routes.followers, actorOnDemand, apex.net.followers.get) | ||
app.get(routes.following, actorOnDemand, apex.net.following.get) | ||
app.get(routes.liked, actorOnDemand, apex.net.liked.get) | ||
app.get(routes.object, apex.net.object.get) | ||
app.get(routes.activity, apex.net.activityStream.get) | ||
app.get(routes.shares, apex.net.shares.get) | ||
app.get(routes.likes, apex.net.likes.get) | ||
app.get( | ||
'/.well-known/webfinger', | ||
apex.net.wellKnown.parseWebfinger, | ||
actorOnDemand, | ||
apex.net.validators.targetActor, | ||
apex.net.wellKnown.respondWebfinger | ||
) | ||
|
||
app.on('apex-inbox', async ({ actor, activity, recipient, object }) => { | ||
switch (activity.type.toLowerCase()) { | ||
// automatically reshare incoming posts | ||
case 'create': { | ||
// check audience to ignore forwarded messages not adddressed to group | ||
const audience = apex.audienceFromActivity(activity) | ||
if (!audience.includes(recipient.id) || !activity.object?.length) { | ||
return | ||
} | ||
const to = [ | ||
recipient.followers[0], | ||
apex.consts.publicAddress | ||
] | ||
const share = await apex.buildActivity('Announce', recipient.id, to, { | ||
object: activity.object[0].id | ||
}) | ||
apex.addToOutbox(recipient, share) | ||
break | ||
} | ||
// automatically accept follow requests | ||
case 'follow': { | ||
const accept = await apex.buildActivity('Accept', recipient.id, actor.id, { | ||
object: activity.id | ||
}) | ||
const { postTask: publishUpdatedFollowers } = await apex.acceptFollow(recipient, activity) | ||
await apex.addToOutbox(recipient, accept) | ||
return publishUpdatedFollowers() | ||
} | ||
} | ||
}) | ||
|
||
/// Guppe web setup | ||
// html/static routes | ||
app.use(history({ | ||
index: '/web/index.html', | ||
rewrites: [ | ||
// do not redirect webfinger et c. | ||
{ from: /^\/\.well-known\//, to: context => context.request.originalUrl } | ||
] | ||
})) | ||
app.use(bodyParser.json({ | ||
type: pub.consts.jsonldTypes | ||
})) // support json encoded bodies | ||
app.use(bodyParser.urlencoded({ extended: true })) // support encoded bodies | ||
|
||
app.param('name', function (req, res, next, id) { | ||
req.user = id | ||
next() | ||
}) | ||
|
||
// json only routes | ||
app.use('/.well-known/webfinger', cors(), routes.webfinger) | ||
app.use('/o', net.validators.jsonld, cors(), routes.object) | ||
app.use('/s', net.validators.jsonld, cors(), routes.stream) | ||
app.use('/u/:name/inbox', net.validators.jsonld, routes.inbox) | ||
app.use('/u/:name/outbox', net.validators.jsonld, routes.outbox) | ||
app.use('/u', cors(), routes.user) | ||
|
||
// html/static routes | ||
app.use('/f', express.static('public/files')) | ||
app.use('/web', express.static('web/dist')) | ||
// web json routes | ||
app.get('/groups', (req, res, next) => { | ||
apex.store.db.collection('streams') | ||
.aggregate([ | ||
{ $sort: { _id: -1 } }, // start from most recent | ||
{ $limit: 10000 }, // don't traverse the entire history | ||
{ $match: { type: 'Announce' } }, | ||
{ $group: { _id: '$actor', postCount: { $sum: 1 } } }, | ||
{ $lookup: { from: 'objects', localField: '_id', foreignField: 'id', as: 'actor' } }, | ||
// merge joined actor up | ||
{ $replaceRoot: { newRoot: { $mergeObjects: [{ $arrayElemAt: ['$actor', 0] }, '$$ROOT'] } } }, | ||
{ $project: { _id: 0, _meta: 0, actor: 0 } } | ||
]) | ||
.sort({ postCount: -1 }) | ||
.limit(Number.parseInt(req.query.n) || 50) | ||
.toArray() | ||
.then(groups => apex.toJSONLD({ | ||
id: `https://${DOMAIN}/groups`, | ||
type: 'OrderedCollection', | ||
totalItems: groups.length, | ||
orderedItems: groups | ||
})) | ||
// .then(groups => { console.log(JSON.stringify(groups)); return groups }) | ||
.then(groups => res.json(groups)) | ||
.catch(err => { | ||
console.log(err.message) | ||
return res.status(500).send() | ||
}) | ||
}) | ||
|
||
// error logging | ||
app.use(function (err, req, res, next) { | ||
console.error(err.message, req.body, err.stack) | ||
res.status(500).send('An error occurred while processing the request') | ||
if (!res.headersSent) { | ||
res.status(500).send('An error occurred while processing the request') | ||
} | ||
}) | ||
|
||
const server = process.env.NODE_ENV === 'production' | ||
? AutoEncrypt.https.createServer({ domains: ['gup.pe'] }, app) | ||
: https.createServer(sslOptions, app) | ||
|
||
client.connect({ useNewUrlParser: true }) | ||
.then(() => { | ||
console.log('Connected successfully to db') | ||
const db = client.db(DB_NAME) | ||
app.set('db', db) | ||
store.connection.setDb(db) | ||
return pub.actor.createLocalActor('dummy', 'Person') | ||
}) | ||
.then(dummy => { | ||
// shortcut to be able to sign GETs, will be factored out via activitypub-express | ||
global.guppeSystemUser = dummy | ||
return store.setup(DOMAIN, dummy) | ||
}) | ||
.then(() => { | ||
server.listen(app.get('port-https'), function () { | ||
console.log('Guppe server listening on port ' + app.get('port-https')) | ||
}) | ||
}) | ||
.catch(err => { | ||
throw new Error(err) | ||
}) | ||
.then(async () => { | ||
const { default: AutoEncrypt } = await import('@small-tech/auto-encrypt') | ||
apex.store.db = client.db(DB_NAME) | ||
await apex.store.setup() | ||
apex.systemUser = await apex.store.getObject(apex.utils.usernameToIRI('system_service'), true) | ||
if (!apex.systemUser) { | ||
const systemUser = await apex.createActor('system_service', `${DOMAIN} system service`, `${DOMAIN} system service`, icon, 'Service') | ||
await apex.store.saveObject(systemUser) | ||
apex.systemUser = systemUser | ||
} | ||
|
||
onShutdown(async () => { | ||
await client.close() | ||
await new Promise((resolve, reject) => { | ||
server.close(err => (err ? reject(err) : resolve())) | ||
const server = process.env.NODE_ENV === 'production' | ||
? AutoEncrypt.https.createServer({ domains: [DOMAIN] }, app) | ||
: https.createServer(sslOptions, app) | ||
server.listen(PORT_HTTPS, function () { | ||
console.log('Guppe server listening on port ' + PORT_HTTPS) | ||
}) | ||
onShutdown(async () => { | ||
await client.close() | ||
await new Promise((resolve, reject) => { | ||
server.close(err => (err ? reject(err) : resolve())) | ||
}) | ||
console.log('Guppe server closed') | ||
}) | ||
}) | ||
console.log('Guppe server closed') | ||
}) |
Oops, something went wrong.