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' )
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
2021-11-01 23:05:14 +00:00
const { DOMAIN , KEY _PATH , CERT _PATH , CA _PATH , PORT _HTTPS , DB _URL , DB _NAME } = process . env
2019-09-13 01:03:04 +00:00
2019-09-24 03:18:35 +00:00
const app = express ( )
2019-09-28 03:17:54 +00:00
2019-09-24 03:18:35 +00:00
const client = new MongoClient ( DB _URL , { useUnifiedTopology : true , useNewUrlParser : true } )
2019-09-13 01:03:04 +00:00
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 ( {
domain : DOMAIN ,
actorParam : 'actor' ,
objectParam : 'id' ,
itemsPerPage : 100 ,
routes
} )
2019-12-25 22:22:31 +00:00
app . use ( morgan ( ':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status Accepts ":req[accept]" ":referrer" ":user-agent"' ) )
2020-10-24 17:29:14 +00:00
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
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' ) )
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
} )
2019-09-22 05:20:37 +00:00
client . connect ( { useNewUrlParser : true } )
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 ( )
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
}
2021-01-21 04:18:30 +00:00
2020-10-24 17:29:14 +00:00
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' )
} )
2021-01-21 04:18:30 +00:00
} )