Port Guppe to activitypub-express
This commit is contained in:
parent
0e5a583e05
commit
18e0c8d602
34 changed files with 22958 additions and 2342 deletions
198
index.js
198
index.js
|
@ -2,18 +2,12 @@ const path = require('path')
|
||||||
const express = require('express')
|
const express = require('express')
|
||||||
const MongoClient = require('mongodb').MongoClient
|
const MongoClient = require('mongodb').MongoClient
|
||||||
const fs = require('fs')
|
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 https = require('https')
|
||||||
const morgan = require('morgan')
|
const morgan = require('morgan')
|
||||||
const history = require('connect-history-api-fallback')
|
const history = require('connect-history-api-fallback')
|
||||||
const { onShutdown } = require('node-graceful-shutdown')
|
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, PORT_HTTPS, DB_URL, DB_NAME } = require('./config.json')
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
|
@ -21,15 +15,132 @@ const app = express()
|
||||||
const client = new MongoClient(DB_URL, { useUnifiedTopology: true, useNewUrlParser: true })
|
const client = new MongoClient(DB_URL, { useUnifiedTopology: true, useNewUrlParser: true })
|
||||||
|
|
||||||
const sslOptions = {
|
const sslOptions = {
|
||||||
key: fs.readFileSync(path.join(__dirname, KEY_PATH)),
|
key: KEY_PATH && fs.readFileSync(path.join(__dirname, KEY_PATH)),
|
||||||
cert: fs.readFileSync(path.join(__dirname, CERT_PATH)),
|
cert: CERT_PATH && fs.readFileSync(path.join(__dirname, CERT_PATH)),
|
||||||
ca: CA_PATH ? fs.readFileSync(path.join(__dirname, CA_PATH)) : undefined
|
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('domain', DOMAIN)
|
||||||
app.set('port', process.env.PORT || PORT)
|
app.set('port', process.env.PORT || PORT)
|
||||||
app.set('port-https', process.env.PORT_HTTPS || PORT_HTTPS)
|
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(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({
|
app.use(history({
|
||||||
index: '/web/index.html',
|
index: '/web/index.html',
|
||||||
rewrites: [
|
rewrites: [
|
||||||
|
@ -37,64 +148,39 @@ app.use(history({
|
||||||
{ from: /^\/\.well-known\//, to: context => context.request.originalUrl }
|
{ 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('/f', express.static('public/files'))
|
||||||
app.use('/web', express.static('web/dist'))
|
app.use('/web', express.static('web/dist'))
|
||||||
|
|
||||||
// error logging
|
|
||||||
app.use(function (err, req, res, next) {
|
app.use(function (err, req, res, next) {
|
||||||
console.error(err.message, req.body, err.stack)
|
console.error(err.message, req.body, err.stack)
|
||||||
|
if (!res.headersSent) {
|
||||||
res.status(500).send('An error occurred while processing the request')
|
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 })
|
client.connect({ useNewUrlParser: true })
|
||||||
.then(() => {
|
.then(async () => {
|
||||||
console.log('Connected successfully to db')
|
const { default: AutoEncrypt } = await import('@small-tech/auto-encrypt')
|
||||||
const db = client.db(DB_NAME)
|
apex.store.db = client.db(DB_NAME)
|
||||||
app.set('db', db)
|
await apex.store.setup()
|
||||||
store.connection.setDb(db)
|
apex.systemUser = await apex.store.getObject(apex.utils.usernameToIRI('system_service'), true)
|
||||||
return pub.actor.createLocalActor('dummy', 'Person')
|
if (!apex.systemUser) {
|
||||||
})
|
const systemUser = await apex.createActor('system_service', `${DOMAIN} system service`, `${DOMAIN} system service`, icon, 'Service')
|
||||||
.then(dummy => {
|
await apex.store.saveObject(systemUser)
|
||||||
// shortcut to be able to sign GETs, will be factored out via activitypub-express
|
apex.systemUser = systemUser
|
||||||
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)
|
|
||||||
})
|
|
||||||
|
|
||||||
onShutdown(async () => {
|
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 client.close()
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve, reject) => {
|
||||||
server.close(err => (err ? reject(err) : resolve()))
|
server.close(err => (err ? reject(err) : resolve()))
|
||||||
})
|
})
|
||||||
console.log('Guppe server closed')
|
console.log('Guppe server closed')
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
|
|
@ -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);
|
|
||||||
});
|
|
|
@ -1,6 +0,0 @@
|
||||||
'use strict'
|
|
||||||
// middleware
|
|
||||||
module.exports = {
|
|
||||||
validators: require('./validators'),
|
|
||||||
security: require('./security')
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
9110
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -4,8 +4,8 @@
|
||||||
"description": "Decentralized social groups with ActivityPub, NodeJS, Express, Vue, and Mongodb",
|
"description": "Decentralized social groups with ActivityPub, NodeJS, Express, Vue, and Mongodb",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@small-tech/auto-encrypt": "2.0.5",
|
"@small-tech/auto-encrypt": "^3.0.1",
|
||||||
"body-parser": "^1.18.3",
|
"activitypub-express": "^2.2.1",
|
||||||
"connect-history-api-fallback": "^1.6.0",
|
"connect-history-api-fallback": "^1.6.0",
|
||||||
"cors": "^2.8.4",
|
"cors": "^2.8.4",
|
||||||
"express": "^4.16.3",
|
"express": "^4.16.3",
|
||||||
|
@ -23,7 +23,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"chokidar": "^3.1.1",
|
"chokidar": "^3.1.1",
|
||||||
"nodemon": "^1.19.3",
|
"nodemon": "^1.19.3",
|
||||||
"standard": "^14.3.1",
|
"standard": "^16.0.4",
|
||||||
"standardjs": "^1.0.0-alpha"
|
"standardjs": "^1.0.0-alpha"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
71
pub/actor.js
71
pub/actor.js
|
@ -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
|
|
||||||
}
|
|
|
@ -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'
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
10
pub/index.js
10
pub/index.js
|
@ -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')
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
69
pub/utils.js
69
pub/utils.js
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
|
@ -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')
|
|
||||||
}
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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()
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
'use strict'
|
|
||||||
module.exports = (function () {
|
|
||||||
let con
|
|
||||||
return {
|
|
||||||
setDb: db => { con = db },
|
|
||||||
getDb: () => con
|
|
||||||
}
|
|
||||||
})()
|
|
|
@ -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')
|
|
||||||
}
|
|
|
@ -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 })
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 })
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
'use strict'
|
|
||||||
// misc utilities
|
|
||||||
module.exports = {
|
|
||||||
consts: require('./consts')
|
|
||||||
}
|
|
2
web/dist/index.html
vendored
2
web/dist/index.html
vendored
|
@ -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
15065
web/package-lock.json
generated
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue