rubberguppe/index.js

516 lines
18 KiB
JavaScript
Raw Permalink Normal View History

2021-11-01 23:05:14 +00:00
require('dotenv').config()
const path = require('path')
2019-09-22 05:20:37 +00:00
const express = require('express')
const MongoClient = require('mongodb').MongoClient
const fs = require('fs')
const https = require('https')
2022-05-08 15:43:33 +00:00
const http = require('http')
2019-09-28 03:24:35 +00:00
const morgan = require('morgan')
const history = require('connect-history-api-fallback')
const { onShutdown } = require('node-graceful-shutdown')
2020-10-24 17:29:14 +00:00
const ActivitypubExpress = require('activitypub-express')
2022-05-01 20:09:23 +00:00
const { version } = require('./package.json')
2023-08-20 21:21:52 +00:00
const { DOMAIN, KEY_PATH, CERT_PATH, CA_PATH, PORT_HTTPS, DB_URL, DB_NAME, PROXY_MODE, ADMIN_SECRET, USE_ATTACHMENTS, GROUPS, ALLOW_DELETE, ALLOW_CREATE } = process.env
const app = express()
2022-02-11 15:43:29 +00:00
const client = new MongoClient(DB_URL)
2019-09-22 05:20:37 +00:00
const sslOptions = {
2020-10-24 17:29:14 +00:00
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))
2018-09-15 07:01:19 +00:00
}
2020-10-24 17:29:14 +00:00
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({
2022-05-01 20:09:23 +00:00
name: 'Guppe Groups',
version,
2020-10-24 17:29:14 +00:00
domain: DOMAIN,
actorParam: 'actor',
objectParam: 'id',
itemsPerPage: 100,
// delivery done in workers only in production
offlineMode: process.env.NODE_ENV === 'production',
context: require('./data/context.json'),
2020-10-24 17:29:14 +00:00
routes
})
app.use(
2023-06-23 18:29:25 +00:00
morgan(':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status ":referrer" ":user-agent"'),
express.json({ type: apex.consts.jsonldTypes }),
apex,
function checkAdminKey (req, res, next) {
if (ADMIN_SECRET && req.get('authorization') === `Bearer ${ADMIN_SECRET}`) {
res.locals.apex.authorized = true
}
next()
}
)
async function createGuppeActor (...args) {
const actor = await apex.createActor(...args)
if (USE_ATTACHMENTS) {
actor.attachment = require('./data/attachments.json')
}
return actor
}
2020-10-24 17:29:14 +00:00
// Piggyback on old function name
async function actorOnDemand (req, res, next) {
const actor = req.params.actor
if (!actor) {
return next()
}
2023-08-20 21:21:52 +00:00
console.log(req.params)
2020-10-24 17:29:14 +00:00
const actorIRI = apex.utils.usernameToIRI(actor)
/*try {
if (!(await apex.store.getObject(actorIRI)) && actor.length <= 255) {
console.log(`Creating group: ${actor}`)
2020-10-24 17:29:14 +00:00
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 createGuppeActor(actor, `${actor} group`, summary, icon, 'Group')
2020-10-24 17:29:14 +00:00
await apex.store.saveObject(actorObj)
}
} catch (err) { return next(err) }
*/
2020-10-24 17:29:14 +00:00
next()
}
2023-06-23 21:20:05 +00:00
const acceptablePublicActivities = ['delete', 'update']
apex.net.inbox.post.splice(
// just after standardizing the jsonld
apex.net.inbox.post.indexOf(apex.net.validators.jsonld) + 1,
0,
2023-06-23 21:20:05 +00:00
function inboxLogger (req, res, next) {
try {
2023-06-23 21:20:05 +00:00
console.log('%s from %s to %s', req.body.type, req.body.actor?.[0], req.params[apex.actorParam])
} finally {
next()
}
},
// Lots of servers are delivering inappropriate activities to Guppe, move the filtering up earlier in the process to save work
function inboxFilter (req, res, next) {
try {
const groupIRI = apex.utils.usernameToIRI(req.params[apex.actorParam])
const activityAudience = apex.audienceFromActivity(req.body)
const activityType = req.body.type?.toLowerCase()
const activityObject = req.body.object?.[0]
if (
!activityAudience.includes(groupIRI) &&
activityObject !== groupIRI &&
!acceptablePublicActivities.includes(activityType)
) {
console.log('Ignoring irrelevant activity sent to %s: %j', groupIRI, req.body)
return res.status(202).send('Irrelevant activity ignored')
}
} catch (err) {
console.warn('Error performing prefilter:', err)
}
next()
}
)
2022-12-29 14:30:59 +00:00
// Do not boost posts from servers who abuse the service.
apex.net.inbox.post.splice(
// Blocked domain check is inserted into apex inbox route right after the sender is verified
apex.net.inbox.post.indexOf(apex.net.security.verifySignature) + 1,
0,
async function rejectBlockedDomains (req, res, next) {
try {
const url = new URL(res.locals.apex.sender.id)
const domain = await req.app.locals.apex.store.db.collection('servers').findOne({
hostname: url.hostname
})
if (domain?.blocked) {
console.log(`Ignoring post from ${url}:`, req.body)
return res.sendStatus(200)
}
} catch (err) {
console.error('Error checking domain blocks:', err)
}
next()
}
)
2020-10-24 17:29:14 +00:00
// define routes using prepacakged middleware collections
app.route(routes.inbox)
.post(actorOnDemand, apex.net.inbox.post)
.get(actorOnDemand, apex.net.inbox.get)
2020-10-24 17:29:14 +00:00
app.route(routes.outbox)
.get(actorOnDemand, apex.net.outbox.get)
.post(apex.net.outbox.post)
2020-10-24 17:29:14 +00:00
// 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)
2020-10-24 17:29:14 +00:00
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,
2020-10-24 17:29:14 +00:00
apex.net.wellKnown.respondWebfinger
)
2022-05-01 20:09:23 +00:00
app.get('/.well-known/nodeinfo', apex.net.nodeInfoLocation.get)
app.get('/nodeinfo/:version', apex.net.nodeInfo.get)
2020-10-24 17:29:14 +00:00
app.on('apex-inbox', async ({ actor, activity, recipient, object }) => {
switch (activity.type.toLowerCase()) {
// automatically reshare incoming posts
2020-10-24 17:29:14 +00:00
case 'create': {
2023-08-20 00:08:20 +00:00
console.log(activity.object[0].inReplyTo)
2020-10-24 17:29:14 +00:00
const to = [
recipient.followers[0],
apex.consts.publicAddress
]
const share = await apex.buildActivity('Announce', recipient.id, to, {
object: activity.object[0].id,
// make sure sender can see it even if they don't follow yet
cc: actor.id
2020-10-24 17:29:14 +00:00
})
//stop here, we can add the activity to the database, we'll handle it if it passes inspection, otherwise no
2023-08-20 00:08:20 +00:00
console.log(activity.object[0].id)
console.log(share)
2023-08-20 21:21:52 +00:00
const blocked = await apex.store.db.collection(`${recipient.id}-blocked`).findOne( {user: share.cc[0]} )
console.log(blocked)
if (!blocked) {
await apex.store.db.collection(`${recipient.id}-queue`).insertOne(share)
}
//apex.addToOutbox(recipient, share)
2020-10-24 17:29:14 +00:00
break
}
// automatically accept follow requests
2020-10-24 17:29:14 +00:00
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()
}
2023-08-20 00:08:20 +00:00
case 'delete': {
const queuefind = await apex.store.db.collection(`${recipient.id}-queue`).findOne( {object: [activity.object[0].id]} )
if (queuefind) {
await apex.store.db.collection(`${recipient.id}-queue`).remove( {object: [activity.object[0].id]} )
}
const postfind = await apex.store.db.collection(`${recipient.id}-posts`).findOne( {object: [activity.object[0].id]} )
if (postfind) {
await apex.store.db.collection(`${recipient.id}-posts`).remove( {object: [activity.object[0].id]} )
}
}
2020-10-24 17:29:14 +00:00
}
})
/// 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 }
]
}))
2019-09-22 05:20:37 +00:00
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()
})
})
app.get('/stats', async (req, res, next) => {
try {
const queueSize = await apex.store.db.collection('deliveryQueue')
.countDocuments({ attempt: 0 })
const uptime = process.uptime()
res.json({ queueSize, uptime })
} catch (err) {
next(err)
}
})
2018-09-15 07:01:19 +00:00
app.get('/mod/userinfo', async (req, res, next) => {
try {
2023-08-20 00:08:20 +00:00
const object = await apex.store.getObject(apex.utils.usernameToIRI(req.query.name))
res.json(object)
} catch (err) {
next(err)
}
})
2023-08-20 21:21:52 +00:00
app.get('/mod/userblocks', async (req, res, next) => {
try {
const object = await apex.store.getObject(apex.utils.usernameToIRI(req.query.name))
const blockSize = await apex.store.db.collection(`${object.id}-blocked`).find().toArray()
// console.log(res.json({queueSize}))
console.log(blockSize)
res.json(blockSize)
} catch (err) {
next(err)
}
})
app.get('/mod/unblockuser', async (req, res, next) => {
try {
const object = await apex.store.getObject(apex.utils.usernameToIRI(req.query.name))
const blockSize = await apex.store.db.collection(`${object.id}-blocked`).find( {user: req.query.id} )
// console.log(res.json({queueSize}))
await apex.store.db.collection(`${object.id}-blocked`).remove( {user: req.query.id} )
console.log(blockSize)
res.json(blockSize)
} catch (err) {
next(err)
}
})
2023-08-20 00:08:20 +00:00
app.get('/mod/changedata', async (req, res, next) => {
try {
var oldobject = await apex.store.getObject(apex.utils.usernameToIRI(req.query.name), true)
const newgroup = await createGuppeActor(req.query.name, req.query.username, req.query.description, icon, 'Group')
const to = [
oldobject.followers[0],
apex.consts.publicAddress
]
const share = await apex.buildActivity('Update', oldobject.id, to, {
object: newgroup
})
apex.addToOutbox(oldobject, share)
2023-08-20 21:21:52 +00:00
await apex.store.updateObject(newgroup, oldobject.id, false)
2023-08-20 00:08:20 +00:00
res.json(newgroup)
} catch (err) {
next(err)
}
})
app.get('/mod/data/queue', async (req, res, next) => {
2023-08-17 20:21:41 +00:00
try {
2023-08-18 14:34:22 +00:00
const object = await apex.store.getObject(apex.utils.usernameToIRI(req.query.name))
2023-08-17 20:21:41 +00:00
const queueSize = await apex.store.db.collection(`${object.id}-queue`).find().toArray()
// console.log(res.json({queueSize}))
console.log(queueSize)
res.json(queueSize)
} catch (err) {
next(err)
}
})
2023-08-20 00:08:20 +00:00
app.get('/mod/data/posts', async (req, res, next) => {
try {
const object = await apex.store.getObject(apex.utils.usernameToIRI(req.query.name))
const queueSize = await apex.store.db.collection(`${object.id}-posts`).find().toArray()
// console.log(res.json({queueSize}))
console.log(queueSize)
res.json(queueSize)
} catch (err) {
next(err)
}
})
2023-08-18 14:34:22 +00:00
app.get('/mod/approve', async (req, res, next) => {
try {
console.log("recived")
const object = await apex.store.getObject(apex.utils.usernameToIRI(req.query.name), true)
2023-08-18 14:34:22 +00:00
//console.log(object)
//onsole.log(object.publicKey)
//const keys = await apex.store.db.collection(`keys`).findOne( {id: object.id} )
2023-08-18 14:34:22 +00:00
const dbout = await apex.store.db.collection(`${object.id}-queue`).findOne( {object: [req.query.id]} )
//console.log(keys)
2023-08-20 00:08:20 +00:00
await apex.store.db.collection(`${object.id}-posts`).insertOne(dbout)
apex.addToOutbox(object, dbout)
2023-08-18 14:34:22 +00:00
await apex.store.db.collection(`${object.id}-queue`).remove( {object: [req.query.id]} )
res.json(dbout)
} catch (err) {
next(err)
}
})
app.get('/mod/deny', async (req, res, next) => {
try {
console.log("recived")
const object = await apex.store.getObject(apex.utils.usernameToIRI(req.query.name))
var dbout = await apex.store.db.collection(`${object.id}-queue`).findOne( {object: [req.query.id]} )
//apex.addToOutbox(await apex.store.getObject(apex.utils.usernameToIRI(req.query.name)), share)
await apex.store.db.collection(`${object.id}-queue`).remove( {object: [req.query.id]} )
res.json(dbout)
} catch (err) {
next(err)
}
})
2023-08-20 21:21:52 +00:00
app.get('/mod/denyblock', async (req, res, next) => {
try {
console.log("recived")
const object = await apex.store.getObject(apex.utils.usernameToIRI(req.query.name))
var dbout = await apex.store.db.collection(`${object.id}-queue`).findOne( {object: [req.query.id]} )
//apex.addToOutbox(await apex.store.getObject(apex.utils.usernameToIRI(req.query.name)), share)
await apex.store.db.collection(`${object.id}-queue`).remove( {cc: [dbout.cc[0]]} )
var create = await apex.store.db.collection(`${object.id}-blocked`).insertOne( {user: dbout.cc[0]} )
console.log(create)
var findexisting = await apex.store.db.collection(`${object.id}-blocked`).find().toArray()
console.log(findexisting)
res.json(dbout)
} catch (err) {
next(err)
}
})
app.get('/mod/delete/user', async (req, res, next) => {
if (ALLOW_DELETE != "true") {
res.json(null)
return
}
try {
console.log("recived")
const actor = await apex.store.getObject(apex.utils.usernameToIRI(req.query.name), true)
//var dbout = await apex.store.db.collection(`${actor.id}-posts`).findOne( {object: [req.query.id]} )
const to = [
actor.followers[0],
apex.consts.publicAddress
]
const share = await apex.buildActivity('Delete', actor.id, to, {
object: actor
})
apex.addToOutbox(actor, share)
await apex.store.db.collection('objects').remove( {id: actor.id} )
await apex.store.db.collection(`${actor.id}-posts`).drop()
await apex.store.db.collection(`${actor.id}-queue`).drop()
const find = await apex.store.db.collection('streams').find().toArray()
console.log(find)
res.json(find)
} catch (err) {
next(err)
}
})
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')
}
})
app.get('/mod/delete/post', async (req, res, next) => {
2023-08-20 00:08:20 +00:00
try {
console.log("recived")
const actor = await apex.store.getObject(apex.utils.usernameToIRI(req.query.name), true)
var dbout = await apex.store.db.collection(`${actor.id}-posts`).findOne( {object: [req.query.id]} )
const to = [
actor.followers[0],
apex.consts.publicAddress
]
const share = await apex.buildActivity('Undo', actor.id, to, {
object: dbout
})
apex.addToOutbox(actor, share)
await apex.store.db.collection(`${actor.id}-posts`).remove( {object: [req.query.id]} )
res.json(share)
} catch (err) {
next(err)
}
})
2019-12-25 21:48:09 +00:00
2022-02-11 15:43:29 +00:00
client.connect()
2020-10-24 17:29:14 +00:00
.then(async () => {
const { default: AutoEncrypt } = await import('@small-tech/auto-encrypt')
apex.store.db = client.db(DB_NAME)
await apex.store.setup()
2022-12-29 14:30:59 +00:00
await apex.store.db.collection('servers').createIndex({
hostname: 1
}, {
name: 'servers-primary',
unique: true
})
2020-10-24 17:29:14 +00:00
apex.systemUser = await apex.store.getObject(apex.utils.usernameToIRI('system_service'), true)
if (!apex.systemUser) {
const systemUser = await createGuppeActor('system_service', `${DOMAIN} system service`, `${DOMAIN} system service`, icon, 'Service')
2020-10-24 17:29:14 +00:00
await apex.store.saveObject(systemUser)
apex.systemUser = systemUser
}
const grouparray = GROUPS.split(" ")
var g = 0
while (g < grouparray.length) {
findgroup = await apex.store.getObject(apex.utils.usernameToIRI(grouparray[g]), true)
console.log(findgroup)
if (!findgroup) {
const newgroup = await createGuppeActor(grouparray[g], grouparray[g], 'A project built on guppe groups\, it features a moderation ui', icon, 'Group')
await apex.store.saveObject(newgroup)
}
g++
}
2022-05-08 15:43:33 +00:00
let server
if (process.env.NODE_ENV === 'production') {
if (PROXY_MODE) {
server = http.createServer(app)
try {
// boolean or number
app.set('trust proxy', JSON.parse(PROXY_MODE))
} catch (ignore) {
// string
app.set('trust proxy', PROXY_MODE)
}
} else {
server = AutoEncrypt.https.createServer({ domains: [DOMAIN] }, app)
}
} else {
server = https.createServer(sslOptions, app)
}
2020-10-24 17:29:14 +00:00
server.listen(PORT_HTTPS, function () {
console.log('Guppe server listening on port ' + PORT_HTTPS)
})
onShutdown(async () => {
await new Promise((resolve, reject) => {
server.close(err => (err ? reject(err) : resolve()))
})
2022-11-08 03:35:16 +00:00
await client.close()
2020-10-24 17:29:14 +00:00
console.log('Guppe server closed')
})
})