2019-02-20 22:12:29 +00:00
import { Router , Request , Response } from "express" ;
import uuidv4 from "uuid/v4" ;
2019-08-18 22:09:28 +00:00
import { createActivity , getActor , signAndSend } from "./federate" ;
2019-08-17 18:50:18 +00:00
import { Activity , FollowActivity , AcceptActivity , UndoActivity , CreateActivity , NoteObject , DeleteActivity } from "./activity" ;
2019-02-22 00:17:59 +00:00
import sanitizeHtml from "sanitize-html" ;
2019-08-17 18:50:18 +00:00
import Note from "../entity/Note" ;
import { getConnection } from "typeorm" ;
import Actor from "../entity/Actor" ;
2019-08-18 22:09:28 +00:00
import Article from "../entity/Article" ;
2019-02-20 22:12:29 +00:00
const domain = process . env . DOMAIN ;
2019-02-20 23:07:29 +00:00
export default function inbox ( router : Router ) {
2019-02-20 22:12:29 +00:00
router . post ( "/ap/inbox" , handleInbox ) ;
router . post ( "/inbox" , handleInbox ) ;
}
2019-08-18 22:09:28 +00:00
function handleInbox ( req : Request , res : Response ) {
2019-02-20 22:12:29 +00:00
const activity = req . body as Activity ;
if ( activity . type === "Follow" ) {
handleFollow ( activity , req , res ) ;
} else if ( activity . type === "Create" ) {
handleCreate ( activity , req , res ) ;
} else if ( activity . type === "Undo" ) {
handleUndo ( activity , req , res ) ;
2019-02-22 00:26:24 +00:00
} else if ( activity . type === "Delete" ) {
handleDelete ( activity , req , res ) ;
2019-02-20 22:12:29 +00:00
} else {
res . end ( ) ; // TODO: handle this better
}
}
async function handleFollow ( activity : Activity , req : Request , res : Response ) {
if ( typeof req . body . object !== "string" ) {
res . end ( ) ; // TODO: handle this better
return ;
}
2019-08-17 18:50:18 +00:00
const follow = activity as FollowActivity ;
2019-02-20 22:12:29 +00:00
if ( follow . object !== ` https:// ${ domain } /ap/actor ` ) {
res . end ( ) ;
return ;
}
2019-08-17 18:50:18 +00:00
const actor = await getActor ( follow . actor , true ) ; // always force re-fetch the actor on follow
2019-06-30 18:59:39 +00:00
if ( ! actor ) {
// if the actor ceases existing between the time the Follow is sent and when receive it, ignore it and end the request
res . end ( ) ;
return ;
}
2019-08-17 18:50:18 +00:00
const acceptObject = {
"@context" : "https://www.w3.org/ns/activitystreams" ,
2019-02-20 22:12:29 +00:00
"id" : ` https:// ${ domain } /ap/ ${ uuidv4 ( ) } ` ,
"type" : "Accept" ,
"actor" : ` https:// ${ domain } /ap/actor ` ,
"object" : follow
2019-08-17 18:50:18 +00:00
} as AcceptActivity ;
2019-08-18 22:09:28 +00:00
try {
await signAndSend ( acceptObject , actor . inbox ) ;
// prefer shared server inbox
const serverInbox = actor . endpoints && actor . endpoints . sharedInbox ? actor.endpoints.sharedInbox : actor.inbox ;
await getConnection ( ) . createQueryBuilder ( )
. update ( Actor )
. set ( {
isFollower : true ,
inbox : serverInbox
} )
. where ( "id = :id" , { id : actor.id } )
. execute ( ) ;
res . end ( ) ;
} catch ( err ) {
res . status ( 500 ) . json ( { message : "Encountered error handling follow." , error : err } ) . end ( ) ;
}
2019-02-20 22:12:29 +00:00
}
async function handleCreate ( activity : Activity , req : Request , res : Response ) {
2019-08-17 18:50:18 +00:00
const create = activity as CreateActivity ;
2019-02-20 22:12:29 +00:00
if ( create . object . type == "Note" ) {
handleCreateNote ( create , req , res ) ;
} else {
res . end ( ) ;
}
}
2019-02-22 00:17:59 +00:00
function sanitizeStatus ( content : string ) : string {
return sanitizeHtml ( content , {
allowedTags : [ "a" , "span" , "p" , "br" , "b" , "strong" , "i" , "em" , "s" , "del" , "u" , "code" , "pre" , "ul" , "ol" , "li" , "blockquote" , "img" ] ,
allowedAttributes : {
"a" : [ "href" , "data-user" ] ,
"img" : [ "src" ]
} ,
transformTags : {
"a" : sanitizeHtml . simpleTransform ( "a" , { rel : "noopener" , target : "_blank" } )
}
} ) ;
}
2019-08-17 18:50:18 +00:00
async function handleCreateNote ( create : CreateActivity , req : Request , res : Response ) {
const noteObject = create . object as NoteObject ;
2019-08-18 21:15:39 +00:00
const actor = await getActor ( noteObject . attributedTo ) ; // get and cache the actor if it's not already cached
2019-08-18 22:09:28 +00:00
const asPublic = "https://www.w3.org/ns/activitystreams#Public" ;
const article = await getConnection ( ) . getRepository ( Article ) . findOne ( { where : { conversation : noteObject.conversation } } ) ;
if ( ! article ) {
console . log ( ` Ignoring message not in response to an article: ${ noteObject . id } ` ) ;
res . end ( ) ;
} else if ( ! process . env . ACCEPT_NON_PUBLIC_NOTES && ! noteObject . to . includes ( asPublic ) && ! noteObject . cc . includes ( asPublic ) ) {
console . log ( ` Ignoring non-public post: ${ noteObject . id } ` ) ;
try {
const note : NoteObject = {
type : "Note" ,
id : ` https:// ${ domain } /ap/object/ ${ uuidv4 ( ) } ` ,
to : [ actor . id ] ,
cc : [ ] ,
directMessage : true ,
attributedTo : ` https:// ${ domain } /ap/actor ` ,
content : ` <a href=" ${ actor . url } " class="mention">@<span> ${ actor . preferredUsername } </span></a> Non-public posts are not accepted. To respond to a blog post, use either Public or Unlisted. ` ,
published : new Date ( ) . toISOString ( ) ,
inReplyTo : noteObject.id ,
conversation : noteObject.conversation || ` https:// ${ domain } /ap/conversation/ ${ uuidv4 ( ) } `
} ;
const responseCreate = createActivity ( note ) ;
signAndSend ( responseCreate , actor . inbox ) ;
} catch ( err ) {
console . error ( ` Couldn't send non-public reply Note to ${ noteObject . id } ` , err ) ;
}
res . end ( ) ;
} else {
const sanitizedContent = sanitizeStatus ( noteObject . content ) ;
const note = new Note ( ) ;
note . id = noteObject . id ;
note . actor = await getConnection ( ) . getRepository ( Actor ) . findOne ( actor . id ) ;
note . content = sanitizedContent ;
note . attributedTo = noteObject . attributedTo ;
note . inReplyTo = noteObject . inReplyTo ;
note . conversation = noteObject . conversation ;
note . published = noteObject . published ;
try {
await getConnection ( ) . getRepository ( Note ) . save ( note ) ;
res . end ( ) ;
} catch ( err ) {
console . error ( ` Encountered error storing reply ${ noteObject . id } ` , err ) ;
res . status ( 500 ) . end ( ) ;
}
2019-08-17 18:50:18 +00:00
}
2019-02-20 22:12:29 +00:00
}
2019-02-22 00:26:24 +00:00
async function handleDelete ( activity : Activity , req : Request , res : Response ) {
2019-08-17 18:50:18 +00:00
const deleteActivity = activity as DeleteActivity ;
try {
await getConnection ( ) . getRepository ( Note ) . createQueryBuilder ( )
. delete ( )
. from ( Note , "note" )
. where ( "note.id = :id" , { id : deleteActivity.object } )
2019-08-18 21:15:39 +00:00
. andWhere ( "note.\"actorId\" = :actor" , { actor : deleteActivity.actor } )
2019-08-17 18:50:18 +00:00
. execute ( ) ;
} catch ( err ) {
console . error ( ` Encountered error deleting ${ deleteActivity . object } ` , err ) ;
}
res . end ( ) ;
2019-02-22 00:26:24 +00:00
}
2019-08-18 22:09:28 +00:00
function handleUndo ( activity : Activity , req : Request , res : Response ) {
2019-08-17 18:50:18 +00:00
const undo = activity as UndoActivity ;
2019-02-20 22:12:29 +00:00
if ( undo . object . type === "Follow" ) {
handleUndoFollow ( undo , req , res ) ;
} else {
res . end ( ) ;
}
}
2019-08-17 18:50:18 +00:00
async function handleUndoFollow ( undo : UndoActivity , req : Request , res : Response ) {
const follow = undo . object as FollowActivity ;
if ( follow . object !== ` https:// ${ domain } /ap/actor ` || undo . actor !== follow . actor ) {
2019-02-20 22:12:29 +00:00
res . end ( ) ;
return ;
}
2019-08-17 18:50:18 +00:00
try {
await getConnection ( ) . createQueryBuilder ( )
. update ( Actor )
. set ( { isFollower : false } )
. where ( "id = :id" , { id : follow.actor } )
. execute ( ) ;
} catch ( err ) {
console . error ( ` Error handling unfollow from ${ follow . actor } ` , err ) ;
}
2019-02-20 22:12:29 +00:00
res . end ( ) ;
2019-08-17 18:50:18 +00:00
}