2021-11-01 23:05:14 +00:00
require ( 'dotenv' ) . config ( )
2019-09-20 01:55:32 +00:00
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' )
2019-09-20 01:55:32 +00:00
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' )
2019-10-04 02:47:44 +00:00
const history = require ( 'connect-history-api-fallback' )
2021-01-21 04:18:30 +00:00
const { onShutdown } = require ( 'node-graceful-shutdown' )
2020-10-24 17:29:14 +00:00
const ActivitypubExpress = require ( 'activitypub-express' )
2019-09-20 01:55:32 +00:00
2022-05-01 20:09:23 +00:00
const { version } = require ( './package.json' )
2023-03-17 16:37:31 +00:00
const { DOMAIN , KEY _PATH , CERT _PATH , CA _PATH , PORT _HTTPS , DB _URL , DB _NAME , PROXY _MODE , ADMIN _SECRET , USE _ATTACHMENTS } = process . env
2019-09-13 01:03:04 +00:00
2019-09-24 03:18:35 +00:00
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 ,
2022-11-08 03:14:30 +00:00
// delivery done in workers only in production
offlineMode : process . env . NODE _ENV === 'production' ,
2023-03-17 16:37:31 +00:00
context : require ( './data/context.json' ) ,
2020-10-24 17:29:14 +00:00
routes
} )
2023-03-17 16:37:31 +00:00
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"' ) ,
2023-03-17 16:37:31 +00:00
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
2021-11-05 03:58:55 +00:00
// Create new groups on demand whenever someone tries to access one
async function actorOnDemand ( req , res , next ) {
const actor = req . params . actor
if ( ! actor ) {
return next ( )
}
2020-10-24 17:29:14 +00:00
const actorIRI = apex . utils . usernameToIRI ( actor )
try {
2021-11-05 03:58:55 +00:00
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 } `
2023-03-17 16:37:31 +00:00
const actorObj = await createGuppeActor ( actor , ` ${ actor } group ` , summary , icon , 'Group' )
2020-10-24 17:29:14 +00:00
await apex . store . saveObject ( actorObj )
2021-11-05 03:58:55 +00:00
}
} 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 (
2023-06-23 16:08:23 +00:00
// 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 ) {
2023-06-23 16:08:23 +00:00
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 )
2023-06-23 16:08:23 +00:00
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 )
2021-11-05 03:58:55 +00:00
. post ( actorOnDemand , apex . net . inbox . post )
. get ( actorOnDemand , apex . net . inbox . get )
2020-10-24 17:29:14 +00:00
app . route ( routes . outbox )
2021-11-05 03:58:55 +00:00
. get ( actorOnDemand , apex . net . outbox . get )
2023-03-17 16:37:31 +00:00
. 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
2021-11-05 03:58:55 +00:00
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 ,
2021-11-05 03:58:55 +00:00
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 ( ) ) {
2021-11-05 03:59:37 +00:00
// automatically reshare incoming posts
2020-10-24 17:29:14 +00:00
case 'create' : {
const to = [
recipient . followers [ 0 ] ,
apex . consts . publicAddress
]
const share = await apex . buildActivity ( 'Announce' , recipient . id , to , {
2022-01-01 22:29:11 +00:00
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
} )
apex . addToOutbox ( recipient , share )
break
}
2021-11-05 03:59:37 +00:00
// 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 ( )
}
}
} )
/// Guppe web setup
// html/static routes
2019-10-04 02:47:44 +00:00
app . use ( history ( {
2019-11-18 01:31:58 +00:00
index : '/web/index.html' ,
rewrites : [
// do not redirect webfinger et c.
{ from : /^\/\.well-known\// , to : context => context . request . originalUrl }
]
2019-10-04 02:47:44 +00:00
} ) )
2019-09-22 05:20:37 +00:00
app . use ( '/f' , express . static ( 'public/files' ) )
2019-10-04 02:47:44 +00:00
app . use ( '/web' , express . static ( 'web/dist' ) )
2021-11-05 03:59:37 +00:00
// 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 ( )
} )
} )
2022-11-08 03:14:30 +00:00
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
2019-12-25 21:48:09 +00:00
app . use ( function ( err , req , res , next ) {
console . error ( err . message , req . body , err . stack )
2020-10-24 17:29:14 +00:00
if ( ! res . headersSent ) {
res . status ( 500 ) . send ( 'An error occurred while processing the request' )
}
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 ) {
2023-03-17 16:37:31 +00:00
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
}
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' )
} )
2021-01-21 04:18:30 +00:00
} )