2018-12-24 17:45:15 +00:00
/ *
2022-11-11 04:49:16 +00:00
* Copyright © 2018 - 2021 Musing Studio LLC .
2018-12-24 17:45:15 +00:00
*
* This file is part of WriteFreely .
*
* WriteFreely is free software : you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License , included
* in the LICENSE file in this source code package .
* /
2018-12-31 06:05:26 +00:00
2018-11-08 06:28:08 +00:00
package writefreely
import (
"bytes"
"crypto/sha256"
"database/sql"
"encoding/base64"
"encoding/json"
"fmt"
2019-06-14 00:44:13 +00:00
"io/ioutil"
"net/http"
"net/http/httputil"
"net/url"
2021-03-08 16:43:38 +00:00
"path/filepath"
2019-06-14 00:44:13 +00:00
"strconv"
"time"
2018-11-08 06:28:08 +00:00
"github.com/gorilla/mux"
"github.com/writeas/activity/streams"
"github.com/writeas/httpsig"
"github.com/writeas/impart"
"github.com/writeas/web-core/activitypub"
"github.com/writeas/web-core/activitystreams"
2021-03-30 16:49:12 +00:00
"github.com/writeas/web-core/id"
2018-11-08 06:28:08 +00:00
"github.com/writeas/web-core/log"
)
const (
// TODO: delete. don't use this!
apCustomHandleDefault = "blog"
2019-09-09 20:04:20 +00:00
apCacheTime = time . Minute
2018-11-08 06:28:08 +00:00
)
2021-03-08 16:43:38 +00:00
var instanceColl * Collection
2021-03-08 17:50:08 +00:00
func initActivityPub ( app * App ) {
ur , _ := url . Parse ( app . cfg . App . Host )
2021-03-08 16:43:38 +00:00
instanceColl = & Collection {
2021-03-08 17:50:08 +00:00
ID : 0 ,
Alias : ur . Host ,
Title : ur . Host ,
db : app . db ,
hostName : app . cfg . App . Host ,
2021-03-08 16:43:38 +00:00
}
}
2018-11-08 06:28:08 +00:00
type RemoteUser struct {
ID int64
ActorID string
Inbox string
SharedInbox string
2019-10-10 12:11:46 +00:00
Handle string
2018-11-08 06:28:08 +00:00
}
func ( ru * RemoteUser ) AsPerson ( ) * activitystreams . Person {
return & activitystreams . Person {
BaseObject : activitystreams . BaseObject {
Type : "Person" ,
Context : [ ] interface { } {
activitystreams . Namespace ,
} ,
ID : ru . ActorID ,
} ,
Inbox : ru . Inbox ,
Endpoints : activitystreams . Endpoints {
SharedInbox : ru . SharedInbox ,
} ,
}
}
2020-01-23 17:03:23 +00:00
func activityPubClient ( ) * http . Client {
return & http . Client {
2020-01-23 16:47:35 +00:00
Timeout : 15 * time . Second ,
}
}
2019-05-12 20:55:30 +00:00
func handleFetchCollectionActivities ( app * App , w http . ResponseWriter , r * http . Request ) error {
2018-11-08 06:28:08 +00:00
w . Header ( ) . Set ( "Server" , serverSoftware )
vars := mux . Vars ( r )
alias := vars [ "alias" ]
2021-03-08 16:43:38 +00:00
if alias == "" {
alias = filepath . Base ( r . RequestURI )
}
2018-11-08 06:28:08 +00:00
// TODO: enforce visibility
// Get base Collection data
var c * Collection
var err error
2021-03-08 16:43:38 +00:00
if alias == r . Host {
c = instanceColl
} else if app . cfg . App . SingleUser {
2018-11-08 06:28:08 +00:00
c , err = app . db . GetCollectionByID ( 1 )
} else {
c , err = app . db . GetCollection ( alias )
}
if err != nil {
return err
}
2019-06-14 22:54:04 +00:00
c . hostName = app . cfg . App . Host
2018-11-08 06:28:08 +00:00
2021-03-08 17:54:50 +00:00
if ! c . IsInstanceColl ( ) {
silenced , err := app . db . IsUserSilenced ( c . OwnerID )
if err != nil {
log . Error ( "fetch collection activities: %v" , err )
return ErrInternalGeneral
}
if silenced {
return ErrCollectionNotFound
}
}
2018-11-08 06:28:08 +00:00
p := c . PersonObject ( )
2019-09-09 20:04:20 +00:00
setCacheControl ( w , apCacheTime )
2018-11-08 06:28:08 +00:00
return impart . RenderActivityJSON ( w , p , http . StatusOK )
}
2019-05-12 20:55:30 +00:00
func handleFetchCollectionOutbox ( app * App , w http . ResponseWriter , r * http . Request ) error {
2018-11-08 06:28:08 +00:00
w . Header ( ) . Set ( "Server" , serverSoftware )
vars := mux . Vars ( r )
alias := vars [ "alias" ]
// TODO: enforce visibility
// Get base Collection data
var c * Collection
var err error
if app . cfg . App . SingleUser {
c , err = app . db . GetCollectionByID ( 1 )
} else {
c , err = app . db . GetCollection ( alias )
}
if err != nil {
return err
}
2019-11-11 23:21:45 +00:00
silenced , err := app . db . IsUserSilenced ( c . OwnerID )
2019-08-28 19:37:45 +00:00
if err != nil {
2019-10-25 19:04:24 +00:00
log . Error ( "fetch collection outbox: %v" , err )
2019-08-28 19:37:45 +00:00
return ErrInternalGeneral
}
2019-11-11 23:21:45 +00:00
if silenced {
2019-08-28 19:37:45 +00:00
return ErrCollectionNotFound
}
2019-06-14 22:54:04 +00:00
c . hostName = app . cfg . App . Host
2018-11-08 06:28:08 +00:00
if app . cfg . App . SingleUser {
if alias != c . Alias {
return ErrCollectionNotFound
}
}
res := & CollectionObj { Collection : * c }
app . db . GetPostsCount ( res , false )
accountRoot := c . FederatedAccount ( )
page := r . FormValue ( "page" )
p , err := strconv . Atoi ( page )
if err != nil || p < 1 {
// Return outbox
oc := activitystreams . NewOrderedCollection ( accountRoot , "outbox" , res . TotalPosts )
return impart . RenderActivityJSON ( w , oc , http . StatusOK )
}
// Return outbox page
ocp := activitystreams . NewOrderedCollectionPage ( accountRoot , "outbox" , res . TotalPosts , p )
ocp . OrderedItems = [ ] interface { } { }
2019-08-07 13:26:07 +00:00
posts , err := app . db . GetPosts ( app . cfg , c , p , false , true , false )
2018-11-08 06:28:08 +00:00
for _ , pp := range * posts {
pp . Collection = res
2019-10-10 12:11:46 +00:00
o := pp . ActivityObject ( app )
2018-11-08 06:28:08 +00:00
a := activitystreams . NewCreateActivity ( o )
2020-04-15 16:28:55 +00:00
a . Context = nil
2018-11-08 06:28:08 +00:00
ocp . OrderedItems = append ( ocp . OrderedItems , * a )
}
2019-09-09 20:04:20 +00:00
setCacheControl ( w , apCacheTime )
2018-11-08 06:28:08 +00:00
return impart . RenderActivityJSON ( w , ocp , http . StatusOK )
}
2019-05-12 20:55:30 +00:00
func handleFetchCollectionFollowers ( app * App , w http . ResponseWriter , r * http . Request ) error {
2018-11-08 06:28:08 +00:00
w . Header ( ) . Set ( "Server" , serverSoftware )
vars := mux . Vars ( r )
alias := vars [ "alias" ]
// TODO: enforce visibility
// Get base Collection data
var c * Collection
var err error
if app . cfg . App . SingleUser {
c , err = app . db . GetCollectionByID ( 1 )
} else {
c , err = app . db . GetCollection ( alias )
}
if err != nil {
return err
}
2019-11-11 23:21:45 +00:00
silenced , err := app . db . IsUserSilenced ( c . OwnerID )
2019-08-28 19:37:45 +00:00
if err != nil {
2019-10-25 19:04:24 +00:00
log . Error ( "fetch collection followers: %v" , err )
2019-08-28 19:37:45 +00:00
return ErrInternalGeneral
}
2019-11-11 23:21:45 +00:00
if silenced {
2019-08-28 19:37:45 +00:00
return ErrCollectionNotFound
}
2019-06-14 22:54:04 +00:00
c . hostName = app . cfg . App . Host
2018-11-08 06:28:08 +00:00
accountRoot := c . FederatedAccount ( )
folls , err := app . db . GetAPFollowers ( c )
if err != nil {
return err
}
page := r . FormValue ( "page" )
p , err := strconv . Atoi ( page )
if err != nil || p < 1 {
// Return outbox
oc := activitystreams . NewOrderedCollection ( accountRoot , "followers" , len ( * folls ) )
return impart . RenderActivityJSON ( w , oc , http . StatusOK )
}
// Return outbox page
ocp := activitystreams . NewOrderedCollectionPage ( accountRoot , "followers" , len ( * folls ) , p )
ocp . OrderedItems = [ ] interface { } { }
/ *
for _ , f := range * folls {
ocp . OrderedItems = append ( ocp . OrderedItems , f . ActorID )
}
* /
2019-09-09 20:04:20 +00:00
setCacheControl ( w , apCacheTime )
2018-11-08 06:28:08 +00:00
return impart . RenderActivityJSON ( w , ocp , http . StatusOK )
}
2019-05-12 20:55:30 +00:00
func handleFetchCollectionFollowing ( app * App , w http . ResponseWriter , r * http . Request ) error {
2018-11-08 06:28:08 +00:00
w . Header ( ) . Set ( "Server" , serverSoftware )
vars := mux . Vars ( r )
alias := vars [ "alias" ]
// TODO: enforce visibility
// Get base Collection data
var c * Collection
var err error
if app . cfg . App . SingleUser {
c , err = app . db . GetCollectionByID ( 1 )
} else {
c , err = app . db . GetCollection ( alias )
}
if err != nil {
return err
}
2019-11-11 23:21:45 +00:00
silenced , err := app . db . IsUserSilenced ( c . OwnerID )
2019-08-28 19:37:45 +00:00
if err != nil {
2019-10-25 19:04:24 +00:00
log . Error ( "fetch collection following: %v" , err )
2019-08-28 19:37:45 +00:00
return ErrInternalGeneral
}
2019-11-11 23:21:45 +00:00
if silenced {
2019-08-28 19:37:45 +00:00
return ErrCollectionNotFound
}
2019-06-14 22:54:04 +00:00
c . hostName = app . cfg . App . Host
2018-11-08 06:28:08 +00:00
accountRoot := c . FederatedAccount ( )
page := r . FormValue ( "page" )
p , err := strconv . Atoi ( page )
if err != nil || p < 1 {
// Return outbox
oc := activitystreams . NewOrderedCollection ( accountRoot , "following" , 0 )
return impart . RenderActivityJSON ( w , oc , http . StatusOK )
}
// Return outbox page
ocp := activitystreams . NewOrderedCollectionPage ( accountRoot , "following" , 0 , p )
ocp . OrderedItems = [ ] interface { } { }
2019-09-09 20:04:20 +00:00
setCacheControl ( w , apCacheTime )
2018-11-08 06:28:08 +00:00
return impart . RenderActivityJSON ( w , ocp , http . StatusOK )
}
2019-05-12 20:55:30 +00:00
func handleFetchCollectionInbox ( app * App , w http . ResponseWriter , r * http . Request ) error {
2018-11-08 06:28:08 +00:00
w . Header ( ) . Set ( "Server" , serverSoftware )
vars := mux . Vars ( r )
alias := vars [ "alias" ]
var c * Collection
var err error
if app . cfg . App . SingleUser {
c , err = app . db . GetCollectionByID ( 1 )
} else {
c , err = app . db . GetCollection ( alias )
}
if err != nil {
// TODO: return Reject?
return err
}
2019-11-11 23:21:45 +00:00
silenced , err := app . db . IsUserSilenced ( c . OwnerID )
2019-08-28 19:37:45 +00:00
if err != nil {
2019-10-25 19:04:24 +00:00
log . Error ( "fetch collection inbox: %v" , err )
2019-08-28 19:37:45 +00:00
return ErrInternalGeneral
}
2019-11-11 23:21:45 +00:00
if silenced {
2019-08-28 19:37:45 +00:00
return ErrCollectionNotFound
}
2019-06-14 22:54:04 +00:00
c . hostName = app . cfg . App . Host
2018-11-08 06:28:08 +00:00
if debugging {
dump , err := httputil . DumpRequest ( r , true )
if err != nil {
log . Error ( "Can't dump: %v" , err )
} else {
log . Info ( "Rec'd! %q" , dump )
}
}
var m map [ string ] interface { }
if err := json . NewDecoder ( r . Body ) . Decode ( & m ) ; err != nil {
return err
}
a := streams . NewAccept ( )
p := c . PersonObject ( )
var to * url . URL
var isFollow , isUnfollow bool
fullActor := & activitystreams . Person { }
var remoteUser * RemoteUser
res := & streams . Resolver {
FollowCallback : func ( f * streams . Follow ) error {
isFollow = true
// 1) Use the Follow concrete type here
// 2) Errors are propagated to res.Deserialize call below
m [ "@context" ] = [ ] string { activitystreams . Namespace }
b , _ := json . Marshal ( m )
2018-11-26 13:39:15 +00:00
if debugging {
log . Info ( "Follow: %s" , b )
}
2018-11-08 06:28:08 +00:00
2018-11-11 18:09:19 +00:00
_ , followID := f . GetId ( )
if followID == nil {
log . Error ( "Didn't resolve follow ID" )
} else {
2021-03-30 16:49:12 +00:00
aID := c . FederatedAccount ( ) + "#accept-" + id . GenerateFriendlyRandomString ( 20 )
2018-11-15 22:05:33 +00:00
acceptID , err := url . Parse ( aID )
2018-11-11 18:09:19 +00:00
if err != nil {
2018-11-15 22:05:33 +00:00
log . Error ( "Couldn't parse generated Accept URL '%s': %v" , aID , err )
2018-11-11 18:09:19 +00:00
}
a . SetId ( acceptID )
}
2018-11-08 06:28:08 +00:00
a . AppendObject ( f . Raw ( ) )
_ , to = f . GetActor ( 0 )
obj := f . Raw ( ) . GetObjectIRI ( 0 )
a . AppendActor ( obj )
// First get actor information
if to == nil {
return fmt . Errorf ( "No valid `to` string" )
}
fullActor , remoteUser , err = getActor ( app , to . String ( ) )
if err != nil {
return err
}
return impart . RenderActivityJSON ( w , m , http . StatusOK )
} ,
UndoCallback : func ( u * streams . Undo ) error {
isUnfollow = true
m [ "@context" ] = [ ] string { activitystreams . Namespace }
b , _ := json . Marshal ( m )
2018-11-26 13:39:15 +00:00
if debugging {
log . Info ( "Undo: %s" , b )
}
2018-11-08 06:28:08 +00:00
a . AppendObject ( u . Raw ( ) )
_ , to = u . GetActor ( 0 )
// TODO: get actor from object.object, not object
obj := u . Raw ( ) . GetObjectIRI ( 0 )
a . AppendActor ( obj )
if to != nil {
// Populate fullActor from DB?
remoteUser , err = getRemoteUser ( app , to . String ( ) )
if err != nil {
if iErr , ok := err . ( * impart . HTTPError ) ; ok {
if iErr . Status == http . StatusNotFound {
log . Error ( "No remoteuser info for Undo event!" )
}
}
return err
} else {
fullActor = remoteUser . AsPerson ( )
}
} else {
log . Error ( "No to on Undo!" )
}
return impart . RenderActivityJSON ( w , m , http . StatusOK )
} ,
}
if err := res . Deserialize ( m ) ; err != nil {
// 3) Any errors from #2 can be handled, or the payload is an unknown type.
log . Error ( "Unable to resolve Follow: %v" , err )
if debugging {
log . Error ( "Map: %s" , m )
}
return err
}
go func ( ) {
2020-01-27 14:19:12 +00:00
if to == nil {
2020-05-15 17:47:58 +00:00
if debugging {
log . Error ( "No `to` value!" )
}
2020-01-27 14:19:12 +00:00
return
}
2018-11-08 06:28:08 +00:00
time . Sleep ( 2 * time . Second )
am , err := a . Serialize ( )
if err != nil {
log . Error ( "Unable to serialize Accept: %v" , err )
return
}
am [ "@context" ] = [ ] string { activitystreams . Namespace }
2019-06-14 22:54:04 +00:00
err = makeActivityPost ( app . cfg . App . Host , p , fullActor . Inbox , am )
2018-11-08 06:28:08 +00:00
if err != nil {
log . Error ( "Unable to make activity POST: %v" , err )
return
}
if isFollow {
t , err := app . db . Begin ( )
if err != nil {
log . Error ( "Unable to start transaction: %v" , err )
return
}
var followerID int64
if remoteUser != nil {
followerID = remoteUser . ID
} else {
// Add follower locally, since it wasn't found before
res , err := t . Exec ( "INSERT INTO remoteusers (actor_id, inbox, shared_inbox) VALUES (?, ?, ?)" , fullActor . ID , fullActor . Inbox , fullActor . Endpoints . SharedInbox )
if err != nil {
2019-08-09 21:04:15 +00:00
// if duplicate key, res will be nil and panic on
// res.LastInsertId below
t . Rollback ( )
log . Error ( "Couldn't add new remoteuser in DB: %v\n" , err )
return
2018-11-08 06:28:08 +00:00
}
followerID , err = res . LastInsertId ( )
if err != nil {
t . Rollback ( )
log . Error ( "no lastinsertid for followers, rolling back: %v" , err )
return
}
// Add in key
_ , err = t . Exec ( "INSERT INTO remoteuserkeys (id, remote_user_id, public_key) VALUES (?, ?, ?)" , fullActor . PublicKey . ID , followerID , fullActor . PublicKey . PublicKeyPEM )
if err != nil {
2019-06-13 17:05:05 +00:00
if ! app . db . isDuplicateKeyErr ( err ) {
2018-11-08 06:28:08 +00:00
t . Rollback ( )
log . Error ( "Couldn't add follower keys in DB: %v\n" , err )
return
}
}
}
// Add follow
2019-01-07 19:35:47 +00:00
_ , err = t . Exec ( "INSERT INTO remotefollows (collection_id, remote_user_id, created) VALUES (?, ?, " + app . db . now ( ) + ")" , c . ID , followerID )
2018-11-08 06:28:08 +00:00
if err != nil {
2019-06-13 17:05:05 +00:00
if ! app . db . isDuplicateKeyErr ( err ) {
2018-11-08 06:28:08 +00:00
t . Rollback ( )
log . Error ( "Couldn't add follower in DB: %v\n" , err )
return
}
}
err = t . Commit ( )
if err != nil {
t . Rollback ( )
log . Error ( "Rolling back after Commit(): %v\n" , err )
return
}
} else if isUnfollow {
// Remove follower locally
_ , err = app . db . Exec ( "DELETE FROM remotefollows WHERE collection_id = ? AND remote_user_id = (SELECT id FROM remoteusers WHERE actor_id = ?)" , c . ID , to . String ( ) )
if err != nil {
log . Error ( "Couldn't remove follower from DB: %v\n" , err )
}
}
} ( )
return nil
}
2019-06-14 22:54:04 +00:00
func makeActivityPost ( hostName string , p * activitystreams . Person , url string , m interface { } ) error {
2018-11-08 06:28:08 +00:00
log . Info ( "POST %s" , url )
b , err := json . Marshal ( m )
if err != nil {
return err
}
r , _ := http . NewRequest ( "POST" , url , bytes . NewBuffer ( b ) )
r . Header . Add ( "Content-Type" , "application/activity+json" )
2020-08-18 16:22:04 +00:00
r . Header . Set ( "User-Agent" , ServerUserAgent ( hostName ) )
2018-11-08 06:28:08 +00:00
h := sha256 . New ( )
h . Write ( b )
r . Header . Add ( "Digest" , "SHA-256=" + base64 . StdEncoding . EncodeToString ( h . Sum ( nil ) ) )
// Sign using the 'Signature' header
privKey , err := activitypub . DecodePrivateKey ( p . GetPrivKey ( ) )
if err != nil {
return err
}
signer := httpsig . NewSigner ( p . PublicKey . ID , privKey , httpsig . RSASHA256 , [ ] string { "(request-target)" , "date" , "host" , "digest" } )
err = signer . SignSigHeader ( r )
if err != nil {
log . Error ( "Can't sign: %v" , err )
}
if debugging {
dump , err := httputil . DumpRequestOut ( r , true )
if err != nil {
log . Error ( "Can't dump: %v" , err )
} else {
log . Info ( "%s" , dump )
}
}
2020-01-23 16:47:35 +00:00
resp , err := activityPubClient ( ) . Do ( r )
2018-11-14 23:30:24 +00:00
if err != nil {
return err
}
2018-11-08 06:28:08 +00:00
if resp != nil && resp . Body != nil {
defer resp . Body . Close ( )
}
body , err := ioutil . ReadAll ( resp . Body )
if err != nil {
return err
}
if debugging {
log . Info ( "Status : %s" , resp . Status )
log . Info ( "Response: %s" , body )
}
return nil
}
2019-06-14 22:54:04 +00:00
func resolveIRI ( hostName , url string ) ( [ ] byte , error ) {
2018-11-08 06:28:08 +00:00
log . Info ( "GET %s" , url )
r , _ := http . NewRequest ( "GET" , url , nil )
r . Header . Add ( "Accept" , "application/activity+json" )
2020-08-18 16:22:04 +00:00
r . Header . Set ( "User-Agent" , ServerUserAgent ( hostName ) )
2018-11-08 06:28:08 +00:00
2021-03-08 16:43:38 +00:00
p := instanceColl . PersonObject ( )
h := sha256 . New ( )
h . Write ( [ ] byte { } )
r . Header . Add ( "Digest" , "SHA-256=" + base64 . StdEncoding . EncodeToString ( h . Sum ( nil ) ) )
// Sign using the 'Signature' header
privKey , err := activitypub . DecodePrivateKey ( p . GetPrivKey ( ) )
if err != nil {
return nil , err
}
signer := httpsig . NewSigner ( p . PublicKey . ID , privKey , httpsig . RSASHA256 , [ ] string { "(request-target)" , "date" , "host" , "digest" } )
err = signer . SignSigHeader ( r )
if err != nil {
log . Error ( "Can't sign: %v" , err )
}
2018-11-08 06:28:08 +00:00
if debugging {
dump , err := httputil . DumpRequestOut ( r , true )
if err != nil {
log . Error ( "Can't dump: %v" , err )
} else {
log . Info ( "%s" , dump )
}
}
2020-01-23 16:47:35 +00:00
resp , err := activityPubClient ( ) . Do ( r )
2018-11-14 23:30:24 +00:00
if err != nil {
return nil , err
}
2018-11-08 06:28:08 +00:00
if resp != nil && resp . Body != nil {
defer resp . Body . Close ( )
}
body , err := ioutil . ReadAll ( resp . Body )
if err != nil {
return nil , err
}
if debugging {
log . Info ( "Status : %s" , resp . Status )
log . Info ( "Response: %s" , body )
}
return body , nil
}
2019-05-12 20:55:30 +00:00
func deleteFederatedPost ( app * App , p * PublicPost , collID int64 ) error {
2018-11-08 06:28:08 +00:00
if debugging {
log . Info ( "Deleting federated post!" )
}
2019-06-21 01:08:30 +00:00
p . Collection . hostName = app . cfg . App . Host
2018-11-08 06:28:08 +00:00
actor := p . Collection . PersonObject ( collID )
2019-10-10 12:11:46 +00:00
na := p . ActivityObject ( app )
2018-11-08 06:28:08 +00:00
// Add followers
p . Collection . ID = collID
followers , err := app . db . GetAPFollowers ( & p . Collection . Collection )
if err != nil {
log . Error ( "Couldn't delete post (get followers)! %v" , err )
return err
}
inboxes := map [ string ] [ ] string { }
for _ , f := range * followers {
2019-06-03 18:53:17 +00:00
inbox := f . SharedInbox
if inbox == "" {
inbox = f . Inbox
}
if _ , ok := inboxes [ inbox ] ; ok {
inboxes [ inbox ] = append ( inboxes [ inbox ] , f . ActorID )
2018-11-08 06:28:08 +00:00
} else {
2019-06-03 18:53:17 +00:00
inboxes [ inbox ] = [ ] string { f . ActorID }
2018-11-08 06:28:08 +00:00
}
}
for si , instFolls := range inboxes {
na . CC = [ ] string { }
for _ , f := range instFolls {
na . CC = append ( na . CC , f )
}
2020-02-08 20:19:06 +00:00
da := activitystreams . NewDeleteActivity ( na )
// Make the ID unique to ensure it works in Pleroma
// See: https://git.pleroma.social/pleroma/pleroma/issues/1481
da . ID += "#Delete"
err = makeActivityPost ( app . cfg . App . Host , actor , si , da )
2018-11-08 06:28:08 +00:00
if err != nil {
log . Error ( "Couldn't delete post! %v" , err )
}
}
return nil
}
2019-05-12 20:55:30 +00:00
func federatePost ( app * App , p * PublicPost , collID int64 , isUpdate bool ) error {
2021-01-28 00:39:46 +00:00
// If app is private, do not federate
if app . cfg . App . Private {
return nil
}
// Do not federate posts from private or protected blogs
if p . Collection . Visibility == CollPrivate || p . Collection . Visibility == CollProtected {
return nil
}
2018-11-08 06:28:08 +00:00
if debugging {
if isUpdate {
log . Info ( "Federating updated post!" )
} else {
log . Info ( "Federating new post!" )
}
}
2021-04-07 15:27:25 +00:00
2018-11-08 06:28:08 +00:00
actor := p . Collection . PersonObject ( collID )
2019-10-10 12:11:46 +00:00
na := p . ActivityObject ( app )
2018-11-08 06:28:08 +00:00
// Add followers
p . Collection . ID = collID
followers , err := app . db . GetAPFollowers ( & p . Collection . Collection )
if err != nil {
log . Error ( "Couldn't post! %v" , err )
return err
}
log . Info ( "Followers for %d: %+v" , collID , followers )
inboxes := map [ string ] [ ] string { }
for _ , f := range * followers {
2019-06-03 18:53:17 +00:00
inbox := f . SharedInbox
if inbox == "" {
inbox = f . Inbox
}
if _ , ok := inboxes [ inbox ] ; ok {
2019-10-09 11:34:31 +00:00
// check if we're already sending to this shared inbox
2019-06-03 18:53:17 +00:00
inboxes [ inbox ] = append ( inboxes [ inbox ] , f . ActorID )
2018-11-08 06:28:08 +00:00
} else {
2019-10-09 11:34:31 +00:00
// add the new shared inbox to the list
2019-06-03 18:53:17 +00:00
inboxes [ inbox ] = [ ] string { f . ActorID }
2018-11-08 06:28:08 +00:00
}
}
2019-10-08 12:58:19 +00:00
var activity * activitystreams . Activity
2019-10-09 11:34:31 +00:00
// for each one of the shared inboxes
2018-11-08 06:28:08 +00:00
for si , instFolls := range inboxes {
2019-10-09 11:34:31 +00:00
// add all followers from that instance
// to the CC field
2018-11-08 06:28:08 +00:00
na . CC = [ ] string { }
for _ , f := range instFolls {
na . CC = append ( na . CC , f )
}
2019-10-09 11:34:31 +00:00
// create a new "Create" activity
// with our article as object
2018-11-08 06:28:08 +00:00
if isUpdate {
activity = activitystreams . NewUpdateActivity ( na )
} else {
activity = activitystreams . NewCreateActivity ( na )
2018-11-11 18:11:01 +00:00
activity . To = na . To
activity . CC = na . CC
2018-11-08 06:28:08 +00:00
}
2019-10-09 11:34:31 +00:00
// and post it to that sharedInbox
2019-06-14 22:54:04 +00:00
err = makeActivityPost ( app . cfg . App . Host , actor , si , activity )
2018-11-08 06:28:08 +00:00
if err != nil {
log . Error ( "Couldn't post! %v" , err )
}
}
2019-10-08 12:58:19 +00:00
2019-10-09 11:34:31 +00:00
// re-create the object so that the CC list gets reset and has
// the mentioned users. This might seem wasteful but the code is
// cleaner than adding the mentioned users to CC here instead of
// in p.ActivityObject()
2019-10-10 12:11:46 +00:00
na = p . ActivityObject ( app )
2019-10-08 12:58:19 +00:00
for _ , tag := range na . Tag {
if tag . Type == "Mention" {
activity = activitystreams . NewCreateActivity ( na )
activity . To = na . To
activity . CC = na . CC
// This here might be redundant in some cases as we might have already
// sent this to the sharedInbox of this instance above, but we need too
// much logic to catch this at the expense of the odd extra request.
// I don't believe we'd ever have too many mentions in a single post that this
// could become a burden.
2019-10-10 12:11:46 +00:00
remoteUser , err := getRemoteUser ( app , tag . HRef )
2020-06-08 17:50:43 +00:00
if err != nil {
log . Error ( "Unable to find remote user %s. Skipping: %v" , tag . HRef , err )
continue
}
2019-10-10 12:11:46 +00:00
err = makeActivityPost ( app . cfg . App . Host , actor , remoteUser . Inbox , activity )
2019-10-08 12:58:19 +00:00
if err != nil {
log . Error ( "Couldn't post! %v" , err )
}
}
}
2018-11-08 06:28:08 +00:00
return nil
}
2019-05-12 20:55:30 +00:00
func getRemoteUser ( app * App , actorID string ) ( * RemoteUser , error ) {
2018-11-08 06:28:08 +00:00
u := RemoteUser { ActorID : actorID }
2020-03-24 11:59:00 +00:00
var handle sql . NullString
err := app . db . QueryRow ( "SELECT id, inbox, shared_inbox, handle FROM remoteusers WHERE actor_id = ?" , actorID ) . Scan ( & u . ID , & u . Inbox , & u . SharedInbox , & handle )
2018-11-08 06:28:08 +00:00
switch {
case err == sql . ErrNoRows :
return nil , impart . HTTPError { http . StatusNotFound , "No remote user with that ID." }
case err != nil :
log . Error ( "Couldn't get remote user %s: %v" , actorID , err )
return nil , err
}
2020-03-24 11:59:00 +00:00
u . Handle = handle . String
2018-11-08 06:28:08 +00:00
return & u , nil
}
2019-10-10 12:11:46 +00:00
// getRemoteUserFromHandle retrieves the profile page of a remote user
// from the @user@server.tld handle
func getRemoteUserFromHandle ( app * App , handle string ) ( * RemoteUser , error ) {
u := RemoteUser { Handle : handle }
err := app . db . QueryRow ( "SELECT id, actor_id, inbox, shared_inbox FROM remoteusers WHERE handle = ?" , handle ) . Scan ( & u . ID , & u . ActorID , & u . Inbox , & u . SharedInbox )
switch {
case err == sql . ErrNoRows :
2020-02-08 18:05:54 +00:00
return nil , ErrRemoteUserNotFound
2019-10-10 12:11:46 +00:00
case err != nil :
log . Error ( "Couldn't get remote user %s: %v" , handle , err )
return nil , err
}
return & u , nil
}
2019-05-12 20:55:30 +00:00
func getActor ( app * App , actorIRI string ) ( * activitystreams . Person , * RemoteUser , error ) {
2018-11-08 06:28:08 +00:00
log . Info ( "Fetching actor %s locally" , actorIRI )
actor := & activitystreams . Person { }
remoteUser , err := getRemoteUser ( app , actorIRI )
if err != nil {
if iErr , ok := err . ( impart . HTTPError ) ; ok {
if iErr . Status == http . StatusNotFound {
// Fetch remote actor
log . Info ( "Not found; fetching actor %s remotely" , actorIRI )
2019-06-14 22:54:04 +00:00
actorResp , err := resolveIRI ( app . cfg . App . Host , actorIRI )
2018-11-08 06:28:08 +00:00
if err != nil {
log . Error ( "Unable to get actor! %v" , err )
return nil , nil , impart . HTTPError { http . StatusInternalServerError , "Couldn't fetch actor." }
}
2019-05-20 21:41:26 +00:00
if err := unmarshalActor ( actorResp , actor ) ; err != nil {
2018-11-08 06:28:08 +00:00
log . Error ( "Unable to unmarshal actor! %v" , err )
return nil , nil , impart . HTTPError { http . StatusInternalServerError , "Couldn't parse actor." }
}
} else {
return nil , nil , err
}
} else {
return nil , nil , err
}
} else {
actor = remoteUser . AsPerson ( )
}
return actor , remoteUser , nil
}
2019-05-20 21:41:26 +00:00
// unmarshal actor normalizes the actor response to conform to
// the type Person from github.com/writeas/web-core/activitysteams
//
// some implementations return different context field types
// this converts any non-slice contexts into a slice
func unmarshalActor ( actorResp [ ] byte , actor * activitystreams . Person ) error {
// FIXME: Hubzilla has an object for the Actor's url: cannot unmarshal object into Go struct field Person.url of type string
// flexActor overrides the Context field to allow
// all valid representations during unmarshal
flexActor := struct {
activitystreams . Person
Context json . RawMessage ` json:"@context,omitempty" `
} { }
if err := json . Unmarshal ( actorResp , & flexActor ) ; err != nil {
return err
}
actor . Endpoints = flexActor . Endpoints
actor . Followers = flexActor . Followers
actor . Following = flexActor . Following
actor . ID = flexActor . ID
actor . Icon = flexActor . Icon
actor . Inbox = flexActor . Inbox
actor . Name = flexActor . Name
actor . Outbox = flexActor . Outbox
actor . PreferredUsername = flexActor . PreferredUsername
actor . PublicKey = flexActor . PublicKey
actor . Summary = flexActor . Summary
actor . Type = flexActor . Type
actor . URL = flexActor . URL
func ( val interface { } ) {
switch val . ( type ) {
case [ ] interface { } :
// already a slice, do nothing
actor . Context = val . ( [ ] interface { } )
default :
actor . Context = [ ] interface { } { val }
}
} ( flexActor . Context )
return nil
}
2019-09-09 20:04:20 +00:00
func setCacheControl ( w http . ResponseWriter , ttl time . Duration ) {
w . Header ( ) . Set ( "Cache-Control" , fmt . Sprintf ( "public, max-age=%.0f" , ttl . Seconds ( ) ) )
}