2023-03-12 16:00:57 +01:00
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
2021-05-27 16:06:24 +02:00
package federatingdb
import (
"context"
"errors"
"fmt"
2021-10-10 12:39:25 +02:00
"strings"
2021-05-27 16:06:24 +02:00
2022-07-19 10:47:55 +02:00
"codeberg.org/gruf/go-kv"
"codeberg.org/gruf/go-logger/v2/level"
2021-11-13 17:29:43 +01:00
"github.com/superseriousbusiness/activity/streams/vocab"
2021-08-31 15:59:12 +02:00
"github.com/superseriousbusiness/gotosocial/internal/ap"
2021-05-27 16:06:24 +02:00
"github.com/superseriousbusiness/gotosocial/internal/db"
2021-10-10 12:39:25 +02:00
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
2021-06-13 18:42:28 +02:00
"github.com/superseriousbusiness/gotosocial/internal/id"
2022-07-19 10:47:55 +02:00
"github.com/superseriousbusiness/gotosocial/internal/log"
2021-08-31 15:59:12 +02:00
"github.com/superseriousbusiness/gotosocial/internal/messages"
2021-05-27 16:06:24 +02:00
)
// Create adds a new entry to the database which must be able to be
// keyed by its id.
//
// Note that Activity values received from federated peers may also be
// created in the database this way if the Federating Protocol is
// enabled. The client may freely decide to store only the id instead of
// the entire value.
//
// The library makes this call only after acquiring a lock first.
//
// Under certain conditions and network activities, Create may be called
// multiple times for the same ActivityStreams object.
func ( f * federatingDB ) Create ( ctx context . Context , asType vocab . Type ) error {
2022-07-19 10:47:55 +02:00
if log . Level ( ) >= level . DEBUG {
2021-10-04 15:24:19 +02:00
i , err := marshalItem ( asType )
if err != nil {
return err
}
2023-02-17 12:02:29 +01:00
l := log . WithContext ( ctx ) .
WithField ( "create" , i )
l . Trace ( "entering Create" )
2021-05-27 16:06:24 +02:00
}
2021-10-04 15:24:19 +02:00
2022-04-28 14:23:11 +02:00
receivingAccount , requestingAccount := extractFromCtx ( ctx )
if receivingAccount == nil {
// If the receiving account wasn't set on the context, that means this request didn't pass
2021-10-04 15:24:19 +02:00
// through the API, but came from inside GtS as the result of another activity on this instance. That being so,
2021-07-27 10:45:22 +02:00
// we can safely just ignore this activity, since we know we've already processed it elsewhere.
2021-05-27 16:06:24 +02:00
return nil
}
switch asType . GetTypeName ( ) {
2021-10-10 12:39:25 +02:00
case ap . ActivityBlock :
// BLOCK SOMETHING
2022-04-28 14:23:11 +02:00
return f . activityBlock ( ctx , asType , receivingAccount , requestingAccount )
2021-08-31 15:59:12 +02:00
case ap . ActivityCreate :
2021-06-17 18:02:33 +02:00
// CREATE SOMETHING
2022-04-28 14:23:11 +02:00
return f . activityCreate ( ctx , asType , receivingAccount , requestingAccount )
2021-08-31 15:59:12 +02:00
case ap . ActivityFollow :
2021-07-11 16:22:21 +02:00
// FOLLOW SOMETHING
2022-04-28 14:23:11 +02:00
return f . activityFollow ( ctx , asType , receivingAccount , requestingAccount )
2021-10-10 12:39:25 +02:00
case ap . ActivityLike :
// LIKE SOMETHING
2022-04-28 14:23:11 +02:00
return f . activityLike ( ctx , asType , receivingAccount , requestingAccount )
2023-01-25 11:12:27 +01:00
case ap . ActivityFlag :
// FLAG / REPORT SOMETHING
return f . activityFlag ( ctx , asType , receivingAccount , requestingAccount )
2021-10-10 12:39:25 +02:00
}
return nil
}
2021-05-27 16:06:24 +02:00
2021-10-10 12:39:25 +02:00
/ *
BLOCK HANDLERS
* /
2021-05-27 16:06:24 +02:00
2022-04-28 14:23:11 +02:00
func ( f * federatingDB ) activityBlock ( ctx context . Context , asType vocab . Type , receiving * gtsmodel . Account , requestingAccount * gtsmodel . Account ) error {
2021-10-10 12:39:25 +02:00
blockable , ok := asType . ( vocab . ActivityStreamsBlock )
if ! ok {
return errors . New ( "activityBlock: could not convert type to block" )
}
2021-06-13 18:42:28 +02:00
2021-10-10 12:39:25 +02:00
block , err := f . typeConverter . ASBlockToBlock ( ctx , blockable )
if err != nil {
return fmt . Errorf ( "activityBlock: could not convert Block to gts model block" )
}
2021-05-27 16:06:24 +02:00
2023-02-03 21:03:05 +01:00
block . ID = id . NewULID ( )
2021-05-27 16:06:24 +02:00
2023-03-01 19:26:53 +01:00
if err := f . state . DB . PutBlock ( ctx , block ) ; err != nil {
2021-10-10 12:39:25 +02:00
return fmt . Errorf ( "activityBlock: database error inserting block: %s" , err )
}
2023-03-01 19:26:53 +01:00
f . state . Workers . EnqueueFederator ( ctx , messages . FromFederator {
2021-10-10 12:39:25 +02:00
APObjectType : ap . ActivityBlock ,
APActivityType : ap . ActivityCreate ,
GTSModel : block ,
ReceivingAccount : receiving ,
2022-04-28 14:23:11 +02:00
} )
2021-10-10 12:39:25 +02:00
return nil
}
/ *
CREATE HANDLERS
* /
2022-04-28 14:23:11 +02:00
func ( f * federatingDB ) activityCreate ( ctx context . Context , asType vocab . Type , receivingAccount * gtsmodel . Account , requestingAccount * gtsmodel . Account ) error {
2021-10-10 12:39:25 +02:00
create , ok := asType . ( vocab . ActivityStreamsCreate )
if ! ok {
return errors . New ( "activityCreate: could not convert type to create" )
}
// create should have an object
object := create . GetActivityStreamsObject ( )
if object == nil {
return errors . New ( "Create had no Object" )
}
errs := [ ] string { }
// iterate through the object(s) to see what we're meant to be creating
for objectIter := object . Begin ( ) ; objectIter != object . End ( ) ; objectIter = objectIter . Next ( ) {
asObjectType := objectIter . GetType ( )
if asObjectType == nil {
// currently we can't do anything with just a Create of something that's not an Object with a type
// TODO: process a Create with an Object that's just a URI or something
errs = append ( errs , "object of Create was not a Type" )
continue
2021-05-27 16:06:24 +02:00
}
2021-10-10 12:39:25 +02:00
// we have a type -- what is it?
asObjectTypeName := asObjectType . GetTypeName ( )
switch asObjectTypeName {
case ap . ObjectNote :
// CREATE A NOTE
2022-04-28 14:23:11 +02:00
if err := f . createNote ( ctx , objectIter . GetActivityStreamsNote ( ) , receivingAccount , requestingAccount ) ; err != nil {
2021-10-10 12:39:25 +02:00
errs = append ( errs , err . Error ( ) )
}
default :
errs = append ( errs , fmt . Sprintf ( "received an object on a Create that we couldn't handle: %s" , asObjectType . GetTypeName ( ) ) )
2021-06-13 18:42:28 +02:00
}
2021-10-10 12:39:25 +02:00
}
if len ( errs ) != 0 {
return fmt . Errorf ( "activityCreate: one or more errors while processing activity: %s" , strings . Join ( errs , "; " ) )
}
2021-06-13 18:42:28 +02:00
2021-10-10 12:39:25 +02:00
return nil
}
// createNote handles a Create activity with a Note type.
2022-04-28 14:23:11 +02:00
func ( f * federatingDB ) createNote ( ctx context . Context , note vocab . ActivityStreamsNote , receivingAccount * gtsmodel . Account , requestingAccount * gtsmodel . Account ) error {
2023-02-17 12:02:29 +01:00
l := log . WithContext ( ctx ) .
WithFields ( kv . Fields {
{ "receivingAccount" , receivingAccount . URI } ,
{ "requestingAccount" , requestingAccount . URI } ,
} ... )
2021-10-10 12:39:25 +02:00
// Check if we have a forward.
// In other words, was the note posted to our inbox by at least one actor who actually created the note, or are they just forwarding it?
forward := true
// note should have an attributedTo
noteAttributedTo := note . GetActivityStreamsAttributedTo ( )
if noteAttributedTo == nil {
return errors . New ( "createNote: note had no attributedTo" )
}
// compare the attributedTo(s) with the actor who posted this to our inbox
for attributedToIter := noteAttributedTo . Begin ( ) ; attributedToIter != noteAttributedTo . End ( ) ; attributedToIter = attributedToIter . Next ( ) {
if ! attributedToIter . IsIRI ( ) {
continue
}
iri := attributedToIter . GetIRI ( )
if requestingAccount . URI == iri . String ( ) {
// at least one creator of the note, and the actor who posted the note to our inbox, are the same, so it's not a forward
forward = false
2021-05-27 16:06:24 +02:00
}
2021-10-10 12:39:25 +02:00
}
2021-05-27 16:06:24 +02:00
2021-10-10 12:39:25 +02:00
// If we do have a forward, we should ignore the content for now and just dereference based on the URL/ID of the note instead, to get the note straight from the horse's mouth
if forward {
l . Trace ( "note is a forward" )
id := note . GetJSONLDId ( )
if ! id . IsIRI ( ) {
// if the note id isn't an IRI, there's nothing we can do here
return nil
}
// pass the note iri into the processor and have it do the dereferencing instead of doing it here
2023-03-01 19:26:53 +01:00
f . state . Workers . EnqueueFederator ( ctx , messages . FromFederator {
2021-10-10 12:39:25 +02:00
APObjectType : ap . ObjectNote ,
2021-08-31 15:59:12 +02:00
APActivityType : ap . ActivityCreate ,
2021-10-10 12:39:25 +02:00
APIri : id . GetIRI ( ) ,
GTSModel : nil ,
ReceivingAccount : receivingAccount ,
2022-04-28 14:23:11 +02:00
} )
2021-10-10 12:39:25 +02:00
return nil
}
2021-07-11 16:22:21 +02:00
2021-10-10 12:39:25 +02:00
// if we reach this point, we know it's not a forwarded status, so proceed with processing it as normal
2021-07-11 16:22:21 +02:00
2021-10-10 12:39:25 +02:00
status , err := f . typeConverter . ASStatusToStatus ( ctx , note )
if err != nil {
return fmt . Errorf ( "createNote: error converting note to status: %s" , err )
}
2021-07-11 16:22:21 +02:00
2021-10-10 12:39:25 +02:00
// id the status based on the time it was created
statusID , err := id . NewULIDFromTime ( status . CreatedAt )
if err != nil {
return err
}
status . ID = statusID
2021-07-11 16:22:21 +02:00
2023-03-01 19:26:53 +01:00
if err := f . state . DB . PutStatus ( ctx , status ) ; err != nil {
2022-09-12 13:03:23 +02:00
if errors . Is ( err , db . ErrAlreadyExists ) {
2021-10-10 12:39:25 +02:00
// the status already exists in the database, which means we've already handled everything else,
// so we can just return nil here and be done with it.
return nil
2021-07-11 16:22:21 +02:00
}
2021-10-10 12:39:25 +02:00
// an actual error has happened
return fmt . Errorf ( "createNote: database error inserting status: %s" , err )
2021-05-27 16:06:24 +02:00
}
2021-10-10 12:39:25 +02:00
2023-03-01 19:26:53 +01:00
f . state . Workers . EnqueueFederator ( ctx , messages . FromFederator {
2021-10-10 12:39:25 +02:00
APObjectType : ap . ObjectNote ,
APActivityType : ap . ActivityCreate ,
GTSModel : status ,
ReceivingAccount : receivingAccount ,
2022-04-28 14:23:11 +02:00
} )
2021-10-10 12:39:25 +02:00
return nil
}
/ *
FOLLOW HANDLERS
* /
2022-04-28 14:23:11 +02:00
func ( f * federatingDB ) activityFollow ( ctx context . Context , asType vocab . Type , receivingAccount * gtsmodel . Account , requestingAccount * gtsmodel . Account ) error {
2021-10-10 12:39:25 +02:00
follow , ok := asType . ( vocab . ActivityStreamsFollow )
if ! ok {
return errors . New ( "activityFollow: could not convert type to follow" )
}
followRequest , err := f . typeConverter . ASFollowToFollowRequest ( ctx , follow )
if err != nil {
return fmt . Errorf ( "activityFollow: could not convert Follow to follow request: %s" , err )
}
2023-02-03 21:03:05 +01:00
followRequest . ID = id . NewULID ( )
2021-10-10 12:39:25 +02:00
[performance] refactoring + add fave / follow / request / visibility caching (#1607)
* refactor visibility checking, add caching for visibility
* invalidate visibility cache items on account / status deletes
* fix requester ID passed to visibility cache nil ptr
* de-interface caches, fix home / public timeline caching + visibility
* finish adding code comments for visibility filter
* fix angry goconst linter warnings
* actually finish adding filter visibility code comments for timeline functions
* move home timeline status author check to after visibility
* remove now-unused code
* add more code comments
* add TODO code comment, update printed cache start names
* update printed cache names on stop
* start adding separate follow(request) delete db functions, add specific visibility cache tests
* add relationship type caching
* fix getting local account follows / followed-bys, other small codebase improvements
* simplify invalidation using cache hooks, add more GetAccountBy___() functions
* fix boosting to return 404 if not boostable but no error (to not leak status ID)
* remove dead code
* improved placement of cache invalidation
* update license headers
* add example follow, follow-request config entries
* add example visibility cache configuration to config file
* use specific PutFollowRequest() instead of just Put()
* add tests for all GetAccountBy()
* add GetBlockBy() tests
* update block to check primitive fields
* update and finish adding Get{Account,Block,Follow,FollowRequest}By() tests
* fix copy-pasted code
* update envparsing test
* whitespace
* fix bun struct tag
* add license header to gtscontext
* fix old license header
* improved error creation to not use fmt.Errorf() when not needed
* fix various rebase conflicts, fix account test
* remove commented-out code, fix-up mention caching
* fix mention select bun statement
* ensure mention target account populated, pass in context to customrenderer logging
* remove more uncommented code, fix typeutil test
* add statusfave database model caching
* add status fave cache configuration
* add status fave cache example config
* woops, catch missed error. nice catch linter!
* add back testrig panic on nil db
* update example configuration to match defaults, slight tweak to cache configuration defaults
* update envparsing test with new defaults
* fetch followingget to use the follow target account
* use accounnt.IsLocal() instead of empty domain check
* use constants for the cache visibility type check
* use bun.In() for notification type restriction in db query
* include replies when fetching PublicTimeline() (to account for single-author threads in Visibility{}.StatusPublicTimelineable())
* use bun query building for nested select statements to ensure working with postgres
* update public timeline future status checks to match visibility filter
* same as previous, for home timeline
* update public timeline tests to dynamically check for appropriate statuses
* migrate accounts to allow unique constraint on public_key
* provide minimal account with publicKey
---------
Signed-off-by: kim <grufwub@gmail.com>
Co-authored-by: tsmethurst <tobi.smethurst@protonmail.com>
2023-03-28 15:03:14 +02:00
if err := f . state . DB . PutFollowRequest ( ctx , followRequest ) ; err != nil {
2021-10-10 12:39:25 +02:00
return fmt . Errorf ( "activityFollow: database error inserting follow request: %s" , err )
}
2023-03-01 19:26:53 +01:00
f . state . Workers . EnqueueFederator ( ctx , messages . FromFederator {
2021-10-10 12:39:25 +02:00
APObjectType : ap . ActivityFollow ,
APActivityType : ap . ActivityCreate ,
GTSModel : followRequest ,
ReceivingAccount : receivingAccount ,
2022-04-28 14:23:11 +02:00
} )
2021-10-10 12:39:25 +02:00
return nil
}
/ *
LIKE HANDLERS
* /
2022-04-28 14:23:11 +02:00
func ( f * federatingDB ) activityLike ( ctx context . Context , asType vocab . Type , receivingAccount * gtsmodel . Account , requestingAccount * gtsmodel . Account ) error {
2021-10-10 12:39:25 +02:00
like , ok := asType . ( vocab . ActivityStreamsLike )
if ! ok {
return errors . New ( "activityLike: could not convert type to like" )
}
fave , err := f . typeConverter . ASLikeToFave ( ctx , like )
if err != nil {
2023-04-05 20:10:05 +02:00
return fmt . Errorf ( "activityLike: could not convert Like to fave: %w" , err )
2021-10-10 12:39:25 +02:00
}
2023-02-03 21:03:05 +01:00
fave . ID = id . NewULID ( )
2021-10-10 12:39:25 +02:00
2023-04-05 20:10:05 +02:00
if err := f . state . DB . PutStatusFave ( ctx , fave ) ; err != nil {
if errors . Is ( err , db . ErrAlreadyExists ) {
// The Like already exists in the database, which
// means we've already handled side effects. We can
// just return nil here and be done with it.
return nil
}
return fmt . Errorf ( "activityLike: database error inserting fave: %w" , err )
2021-10-10 12:39:25 +02:00
}
2023-03-01 19:26:53 +01:00
f . state . Workers . EnqueueFederator ( ctx , messages . FromFederator {
2021-10-10 12:39:25 +02:00
APObjectType : ap . ActivityLike ,
APActivityType : ap . ActivityCreate ,
GTSModel : fave ,
ReceivingAccount : receivingAccount ,
2022-04-28 14:23:11 +02:00
} )
2021-10-10 12:39:25 +02:00
2021-05-27 16:06:24 +02:00
return nil
}
2023-01-25 11:12:27 +01:00
/ *
FLAG HANDLERS
* /
func ( f * federatingDB ) activityFlag ( ctx context . Context , asType vocab . Type , receivingAccount * gtsmodel . Account , requestingAccount * gtsmodel . Account ) error {
flag , ok := asType . ( vocab . ActivityStreamsFlag )
if ! ok {
return errors . New ( "activityFlag: could not convert type to flag" )
}
report , err := f . typeConverter . ASFlagToReport ( ctx , flag )
if err != nil {
return fmt . Errorf ( "activityFlag: could not convert Flag to report: %w" , err )
}
2023-02-03 21:03:05 +01:00
report . ID = id . NewULID ( )
2023-01-25 11:12:27 +01:00
2023-03-01 19:26:53 +01:00
if err := f . state . DB . PutReport ( ctx , report ) ; err != nil {
2023-01-25 11:12:27 +01:00
return fmt . Errorf ( "activityFlag: database error inserting report: %w" , err )
}
2023-03-01 19:26:53 +01:00
f . state . Workers . EnqueueFederator ( ctx , messages . FromFederator {
2023-01-25 11:12:27 +01:00
APObjectType : ap . ActivityFlag ,
APActivityType : ap . ActivityCreate ,
GTSModel : report ,
ReceivingAccount : receivingAccount ,
} )
return nil
}