rubberguppe/index.js
2021-11-01 18:05:14 -05:00

184 lines
6.2 KiB
JavaScript

require('dotenv').config()
const path = require('path')
const express = require('express')
const MongoClient = require('mongodb').MongoClient
const fs = require('fs')
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 { 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: 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.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: [
// do not redirect webfinger et c.
{ from: /^\/\.well-known\//, to: context => context.request.originalUrl }
]
}))
app.use('/f', express.static('public/files'))
app.use('/web', express.static('web/dist'))
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')
}
})
client.connect({ useNewUrlParser: true })
.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) => {
server.close(err => (err ? reject(err) : resolve()))
})
console.log('Guppe server closed')
})
})