2018-12-24 18:45:15 +01:00
/ *
2022-11-11 05:49:16 +01:00
* Copyright © 2018 - 2021 Musing Studio LLC .
2018-12-24 18:45:15 +01: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 07:05:26 +01:00
2018-11-08 07:28:08 +01:00
package writefreely
import (
"bytes"
"crypto/sha256"
"database/sql"
"encoding/base64"
"encoding/json"
"fmt"
2023-07-10 11:55:04 +02:00
"io"
2019-06-14 02:44:13 +02:00
"net/http"
"net/http/httputil"
"net/url"
2021-03-08 17:43:38 +01:00
"path/filepath"
2019-06-14 02:44:13 +02:00
"strconv"
2023-09-22 01:04:34 +02:00
"strings"
2019-06-14 02:44:13 +02:00
"time"
2018-11-08 07:28:08 +01:00
"github.com/gorilla/mux"
"github.com/writeas/activity/streams"
2023-09-22 01:04:34 +02:00
"github.com/writeas/activityserve"
2018-11-08 07:28:08 +01:00
"github.com/writeas/httpsig"
"github.com/writeas/impart"
"github.com/writeas/web-core/activitypub"
"github.com/writeas/web-core/activitystreams"
2021-03-30 18:49:12 +02:00
"github.com/writeas/web-core/id"
2018-11-08 07:28:08 +01:00
"github.com/writeas/web-core/log"
2023-09-22 01:04:34 +02:00
"github.com/writeas/web-core/silobridge"
2018-11-08 07:28:08 +01:00
)
const (
// TODO: delete. don't use this!
apCustomHandleDefault = "blog"
2019-09-09 22:04:20 +02:00
apCacheTime = time . Minute
2018-11-08 07:28:08 +01:00
)
2021-03-08 17:43:38 +01:00
var instanceColl * Collection
2021-03-08 18:50:08 +01:00
func initActivityPub ( app * App ) {
ur , _ := url . Parse ( app . cfg . App . Host )
2021-03-08 17:43:38 +01:00
instanceColl = & Collection {
2021-03-08 18:50:08 +01:00
ID : 0 ,
Alias : ur . Host ,
Title : ur . Host ,
db : app . db ,
hostName : app . cfg . App . Host ,
2021-03-08 17:43:38 +01:00
}
}
2018-11-08 07:28:08 +01:00
type RemoteUser struct {
ID int64
ActorID string
Inbox string
SharedInbox string
2023-09-22 01:04:34 +02:00
URL string
2019-10-10 14:11:46 +02:00
Handle string
2023-10-13 22:45:12 +02:00
Created time . Time
}
func ( ru * RemoteUser ) CreatedFriendly ( ) string {
return ru . Created . Format ( "January 2, 2006" )
}
func ( ru * RemoteUser ) EstimatedHandle ( ) string {
if ru . Handle != "" {
return ru . Handle
}
username := filepath . Base ( ru . ActorID )
host , _ := url . Parse ( ru . ActorID )
return username + "@" + host . Host
2018-11-08 07:28:08 +01: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 18:03:23 +01:00
func activityPubClient ( ) * http . Client {
return & http . Client {
2020-01-23 17:47:35 +01:00
Timeout : 15 * time . Second ,
}
}
2019-05-12 22:55:30 +02:00
func handleFetchCollectionActivities ( app * App , w http . ResponseWriter , r * http . Request ) error {
2018-11-08 07:28:08 +01:00
w . Header ( ) . Set ( "Server" , serverSoftware )
vars := mux . Vars ( r )
alias := vars [ "alias" ]
2021-03-08 17:43:38 +01:00
if alias == "" {
alias = filepath . Base ( r . RequestURI )
}
2018-11-08 07:28:08 +01:00
// TODO: enforce visibility
// Get base Collection data
var c * Collection
var err error
2021-03-08 17:43:38 +01:00
if alias == r . Host {
c = instanceColl
} else if app . cfg . App . SingleUser {
2018-11-08 07:28:08 +01:00
c , err = app . db . GetCollectionByID ( 1 )
} else {
c , err = app . db . GetCollection ( alias )
}
if err != nil {
return err
}
2019-06-15 00:54:04 +02:00
c . hostName = app . cfg . App . Host
2018-11-08 07:28:08 +01:00
2021-03-08 18:54:50 +01: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 07:28:08 +01:00
p := c . PersonObject ( )
2019-09-09 22:04:20 +02:00
setCacheControl ( w , apCacheTime )
2018-11-08 07:28:08 +01:00
return impart . RenderActivityJSON ( w , p , http . StatusOK )
}
2019-05-12 22:55:30 +02:00
func handleFetchCollectionOutbox ( app * App , w http . ResponseWriter , r * http . Request ) error {
2018-11-08 07:28:08 +01: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-12 00:21:45 +01:00
silenced , err := app . db . IsUserSilenced ( c . OwnerID )
2019-08-28 21:37:45 +02:00
if err != nil {
2019-10-25 21:04:24 +02:00
log . Error ( "fetch collection outbox: %v" , err )
2019-08-28 21:37:45 +02:00
return ErrInternalGeneral
}
2019-11-12 00:21:45 +01:00
if silenced {
2019-08-28 21:37:45 +02:00
return ErrCollectionNotFound
}
2019-06-15 00:54:04 +02:00
c . hostName = app . cfg . App . Host
2018-11-08 07:28:08 +01: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 15:26:07 +02:00
posts , err := app . db . GetPosts ( app . cfg , c , p , false , true , false )
2018-11-08 07:28:08 +01:00
for _ , pp := range * posts {
pp . Collection = res
2019-10-10 14:11:46 +02:00
o := pp . ActivityObject ( app )
2018-11-08 07:28:08 +01:00
a := activitystreams . NewCreateActivity ( o )
2020-04-15 18:28:55 +02:00
a . Context = nil
2018-11-08 07:28:08 +01:00
ocp . OrderedItems = append ( ocp . OrderedItems , * a )
}
2019-09-09 22:04:20 +02:00
setCacheControl ( w , apCacheTime )
2018-11-08 07:28:08 +01:00
return impart . RenderActivityJSON ( w , ocp , http . StatusOK )
}
2019-05-12 22:55:30 +02:00
func handleFetchCollectionFollowers ( app * App , w http . ResponseWriter , r * http . Request ) error {
2018-11-08 07:28:08 +01: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-12 00:21:45 +01:00
silenced , err := app . db . IsUserSilenced ( c . OwnerID )
2019-08-28 21:37:45 +02:00
if err != nil {
2019-10-25 21:04:24 +02:00
log . Error ( "fetch collection followers: %v" , err )
2019-08-28 21:37:45 +02:00
return ErrInternalGeneral
}
2019-11-12 00:21:45 +01:00
if silenced {
2019-08-28 21:37:45 +02:00
return ErrCollectionNotFound
}
2019-06-15 00:54:04 +02:00
c . hostName = app . cfg . App . Host
2018-11-08 07:28:08 +01: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 22:04:20 +02:00
setCacheControl ( w , apCacheTime )
2018-11-08 07:28:08 +01:00
return impart . RenderActivityJSON ( w , ocp , http . StatusOK )
}
2019-05-12 22:55:30 +02:00
func handleFetchCollectionFollowing ( app * App , w http . ResponseWriter , r * http . Request ) error {
2018-11-08 07:28:08 +01: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-12 00:21:45 +01:00
silenced , err := app . db . IsUserSilenced ( c . OwnerID )
2019-08-28 21:37:45 +02:00
if err != nil {
2019-10-25 21:04:24 +02:00
log . Error ( "fetch collection following: %v" , err )
2019-08-28 21:37:45 +02:00
return ErrInternalGeneral
}
2019-11-12 00:21:45 +01:00
if silenced {
2019-08-28 21:37:45 +02:00
return ErrCollectionNotFound
}
2019-06-15 00:54:04 +02:00
c . hostName = app . cfg . App . Host
2018-11-08 07:28:08 +01: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 22:04:20 +02:00
setCacheControl ( w , apCacheTime )
2018-11-08 07:28:08 +01:00
return impart . RenderActivityJSON ( w , ocp , http . StatusOK )
}
2019-05-12 22:55:30 +02:00
func handleFetchCollectionInbox ( app * App , w http . ResponseWriter , r * http . Request ) error {
2018-11-08 07:28:08 +01: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-12 00:21:45 +01:00
silenced , err := app . db . IsUserSilenced ( c . OwnerID )
2019-08-28 21:37:45 +02:00
if err != nil {
2019-10-25 21:04:24 +02:00
log . Error ( "fetch collection inbox: %v" , err )
2019-08-28 21:37:45 +02:00
return ErrInternalGeneral
}
2019-11-12 00:21:45 +01:00
if silenced {
2019-08-28 21:37:45 +02:00
return ErrCollectionNotFound
}
2019-06-15 00:54:04 +02:00
c . hostName = app . cfg . App . Host
2018-11-08 07:28:08 +01: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 14:39:15 +01:00
if debugging {
log . Info ( "Follow: %s" , b )
}
2018-11-08 07:28:08 +01:00
2018-11-11 19:09:19 +01:00
_ , followID := f . GetId ( )
if followID == nil {
log . Error ( "Didn't resolve follow ID" )
} else {
2021-03-30 18:49:12 +02:00
aID := c . FederatedAccount ( ) + "#accept-" + id . GenerateFriendlyRandomString ( 20 )
2018-11-15 23:05:33 +01:00
acceptID , err := url . Parse ( aID )
2018-11-11 19:09:19 +01:00
if err != nil {
2018-11-15 23:05:33 +01:00
log . Error ( "Couldn't parse generated Accept URL '%s': %v" , aID , err )
2018-11-11 19:09:19 +01:00
}
a . SetId ( acceptID )
}
2018-11-08 07:28:08 +01: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 14:39:15 +01:00
if debugging {
log . Info ( "Undo: %s" , b )
}
2018-11-08 07:28:08 +01: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 15:19:12 +01:00
if to == nil {
2020-05-15 19:47:58 +02:00
if debugging {
log . Error ( "No `to` value!" )
}
2020-01-27 15:19:12 +01:00
return
}
2018-11-08 07:28:08 +01: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-15 00:54:04 +02:00
err = makeActivityPost ( app . cfg . App . Host , p , fullActor . Inbox , am )
2018-11-08 07:28:08 +01: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
2023-09-22 01:04:34 +02:00
res , err := t . Exec ( "INSERT INTO remoteusers (actor_id, inbox, shared_inbox, url) VALUES (?, ?, ?, ?)" , fullActor . ID , fullActor . Inbox , fullActor . Endpoints . SharedInbox , fullActor . URL )
2018-11-08 07:28:08 +01:00
if err != nil {
2019-08-09 23:04:15 +02: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 07:28:08 +01: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 19:05:05 +02:00
if ! app . db . isDuplicateKeyErr ( err ) {
2018-11-08 07:28:08 +01:00
t . Rollback ( )
log . Error ( "Couldn't add follower keys in DB: %v\n" , err )
return
}
}
}
// Add follow
2019-01-07 20:35:47 +01:00
_ , err = t . Exec ( "INSERT INTO remotefollows (collection_id, remote_user_id, created) VALUES (?, ?, " + app . db . now ( ) + ")" , c . ID , followerID )
2018-11-08 07:28:08 +01:00
if err != nil {
2019-06-13 19:05:05 +02:00
if ! app . db . isDuplicateKeyErr ( err ) {
2018-11-08 07:28:08 +01: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-15 00:54:04 +02:00
func makeActivityPost ( hostName string , p * activitystreams . Person , url string , m interface { } ) error {
2018-11-08 07:28:08 +01: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 18:22:04 +02:00
r . Header . Set ( "User-Agent" , ServerUserAgent ( hostName ) )
2018-11-08 07:28:08 +01: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 17:47:35 +01:00
resp , err := activityPubClient ( ) . Do ( r )
2018-11-15 00:30:24 +01:00
if err != nil {
return err
}
2018-11-08 07:28:08 +01:00
if resp != nil && resp . Body != nil {
defer resp . Body . Close ( )
}
2023-07-10 11:55:04 +02:00
body , err := io . ReadAll ( resp . Body )
2018-11-08 07:28:08 +01:00
if err != nil {
return err
}
if debugging {
log . Info ( "Status : %s" , resp . Status )
log . Info ( "Response: %s" , body )
}
return nil
}
2019-06-15 00:54:04 +02:00
func resolveIRI ( hostName , url string ) ( [ ] byte , error ) {
2018-11-08 07:28:08 +01:00
log . Info ( "GET %s" , url )
r , _ := http . NewRequest ( "GET" , url , nil )
r . Header . Add ( "Accept" , "application/activity+json" )
2020-08-18 18:22:04 +02:00
r . Header . Set ( "User-Agent" , ServerUserAgent ( hostName ) )
2018-11-08 07:28:08 +01:00
2021-03-08 17:43:38 +01: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 07:28:08 +01: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 17:47:35 +01:00
resp , err := activityPubClient ( ) . Do ( r )
2018-11-15 00:30:24 +01:00
if err != nil {
return nil , err
}
2018-11-08 07:28:08 +01:00
if resp != nil && resp . Body != nil {
defer resp . Body . Close ( )
}
2023-07-10 11:55:04 +02:00
body , err := io . ReadAll ( resp . Body )
2018-11-08 07:28:08 +01:00
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 22:55:30 +02:00
func deleteFederatedPost ( app * App , p * PublicPost , collID int64 ) error {
2018-11-08 07:28:08 +01:00
if debugging {
log . Info ( "Deleting federated post!" )
}
2019-06-21 03:08:30 +02:00
p . Collection . hostName = app . cfg . App . Host
2018-11-08 07:28:08 +01:00
actor := p . Collection . PersonObject ( collID )
2019-10-10 14:11:46 +02:00
na := p . ActivityObject ( app )
2018-11-08 07:28:08 +01: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 20:53:17 +02:00
inbox := f . SharedInbox
if inbox == "" {
inbox = f . Inbox
}
if _ , ok := inboxes [ inbox ] ; ok {
inboxes [ inbox ] = append ( inboxes [ inbox ] , f . ActorID )
2018-11-08 07:28:08 +01:00
} else {
2019-06-03 20:53:17 +02:00
inboxes [ inbox ] = [ ] string { f . ActorID }
2018-11-08 07:28:08 +01:00
}
}
for si , instFolls := range inboxes {
na . CC = [ ] string { }
2023-07-11 06:17:34 +02:00
na . CC = append ( na . CC , instFolls ... )
2020-02-08 21:19:06 +01: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 07:28:08 +01:00
if err != nil {
log . Error ( "Couldn't delete post! %v" , err )
}
}
return nil
}
2019-05-12 22:55:30 +02:00
func federatePost ( app * App , p * PublicPost , collID int64 , isUpdate bool ) error {
2021-01-28 01:39:46 +01: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 07:28:08 +01:00
if debugging {
if isUpdate {
log . Info ( "Federating updated post!" )
} else {
log . Info ( "Federating new post!" )
}
}
2021-04-07 17:27:25 +02:00
2018-11-08 07:28:08 +01:00
actor := p . Collection . PersonObject ( collID )
2019-10-10 14:11:46 +02:00
na := p . ActivityObject ( app )
2018-11-08 07:28:08 +01: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 20:53:17 +02:00
inbox := f . SharedInbox
if inbox == "" {
inbox = f . Inbox
}
if _ , ok := inboxes [ inbox ] ; ok {
2019-10-09 13:34:31 +02:00
// check if we're already sending to this shared inbox
2019-06-03 20:53:17 +02:00
inboxes [ inbox ] = append ( inboxes [ inbox ] , f . ActorID )
2018-11-08 07:28:08 +01:00
} else {
2019-10-09 13:34:31 +02:00
// add the new shared inbox to the list
2019-06-03 20:53:17 +02:00
inboxes [ inbox ] = [ ] string { f . ActorID }
2018-11-08 07:28:08 +01:00
}
}
2019-10-08 14:58:19 +02:00
var activity * activitystreams . Activity
2019-10-09 13:34:31 +02:00
// for each one of the shared inboxes
2018-11-08 07:28:08 +01:00
for si , instFolls := range inboxes {
2019-10-09 13:34:31 +02:00
// add all followers from that instance
// to the CC field
2018-11-08 07:28:08 +01:00
na . CC = [ ] string { }
2023-07-11 06:17:34 +02:00
na . CC = append ( na . CC , instFolls ... )
2019-10-09 13:34:31 +02:00
// create a new "Create" activity
// with our article as object
2018-11-08 07:28:08 +01:00
if isUpdate {
2023-10-03 03:35:23 +02:00
na . Updated = & p . Updated
2018-11-08 07:28:08 +01:00
activity = activitystreams . NewUpdateActivity ( na )
} else {
activity = activitystreams . NewCreateActivity ( na )
2018-11-11 19:11:01 +01:00
activity . To = na . To
activity . CC = na . CC
2018-11-08 07:28:08 +01:00
}
2019-10-09 13:34:31 +02:00
// and post it to that sharedInbox
2019-06-15 00:54:04 +02:00
err = makeActivityPost ( app . cfg . App . Host , actor , si , activity )
2018-11-08 07:28:08 +01:00
if err != nil {
log . Error ( "Couldn't post! %v" , err )
}
}
2019-10-08 14:58:19 +02:00
2019-10-09 13:34:31 +02: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 14:11:46 +02:00
na = p . ActivityObject ( app )
2019-10-08 14:58:19 +02: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 14:11:46 +02:00
remoteUser , err := getRemoteUser ( app , tag . HRef )
2020-06-08 19:50:43 +02:00
if err != nil {
log . Error ( "Unable to find remote user %s. Skipping: %v" , tag . HRef , err )
continue
}
2019-10-10 14:11:46 +02:00
err = makeActivityPost ( app . cfg . App . Host , actor , remoteUser . Inbox , activity )
2019-10-08 14:58:19 +02:00
if err != nil {
log . Error ( "Couldn't post! %v" , err )
}
}
}
2018-11-08 07:28:08 +01:00
return nil
}
2019-05-12 22:55:30 +02:00
func getRemoteUser ( app * App , actorID string ) ( * RemoteUser , error ) {
2018-11-08 07:28:08 +01:00
u := RemoteUser { ActorID : actorID }
2023-09-22 01:04:34 +02:00
var urlVal , handle sql . NullString
err := app . db . QueryRow ( "SELECT id, inbox, shared_inbox, url, handle FROM remoteusers WHERE actor_id = ?" , actorID ) . Scan ( & u . ID , & u . Inbox , & u . SharedInbox , & urlVal , & handle )
2018-11-08 07:28:08 +01: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
}
2023-09-22 01:04:34 +02:00
u . URL = urlVal . String
2020-03-24 12:59:00 +01:00
u . Handle = handle . String
2018-11-08 07:28:08 +01:00
return & u , nil
}
2019-10-10 14:11:46 +02: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 }
2023-09-22 01:04:34 +02:00
var urlVal sql . NullString
err := app . db . QueryRow ( "SELECT id, actor_id, inbox, shared_inbox, url FROM remoteusers WHERE handle = ?" , handle ) . Scan ( & u . ID , & u . ActorID , & u . Inbox , & u . SharedInbox , & urlVal )
2019-10-10 14:11:46 +02:00
switch {
case err == sql . ErrNoRows :
2020-02-08 19:05:54 +01:00
return nil , ErrRemoteUserNotFound
2019-10-10 14:11:46 +02:00
case err != nil :
log . Error ( "Couldn't get remote user %s: %v" , handle , err )
return nil , err
}
2023-09-22 01:04:34 +02:00
u . URL = urlVal . String
2019-10-10 14:11:46 +02:00
return & u , nil
}
2019-05-12 22:55:30 +02:00
func getActor ( app * App , actorIRI string ) ( * activitystreams . Person , * RemoteUser , error ) {
2018-11-08 07:28:08 +01: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-15 00:54:04 +02:00
actorResp , err := resolveIRI ( app . cfg . App . Host , actorIRI )
2018-11-08 07:28:08 +01:00
if err != nil {
2024-06-10 07:57:22 +02:00
log . Error ( "Unable to get base actor! %v" , err )
2018-11-08 07:28:08 +01:00
return nil , nil , impart . HTTPError { http . StatusInternalServerError , "Couldn't fetch actor." }
}
2019-05-20 23:41:26 +02:00
if err := unmarshalActor ( actorResp , actor ) ; err != nil {
2024-06-10 07:57:22 +02:00
log . Error ( "Unable to unmarshal base actor! %v" , err )
2018-11-08 07:28:08 +01:00
return nil , nil , impart . HTTPError { http . StatusInternalServerError , "Couldn't parse actor." }
}
2024-06-10 07:57:22 +02:00
baseActor := & activitystreams . Person { }
if err := unmarshalActor ( actorResp , baseActor ) ; err != nil {
log . Error ( "Unable to unmarshal actual actor! %v" , err )
return nil , nil , impart . HTTPError { http . StatusInternalServerError , "Couldn't parse actual actor." }
}
// Fetch the actual actor using the owner field from the publicKey object
actualActorResp , err := resolveIRI ( app . cfg . App . Host , baseActor . PublicKey . Owner )
if err != nil {
log . Error ( "Unable to get actual actor! %v" , err )
return nil , nil , impart . HTTPError { http . StatusInternalServerError , "Couldn't fetch actual actor." }
}
if err := unmarshalActor ( actualActorResp , actor ) ; err != nil {
log . Error ( "Unable to unmarshal actual actor! %v" , err )
return nil , nil , impart . HTTPError { http . StatusInternalServerError , "Couldn't parse actual actor." }
}
2018-11-08 07:28:08 +01:00
} else {
return nil , nil , err
}
} else {
return nil , nil , err
}
} else {
actor = remoteUser . AsPerson ( )
}
return actor , remoteUser , nil
}
2019-05-20 23:41:26 +02:00
2023-09-22 01:04:34 +02:00
func GetProfileURLFromHandle ( app * App , handle string ) ( string , error ) {
handle = strings . TrimLeft ( handle , "@" )
actorIRI := ""
parts := strings . Split ( handle , "@" )
if len ( parts ) != 2 {
return "" , fmt . Errorf ( "invalid handle format" )
}
domain := parts [ 1 ]
// Check non-AP instances
if siloProfileURL := silobridge . Profile ( parts [ 0 ] , domain ) ; siloProfileURL != "" {
return siloProfileURL , nil
}
remoteUser , err := getRemoteUserFromHandle ( app , handle )
if err != nil {
// can't find using handle in the table but the table may already have this user without
// handle from a previous version
// TODO: Make this determination. We should know whether a user exists without a handle, or doesn't exist at all
actorIRI = RemoteLookup ( handle )
_ , errRemoteUser := getRemoteUser ( app , actorIRI )
// if it exists then we need to update the handle
if errRemoteUser == nil {
_ , err := app . db . Exec ( "UPDATE remoteusers SET handle = ? WHERE actor_id = ?" , handle , actorIRI )
if err != nil {
log . Error ( "Couldn't update handle '%s' for user %s" , handle , actorIRI )
}
} else {
// this probably means we don't have the user in the table so let's try to insert it
// here we need to ask the server for the inboxes
remoteActor , err := activityserve . NewRemoteActor ( actorIRI )
if err != nil {
log . Error ( "Couldn't fetch remote actor: %v" , err )
}
if debugging {
log . Info ( "Got remote actor: %s %s %s %s %s" , actorIRI , remoteActor . GetInbox ( ) , remoteActor . GetSharedInbox ( ) , remoteActor . URL ( ) , handle )
}
_ , err = app . db . Exec ( "INSERT INTO remoteusers (actor_id, inbox, shared_inbox, url, handle) VALUES(?, ?, ?, ?, ?)" , actorIRI , remoteActor . GetInbox ( ) , remoteActor . GetSharedInbox ( ) , remoteActor . URL ( ) , handle )
if err != nil {
log . Error ( "Couldn't insert remote user: %v" , err )
return "" , err
}
actorIRI = remoteActor . URL ( )
}
} else if remoteUser . URL == "" {
log . Info ( "Remote user %s URL empty, fetching" , remoteUser . ActorID )
newRemoteActor , err := activityserve . NewRemoteActor ( remoteUser . ActorID )
if err != nil {
log . Error ( "Couldn't fetch remote actor: %v" , err )
} else {
_ , err := app . db . Exec ( "UPDATE remoteusers SET url = ? WHERE actor_id = ?" , newRemoteActor . URL ( ) , remoteUser . ActorID )
if err != nil {
log . Error ( "Couldn't update handle '%s' for user %s" , handle , actorIRI )
} else {
actorIRI = newRemoteActor . URL ( )
}
}
} else {
actorIRI = remoteUser . URL
}
return actorIRI , nil
}
2019-05-20 23:41:26 +02: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 22:04:20 +02:00
func setCacheControl ( w http . ResponseWriter , ttl time . Duration ) {
w . Header ( ) . Set ( "Cache-Control" , fmt . Sprintf ( "public, max-age=%.0f" , ttl . Seconds ( ) ) )
}