2021-08-10 13:32:39 +02:00
/ *
GoToSocial
2021-12-20 18:42:19 +01:00
Copyright ( C ) 2021 - 2022 GoToSocial Authors admin @ gotosocial . org
2021-08-10 13:32:39 +02:00
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/>.
* /
package dereferencing
import (
"context"
"encoding/json"
"errors"
"fmt"
2022-01-16 18:52:55 +01:00
"io"
2021-08-10 13:32:39 +02:00
"net/url"
2021-08-25 15:34:33 +02:00
"strings"
2022-01-24 13:12:17 +01:00
"sync"
2022-01-24 18:12:04 +01:00
"time"
2021-08-10 13:32:39 +02:00
2022-09-23 21:27:35 +02:00
"github.com/miekg/dns"
2021-11-13 17:29:43 +01:00
"github.com/superseriousbusiness/activity/streams"
"github.com/superseriousbusiness/activity/streams/vocab"
2021-08-10 13:32:39 +02:00
"github.com/superseriousbusiness/gotosocial/internal/ap"
2022-08-20 22:47:19 +02:00
"github.com/superseriousbusiness/gotosocial/internal/config"
2022-06-11 11:01:34 +02:00
"github.com/superseriousbusiness/gotosocial/internal/db"
2021-08-10 13:32:39 +02:00
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
2022-07-19 10:47:55 +02:00
"github.com/superseriousbusiness/gotosocial/internal/log"
2022-01-09 18:41:22 +01:00
"github.com/superseriousbusiness/gotosocial/internal/media"
2021-08-10 13:32:39 +02:00
"github.com/superseriousbusiness/gotosocial/internal/transport"
)
2022-06-11 11:01:34 +02:00
var webfingerInterval = - 48 * time . Hour // 2 days in the past
2021-08-25 15:34:33 +02:00
func instanceAccount ( account * gtsmodel . Account ) bool {
return strings . EqualFold ( account . Username , account . Domain ) ||
account . FollowersURI == "" ||
account . FollowingURI == "" ||
( account . Username == "internal.fetch" && strings . Contains ( account . Note , "internal service actor" ) )
}
2022-06-11 11:01:34 +02:00
// GetRemoteAccountParams wraps parameters for a remote account lookup.
type GetRemoteAccountParams struct {
// The username of the user doing the lookup request (optional).
// If not set, then the GtS instance account will be used to do the lookup.
RequestingUsername string
// The ActivityPub URI of the remote account (optional).
// If not set (nil), the ActivityPub URI of the remote account will be discovered
// via webfinger, so you must set RemoteAccountUsername and RemoteAccountHost
// if this parameter is not set.
RemoteAccountID * url . URL
// The username of the remote account (optional).
// If RemoteAccountID is not set, then this value must be set.
RemoteAccountUsername string
// The host of the remote account (optional).
// If RemoteAccountID is not set, then this value must be set.
RemoteAccountHost string
// Whether to do a blocking call to the remote instance. If true,
// then the account's media and other fields will be fully dereferenced before it is returned.
// If false, then the account's media and other fields will be dereferenced in the background,
// so only a minimal account representation will be returned by GetRemoteAccount.
Blocking bool
// Whether to skip making calls to remote instances. This is useful when you want to
// quickly fetch a remote account from the database or fail, and don't want to cause
// http requests to go flying around.
SkipResolve bool
2022-09-26 11:56:01 +02:00
// PartialAccount can be used if the GetRemoteAccount call results from a federated/ap
// account update. In this case, we will already have a partial representation of the account,
// derived from converting the AP representation to a gtsmodel representation. If this field
// is provided, then GetRemoteAccount will use this as a basis for building the full account.
PartialAccount * gtsmodel . Account
2022-06-11 11:01:34 +02:00
}
2021-08-10 13:32:39 +02:00
// GetRemoteAccount completely dereferences a remote account, converts it to a GtS model account,
2022-06-11 11:01:34 +02:00
// puts or updates it in the database (if necessary), and returns it to a caller.
2022-08-20 22:47:19 +02:00
//
// If a local account is passed into this function for whatever reason (hey, it happens!), then it
// will be returned from the database without making any remote calls.
func ( d * deref ) GetRemoteAccount ( ctx context . Context , params GetRemoteAccountParams ) ( foundAccount * gtsmodel . Account , err error ) {
2022-06-11 11:01:34 +02:00
/ *
In this function we want to retrieve a gtsmodel representation of a remote account , with its proper
accountDomain set , while making as few calls to remote instances as possible to save time and bandwidth .
There are a few different paths through this function , and the path taken depends on how much
initial information we are provided with via parameters , how much information we already have stored ,
and what we ' re allowed to do according to the parameters we ' ve been passed .
Scenario 1 : We ' re not allowed to resolve remotely , but we ' ve got either the account URI or the
account username + host , so we can check in our database and return if possible .
Scenario 2 : We are allowed to resolve remotely , and we have an account URI but no username or host .
In this case , we can use the URI to resolve the remote account and find the username ,
and then we can webfinger the account to discover the accountDomain if necessary .
Scenario 3 : We are allowed to resolve remotely , and we have the username and host but no URI .
In this case , we can webfinger the account to discover the URI , and then dereference
from that .
* /
2022-08-20 22:47:19 +02:00
skipResolve := params . SkipResolve
// this first step checks if we have the
2022-09-26 11:56:01 +02:00
// account in the database somewhere already,
// or if we've been provided it as a partial
2022-06-11 11:01:34 +02:00
switch {
2022-09-26 11:56:01 +02:00
case params . PartialAccount != nil :
foundAccount = params . PartialAccount
if foundAccount . Domain == "" || foundAccount . Domain == config . GetHost ( ) || foundAccount . Domain == config . GetAccountDomain ( ) {
// this is actually a local account,
// make sure we don't try to resolve
skipResolve = true
}
2022-06-11 11:01:34 +02:00
case params . RemoteAccountID != nil :
2022-08-20 22:47:19 +02:00
uri := params . RemoteAccountID
host := uri . Host
if host == config . GetHost ( ) || host == config . GetAccountDomain ( ) {
// this is actually a local account,
// make sure we don't try to resolve
skipResolve = true
}
if a , dbErr := d . db . GetAccountByURI ( ctx , uri . String ( ) ) ; dbErr == nil {
foundAccount = a
2022-06-11 11:01:34 +02:00
} else if dbErr != db . ErrNoEntries {
2022-08-20 22:47:19 +02:00
err = fmt . Errorf ( "GetRemoteAccount: database error looking for account with uri %s: %s" , uri , err )
}
case params . RemoteAccountUsername != "" && ( params . RemoteAccountHost == "" || params . RemoteAccountHost == config . GetHost ( ) || params . RemoteAccountHost == config . GetAccountDomain ( ) ) :
// either no domain is provided or this seems
// to be a local account, so don't resolve
skipResolve = true
2022-09-02 11:56:33 +02:00
if a , dbErr := d . db . GetAccountByUsernameDomain ( ctx , params . RemoteAccountUsername , "" ) ; dbErr == nil {
2022-08-20 22:47:19 +02:00
foundAccount = a
} else if dbErr != db . ErrNoEntries {
err = fmt . Errorf ( "GetRemoteAccount: database error looking for local account with username %s: %s" , params . RemoteAccountUsername , err )
2022-06-11 11:01:34 +02:00
}
case params . RemoteAccountUsername != "" && params . RemoteAccountHost != "" :
2022-08-20 22:47:19 +02:00
if a , dbErr := d . db . GetAccountByUsernameDomain ( ctx , params . RemoteAccountUsername , params . RemoteAccountHost ) ; dbErr == nil {
foundAccount = a
2022-06-11 11:01:34 +02:00
} else if dbErr != db . ErrNoEntries {
2022-08-20 22:47:19 +02:00
err = fmt . Errorf ( "GetRemoteAccount: database error looking for account with username %s and domain %s: %s" , params . RemoteAccountUsername , params . RemoteAccountHost , err )
2022-06-11 11:01:34 +02:00
}
default :
err = errors . New ( "GetRemoteAccount: no identifying parameters were set so we cannot get account" )
}
if err != nil {
return
}
2022-08-20 22:47:19 +02:00
if skipResolve {
// if we can't resolve, return already
// since there's nothing more we can do
if foundAccount == nil {
err = errors . New ( "GetRemoteAccount: couldn't retrieve account locally and won't try to resolve it" )
2022-06-11 11:01:34 +02:00
}
return
}
var accountable ap . Accountable
if params . RemoteAccountUsername == "" || params . RemoteAccountHost == "" {
// try to populate the missing params
// the first one is easy ...
params . RemoteAccountHost = params . RemoteAccountID . Host
// ... but we still need the username so we can do a finger for the accountDomain
2022-09-26 11:56:01 +02:00
// check if we got the account earlier
2022-08-20 22:47:19 +02:00
if foundAccount != nil {
params . RemoteAccountUsername = foundAccount . Username
2022-06-11 11:01:34 +02:00
} else {
// if we didn't already have it, we have dereference it from remote and just...
accountable , err = d . dereferenceAccountable ( ctx , params . RequestingUsername , params . RemoteAccountID )
2022-01-25 11:21:22 +01:00
if err != nil {
2022-06-11 11:01:34 +02:00
err = fmt . Errorf ( "GetRemoteAccount: error dereferencing accountable: %s" , err )
return
2022-01-24 18:12:04 +01:00
}
2022-06-11 11:01:34 +02:00
// ... take the username (for now)
params . RemoteAccountUsername , err = ap . ExtractPreferredUsername ( accountable )
if err != nil {
err = fmt . Errorf ( "GetRemoteAccount: error extracting accountable username: %s" , err )
return
2022-01-24 18:12:04 +01:00
}
2021-08-10 13:32:39 +02:00
}
}
2022-06-11 11:01:34 +02:00
// if we reach this point, params.RemoteAccountHost and params.RemoteAccountUsername must be set
// params.RemoteAccountID may or may not be set, but we have enough information to fetch it if we need it
// we finger to fetch the account domain but just in case we're not fingering, make a best guess
// already about what the account domain might be; this var will be overwritten later if necessary
var accountDomain string
switch {
2022-08-20 22:47:19 +02:00
case foundAccount != nil :
accountDomain = foundAccount . Domain
2022-06-11 11:01:34 +02:00
case params . RemoteAccountID != nil :
accountDomain = params . RemoteAccountID . Host
default :
accountDomain = params . RemoteAccountHost
}
2022-09-23 21:27:35 +02:00
// to save on remote calls, only webfinger if:
// - we don't know the remote account ActivityPub ID yet OR
// - we haven't found the account yet in some other way OR
2022-09-26 11:56:01 +02:00
// - we were passed a partial account in params OR
2022-09-23 21:27:35 +02:00
// - we haven't webfingered the account for two days AND the account isn't an instance account
2022-06-11 11:01:34 +02:00
var fingered time . Time
2022-09-26 11:56:01 +02:00
if params . RemoteAccountID == nil || foundAccount == nil || params . PartialAccount != nil || ( foundAccount . LastWebfingeredAt . Before ( time . Now ( ) . Add ( webfingerInterval ) ) && ! instanceAccount ( foundAccount ) ) {
2022-06-11 11:01:34 +02:00
accountDomain , params . RemoteAccountID , err = d . fingerRemoteAccount ( ctx , params . RequestingUsername , params . RemoteAccountUsername , params . RemoteAccountHost )
2021-08-10 13:32:39 +02:00
if err != nil {
2022-06-11 11:01:34 +02:00
err = fmt . Errorf ( "GetRemoteAccount: error while fingering: %s" , err )
return
}
fingered = time . Now ( )
}
2022-08-20 22:47:19 +02:00
if ! fingered . IsZero ( ) && foundAccount == nil {
2022-06-11 11:01:34 +02:00
// if we just fingered and now have a discovered account domain but still no account,
// we should do a final lookup in the database with the discovered username + accountDomain
// to make absolutely sure we don't already have this account
a := & gtsmodel . Account { }
where := [ ] db . Where { { Key : "username" , Value : params . RemoteAccountUsername } , { Key : "domain" , Value : accountDomain } }
if dbErr := d . db . GetWhere ( ctx , where , a ) ; dbErr == nil {
2022-08-20 22:47:19 +02:00
foundAccount = a
2022-06-11 11:01:34 +02:00
} else if dbErr != db . ErrNoEntries {
err = fmt . Errorf ( "GetRemoteAccount: database error looking for account with username %s and host %s: %s" , params . RemoteAccountUsername , params . RemoteAccountHost , err )
return
}
}
// we may also have some extra information already, like the account we had in the db, or the
// accountable representation that we dereferenced from remote
2022-08-20 22:47:19 +02:00
if foundAccount == nil {
2022-06-11 11:01:34 +02:00
// we still don't have the account, so deference it if we didn't earlier
if accountable == nil {
accountable , err = d . dereferenceAccountable ( ctx , params . RequestingUsername , params . RemoteAccountID )
if err != nil {
err = fmt . Errorf ( "GetRemoteAccount: error dereferencing accountable: %s" , err )
return
}
2021-08-10 13:32:39 +02:00
}
2022-06-11 11:01:34 +02:00
// then convert
2022-08-20 22:47:19 +02:00
foundAccount , err = d . typeConverter . ASRepresentationToAccount ( ctx , accountable , accountDomain , false )
2022-01-24 13:12:17 +01:00
if err != nil {
2022-06-11 11:01:34 +02:00
err = fmt . Errorf ( "GetRemoteAccount: error converting accountable to account: %s" , err )
return
2021-08-10 13:32:39 +02:00
}
2022-06-11 11:01:34 +02:00
// this is a new account so we need to generate a new ID for it
var ulid string
ulid , err = id . NewRandomULID ( )
2022-01-24 13:12:17 +01:00
if err != nil {
2022-06-11 11:01:34 +02:00
err = fmt . Errorf ( "GetRemoteAccount: error generating new id for account: %s" , err )
return
2021-08-10 13:32:39 +02:00
}
2022-08-20 22:47:19 +02:00
foundAccount . ID = ulid
2022-01-24 13:12:17 +01:00
2022-08-20 22:47:19 +02:00
_ , err = d . populateAccountFields ( ctx , foundAccount , params . RequestingUsername , params . Blocking )
2022-06-11 11:01:34 +02:00
if err != nil {
err = fmt . Errorf ( "GetRemoteAccount: error populating further account fields: %s" , err )
return
2021-08-10 13:32:39 +02:00
}
2022-08-20 22:47:19 +02:00
foundAccount . LastWebfingeredAt = fingered
foundAccount . UpdatedAt = time . Now ( )
2022-06-11 11:01:34 +02:00
2022-09-26 11:56:01 +02:00
foundAccount , err = d . db . PutAccount ( ctx , foundAccount )
2022-06-11 11:01:34 +02:00
if err != nil {
err = fmt . Errorf ( "GetRemoteAccount: error putting new account: %s" , err )
return
2021-08-10 13:32:39 +02:00
}
2022-01-24 13:12:17 +01:00
2022-06-11 11:01:34 +02:00
return // the new account
2022-01-24 13:12:17 +01:00
}
2022-06-11 11:01:34 +02:00
// we had the account already, but now we know the account domain, so update it if it's different
2022-09-26 11:56:01 +02:00
var accountDomainChanged bool
2022-08-20 22:47:19 +02:00
if ! strings . EqualFold ( foundAccount . Domain , accountDomain ) {
2022-09-26 11:56:01 +02:00
accountDomainChanged = true
2022-08-20 22:47:19 +02:00
foundAccount . Domain = accountDomain
2021-08-10 13:32:39 +02:00
}
2022-09-23 21:27:35 +02:00
// if SharedInboxURI is nil, that means we don't know yet if this account has
// a shared inbox available for it, so we need to check this here
var sharedInboxChanged bool
if foundAccount . SharedInboxURI == nil {
// we need the accountable for this, so get it if we don't have it yet
if accountable == nil {
accountable , err = d . dereferenceAccountable ( ctx , params . RequestingUsername , params . RemoteAccountID )
if err != nil {
err = fmt . Errorf ( "GetRemoteAccount: error dereferencing accountable: %s" , err )
return
}
}
// This can be:
// - an empty string (we know it doesn't have a shared inbox) OR
// - a string URL (we know it does a shared inbox).
// Set it either way!
var sharedInbox string
if sharedInboxURI := ap . ExtractSharedInbox ( accountable ) ; sharedInboxURI != nil {
// only trust shared inbox if it has at least two domains,
// from the right, in common with the domain of the account
if dns . CompareDomainName ( foundAccount . Domain , sharedInboxURI . Host ) >= 2 {
sharedInbox = sharedInboxURI . String ( )
}
}
sharedInboxChanged = true
foundAccount . SharedInboxURI = & sharedInbox
}
2022-06-11 11:01:34 +02:00
// make sure the account fields are populated before returning:
// the caller might want to block until everything is loaded
var fieldsChanged bool
2022-08-20 22:47:19 +02:00
fieldsChanged , err = d . populateAccountFields ( ctx , foundAccount , params . RequestingUsername , params . Blocking )
2022-01-24 13:12:17 +01:00
if err != nil {
2022-06-11 11:01:34 +02:00
return nil , fmt . Errorf ( "GetRemoteAccount: error populating remoteAccount fields: %s" , err )
2022-01-24 13:12:17 +01:00
}
2022-06-11 11:01:34 +02:00
var fingeredChanged bool
if ! fingered . IsZero ( ) {
fingeredChanged = true
2022-08-20 22:47:19 +02:00
foundAccount . LastWebfingeredAt = fingered
2022-01-24 13:12:17 +01:00
}
2022-09-26 11:56:01 +02:00
if accountDomainChanged || sharedInboxChanged || fieldsChanged || fingeredChanged {
2022-08-20 22:47:19 +02:00
foundAccount , err = d . db . UpdateAccount ( ctx , foundAccount )
2022-01-25 11:21:22 +01:00
if err != nil {
2022-06-11 11:01:34 +02:00
return nil , fmt . Errorf ( "GetRemoteAccount: error updating remoteAccount: %s" , err )
2022-01-25 11:21:22 +01:00
}
2022-01-24 13:12:17 +01:00
}
2022-06-11 11:01:34 +02:00
return // the account we already had + possibly updated
2021-08-10 13:32:39 +02:00
}
// dereferenceAccountable calls remoteAccountID with a GET request, and tries to parse whatever
// it finds as something that an account model can be constructed out of.
//
// Will work for Person, Application, or Service models.
2021-08-25 15:34:33 +02:00
func ( d * deref ) dereferenceAccountable ( ctx context . Context , username string , remoteAccountID * url . URL ) ( ap . Accountable , error ) {
2021-08-10 13:32:39 +02:00
d . startHandshake ( username , remoteAccountID )
defer d . stopHandshake ( username , remoteAccountID )
2021-08-25 15:34:33 +02:00
if blocked , err := d . db . IsDomainBlocked ( ctx , remoteAccountID . Host ) ; blocked || err != nil {
2021-08-10 13:32:39 +02:00
return nil , fmt . Errorf ( "DereferenceAccountable: domain %s is blocked" , remoteAccountID . Host )
}
2021-08-25 15:34:33 +02:00
transport , err := d . transportController . NewTransportForUsername ( ctx , username )
2021-08-10 13:32:39 +02:00
if err != nil {
return nil , fmt . Errorf ( "DereferenceAccountable: transport err: %s" , err )
}
2021-10-04 15:24:19 +02:00
b , err := transport . Dereference ( ctx , remoteAccountID )
2021-08-10 13:32:39 +02:00
if err != nil {
return nil , fmt . Errorf ( "DereferenceAccountable: error deferencing %s: %s" , remoteAccountID . String ( ) , err )
}
m := make ( map [ string ] interface { } )
if err := json . Unmarshal ( b , & m ) ; err != nil {
return nil , fmt . Errorf ( "DereferenceAccountable: error unmarshalling bytes into json: %s" , err )
}
2021-10-04 15:24:19 +02:00
t , err := streams . ToType ( ctx , m )
2021-08-10 13:32:39 +02:00
if err != nil {
return nil , fmt . Errorf ( "DereferenceAccountable: error resolving json into ap vocab type: %s" , err )
}
switch t . GetTypeName ( ) {
2021-09-03 10:30:40 +02:00
case ap . ActorApplication :
2021-08-10 13:32:39 +02:00
p , ok := t . ( vocab . ActivityStreamsApplication )
if ! ok {
return nil , errors . New ( "DereferenceAccountable: error resolving type as activitystreams application" )
}
return p , nil
2021-09-30 12:27:42 +02:00
case ap . ActorGroup :
p , ok := t . ( vocab . ActivityStreamsGroup )
if ! ok {
return nil , errors . New ( "DereferenceAccountable: error resolving type as activitystreams group" )
}
return p , nil
case ap . ActorOrganization :
p , ok := t . ( vocab . ActivityStreamsOrganization )
if ! ok {
return nil , errors . New ( "DereferenceAccountable: error resolving type as activitystreams organization" )
}
return p , nil
case ap . ActorPerson :
p , ok := t . ( vocab . ActivityStreamsPerson )
if ! ok {
return nil , errors . New ( "DereferenceAccountable: error resolving type as activitystreams person" )
}
return p , nil
2021-09-03 10:30:40 +02:00
case ap . ActorService :
2021-08-10 13:32:39 +02:00
p , ok := t . ( vocab . ActivityStreamsService )
if ! ok {
return nil , errors . New ( "DereferenceAccountable: error resolving type as activitystreams service" )
}
return p , nil
}
return nil , fmt . Errorf ( "DereferenceAccountable: type name %s not supported" , t . GetTypeName ( ) )
}
2022-01-24 13:12:17 +01:00
// populateAccountFields populates any fields on the given account that weren't populated by the initial
2021-08-10 13:32:39 +02:00
// dereferencing. This includes things like header and avatar etc.
2022-06-11 11:01:34 +02:00
func ( d * deref ) populateAccountFields ( ctx context . Context , account * gtsmodel . Account , requestingUsername string , blocking bool ) ( bool , error ) {
2022-01-24 13:12:17 +01:00
// if we're dealing with an instance account, just bail, we don't need to do anything
if instanceAccount ( account ) {
2022-01-25 11:21:22 +01:00
return false , nil
2022-01-24 13:12:17 +01:00
}
2021-08-10 13:32:39 +02:00
accountURI , err := url . Parse ( account . URI )
if err != nil {
2022-01-25 11:21:22 +01:00
return false , fmt . Errorf ( "populateAccountFields: couldn't parse account URI %s: %s" , account . URI , err )
2021-08-10 13:32:39 +02:00
}
2022-01-24 13:12:17 +01:00
2021-08-25 15:34:33 +02:00
if blocked , err := d . db . IsDomainBlocked ( ctx , accountURI . Host ) ; blocked || err != nil {
2022-01-25 11:21:22 +01:00
return false , fmt . Errorf ( "populateAccountFields: domain %s is blocked" , accountURI . Host )
2021-08-10 13:32:39 +02:00
}
2022-09-26 11:56:01 +02:00
var changed bool
2021-08-10 13:32:39 +02:00
// fetch the header and avatar
2022-09-26 11:56:01 +02:00
if mediaChanged , err := d . fetchRemoteAccountMedia ( ctx , account , requestingUsername , blocking ) ; err != nil {
2022-01-25 11:21:22 +01:00
return false , fmt . Errorf ( "populateAccountFields: error fetching header/avi for account: %s" , err )
2022-09-26 11:56:01 +02:00
} else if mediaChanged {
changed = mediaChanged
}
// fetch any emojis used in note, fields, display name, etc
if emojisChanged , err := d . fetchRemoteAccountEmojis ( ctx , account , requestingUsername ) ; err != nil {
return false , fmt . Errorf ( "populateAccountFields: error fetching emojis for account: %s" , err )
} else if emojisChanged {
changed = emojisChanged
2021-08-10 13:32:39 +02:00
}
2022-01-25 11:21:22 +01:00
return changed , nil
2021-08-10 13:32:39 +02:00
}
2022-01-24 13:12:17 +01:00
// fetchRemoteAccountMedia fetches and stores the header and avatar for a remote account,
// using a transport on behalf of requestingUsername.
2021-08-10 13:32:39 +02:00
//
2022-01-25 11:21:22 +01:00
// The returned boolean indicates whether anything changed -- in other words, whether the
// account should be updated in the database.
//
2021-08-10 13:32:39 +02:00
// targetAccount's AvatarMediaAttachmentID and HeaderMediaAttachmentID will be updated as necessary.
//
2022-01-24 13:12:17 +01:00
// If refresh is true, then the media will be fetched again even if it's already been fetched before.
//
// If blocking is true, then the calls to the media manager made by this function will be blocking:
// in other words, the function won't return until the header and the avatar have been fully processed.
2022-09-26 11:56:01 +02:00
func ( d * deref ) fetchRemoteAccountMedia ( ctx context . Context , targetAccount * gtsmodel . Account , requestingUsername string , blocking bool ) ( bool , error ) {
var (
changed bool
t transport . Transport
)
2021-08-10 13:32:39 +02:00
2022-06-11 11:01:34 +02:00
if targetAccount . AvatarRemoteURL != "" && ( targetAccount . AvatarMediaAttachmentID == "" ) {
2022-01-24 13:12:17 +01:00
var processingMedia * media . ProcessingMedia
2022-01-08 17:17:01 +01:00
2022-02-08 13:17:10 +01:00
d . dereferencingAvatarsLock . Lock ( ) // LOCK HERE
2022-01-24 13:12:17 +01:00
// first check if we're already processing this media
if alreadyProcessing , ok := d . dereferencingAvatars [ targetAccount . ID ] ; ok {
// we're already on it, no worries
processingMedia = alreadyProcessing
2022-01-08 17:17:01 +01:00
}
2022-01-24 13:12:17 +01:00
if processingMedia == nil {
// we're not already processing it so start now
avatarIRI , err := url . Parse ( targetAccount . AvatarRemoteURL )
if err != nil {
2022-02-08 13:17:10 +01:00
d . dereferencingAvatarsLock . Unlock ( )
2022-01-25 11:21:22 +01:00
return changed , err
2022-01-24 13:12:17 +01:00
}
2022-09-26 11:56:01 +02:00
if t == nil {
var err error
t , err = d . transportController . NewTransportForUsername ( ctx , requestingUsername )
if err != nil {
return false , fmt . Errorf ( "fetchRemoteAccountMedia: error getting transport for user: %s" , err )
}
}
2022-09-29 22:50:43 +02:00
data := func ( innerCtx context . Context ) ( io . Reader , int64 , error ) {
2022-01-24 13:12:17 +01:00
return t . DereferenceMedia ( innerCtx , avatarIRI )
}
avatar := true
2022-02-22 13:50:33 +01:00
newProcessing , err := d . mediaManager . ProcessMedia ( ctx , data , nil , targetAccount . ID , & media . AdditionalMediaInfo {
2022-01-24 13:12:17 +01:00
RemoteURL : & targetAccount . AvatarRemoteURL ,
Avatar : & avatar ,
} )
if err != nil {
2022-02-08 13:17:10 +01:00
d . dereferencingAvatarsLock . Unlock ( )
2022-01-25 11:21:22 +01:00
return changed , err
2022-01-24 13:12:17 +01:00
}
// store it in our map to indicate it's in process
d . dereferencingAvatars [ targetAccount . ID ] = newProcessing
processingMedia = newProcessing
2022-01-08 17:17:01 +01:00
}
2022-02-08 13:17:10 +01:00
d . dereferencingAvatarsLock . Unlock ( ) // UNLOCK HERE
2022-01-08 17:17:01 +01:00
2022-01-24 13:12:17 +01:00
// block until loaded if required...
if blocking {
if err := lockAndLoad ( ctx , d . dereferencingAvatarsLock , processingMedia , d . dereferencingAvatars , targetAccount . ID ) ; err != nil {
2022-01-25 11:21:22 +01:00
return changed , err
2022-01-24 13:12:17 +01:00
}
} else {
// ...otherwise do it async
go func ( ) {
2022-01-24 18:12:04 +01:00
dlCtx , done := context . WithDeadline ( context . Background ( ) , time . Now ( ) . Add ( 1 * time . Minute ) )
if err := lockAndLoad ( dlCtx , d . dereferencingAvatarsLock , processingMedia , d . dereferencingAvatars , targetAccount . ID ) ; err != nil {
2022-07-19 10:47:55 +02:00
log . Errorf ( "fetchRemoteAccountMedia: error during async lock and load of avatar: %s" , err )
2022-01-24 13:12:17 +01:00
}
2022-01-24 18:12:04 +01:00
done ( )
2022-01-24 13:12:17 +01:00
} ( )
}
2022-01-24 18:12:04 +01:00
targetAccount . AvatarMediaAttachmentID = processingMedia . AttachmentID ( )
2022-01-25 11:21:22 +01:00
changed = true
2021-08-10 13:32:39 +02:00
}
2022-06-11 11:01:34 +02:00
if targetAccount . HeaderRemoteURL != "" && ( targetAccount . HeaderMediaAttachmentID == "" ) {
2022-01-24 13:12:17 +01:00
var processingMedia * media . ProcessingMedia
2022-01-08 17:17:01 +01:00
2022-02-08 13:17:10 +01:00
d . dereferencingHeadersLock . Lock ( ) // LOCK HERE
2022-01-24 13:12:17 +01:00
// first check if we're already processing this media
if alreadyProcessing , ok := d . dereferencingHeaders [ targetAccount . ID ] ; ok {
// we're already on it, no worries
processingMedia = alreadyProcessing
2022-01-08 17:17:01 +01:00
}
2022-01-24 13:12:17 +01:00
if processingMedia == nil {
// we're not already processing it so start now
headerIRI , err := url . Parse ( targetAccount . HeaderRemoteURL )
if err != nil {
2022-02-08 13:17:10 +01:00
d . dereferencingAvatarsLock . Unlock ( )
2022-01-25 11:21:22 +01:00
return changed , err
2022-01-24 13:12:17 +01:00
}
2022-09-26 11:56:01 +02:00
if t == nil {
var err error
t , err = d . transportController . NewTransportForUsername ( ctx , requestingUsername )
if err != nil {
return false , fmt . Errorf ( "fetchRemoteAccountMedia: error getting transport for user: %s" , err )
}
}
2022-09-29 22:50:43 +02:00
data := func ( innerCtx context . Context ) ( io . Reader , int64 , error ) {
2022-01-24 13:12:17 +01:00
return t . DereferenceMedia ( innerCtx , headerIRI )
}
header := true
2022-02-22 13:50:33 +01:00
newProcessing , err := d . mediaManager . ProcessMedia ( ctx , data , nil , targetAccount . ID , & media . AdditionalMediaInfo {
2022-01-24 13:12:17 +01:00
RemoteURL : & targetAccount . HeaderRemoteURL ,
Header : & header ,
} )
if err != nil {
2022-02-08 13:17:10 +01:00
d . dereferencingAvatarsLock . Unlock ( )
2022-01-25 11:21:22 +01:00
return changed , err
2022-01-24 13:12:17 +01:00
}
// store it in our map to indicate it's in process
d . dereferencingHeaders [ targetAccount . ID ] = newProcessing
processingMedia = newProcessing
2022-01-08 17:17:01 +01:00
}
2022-02-08 13:17:10 +01:00
d . dereferencingHeadersLock . Unlock ( ) // UNLOCK HERE
2022-01-08 17:17:01 +01:00
2022-01-24 13:12:17 +01:00
// block until loaded if required...
if blocking {
if err := lockAndLoad ( ctx , d . dereferencingHeadersLock , processingMedia , d . dereferencingHeaders , targetAccount . ID ) ; err != nil {
2022-01-25 11:21:22 +01:00
return changed , err
2022-01-24 13:12:17 +01:00
}
} else {
// ...otherwise do it async
go func ( ) {
2022-01-24 18:12:04 +01:00
dlCtx , done := context . WithDeadline ( context . Background ( ) , time . Now ( ) . Add ( 1 * time . Minute ) )
if err := lockAndLoad ( dlCtx , d . dereferencingHeadersLock , processingMedia , d . dereferencingHeaders , targetAccount . ID ) ; err != nil {
2022-07-19 10:47:55 +02:00
log . Errorf ( "fetchRemoteAccountMedia: error during async lock and load of header: %s" , err )
2022-01-24 13:12:17 +01:00
}
2022-01-24 18:12:04 +01:00
done ( )
2022-01-24 13:12:17 +01:00
} ( )
}
2022-01-24 18:12:04 +01:00
targetAccount . HeaderMediaAttachmentID = processingMedia . AttachmentID ( )
2022-01-25 11:21:22 +01:00
changed = true
2021-08-10 13:32:39 +02:00
}
2022-01-24 13:12:17 +01:00
2022-01-25 11:21:22 +01:00
return changed , nil
2021-08-10 13:32:39 +02:00
}
2022-01-24 13:12:17 +01:00
2022-09-26 11:56:01 +02:00
func ( d * deref ) fetchRemoteAccountEmojis ( ctx context . Context , targetAccount * gtsmodel . Account , requestingUsername string ) ( bool , error ) {
maybeEmojis := targetAccount . Emojis
maybeEmojiIDs := targetAccount . EmojiIDs
// It's possible that the account had emoji IDs set on it, but not Emojis
// themselves, depending on how it was fetched before being passed to us.
//
// If we only have IDs, fetch the emojis from the db. We know they're in
// there or else they wouldn't have IDs.
if len ( maybeEmojiIDs ) > len ( maybeEmojis ) {
maybeEmojis = [ ] * gtsmodel . Emoji { }
for _ , emojiID := range maybeEmojiIDs {
maybeEmoji , err := d . db . GetEmojiByID ( ctx , emojiID )
if err != nil {
return false , err
}
maybeEmojis = append ( maybeEmojis , maybeEmoji )
}
}
// For all the maybe emojis we have, we either fetch them from the database
// (if we haven't already), or dereference them from the remote instance.
gotEmojis , err := d . populateEmojis ( ctx , maybeEmojis , requestingUsername )
if err != nil {
return false , err
}
// Extract the ID of each fetched or dereferenced emoji, so we can attach
// this to the account if necessary.
gotEmojiIDs := make ( [ ] string , 0 , len ( gotEmojis ) )
for _ , e := range gotEmojis {
gotEmojiIDs = append ( gotEmojiIDs , e . ID )
}
var (
changed = false // have the emojis for this account changed?
maybeLen = len ( maybeEmojis )
gotLen = len ( gotEmojis )
)
// if the length of everything is zero, this is simple:
// nothing has changed and there's nothing to do
if maybeLen == 0 && gotLen == 0 {
return changed , nil
}
// if the *amount* of emojis on the account has changed, then the got emojis
// are definitely different from the previous ones (if there were any) --
// the account has either more or fewer emojis set on it now, so take the
// discovered emojis as the new correct ones.
if maybeLen != gotLen {
changed = true
targetAccount . Emojis = gotEmojis
targetAccount . EmojiIDs = gotEmojiIDs
return changed , nil
}
// if the lengths are the same but not all of the slices are
// zero, something *might* have changed, so we have to check
// 1. did we have emojis before that we don't have now?
for _ , maybeEmoji := range maybeEmojis {
var stillPresent bool
for _ , gotEmoji := range gotEmojis {
if maybeEmoji . URI == gotEmoji . URI {
// the emoji we maybe had is still present now,
// so we can stop checking gotEmojis
stillPresent = true
break
}
}
if ! stillPresent {
// at least one maybeEmoji is no longer present in
// the got emojis, so we can stop checking now
changed = true
targetAccount . Emojis = gotEmojis
targetAccount . EmojiIDs = gotEmojiIDs
return changed , nil
}
}
// 2. do we have emojis now that we didn't have before?
for _ , gotEmoji := range gotEmojis {
var wasPresent bool
for _ , maybeEmoji := range maybeEmojis {
// check emoji IDs here as well, because unreferenced
// maybe emojis we didn't already have would not have
// had IDs set on them yet
if gotEmoji . URI == maybeEmoji . URI && gotEmoji . ID == maybeEmoji . ID {
// this got emoji was present already in the maybeEmoji,
// so we can stop checking through maybeEmojis
wasPresent = true
break
}
}
if ! wasPresent {
// at least one gotEmojis was not present in
// the maybeEmojis, so we can stop checking now
changed = true
targetAccount . Emojis = gotEmojis
targetAccount . EmojiIDs = gotEmojiIDs
return changed , nil
}
}
return changed , nil
}
2022-01-24 13:12:17 +01:00
func lockAndLoad ( ctx context . Context , lock * sync . Mutex , processing * media . ProcessingMedia , processingMap map [ string ] * media . ProcessingMedia , accountID string ) error {
// whatever happens, remove the in-process media from the map
defer func ( ) {
lock . Lock ( )
delete ( processingMap , accountID )
lock . Unlock ( )
} ( )
// try and load it
_ , err := processing . LoadAttachment ( ctx )
return err
}