Port Guppe to activitypub-express

This commit is contained in:
Will Murphy 2020-10-24 12:29:14 -05:00
parent 0e5a583e05
commit 18e0c8d602
34 changed files with 22958 additions and 2342 deletions

194
index.js
View file

@ -2,18 +2,12 @@ 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 app = express()
@ -21,15 +15,132 @@ 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)
// Guppe's magic: create new groups on demand whenever someone tries to access it
async function getOrCreateActor (req, res, next) {
const apex = req.app.locals.apex
const actor = req.params[apex.actorParam]
const actorIRI = apex.utils.usernameToIRI(actor)
let actorObj
try {
actorObj = await apex.store.getObject(actorIRI)
} catch (err) { return next(err) }
if (!actorObj && actor.length <= 100) {
try {
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}`
actorObj = await apex.createActor(actor, `${actor} group`, summary, icon, 'Group')
await apex.store.saveObject(actorObj)
} catch (err) { return next(err) }
res.locals.apex.target = actorObj
} else if (actor.length > 100) {
res.locals.apex.status = 400
res.locals.apex.statusMessage = 'Group names are limited to 100 characters'
} else if (actorObj.type === 'Tombstone') {
res.locals.apex.status = 410
} else {
res.locals.apex.target = actorObj
}
next()
}
// define routes using prepacakged middleware collections
app.route(routes.inbox)
.post(apex.net.inbox.post)
// no C2S at present
// .get(apex.net.inbox.get)
app.route(routes.outbox)
.get(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, apex.net.validators.jsonld, getOrCreateActor, apex.net.responders.target)
app.get(routes.followers, apex.net.followers.get)
app.get(routes.following, apex.net.following.get)
app.get(routes.liked, 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,
// replace apex's target actor validator with our create on demand method
getOrCreateActor,
apex.net.wellKnown.respondWebfinger
)
app.on('apex-inbox', async ({ actor, activity, recipient, object }) => {
switch (activity.type.toLowerCase()) {
case 'create': {
// ignore forwarded messages that aren't directly adddressed to group
if (!activity.to?.includes(recipient.id)) {
return
}
const to = [
recipient.followers[0],
apex.consts.publicAddress
]
const share = await apex.buildActivity('Announce', recipient.id, to, {
object: activity.id
})
apex.addToOutbox(recipient, share)
break
}
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: [
@ -37,60 +148,34 @@ app.use(history({
{ 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'))
// error logging
app.use(function (err, req, res, next) {
console.error(err.message, req.body, err.stack)
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
}
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) => {
@ -98,3 +183,4 @@ onShutdown(async () => {
})
console.log('Guppe server closed')
})
})

View file

@ -1,12 +0,0 @@
db.objects.find({type: "Group"}).forEach(function(d){
d.id = d.id.toLowerCase();
db.objects.save(d);
});
db.streams.find({"_meta._target": {$ne: null}}).forEach(function(d){
d._meta._target = d._meta._target.toLowerCase();
db.objects.save(d);
});
db.streams.find({actor: /gup\.pe/}).forEach(function(d){
d.actor = d.actor.toLowerCase();
db.streams.save(d);
});

View file

@ -1,11 +0,0 @@
db.objects.find({type: "Group"}).forEach(function(d) {
if(typeof d.icon !== 'string') {
return;
}
d.icon = {
type: 'Image',
mediaType: 'image/jpeg',
url: d.icon
};
db.objects.save(d);
});

View file

@ -1,6 +0,0 @@
'use strict'
// middleware
module.exports = {
validators: require('./validators'),
security: require('./security')
}

View file

@ -1,46 +0,0 @@
'use strict'
const httpSignature = require('http-signature')
const pub = require('../pub')
// http communication middleware
module.exports = {
auth,
verifySignature
}
function auth (req, res, next) {
// no client-to-server support at this time
if (req.app.get('env') !== 'development') {
return res.status(405).send()
}
next()
}
async function verifySignature (req, res, next) {
try {
if (!req.get('authorization') && !req.get('signature')) {
// support for apps not using signature extension to ActivityPub
const actor = await pub.object.resolveObject(pub.utils.actorFromActivity(req.body))
if (actor.publicKey && req.app.get('env') !== 'development') {
console.log('Missing http signature')
return res.status(400).send('Missing http signature')
}
return next()
}
const sigHead = httpSignature.parse(req)
const signer = await pub.object.resolveObject(sigHead.keyId, req.app.get('db'))
const valid = httpSignature.verifySignature(sigHead, signer.publicKey.publicKeyPem)
if (!valid) {
console.log('signature validation failure', sigHead.keyId)
return res.status(400).send('Invalid http signature')
}
next()
} catch (err) {
if (req.body.type === 'Delete' && err.message.startsWith('410')) {
// user delete message that can't be verified because we don't have the user cached
console.log('Unverifiable delete')
return res.status(200).send()
}
console.log('error during signature verification', err.message, req.body)
return res.status(500).send()
}
}

View file

@ -1,45 +0,0 @@
const pub = require('../pub')
module.exports = {
activity,
jsonld,
outboxActivity
}
function activity (req, res, next) {
if (!pub.utils.validateActivity(req.body)) {
return res.status(400).send('Invalid activity')
}
next()
}
function jsonld (req, res, next) {
// we don't have to rule out HTML/browsers
// because history fallback catches them first
if (req.method === 'GET' && req.accepts(pub.consts.jsonldTypes)) {
return next()
}
if (req.method === 'POST' && req.is(pub.consts.jsonldTypes)) {
return next()
}
next('route')
}
function outboxActivity (req, res, next) {
if (!pub.utils.validateActivity(req.body)) {
if (!pub.utils.validateObject(req.body)) {
return res.status(400).send('Invalid activity')
}
const actor = pub.utils.usernameToIRI(req.user)
const extras = {}
if (req.body.bcc) {
extras.bcc = req.body.bcc
}
if (req.body.audience) {
extras.audience = req.body.audience
}
req.body = pub.activity
.build('Create', actor, req.body, req.body.to, req.body.cc, extras)
}
next()
}

9110
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -4,8 +4,8 @@
"description": "Decentralized social groups with ActivityPub, NodeJS, Express, Vue, and Mongodb",
"main": "index.js",
"dependencies": {
"@small-tech/auto-encrypt": "2.0.5",
"body-parser": "^1.18.3",
"@small-tech/auto-encrypt": "^3.0.1",
"activitypub-express": "^2.2.1",
"connect-history-api-fallback": "^1.6.0",
"cors": "^2.8.4",
"express": "^4.16.3",
@ -23,7 +23,7 @@
"devDependencies": {
"chokidar": "^3.1.1",
"nodemon": "^1.19.3",
"standard": "^14.3.1",
"standard": "^16.0.4",
"standardjs": "^1.0.0-alpha"
},
"scripts": {

View file

@ -1,87 +0,0 @@
'use strict'
const { ObjectId } = require('mongodb')
const store = require('../store')
const pubUtils = require('./utils')
const pubObject = require('./object')
const pubFederation = require('./federation')
module.exports = {
address,
addToOutbox,
build,
undo
}
function build (type, actorId, object, to, cc, etc) {
const oid = new ObjectId()
const act = Object.assign({
// _id: oid,
id: pubUtils.actvityIdToIRI(oid),
type,
actor: actorId,
object,
to,
cc,
published: new Date().toISOString()
}, etc)
return act
}
async function address (activity) {
let audience = []
;['to', 'bto', 'cc', 'bcc', 'audience'].forEach(t => {
if (activity[t]) {
audience = audience.concat(activity[t])
}
})
audience = audience.map(t => {
if (t === 'https://www.w3.org/ns/activitystreams#Public') {
return null
}
return pubObject.resolveObject(t)
})
audience = await Promise.all(audience).then(addresses => {
// TODO: spec says only deliver to actor-owned collections
addresses = addresses.map(t => {
if (t && t.inbox) {
return t
}
if (t && t.items) {
return t.items.map(pubObject.resolveObject)
}
if (t && t.orderedItems) {
return t.orderedItems.map(pubObject.resolveObject)
}
})
// flattens and resolves collections
return Promise.all([].concat(...addresses))
})
audience = audience.filter(t => t && t.inbox)
.map(t => t.inbox)
// de-dupe
return Array.from(new Set(audience))
}
function addToOutbox (actor, activity) {
const tasks = [
store.stream.save(activity),
address(activity).then(addresses => pubFederation.deliver(actor, activity, addresses))
]
// ensure activity object is cached if local, but do not try to resolve links
// because Mastodon won't resolve activity IRIs
if (pubUtils.validateObject(activity.object)) {
tasks.push(pubObject.resolve(activity.object))
}
return Promise.all(tasks)
}
function undo (activity, undoActor) {
if (!pubUtils.validateActivity(activity)) {
if (!activity || Object.prototype.toString.call(activity) !== '[object String]') {
throw new Error('Invalid undo target')
}
activity = { id: activity }
}
// matches the target activity with the actor from the undo
// so actors can only undo their own activities
return store.stream.remove(activity, undoActor)
}

View file

@ -1,71 +0,0 @@
const crypto = require('crypto')
const { promisify } = require('util')
const store = require('../store')
const pubUtils = require('./utils')
const config = require('../config.json')
const generateKeyPairPromise = promisify(crypto.generateKeyPair)
module.exports = {
createLocalActor,
getOrCreateActor
}
function createLocalActor (name, type) {
return generateKeyPairPromise('rsa', {
modulusLength: 4096,
publicKeyEncoding: {
type: 'spki',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem'
}
}).then(pair => {
const actorBase = pubUtils.usernameToIRI(name)
return {
_meta: {
privateKey: pair.privateKey
},
id: `${actorBase}`,
type: type,
following: `${actorBase}/following`,
followers: `${actorBase}/followers`,
liked: `${actorBase}/liked`,
inbox: `${actorBase}/inbox`,
outbox: `${actorBase}/outbox`,
preferredUsername: name,
name: `${name} group`,
summary: `I'm a group about ${name}. Follow me to get all the group posts. Tag me to share with the group. Create other groups by searching for or tagging @yourGroupName@${config.DOMAIN}`,
icon: {
type: 'Image',
mediaType: 'image/jpeg',
url: `https://${config.DOMAIN}/f/guppe.png`
},
publicKey: {
id: `${actorBase}#main-key`,
owner: `${actorBase}`,
publicKeyPem: pair.publicKey
}
}
})
}
async function getOrCreateActor (preferredUsername, includeMeta) {
const id = pubUtils.usernameToIRI(preferredUsername)
let user = await store.actor.getActor(id, includeMeta)
if (user) {
return user
}
// auto create groups whenever an unknown actor is referenced
user = await createLocalActor(preferredUsername, 'Group')
await store.object.save(user)
// only executed on success
delete user._id
if (includeMeta !== true) {
delete user._meta
}
return user
}

View file

@ -1,11 +0,0 @@
module.exports = {
ASContext: [
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1'
],
jsonldTypes: [
'application/activity+json',
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
'application/json'
]
}

View file

@ -1,60 +0,0 @@
'use strict'
const crypto = require('crypto')
const request = require('request-promise-native')
const pubUtils = require('./utils')
// federation communication utilities
module.exports = {
requestObject,
deliver
}
function requestObject (id) {
return request({
url: id,
headers: { Accept: 'application/activity+json' },
json: true,
httpSignature: {
key: global.guppeSystemUser._meta.privateKey,
keyId: global.guppeSystemUser.id,
headers: ['(request-target)', 'host', 'date'],
authorizationHeaderName: 'Signature'
}
})
}
function deliver (actor, activity, addresses) {
if (activity.bto) {
delete activity.bto
}
if (activity.bcc) {
delete activity.bcc
}
const requests = addresses.map(addr => {
const body = pubUtils.toJSONLD(activity)
const digest = crypto.createHash('sha256')
.update(JSON.stringify(body))
.digest('base64')
return request({
method: 'POST',
url: addr,
headers: {
'Content-Type': 'application/activity+json',
Digest: `SHA-256=${digest}`
},
httpSignature: {
key: actor._meta.privateKey,
keyId: actor.id,
headers: ['(request-target)', 'host', 'date', 'digest'],
authorizationHeaderName: 'Signature'
},
json: true,
resolveWithFullResponse: true,
simple: false,
body
})
.then(result => console.log('delivery:', addr, result.statusCode))
.catch(err => console.log(err.message))
})
return Promise.all(requests)
}

View file

@ -1,10 +0,0 @@
'use strict'
// ActivityPub / ActivityStreams utils
module.exports = {
activity: require('./activity'),
actor: require('./actor'),
consts: require('./consts'),
federation: require('./federation'),
object: require('./object'),
utils: require('./utils')
}

View file

@ -1,39 +0,0 @@
'use strict'
const store = require('../store')
const federation = require('./federation')
const pubUtils = require('./utils')
module.exports = {
resolveObject,
resolve: resolveObject
}
// find object in local DB or fetch from origin server
async function resolveObject (id) {
let object
let parseCheck
if (pubUtils.validateObject(id)) {
// already an object
object = id
} else {
// resolve id to local object
try {
// check if full IRI id or remote id
parseCheck = new URL(id)
} catch (ignore) {}
if (!parseCheck) {
// convert bare ObjectId to local IRI id
id = pubUtils.objectIdToIRI(id)
}
object = await store.object.get(id)
if (object) {
return object
}
// resolve remote object from id
object = await federation.requestObject(id)
}
// cache non-collection objects
if (object.type !== 'Collection' && object.type !== 'OrderedCollection') {
await store.object.save(object)
}
return object
}

View file

@ -1,69 +0,0 @@
'use strict'
const config = require('../config.json')
const pubConsts = require('./consts')
module.exports = {
actvityIdToIRI,
usernameToIRI,
toJSONLD,
arrayToCollection,
actorFromActivity,
objectIdToIRI,
validateActivity,
validateObject
}
function actorFromActivity (activity) {
if (Object.prototype.toString.call(activity.actor) === '[object String]') {
return activity.actor
}
if (activity.actor.type === 'Link') {
return activity.actor.href
}
return activity.actor.id
}
function arrayToCollection (arr, ordered) {
return {
'@context': pubConsts.ASContext,
totalItems: arr.length,
type: ordered ? 'orderedCollection' : 'collection',
[ordered ? 'orderedItems' : 'items']: arr
}
}
function toJSONLD (obj) {
obj['@context'] = obj['@context'] || pubConsts.ASContext
return obj
}
function usernameToIRI (user) {
return `https://${config.DOMAIN}/u/${user}`.toLowerCase()
}
function objectIdToIRI (oid) {
if (oid.toHexString) {
oid = oid.toHexString()
}
return `https://${config.DOMAIN}/o/${oid}`.toLowerCase()
}
function actvityIdToIRI (oid) {
if (oid.toHexString) {
oid = oid.toHexString()
}
return `https://${config.DOMAIN}/s/${oid}`.toLowerCase()
}
function validateObject (object) {
if (object && object.id) {
// object['@context'] = object['@context'] || pubConsts.ASContext
return true
}
}
function validateActivity (object) {
if (object && object.id && object.actor) {
return true
}
}

View file

@ -1,77 +0,0 @@
const express = require('express')
const router = express.Router()
const pub = require('../pub')
const net = require('../net')
const store = require('../store')
router.post('/', net.validators.activity, net.security.verifySignature, function (req, res) {
req.body._meta = { _target: pub.utils.usernameToIRI(req.user) }
const toDo = {
saveActivity: true,
saveObject: false
}
// side effects
switch (req.body.type) {
case 'Accept':
// TODO - side effect necessary for following collection?
break
case 'Follow':
// TODO resolve object and ensure specified target matches inbox user
// req.body._meta._target = req.body.object.id
// send acceptance reply
pub.actor.getOrCreateActor(req.user, true)
.then(user => {
const to = [pub.utils.actorFromActivity(req.body)]
const accept = pub.activity.build('Accept', user.id, req.body, to)
return pub.activity.addToOutbox(user, accept)
})
.catch(e => console.log(e))
break
case 'Create':
// toDo.saveObject = true
pub.actor.getOrCreateActor(req.user, true)
.then(user => {
const to = [
user.followers,
'https://www.w3.org/ns/activitystreams#Public'
]
const cc = [pub.utils.actorFromActivity(req.body)]
const announce = pub.activity.build('Announce', user.id, req.body.object.id, to, cc)
return pub.activity.addToOutbox(user, announce)
}).catch(e => console.log(e))
break
case 'Delete':
case 'Undo':
pub.activity.undo(req.body.object, req.body.actor)
.catch(err => console.log(err.message))
break
}
const tasks = []
if (toDo.saveObject) {
tasks.push(pub.object.resolve(req.body.object))
}
if (toDo.saveActivity) {
tasks.push(store.stream.save(req.body))
}
Promise.all(tasks).then(() => res.status(200).send())
.catch(err => {
console.log(err.message)
res.status(500).send()
})
})
router.get('/', function (req, res) {
const db = req.app.get('db')
db.collection('streams')
.find({ '_meta._target': pub.utils.usernameToIRI(req.user) })
.sort({ _id: -1 })
.project({ _id: 0, _meta: 0, '@context': 0, 'object._id': 0, 'object.@context': 0, 'object._meta': 0 })
.toArray()
.then(stream => res.json(pub.utils.arrayToCollection(stream, true)))
.catch(err => {
console.log(err.message)
return res.status(500).send()
})
})
module.exports = router

View file

@ -1,10 +0,0 @@
'use strict'
module.exports = {
inbox: require('./inbox'),
object: require('./object'),
outbox: require('./outbox'),
stream: require('./stream'),
user: require('./user'),
webfinger: require('./webfinger')
}

View file

@ -1,27 +0,0 @@
'use strict'
const express = require('express')
const router = express.Router()
const pub = require('../pub')
router.get('/:name', function (req, res) {
const id = req.params.name
if (!id) {
return res.status(400).send('Bad request.')
} else {
// TODO: don't attempt to resolve remote ids if request is cross-origin
// (to prevent abuse of guppe server to DDoS other servers)
pub.object.resolve(id)
.then(obj => {
if (obj) {
return res.json(pub.utils.toJSONLD(obj))
}
return res.status(404).send('Object not found')
})
.catch(err => {
console.log(err.message)
res.status(500).send()
})
}
})
module.exports = router

View file

@ -1,33 +0,0 @@
const express = require('express')
const router = express.Router()
const net = require('../net')
const pub = require('../pub')
const store = require('../store')
router.post('/', net.security.auth, net.validators.outboxActivity, function (req, res) {
store.actor.get(pub.utils.usernameToIRI(req.user), true)
.then(actor => {
return pub.activity.addToOutbox(actor, req.body)
})
.then(() => res.status(200).send())
.catch(err => {
console.log(err.message)
res.status(500).send()
})
})
router.get('/', function (req, res) {
const db = req.app.get('db')
db.collection('streams')
.find({ actor: pub.utils.usernameToIRI(req.user), type: { $in: ['Announce', 'Create'] } })
.sort({ _id: -1 })
.project({ _id: 0, _meta: 0, 'object._id': 0, 'object.@context': 0, 'object._meta': 0 })
.toArray()
.then(stream => res.json(pub.utils.arrayToCollection(stream, true)))
.catch(err => {
console.log(err.message)
return res.status(500).send()
})
})
module.exports = router

View file

@ -1,26 +0,0 @@
'use strict'
const express = require('express')
const router = express.Router()
const store = require('../store')
const pub = require('../pub')
router.get('/:name', function (req, res) {
const name = req.params.name
if (!name) {
return res.status(400).send('Bad request.')
} else {
store.stream.get(pub.utils.actvityIdToIRI(name))
.then(obj => {
if (obj) {
return res.json(pub.utils.toJSONLD(obj))
}
return res.status(404).send('Activity not found')
})
.catch(err => {
console.log(err.message)
res.status(500).send()
})
}
})
module.exports = router

View file

@ -1,71 +0,0 @@
'use strict'
const express = require('express')
const router = express.Router()
const pub = require('../pub')
const net = require('../net')
// list active groups
router.get('/', net.validators.jsonld, function (req, res) {
const db = req.app.get('db')
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 => { console.log(JSON.stringify(groups)); return groups })
.then(groups => res.json(groups))
.catch(err => {
console.log(err.message)
return res.status(500).send()
})
})
router.get('/:name', net.validators.jsonld, function (req, res) {
const name = req.params.name
if (!name) {
return res.status(400).send('Bad request.')
}
console.log('User json ', name)
pub.actor.getOrCreateActor(name)
.then(group => {
return res.json(pub.utils.toJSONLD(group))
})
.catch(err => {
console.log(err.message)
res.status(500).send(`Error creating group ${name}`)
})
})
router.get('/:name/followers', net.validators.jsonld, function (req, res) {
const name = req.params.name
if (!name) {
return res.status(400).send('Bad request.')
}
const db = req.app.get('db')
db.collection('streams')
.find({
type: 'Follow',
'_meta._target': pub.utils.usernameToIRI(name)
})
.project({ _id: 0, actor: 1 })
.toArray()
.then(follows => {
const followers = follows.map(pub.utils.actorFromActivity)
return res.json(pub.utils.arrayToCollection(followers))
})
.catch(err => {
console.log(err.message)
return res.status(500).send()
})
})
module.exports = router

View file

@ -1,40 +0,0 @@
'use strict'
const express = require('express')
const router = express.Router()
const pub = require('../pub')
const acctReg = /acct:[@~]?([^@]+)@?(.*)/
router.get('/', function (req, res) {
const resource = req.query.resource
const acct = acctReg.exec(resource)
if (!acct || acct.length < 2) {
return res.status(400).send('Bad request. Please make sure "acct:USER@DOMAIN" is what you are sending as the "resource" query parameter.')
}
if (acct[2] && acct[2].toLowerCase() !== req.app.get('domain').toLowerCase()) {
return res.status(400).send('Requested user is not from this domain')
}
const db = req.app.get('db')
pub.actor.getOrCreateActor(acct[1], db)
.then(result => {
if (!result) {
return res.status(404).send(`${acct[1]}@${acct[2]} not found`)
}
const finger = {
subject: resource,
links: [
{
rel: 'self',
type: 'application/activity+json',
href: result.id
}
]
}
return res.json(finger)
})
.catch(err => {
console.log(err.message)
res.status(500).send()
})
})
module.exports = router

View file

@ -1,19 +0,0 @@
'use strict'
const connection = require('./connection')
module.exports = {
getActor,
get: getActor
}
const actorProj = { _id: 0, _meta: 0 }
const metaActorProj = { _id: 0 }
function getActor (id, includeMeta) {
const db = connection.getDb()
return db.collection('objects')
.find({ id: id })
.limit(1)
// strict comparison as we don't want to return private keys on accident
.project(includeMeta === true ? metaActorProj : actorProj)
.next()
}

View file

@ -1,8 +0,0 @@
'use strict'
module.exports = (function () {
let con
return {
setDb: db => { con = db },
getDb: () => con
}
})()

View file

@ -1,9 +0,0 @@
'use strict'
// database interface
module.exports = {
setup: require('./setup'),
actor: require('./actor'),
object: require('./object'),
stream: require('./stream'),
connection: require('./connection')
}

View file

@ -1,29 +0,0 @@
'use strict'
const connection = require('./connection')
module.exports = {
get,
save
}
function get (id) {
return connection.getDb()
.collection('objects')
.find({ id: id })
.limit(1)
.project({ _id: 0, _meta: 0 })
.next()
}
async function save (object) {
const db = connection.getDb()
const exists = await db.collection('objects')
.find({ id: object.id })
.project({ _id: 1 })
.limit(1)
.hasNext()
if (exists) {
return false
}
return db.collection('objects')
.insertOne(object, { forceServerObjectId: true })
}

View file

@ -1,38 +0,0 @@
'use strict'
const connection = require('./connection')
module.exports = async function dbSetup (domain, dummyUser) {
const db = connection.getDb()
// inbox
await db.collection('streams').createIndex({
'_meta._target': 1,
_id: -1
}, {
name: 'inbox'
})
// followers
await db.collection('streams').createIndex({
'_meta._target': 1
}, {
partialFilterExpression: { type: 'Follow' },
name: 'followers'
})
// outbox
await db.collection('streams').createIndex({
actor: 1,
_id: -1
})
// object lookup
await db.collection('objects').createIndex({
id: 1
})
if (dummyUser) {
return db.collection('objects').findOneAndReplace(
{ preferredUsername: 'dummy' },
dummyUser,
{
upsert: true,
returnOriginal: false
}
)
}
}

View file

@ -1,42 +0,0 @@
'use strict'
const connection = require('./connection')
module.exports = {
get,
remove,
save
}
function get (id) {
const db = connection.getDb()
return db.collection('streams')
.find({ id: id })
.limit(1)
.project({ _id: 0, _meta: 0 })
.next()
}
async function save (activity) {
const db = connection.getDb()
const q = { id: activity.id }
// activities may be duplicated for multiple local targets
if (activity._meta && activity._meta._target) {
q['_meta._target'] = activity._meta._target
}
const exists = await db.collection('streams')
.find(q)
.project({ _id: 1 })
.limit(1)
.hasNext()
if (exists) {
return false
}
return db.collection('streams')
// server object ID avoids mutating local copy of document
.insertOne(activity, { forceServerObjectId: true })
}
function remove (activity, actor) {
return connection.getDb().collection('streams')
.deleteMany({ id: activity.id, actor: actor })
}

View file

@ -1,2 +0,0 @@
module.exports = {
}

View file

@ -1,5 +0,0 @@
'use strict'
// misc utilities
module.exports = {
consts: require('./consts')
}

2
web/dist/index.html vendored
View file

@ -1 +1 @@
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1"><link rel=icon href=/web/favicon.ico><link rel=stylesheet href=/web/w3.css><link rel=stylesheet href="https://fonts.googleapis.com/css?family=Lato"><link rel=stylesheet href=/web/fontawesome/all.min.css><title>web</title><link href=/web/css/app.bf8cceb6.css rel=preload as=style><link href=/web/js/app.636bc9fa.js rel=preload as=script><link href=/web/js/chunk-vendors.e214c0f7.js rel=preload as=script><link href=/web/css/app.bf8cceb6.css rel=stylesheet></head><body><noscript><strong>We're sorry but web doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div><script src=/web/js/chunk-vendors.e214c0f7.js></script><script src=/web/js/app.636bc9fa.js></script></body></html>
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1"><link rel=icon href=/web/favicon.ico><link rel=stylesheet href=/web/w3.css><link rel=stylesheet href="https://fonts.googleapis.com/css?family=Lato"><link rel=stylesheet href=/web/fontawesome/all.min.css><title>web</title><link href=/web/css/app.bf8cceb6.css rel=preload as=style><link href=/web/js/app.62fd8922.js rel=preload as=script><link href=/web/js/chunk-vendors.e214c0f7.js rel=preload as=script><link href=/web/css/app.bf8cceb6.css rel=stylesheet></head><body><noscript><strong>We're sorry but web doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div><script src=/web/js/chunk-vendors.e214c0f7.js></script><script src=/web/js/app.62fd8922.js></script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

15065
web/package-lock.json generated

File diff suppressed because it is too large Load diff