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-08-10 13:32:39 +02:00
package dereferencing
import (
"context"
"errors"
2024-09-10 14:33:32 +02:00
"net/http"
2021-08-10 13:32:39 +02:00
"net/url"
2023-10-25 16:04:53 +02:00
"slices"
2023-11-04 21:21:20 +01:00
"time"
2023-10-25 16:04:53 +02:00
2021-08-10 13:32:39 +02:00
"github.com/superseriousbusiness/gotosocial/internal/ap"
2022-11-29 10:24:55 +01:00
"github.com/superseriousbusiness/gotosocial/internal/config"
2022-09-12 13:03:23 +02:00
"github.com/superseriousbusiness/gotosocial/internal/db"
2023-05-31 10:39:54 +02:00
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
2023-05-28 14:08:35 +02:00
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
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"
2023-10-31 12:05:17 +01:00
"github.com/superseriousbusiness/gotosocial/internal/util"
2021-08-10 13:32:39 +02:00
)
2024-02-09 15:24:49 +01:00
// statusFresh returns true if the given status is still
// considered "fresh" according to the desired freshness
// window (falls back to default status freshness if nil).
//
// Local statuses will always be considered fresh,
// because there's no remote state that may have changed.
//
// Return value of false indicates that the status
// is not fresh and should be refreshed from remote.
func statusFresh (
status * gtsmodel . Status ,
window * FreshnessWindow ,
) bool {
// Take default if no
// freshness window preferred.
if window == nil {
window = DefaultStatusFreshness
2023-12-15 15:24:39 +01:00
}
2024-02-09 15:24:49 +01:00
if status . IsLocal ( ) {
// Can't refresh
// local statuses.
2023-05-12 11:15:54 +02:00
return true
2022-11-15 19:45:15 +01:00
}
2023-05-12 11:15:54 +02:00
2024-02-09 15:24:49 +01:00
// Moment when the status is
// considered stale according to
// desired freshness window.
staleAt := status . FetchedAt . Add (
time . Duration ( * window ) ,
)
// It's still fresh if the time now
// is not past the point of staleness.
return ! time . Now ( ) . After ( staleAt )
2021-08-10 13:32:39 +02:00
}
2023-11-04 21:21:20 +01:00
// GetStatusByURI will attempt to fetch a status by its URI, first checking the database. In the case of a newly-met remote model, or a remote model whose 'last_fetched' date
// is beyond a certain interval, the status will be dereferenced. In the case of dereferencing, some low-priority status information may be enqueued for asynchronous fetching,
2024-09-10 14:33:32 +02:00
// e.g. dereferencing the status thread. An ActivityPub object indicates the status was dereferenced.
2023-10-23 11:58:13 +02:00
func ( d * Dereferencer ) GetStatusByURI ( ctx context . Context , requestUser string , uri * url . URL ) ( * gtsmodel . Status , ap . Statusable , error ) {
2024-01-31 14:29:47 +01:00
// Fetch and dereference / update status if necessary.
2023-11-04 21:21:20 +01:00
status , statusable , isNew , err := d . getStatusByURI ( ctx ,
2023-05-12 11:15:54 +02:00
requestUser ,
uri ,
)
2024-01-31 14:29:47 +01:00
2023-05-12 11:15:54 +02:00
if err != nil {
2024-01-31 14:29:47 +01:00
if status == nil {
// err with no existing
// status for fallback.
return nil , nil , err
}
log . Errorf ( ctx , "error updating status %s: %v" , uri , err )
} else if statusable != nil {
2023-05-12 11:15:54 +02:00
2023-11-04 21:21:20 +01:00
// Deref parents + children.
d . dereferenceThread ( ctx ,
requestUser ,
uri ,
status ,
statusable ,
isNew ,
)
2023-05-12 11:15:54 +02:00
}
2023-11-04 21:21:20 +01:00
return status , statusable , nil
2023-05-12 11:15:54 +02:00
}
2024-01-31 14:29:47 +01:00
// getStatusByURI is a package internal form of .GetStatusByURI() that doesn't dereference thread on update, and may return an existing status with error on failed re-fetch.
2023-11-04 21:21:20 +01:00
func ( d * Dereferencer ) getStatusByURI ( ctx context . Context , requestUser string , uri * url . URL ) ( * gtsmodel . Status , ap . Statusable , bool , error ) {
2023-05-12 11:15:54 +02:00
var (
status * gtsmodel . Status
uriStr = uri . String ( )
err error
)
2024-01-31 14:29:47 +01:00
// Search the database for existing by URI.
2023-05-31 10:39:54 +02:00
status , err = d . state . DB . GetStatusByURI (
2024-01-31 14:29:47 +01:00
2023-05-31 10:39:54 +02:00
// request a barebones object, it may be in the
// db but with related models not yet dereferenced.
gtscontext . SetBarebones ( ctx ) ,
uriStr ,
)
2023-05-12 11:15:54 +02:00
if err != nil && ! errors . Is ( err , db . ErrNoEntries ) {
2023-11-04 21:21:20 +01:00
return nil , nil , false , gtserror . Newf ( "error checking database for status %s by uri: %w" , uriStr , err )
2021-08-10 13:32:39 +02:00
}
2022-11-29 10:24:55 +01:00
if status == nil {
2024-01-31 14:29:47 +01:00
// Else, search database for existing by URL.
2023-05-31 10:39:54 +02:00
status , err = d . state . DB . GetStatusByURL (
gtscontext . SetBarebones ( ctx ) ,
uriStr ,
)
2023-05-12 11:15:54 +02:00
if err != nil && ! errors . Is ( err , db . ErrNoEntries ) {
2023-11-04 21:21:20 +01:00
return nil , nil , false , gtserror . Newf ( "error checking database for status %s by url: %w" , uriStr , err )
2022-11-29 10:24:55 +01:00
}
2022-05-23 17:40:03 +02:00
}
2023-05-12 11:15:54 +02:00
if status == nil {
2024-01-31 14:29:47 +01:00
// Ensure not a failed search for a local
// status, if so we know it doesn't exist.
if uri . Host == config . GetHost ( ) ||
uri . Host == config . GetAccountDomain ( ) {
return nil , nil , false , gtserror . SetUnretrievable ( err )
2022-11-29 10:24:55 +01:00
}
2023-05-12 11:15:54 +02:00
// Create and pass-through a new bare-bones model for deref.
2023-10-31 12:12:22 +01:00
return d . enrichStatusSafely ( ctx , requestUser , uri , & gtsmodel . Status {
2024-01-31 14:29:47 +01:00
Local : util . Ptr ( false ) ,
2023-05-12 11:15:54 +02:00
URI : uriStr ,
} , nil )
2022-11-29 10:24:55 +01:00
}
2024-02-09 15:24:49 +01:00
if statusFresh ( status , DefaultStatusFreshness ) {
2024-01-09 10:42:39 +01:00
// This is an existing status that is up-to-date,
// before returning ensure it is fully populated.
2023-05-31 10:39:54 +02:00
if err := d . state . DB . PopulateStatus ( ctx , status ) ; err != nil {
log . Errorf ( ctx , "error populating existing status: %v" , err )
}
2024-01-09 10:42:39 +01:00
2023-11-04 21:21:20 +01:00
return status , nil , false , nil
2023-05-31 10:39:54 +02:00
}
2024-09-10 14:33:32 +02:00
// Try to deref and update existing.
return d . enrichStatusSafely ( ctx ,
2023-05-12 11:15:54 +02:00
requestUser ,
uri ,
status ,
nil ,
)
}
2021-08-10 13:32:39 +02:00
2023-11-04 21:21:20 +01:00
// RefreshStatus is functionally equivalent to GetStatusByURI(), except that it requires a pre
// populated status model (with AT LEAST uri set), and ALL thread dereferencing is asynchronous.
func ( d * Dereferencer ) RefreshStatus (
ctx context . Context ,
requestUser string ,
status * gtsmodel . Status ,
statusable ap . Statusable ,
2024-02-09 15:24:49 +01:00
window * FreshnessWindow ,
2023-11-04 21:21:20 +01:00
) ( * gtsmodel . Status , ap . Statusable , error ) {
2024-01-09 10:42:39 +01:00
// If no incoming data is provided,
// check whether status needs update.
if statusable == nil &&
2024-02-09 15:24:49 +01:00
statusFresh ( status , window ) {
2023-05-12 11:15:54 +02:00
return status , nil , nil
2021-08-10 13:32:39 +02:00
}
2023-05-12 11:15:54 +02:00
// Parse the URI from status.
uri , err := url . Parse ( status . URI )
2021-08-10 13:32:39 +02:00
if err != nil {
2023-05-28 14:08:35 +02:00
return nil , nil , gtserror . Newf ( "invalid status uri %q: %w" , status . URI , err )
2021-08-10 13:32:39 +02:00
}
2023-11-04 21:21:20 +01:00
// Try to update + dereference the passed status model.
latest , statusable , isNew , err := d . enrichStatusSafely ( ctx ,
2023-05-12 11:15:54 +02:00
requestUser ,
uri ,
status ,
2023-11-04 21:21:20 +01:00
statusable ,
2023-05-12 11:15:54 +02:00
)
2021-08-10 13:32:39 +02:00
2023-11-04 21:21:20 +01:00
if statusable != nil {
// Deref parents + children.
d . dereferenceThread ( ctx ,
requestUser ,
uri ,
2024-02-18 10:49:40 +01:00
latest ,
2023-11-04 21:21:20 +01:00
statusable ,
isNew ,
)
}
2021-08-10 13:32:39 +02:00
2024-09-10 14:33:32 +02:00
return latest , statusable , err
2021-08-10 13:32:39 +02:00
}
2023-11-04 21:21:20 +01:00
// RefreshStatusAsync is functionally equivalent to RefreshStatus(), except that ALL
// dereferencing is queued for asynchronous processing, (both thread AND status).
func ( d * Dereferencer ) RefreshStatusAsync (
ctx context . Context ,
requestUser string ,
status * gtsmodel . Status ,
statusable ap . Statusable ,
2024-02-09 15:24:49 +01:00
window * FreshnessWindow ,
2023-11-04 21:21:20 +01:00
) {
2024-01-09 10:42:39 +01:00
// If no incoming data is provided,
// check whether status needs update.
if statusable == nil &&
2024-02-09 15:24:49 +01:00
statusFresh ( status , window ) {
2023-05-12 11:15:54 +02:00
return
2021-08-10 13:32:39 +02:00
}
2023-05-12 11:15:54 +02:00
// Parse the URI from status.
uri , err := url . Parse ( status . URI )
2021-08-10 13:32:39 +02:00
if err != nil {
2023-05-28 14:08:35 +02:00
log . Errorf ( ctx , "invalid status uri %q: %v" , status . URI , err )
2023-05-12 11:15:54 +02:00
return
2021-08-10 13:32:39 +02:00
}
2023-11-04 21:21:20 +01:00
// Enqueue a worker function to re-fetch this status entirely async.
2024-04-26 14:50:46 +02:00
d . state . Workers . Dereference . Queue . Push ( func ( ctx context . Context ) {
2023-11-04 21:21:20 +01:00
latest , statusable , _ , err := d . enrichStatusSafely ( ctx ,
requestUser ,
uri ,
status ,
statusable ,
)
2023-05-12 11:15:54 +02:00
if err != nil {
log . Errorf ( ctx , "error enriching remote status: %v" , err )
return
}
2023-11-04 21:21:20 +01:00
if statusable != nil {
if err := d . DereferenceStatusAncestors ( ctx , requestUser , latest ) ; err != nil {
log . Error ( ctx , err )
}
if err := d . DereferenceStatusDescendants ( ctx , requestUser , uri , statusable ) ; err != nil {
log . Error ( ctx , err )
}
2023-10-31 12:12:22 +01:00
}
2023-05-12 11:15:54 +02:00
} )
2021-08-10 13:32:39 +02:00
}
2024-09-10 14:33:32 +02:00
// enrichStatusSafely wraps enrichStatus() to perform it within
// a State{}.FedLocks mutexmap, which protects it within per-URI
// mutex locks. This also handles necessary delete of now-deleted
// statuses, and updating fetched_at on returned HTTP errors.
2023-10-31 12:12:22 +01:00
func ( d * Dereferencer ) enrichStatusSafely (
ctx context . Context ,
requestUser string ,
uri * url . URL ,
status * gtsmodel . Status ,
2024-06-06 10:50:14 +02:00
statusable ap . Statusable ,
2023-11-04 21:21:20 +01:00
) ( * gtsmodel . Status , ap . Statusable , bool , error ) {
2023-10-31 12:12:22 +01:00
uriStr := status . URI
2023-11-30 12:32:45 +01:00
var isNew bool
// Check if this is a new status (to us).
if isNew = ( status . ID == "" ) ; ! isNew {
2023-10-31 12:12:22 +01:00
// This is an existing status, first try to populate it. This
// is required by the checks below for existing tags, media etc.
if err := d . state . DB . PopulateStatus ( ctx , status ) ; err != nil {
log . Errorf ( ctx , "error populating existing status %s: %v" , uriStr , err )
}
}
// Acquire per-URI deref lock, wraping unlock
// to safely defer in case of panic, while still
// performing more granular unlocks when needed.
unlock := d . state . FedLocks . Lock ( uriStr )
2024-02-09 12:38:51 +01:00
unlock = util . DoOnce ( unlock )
2023-10-31 12:12:22 +01:00
defer unlock ( )
// Perform status enrichment with passed vars.
2024-09-10 14:33:32 +02:00
latest , statusable , err := d . enrichStatus ( ctx ,
2023-10-31 12:12:22 +01:00
requestUser ,
uri ,
status ,
2024-06-06 10:50:14 +02:00
statusable ,
2024-12-05 14:35:07 +01:00
isNew ,
2023-10-31 12:12:22 +01:00
)
2024-09-10 14:33:32 +02:00
// Check for a returned HTTP code via error.
switch code := gtserror . StatusCode ( err ) ; {
// Gone (410) definitely indicates deletion.
// Remove status if it was an existing one.
case code == http . StatusGone && ! isNew :
if err := d . state . DB . DeleteStatusByID ( ctx , status . ID ) ; err != nil {
log . Error ( ctx , "error deleting gone status %s: %v" , uriStr , err )
2024-01-26 14:17:10 +01:00
}
2024-09-10 14:33:32 +02:00
// Don't return any status.
return nil , nil , false , err
// Any other HTTP error mesg
// code, with existing status.
case code >= 400 && ! isNew :
2024-01-26 14:17:10 +01:00
// Update fetched_at to slow re-attempts
// but don't return early. We can still
// return the model we had stored already.
2023-10-31 12:12:22 +01:00
status . FetchedAt = time . Now ( )
2024-01-26 14:17:10 +01:00
if err := d . state . DB . UpdateStatus ( ctx , status , "fetched_at" ) ; err != nil {
2024-01-31 14:29:47 +01:00
log . Error ( ctx , "error updating %s fetched_at: %v" , uriStr , err )
2024-01-26 14:17:10 +01:00
}
2024-09-10 14:33:32 +02:00
// See below.
fallthrough
// In case of error with an existing
// status in the database, return error
// but still return existing status.
case err != nil && ! isNew :
latest = status
statusable = nil
2023-10-31 12:12:22 +01:00
}
// Unlock now
// we're done.
unlock ( )
if errors . Is ( err , db . ErrAlreadyExists ) {
2023-11-30 12:32:45 +01:00
// We leave 'isNew' set so that caller
// still dereferences parents, otherwise
// the version we pass back may not have
// these attached as inReplyTos yet (since
// those happen OUTSIDE federator lock).
//
// TODO: performance-wise, this won't be
// great. should improve this if we can!
2023-10-31 12:12:22 +01:00
// DATA RACE! We likely lost out to another goroutine
// in a call to db.Put(Status). Look again in DB by URI.
latest , err = d . state . DB . GetStatusByURI ( ctx , status . URI )
if err != nil {
err = gtserror . Newf ( "error getting status %s from database after race: %w" , uriStr , err )
}
}
2024-09-10 14:33:32 +02:00
return latest , statusable , isNew , err
2023-10-31 12:12:22 +01:00
}
2023-06-24 09:32:10 +02:00
// enrichStatus will enrich the given status, whether a new
// barebones model, or existing model from the database.
// It handles necessary dereferencing, database updates, etc.
2023-10-23 11:58:13 +02:00
func ( d * Dereferencer ) enrichStatus (
2023-06-24 09:32:10 +02:00
ctx context . Context ,
requestUser string ,
uri * url . URL ,
status * gtsmodel . Status ,
2024-09-16 14:08:42 +02:00
statusable ap . Statusable ,
2024-12-05 14:35:07 +01:00
isNew bool ,
2024-09-10 14:33:32 +02:00
) (
* gtsmodel . Status ,
ap . Statusable ,
error ,
) {
2023-05-12 11:15:54 +02:00
// Pre-fetch a transport for requesting username, used by later dereferencing.
tsport , err := d . transportController . NewTransportForUsername ( ctx , requestUser )
2021-08-10 13:32:39 +02:00
if err != nil {
2023-05-28 14:08:35 +02:00
return nil , nil , gtserror . Newf ( "couldn't create transport: %w" , err )
2021-08-10 13:32:39 +02:00
}
2023-05-12 11:15:54 +02:00
// Check whether this account URI is a blocked domain / subdomain.
if blocked , err := d . state . DB . IsDomainBlocked ( ctx , uri . Host ) ; err != nil {
2023-05-28 14:08:35 +02:00
return nil , nil , gtserror . Newf ( "error checking blocked domain: %w" , err )
2023-05-12 11:15:54 +02:00
} else if blocked {
2024-09-10 14:33:32 +02:00
err := gtserror . Newf ( "%s is blocked" , uri . Host )
2023-06-24 09:32:10 +02:00
return nil , nil , gtserror . SetUnretrievable ( err )
2021-08-10 13:32:39 +02:00
}
2024-09-16 14:08:42 +02:00
if statusable == nil {
2023-05-12 11:15:54 +02:00
// Dereference latest version of the status.
2024-02-23 16:24:40 +01:00
rsp , err := tsport . Dereference ( ctx , uri )
2021-08-10 13:32:39 +02:00
if err != nil {
2024-01-26 14:17:10 +01:00
err := gtserror . Newf ( "error dereferencing %s: %w" , uri , err )
2023-06-22 21:46:36 +02:00
return nil , nil , gtserror . SetUnretrievable ( err )
2021-08-10 13:32:39 +02:00
}
2024-02-23 16:24:40 +01:00
// Attempt to resolve ActivityPub status from response.
2024-09-16 14:08:42 +02:00
statusable , err = ap . ResolveStatusable ( ctx , rsp . Body )
2024-02-23 16:24:40 +01:00
// Tidy up now done.
_ = rsp . Body . Close ( )
2023-05-12 11:15:54 +02:00
if err != nil {
2024-02-23 16:24:40 +01:00
// ResolveStatusable will set gtserror.WrongType
// on the returned error, so we don't need to do it here.
2024-09-10 14:33:32 +02:00
err := gtserror . Newf ( "error resolving statusable %s: %w" , uri , err )
2024-02-23 16:24:40 +01:00
return nil , nil , err
}
// Check whether input URI and final returned URI
// have changed (i.e. we followed some redirects).
if finalURIStr := rsp . Request . URL . String ( ) ; //
finalURIStr != uri . String ( ) {
// NOTE: this URI check + database call is performed
// AFTER reading and closing response body, for performance.
//
// Check whether we have this status stored under *final* URI.
alreadyStatus , err := d . state . DB . GetStatusByURI ( ctx , finalURIStr )
if err != nil && ! errors . Is ( err , db . ErrNoEntries ) {
return nil , nil , gtserror . Newf ( "db error getting status after redirects: %w" , err )
}
if alreadyStatus != nil {
// We had this status stored
// under discovered final URI.
//
// Proceed with this status.
status = alreadyStatus
}
// Update the input URI to
// the final determined URI
// for later URI checks.
uri = rsp . Request . URL
2023-05-12 11:15:54 +02:00
}
}
2021-08-10 13:32:39 +02:00
2023-06-17 17:49:11 +02:00
// Get the attributed-to account in order to fetch profile.
2024-09-16 14:08:42 +02:00
attributedTo , err := ap . ExtractAttributedToURI ( statusable )
2023-05-12 11:15:54 +02:00
if err != nil {
2023-05-28 14:08:35 +02:00
return nil , nil , gtserror . New ( "attributedTo was empty" )
2022-09-12 13:03:23 +02:00
}
2021-08-10 13:32:39 +02:00
2023-10-31 12:12:22 +01:00
// Ensure we have the author account of the status dereferenced (+ up-to-date). If this is a new status
2024-02-14 12:13:38 +01:00
// (i.e. status.AccountID == "") then any error here is irrecoverable. status.AccountID must ALWAYS be set.
2023-10-31 12:12:22 +01:00
if _ , _ , err := d . getAccountByURI ( ctx , requestUser , attributedTo ) ; err != nil && status . AccountID == "" {
2024-09-10 14:33:32 +02:00
// Note that we specifically DO NOT wrap the error, instead collapsing it as string.
// Errors fetching an account do not necessarily relate to dereferencing the status.
return nil , nil , gtserror . Newf ( "failed to dereference status author %s: %v" , uri , err )
2021-08-29 12:03:08 +02:00
}
2024-09-10 14:33:32 +02:00
// ActivityPub model was recently dereferenced, so assume passed status
// may contain out-of-date information. Convert AP model to our GTS model.
2024-09-16 14:08:42 +02:00
latestStatus , err := d . converter . ASStatusToStatus ( ctx , statusable )
2023-06-22 21:46:36 +02:00
if err != nil {
return nil , nil , gtserror . Newf ( "error converting statusable to gts model for status %s: %w" , uri , err )
2021-08-29 12:03:08 +02:00
}
2024-02-14 12:13:38 +01:00
// Ensure final status isn't attempting
// to claim being authored by local user.
if latestStatus . Account . IsLocal ( ) {
return nil , nil , gtserror . Newf (
"dereferenced status %s claiming to be local" ,
latestStatus . URI ,
)
}
2024-07-26 13:11:07 +02:00
// Ensure the final parsed status URI or URL matches
2024-02-14 12:13:38 +01:00
// the input URI we fetched (or received) it as.
2024-12-05 14:35:07 +01:00
matches , err := util . URIMatches ( uri ,
2024-07-26 13:11:07 +02:00
append (
2024-09-16 14:08:42 +02:00
ap . GetURL ( statusable ) , // status URL(s)
ap . GetJSONLDId ( statusable ) , // status URI
2024-07-26 13:11:07 +02:00
) ... ,
)
if err != nil {
return nil , nil , gtserror . Newf (
"error checking dereferenced status uri %s: %w" ,
latestStatus . URI , err ,
)
}
if ! matches {
2024-02-14 12:13:38 +01:00
return nil , nil , gtserror . Newf (
"dereferenced status uri %s does not match %s" ,
2024-07-26 13:11:07 +02:00
latestStatus . URI , uri . String ( ) ,
2024-02-14 12:13:38 +01:00
)
}
2024-12-05 14:35:07 +01:00
if isNew {
2024-01-31 14:29:47 +01:00
// Generate new status ID from the provided creation date.
2024-12-05 14:35:07 +01:00
latestStatus . ID = id . NewULIDFromTime ( latestStatus . CreatedAt )
2024-01-26 14:17:10 +01:00
} else {
2024-01-31 14:29:47 +01:00
2024-01-26 14:17:10 +01:00
// Reuse existing status ID.
latestStatus . ID = status . ID
2023-05-12 11:15:54 +02:00
}
2021-08-10 13:32:39 +02:00
2024-09-29 14:46:52 +02:00
// Set latest fetch time and carry-
// over some values from "old" status.
2023-05-12 11:15:54 +02:00
latestStatus . FetchedAt = time . Now ( )
latestStatus . Local = status . Local
2024-09-29 14:46:52 +02:00
latestStatus . PinnedAt = status . PinnedAt
2023-05-12 11:15:54 +02:00
2024-08-25 12:18:39 +02:00
// Carry-over approvals. Remote instances might not yet
// serve statuses with the `approved_by` field, but we
// might have marked a status as pre-approved on our side
// based on the author's inclusion in a followers/following
2024-10-08 10:51:13 +02:00
// collection, or by providing pre-approval URI on the bare
// status passed to RefreshStatus. By carrying over previously
// set values we can avoid marking such statuses as "pending".
2024-08-25 12:18:39 +02:00
//
// If a remote has in the meantime retracted its approval,
// the next call to 'isPermittedStatus' will catch that.
2024-08-25 15:44:08 +02:00
if latestStatus . ApprovedByURI == "" && status . ApprovedByURI != "" {
latestStatus . ApprovedByURI = status . ApprovedByURI
}
2024-08-25 12:18:39 +02:00
2024-03-04 13:30:12 +01:00
// Check if this is a permitted status we should accept.
2024-12-05 14:35:07 +01:00
// Function also sets "PendingApproval" bool as necessary,
// and handles removal of existing statuses no longer permitted.
permit , err := d . isPermittedStatus ( ctx , requestUser , status , latestStatus , isNew )
2024-03-04 13:30:12 +01:00
if err != nil {
return nil , nil , gtserror . Newf ( "error checking permissibility for status %s: %w" , uri , err )
}
if ! permit {
// Return a checkable error type that can be ignored.
err := gtserror . Newf ( "dropping unpermitted status: %s" , uri )
return nil , nil , gtserror . SetNotPermitted ( err )
2023-11-08 15:32:17 +01:00
}
2024-12-05 14:35:07 +01:00
// Insert / update any attached status poll.
pollChanged , err := d . handleStatusPoll ( ctx ,
status ,
latestStatus ,
)
if err != nil {
return nil , nil , gtserror . Newf ( "error handling poll for status %s: %w" , uri , err )
2023-05-12 11:15:54 +02:00
}
2024-12-05 14:35:07 +01:00
// Populate mentions associated with status, passing
// in existing status to reuse old where possible.
// (especially important here to reduce need to dereference).
mentionsChanged , err := d . fetchStatusMentions ( ctx ,
requestUser ,
status ,
latestStatus ,
)
if err != nil {
return nil , nil , gtserror . Newf ( "error populating mentions for status %s: %w" , uri , err )
2024-03-04 13:30:12 +01:00
}
2024-12-05 14:35:07 +01:00
// Ensure status in a thread is connected.
threadChanged , err := d . threadStatus ( ctx ,
status ,
latestStatus ,
)
if err != nil {
return nil , nil , gtserror . Newf ( "error handling threading for status %s: %w" , uri , err )
2023-10-25 16:04:53 +02:00
}
2024-12-05 14:35:07 +01:00
// Populate tags associated with status, passing
// in existing status to reuse old where possible.
tagsChanged , err := d . fetchStatusTags ( ctx ,
status ,
latestStatus ,
)
if err != nil {
2023-07-31 15:47:35 +02:00
return nil , nil , gtserror . Newf ( "error populating tags for status %s: %w" , uri , err )
}
2023-05-12 11:15:54 +02:00
2024-12-05 14:35:07 +01:00
// Populate media attachments associated with status,
// passing in existing status to reuse old where possible
// (especially important here to reduce need to dereference).
mediaChanged , err := d . fetchStatusAttachments ( ctx ,
requestUser ,
status ,
latestStatus ,
)
if err != nil {
2023-05-28 14:08:35 +02:00
return nil , nil , gtserror . Newf ( "error populating attachments for status %s: %w" , uri , err )
2023-05-12 11:15:54 +02:00
}
2024-12-05 14:35:07 +01:00
// Populate emoji associated with status, passing
// in existing status to reuse old where possible
// (especially important here to reduce need to dereference).
emojiChanged , err := d . fetchStatusEmojis ( ctx ,
status ,
latestStatus ,
)
if err != nil {
2023-05-28 14:08:35 +02:00
return nil , nil , gtserror . Newf ( "error populating emojis for status %s: %w" , uri , err )
2023-05-12 11:15:54 +02:00
}
2024-01-26 14:17:10 +01:00
if isNew {
2024-12-05 14:35:07 +01:00
// Simplest case, insert this new status into the database.
if err := d . state . DB . PutStatus ( ctx , latestStatus ) ; err != nil {
return nil , nil , gtserror . Newf ( "error inserting new status %s: %w" , uri , err )
2021-08-10 13:32:39 +02:00
}
2023-05-12 11:15:54 +02:00
} else {
2024-12-05 14:35:07 +01:00
// Check for and handle any edits to status, inserting
// historical edit if necessary. Also determines status
// columns that need updating in below query.
cols , err := d . handleStatusEdit ( ctx ,
status ,
latestStatus ,
pollChanged ,
mentionsChanged ,
threadChanged ,
tagsChanged ,
mediaChanged ,
emojiChanged ,
)
if err != nil {
return nil , nil , gtserror . Newf ( "error handling edit for status %s: %w" , uri , err )
}
// With returned changed columns, now update the existing status entry.
if err := d . state . DB . UpdateStatus ( ctx , latestStatus , cols ... ) ; err != nil {
return nil , nil , gtserror . Newf ( "error updating existing status %s: %w" , uri , err )
2023-05-12 11:15:54 +02:00
}
}
2024-09-16 14:08:42 +02:00
return latestStatus , statusable , nil
2023-05-12 11:15:54 +02:00
}
2021-08-10 13:32:39 +02:00
2024-12-05 14:35:07 +01:00
// fetchStatusMentions populates the mentions on 'status', creating
// new where needed, or using unchanged mentions from 'existing' status.
2024-06-26 17:01:16 +02:00
func ( d * Dereferencer ) fetchStatusMentions (
2023-10-31 12:05:17 +01:00
ctx context . Context ,
requestUser string ,
2024-06-26 17:01:16 +02:00
existing * gtsmodel . Status ,
status * gtsmodel . Status ,
2024-12-05 14:35:07 +01:00
) (
changed bool ,
err error ,
) {
2023-10-31 12:05:17 +01:00
// Allocate new slice to take the yet-to-be created mention IDs.
status . MentionIDs = make ( [ ] string , len ( status . Mentions ) )
for i := range status . Mentions {
var (
mention = status . Mentions [ i ]
alreadyExists bool
)
2024-09-10 14:33:32 +02:00
// Search existing status for a mention already stored,
// else ensure new mention's target account is populated.
mention , alreadyExists , err = d . getPopulatedMention ( ctx ,
2023-10-31 12:05:17 +01:00
requestUser ,
existing ,
2024-06-26 17:01:16 +02:00
mention ,
2023-10-31 12:05:17 +01:00
)
if err != nil {
log . Errorf ( ctx , "failed to derive mention: %v" , err )
continue
}
if alreadyExists {
// This mention was already attached
// to the status, use it and continue.
status . Mentions [ i ] = mention
status . MentionIDs [ i ] = mention . ID
2023-05-12 11:15:54 +02:00
continue
2021-08-20 12:26:56 +02:00
}
2024-12-05 14:35:07 +01:00
// Mark status as
// having changed.
changed = true
2023-10-31 12:05:17 +01:00
// This mention didn't exist yet.
2024-12-05 14:35:07 +01:00
// Generate new ID according to latest update.
mention . ID = id . NewULIDFromTime ( status . UpdatedAt )
2021-08-10 13:32:39 +02:00
2023-05-12 11:15:54 +02:00
// Set known further mention details.
2024-12-05 14:35:07 +01:00
mention . CreatedAt = status . UpdatedAt
2023-05-12 11:15:54 +02:00
mention . OriginAccount = status . Account
mention . OriginAccountID = status . AccountID
mention . OriginAccountURI = status . AccountURI
mention . TargetAccountID = mention . TargetAccount . ID
mention . TargetAccountURI = mention . TargetAccount . URI
mention . TargetAccountURL = mention . TargetAccount . URL
mention . StatusID = status . ID
mention . Status = status
// Place the new mention into the database.
if err := d . state . DB . PutMention ( ctx , mention ) ; err != nil {
2024-12-05 14:35:07 +01:00
return changed , gtserror . Newf ( "error putting mention in database: %w" , err )
2021-08-10 13:32:39 +02:00
}
2021-08-29 12:03:08 +02:00
2023-05-12 11:15:54 +02:00
// Set the *new* mention and ID.
status . Mentions [ i ] = mention
status . MentionIDs [ i ] = mention . ID
2021-08-10 13:32:39 +02:00
}
2021-08-29 12:03:08 +02:00
2023-06-22 21:46:36 +02:00
for i := 0 ; i < len ( status . MentionIDs ) ; {
2023-05-12 11:15:54 +02:00
if status . MentionIDs [ i ] == "" {
// This is a failed mention population, likely due
// to invalid incoming data / now-deleted accounts.
copy ( status . Mentions [ i : ] , status . Mentions [ i + 1 : ] )
copy ( status . MentionIDs [ i : ] , status . MentionIDs [ i + 1 : ] )
status . Mentions = status . Mentions [ : len ( status . Mentions ) - 1 ]
status . MentionIDs = status . MentionIDs [ : len ( status . MentionIDs ) - 1 ]
2023-06-22 21:46:36 +02:00
continue
2023-05-12 11:15:54 +02:00
}
2023-06-22 21:46:36 +02:00
i ++
2023-05-12 11:15:54 +02:00
}
2021-08-10 13:32:39 +02:00
2024-12-05 14:35:07 +01:00
return changed , nil
2021-08-29 12:03:08 +02:00
}
2024-12-05 14:35:07 +01:00
// threadStatus ensures that given status is threaded correctly
// where necessary. that is it will inherit a thread ID from the
// existing copy if it is threaded correctly, else it will inherit
// a thread ID from a parent with existing thread, else it will
// generate a new thread ID if status mentions a local account.
func ( d * Dereferencer ) threadStatus (
ctx context . Context ,
existing * gtsmodel . Status ,
status * gtsmodel . Status ,
) (
changed bool ,
err error ,
) {
// Check for existing status
// that is already threaded.
if existing . ThreadID != "" {
// Existing is threaded correctly.
if existing . InReplyTo == nil ||
existing . InReplyTo . ThreadID == existing . ThreadID {
status . ThreadID = existing . ThreadID
return false , nil
2023-10-25 16:04:53 +02:00
}
2024-12-05 14:35:07 +01:00
// TODO: delete incorrect thread
}
// Check for existing parent to inherit threading from.
if inReplyTo := status . InReplyTo ; inReplyTo != nil &&
inReplyTo . ThreadID != "" {
status . ThreadID = inReplyTo . ThreadID
return true , nil
2023-10-25 16:04:53 +02:00
}
// Parent wasn't threaded. If this
// status mentions a local account,
// we should thread it so that local
// account can mute it if they want.
mentionsLocal := slices . ContainsFunc (
status . Mentions ,
func ( m * gtsmodel . Mention ) bool {
// If TargetAccount couldn't
// be deref'd, we know it's not
// a local account, so only
// check for non-nil accounts.
return m . TargetAccount != nil &&
m . TargetAccount . IsLocal ( )
} ,
)
if ! mentionsLocal {
// Status doesn't mention a
// local account, so we don't
// need to thread it.
2024-12-05 14:35:07 +01:00
return false , nil
2023-10-25 16:04:53 +02:00
}
// Status mentions a local account.
// Create a new thread and assign
// it to the status.
threadID := id . NewULID ( )
2024-12-05 14:35:07 +01:00
// Insert new thread model into db.
if err := d . state . DB . PutThread ( ctx ,
& gtsmodel . Thread { ID : threadID } ,
2023-10-25 16:04:53 +02:00
) ; err != nil {
2024-12-05 14:35:07 +01:00
return false , gtserror . Newf ( "error inserting new thread in db: %w" , err )
2023-10-25 16:04:53 +02:00
}
2024-12-05 14:35:07 +01:00
// Set thread on latest status.
2023-10-25 16:04:53 +02:00
status . ThreadID = threadID
2024-12-05 14:35:07 +01:00
return true , nil
2023-10-25 16:04:53 +02:00
}
2024-12-05 14:35:07 +01:00
// fetchStatusTags populates the tags on 'status', fetching existing
// from the database and creating new where needed. 'existing' is used
// to fetch tags that have not changed since previous stored status.
2024-06-26 17:01:16 +02:00
func ( d * Dereferencer ) fetchStatusTags (
ctx context . Context ,
existing * gtsmodel . Status ,
status * gtsmodel . Status ,
2024-12-05 14:35:07 +01:00
) (
changed bool ,
err error ,
) {
2023-07-31 15:47:35 +02:00
// Allocate new slice to take the yet-to-be determined tag IDs.
status . TagIDs = make ( [ ] string , len ( status . Tags ) )
for i := range status . Tags {
2023-10-31 12:12:22 +01:00
tag := status . Tags [ i ]
// Look for tag in existing status with name.
existing , ok := existing . GetTagByName ( tag . Name )
if ok && existing . ID != "" {
status . Tags [ i ] = existing
status . TagIDs [ i ] = existing . ID
continue
}
2023-07-31 15:47:35 +02:00
2024-12-05 14:35:07 +01:00
// Mark status as
// having changed.
changed = true
2023-10-31 12:12:22 +01:00
// Look for existing tag with name in the database.
existing , err := d . state . DB . GetTagByName ( ctx , tag . Name )
2023-07-31 15:47:35 +02:00
if err != nil && ! errors . Is ( err , db . ErrNoEntries ) {
2024-12-05 14:35:07 +01:00
return changed , gtserror . Newf ( "db error getting tag %s: %w" , tag . Name , err )
2023-10-31 12:12:22 +01:00
} else if existing != nil {
status . Tags [ i ] = existing
status . TagIDs [ i ] = existing . ID
2023-07-31 15:47:35 +02:00
continue
}
2023-10-31 12:12:22 +01:00
// Create new ID for tag.
tag . ID = id . NewULID ( )
2023-07-31 15:47:35 +02:00
2023-10-31 12:12:22 +01:00
// Insert this tag with new name into the database.
if err := d . state . DB . PutTag ( ctx , tag ) ; err != nil {
log . Errorf ( ctx , "db error putting tag %s: %v" , tag . Name , err )
continue
2023-07-31 15:47:35 +02:00
}
2023-10-31 12:12:22 +01:00
// Set new tag ID in slice.
2023-07-31 15:47:35 +02:00
status . TagIDs [ i ] = tag . ID
}
// Remove any tag we couldn't get or create.
for i := 0 ; i < len ( status . TagIDs ) ; {
if status . TagIDs [ i ] == "" {
// This is a failed tag population, likely due
// to some database peculiarity / race condition.
copy ( status . Tags [ i : ] , status . Tags [ i + 1 : ] )
copy ( status . TagIDs [ i : ] , status . TagIDs [ i + 1 : ] )
status . Tags = status . Tags [ : len ( status . Tags ) - 1 ]
status . TagIDs = status . TagIDs [ : len ( status . TagIDs ) - 1 ]
continue
}
i ++
}
2024-12-05 14:35:07 +01:00
return changed , nil
2023-11-08 15:32:17 +01:00
}
2024-12-05 14:35:07 +01:00
// fetchStatusAttachments populates the attachments on 'status', creating new database
// entries where needed and dereferencing it, or using unchanged from 'existing' status.
2024-06-26 17:01:16 +02:00
func ( d * Dereferencer ) fetchStatusAttachments (
ctx context . Context ,
requestUser string ,
existing * gtsmodel . Status ,
status * gtsmodel . Status ,
2024-12-05 14:35:07 +01:00
) (
changed bool ,
err error ,
) {
2023-05-12 11:15:54 +02:00
// Allocate new slice to take the yet-to-be fetched attachment IDs.
status . AttachmentIDs = make ( [ ] string , len ( status . Attachments ) )
for i := range status . Attachments {
2024-06-26 17:01:16 +02:00
placeholder := status . Attachments [ i ]
2023-05-12 11:15:54 +02:00
2023-11-10 19:29:26 +01:00
// Look for existing media attachment with remote URL first.
2024-06-26 17:01:16 +02:00
existing , ok := existing . GetAttachmentByRemoteURL ( placeholder . RemoteURL )
2024-06-06 16:35:50 +02:00
if ok && existing . ID != "" {
2024-12-05 14:35:07 +01:00
var info media . AdditionalMediaInfo
2024-06-06 16:35:50 +02:00
2024-12-05 14:35:07 +01:00
// Look for any difference in stored media description.
diff := ( existing . Description != placeholder . Description )
if diff {
info . Description = & placeholder . Description
}
// If description changed,
// we mark media as changed.
changed = changed || diff
// Store any attachment updates and
// ensure media is locally cached.
existing , err := d . RefreshMedia ( ctx ,
requestUser ,
existing ,
info ,
diff ,
)
2024-06-06 16:35:50 +02:00
if err != nil {
log . Errorf ( ctx , "error updating existing attachment: %v" , err )
// specifically do NOT continue here,
// we already have a model, we don't
// want to drop it from the status, just
// log that an update for it failed.
}
// Set the existing attachment.
2023-05-12 11:15:54 +02:00
status . Attachments [ i ] = existing
status . AttachmentIDs [ i ] = existing . ID
continue
}
2024-12-05 14:35:07 +01:00
// Mark status as
// having changed.
changed = true
2024-06-06 16:35:50 +02:00
// Load this new media attachment.
2024-12-05 14:35:07 +01:00
attachment , err := d . GetMedia ( ctx ,
2024-06-26 17:01:16 +02:00
requestUser ,
2024-06-06 16:35:50 +02:00
status . AccountID ,
2024-06-26 17:01:16 +02:00
placeholder . RemoteURL ,
media . AdditionalMediaInfo {
2024-06-06 16:35:50 +02:00
StatusID : & status . ID ,
2024-06-26 17:01:16 +02:00
RemoteURL : & placeholder . RemoteURL ,
Description : & placeholder . Description ,
Blurhash : & placeholder . Blurhash ,
2024-06-06 16:35:50 +02:00
} ,
)
2022-01-08 17:17:01 +01:00
if err != nil {
2024-06-26 17:01:16 +02:00
if attachment == nil {
log . Errorf ( ctx , "error loading attachment %s: %v" , placeholder . RemoteURL , err )
continue
}
// non-fatal error occurred during loading, still use it.
2023-11-10 19:29:26 +01:00
log . Warnf ( ctx , "partially loaded attachment: %v" , err )
2021-08-29 12:03:08 +02:00
}
2023-05-12 11:15:54 +02:00
// Set the *new* attachment and ID.
2023-10-31 12:12:22 +01:00
status . Attachments [ i ] = attachment
status . AttachmentIDs [ i ] = attachment . ID
2021-08-29 12:03:08 +02:00
}
2023-06-22 21:46:36 +02:00
for i := 0 ; i < len ( status . AttachmentIDs ) ; {
2023-05-12 11:15:54 +02:00
if status . AttachmentIDs [ i ] == "" {
2023-11-10 19:29:26 +01:00
// Remove totally failed attachment populations
2023-05-12 11:15:54 +02:00
copy ( status . Attachments [ i : ] , status . Attachments [ i + 1 : ] )
copy ( status . AttachmentIDs [ i : ] , status . AttachmentIDs [ i + 1 : ] )
status . Attachments = status . Attachments [ : len ( status . Attachments ) - 1 ]
status . AttachmentIDs = status . AttachmentIDs [ : len ( status . AttachmentIDs ) - 1 ]
2023-06-22 21:46:36 +02:00
continue
2023-05-12 11:15:54 +02:00
}
2023-06-22 21:46:36 +02:00
i ++
2023-05-12 11:15:54 +02:00
}
2021-08-29 12:03:08 +02:00
2024-12-05 14:35:07 +01:00
return changed , nil
2021-08-29 12:03:08 +02:00
}
2024-12-05 14:35:07 +01:00
// fetchStatusEmojis populates the emojis on 'status', creating new database entries
// where needed and dereferencing it, or using unchanged from 'existing' status.
2024-06-26 17:01:16 +02:00
func ( d * Dereferencer ) fetchStatusEmojis (
ctx context . Context ,
existing * gtsmodel . Status ,
status * gtsmodel . Status ,
2024-12-05 14:35:07 +01:00
) (
changed bool ,
err error ,
) {
2024-06-26 17:01:16 +02:00
// Fetch the updated emojis for our status.
emojis , changed , err := d . fetchEmojis ( ctx ,
existing . Emojis ,
status . Emojis ,
)
2022-09-26 11:56:01 +02:00
if err != nil {
2024-12-05 14:35:07 +01:00
return changed , gtserror . Newf ( "error fetching emojis: %w" , err )
2022-09-26 11:56:01 +02:00
}
2022-09-12 13:03:23 +02:00
2024-06-26 17:01:16 +02:00
if ! changed {
// Use existing status emoji objects.
status . EmojiIDs = existing . EmojiIDs
status . Emojis = existing . Emojis
2024-12-05 14:35:07 +01:00
return false , nil
2022-09-12 13:03:23 +02:00
}
2024-06-26 17:01:16 +02:00
// Set latest emojis.
2022-09-26 11:56:01 +02:00
status . Emojis = emojis
2024-06-26 17:01:16 +02:00
// Iterate over and set changed emoji IDs.
status . EmojiIDs = make ( [ ] string , len ( emojis ) )
for i , emoji := range emojis {
status . EmojiIDs [ i ] = emoji . ID
}
2021-08-29 12:03:08 +02:00
2024-12-05 14:35:07 +01:00
return true , nil
}
// handleStatusPoll handles both inserting of new status poll or the
// update of an existing poll. this handles the case of simple vote
// count updates (without being classified as a change of the poll
// itself), as well as full poll changes that delete existing instance.
func ( d * Dereferencer ) handleStatusPoll (
ctx context . Context ,
existing * gtsmodel . Status ,
status * gtsmodel . Status ,
) (
changed bool ,
err error ,
) {
switch {
case existing . Poll == nil && status . Poll == nil :
// no poll before or after, nothing to do.
return false , nil
case existing . Poll == nil && status . Poll != nil :
// no previous poll, insert new status poll!
return true , d . insertStatusPoll ( ctx , status )
case status . Poll == nil :
// existing status poll has been deleted, remove this from the database.
if err = d . state . DB . DeletePollByID ( ctx , existing . Poll . ID ) ; err != nil {
err = gtserror . Newf ( "error deleting poll from database: %w" , err )
}
return true , err
case pollChanged ( existing . Poll , status . Poll ) :
// existing status poll has been changed, remove this from the database.
if err = d . state . DB . DeletePollByID ( ctx , existing . Poll . ID ) ; err != nil {
return true , gtserror . Newf ( "error deleting poll from database: %w" , err )
}
// insert latest poll version into database.
return true , d . insertStatusPoll ( ctx , status )
case pollStateUpdated ( existing . Poll , status . Poll ) :
// Since we last saw it, the poll has updated!
// Whether that be stats, or close time.
poll := existing . Poll
poll . Closing = pollJustClosed ( existing . Poll , status . Poll )
poll . ClosedAt = status . Poll . ClosedAt
poll . Voters = status . Poll . Voters
poll . Votes = status . Poll . Votes
// Update poll model in the database (specifically only the possible changed columns).
if err = d . state . DB . UpdatePoll ( ctx , poll , "closed_at" , "voters" , "votes" ) ; err != nil {
return false , gtserror . Newf ( "error updating poll: %w" , err )
}
// Update poll on status.
status . PollID = poll . ID
status . Poll = poll
return false , nil
default :
// latest and existing
// polls are up to date.
poll := existing . Poll
status . PollID = poll . ID
status . Poll = poll
return false , nil
}
}
// insertStatusPoll inserts an assumed new poll attached to status into the database, this
// also handles generating new ID for the poll and setting necessary fields on the status.
func ( d * Dereferencer ) insertStatusPoll ( ctx context . Context , status * gtsmodel . Status ) error {
var err error
// Generate new ID for poll from latest updated time.
status . Poll . ID = id . NewULIDFromTime ( status . UpdatedAt )
// Update the status<->poll links.
status . PollID = status . Poll . ID
status . Poll . StatusID = status . ID
status . Poll . Status = status
// Insert this latest poll into the database.
err = d . state . DB . PutPoll ( ctx , status . Poll )
if err != nil {
return gtserror . Newf ( "error putting poll in database: %w" , err )
}
2021-08-10 13:32:39 +02:00
return nil
}
2024-06-26 17:01:16 +02:00
2024-12-05 14:35:07 +01:00
// handleStatusEdit compiles a list of changed status table columns between
// existing and latest status model, and where necessary inserts a historic
// edit of the status into the database to store its previous state. the
// returned slice is a list of columns requiring updating in the database.
func ( d * Dereferencer ) handleStatusEdit (
ctx context . Context ,
existing * gtsmodel . Status ,
status * gtsmodel . Status ,
pollChanged bool ,
mentionsChanged bool ,
threadChanged bool ,
tagsChanged bool ,
mediaChanged bool ,
emojiChanged bool ,
) (
cols [ ] string ,
err error ,
) {
var edited bool
// Preallocate max slice length.
cols = make ( [ ] string , 0 , 13 )
// Always update `fetched_at`.
cols = append ( cols , "fetched_at" )
// Check for edited status content.
if existing . Content != status . Content {
cols = append ( cols , "content" )
edited = true
}
// Check for edited status content warning.
if existing . ContentWarning != status . ContentWarning {
cols = append ( cols , "content_warning" )
edited = true
}
// Check for edited status sensitive flag.
if * existing . Sensitive != * status . Sensitive {
cols = append ( cols , "sensitive" )
edited = true
}
// Check for edited status language tag.
if existing . Language != status . Language {
cols = append ( cols , "language" )
edited = true
}
if pollChanged {
// Attached poll was changed.
cols = append ( cols , "poll_id" )
edited = true
}
if mentionsChanged {
cols = append ( cols , "mentions" ) // i.e. MentionIDs
// Mentions changed doesn't necessarily
// indicate an edit, it may just not have
// been previously populated properly.
}
if threadChanged {
cols = append ( cols , "thread_id" )
// Thread changed doesn't necessarily
// indicate an edit, it may just now
// actually be included in a thread.
}
if tagsChanged {
cols = append ( cols , "tags" ) // i.e. TagIDs
// Tags changed doesn't necessarily
// indicate an edit, it may just not have
// been previously populated properly.
}
if mediaChanged {
// Attached media was changed.
cols = append ( cols , "attachments" ) // i.e. AttachmentIDs
edited = true
}
if emojiChanged {
// Attached emojis changed.
cols = append ( cols , "emojis" ) // i.e. EmojiIDs
// Emojis changed doesn't necessarily
// indicate an edit, it may just not have
// been previously populated properly.
}
if edited {
// We prefer to use provided 'upated_at', but ensure
// it fits chronologically with creation / last update.
if ! status . UpdatedAt . After ( status . CreatedAt ) ||
! status . UpdatedAt . After ( existing . UpdatedAt ) {
// Else fallback to now as update time.
status . UpdatedAt = status . FetchedAt
}
// Status has been editted since last
// we saw it, take snapshot of existing.
var edit gtsmodel . StatusEdit
edit . ID = id . NewULIDFromTime ( status . UpdatedAt )
edit . Content = existing . Content
edit . ContentWarning = existing . ContentWarning
edit . Text = existing . Text
edit . Language = existing . Language
edit . Sensitive = existing . Sensitive
edit . StatusID = status . ID
// Copy existing attachments and descriptions.
edit . AttachmentIDs = existing . AttachmentIDs
edit . Attachments = existing . Attachments
if l := len ( existing . Attachments ) ; l > 0 {
edit . AttachmentDescriptions = make ( [ ] string , l )
for i , attach := range existing . Attachments {
edit . AttachmentDescriptions [ i ] = attach . Description
}
}
// Edit creation is last update time.
edit . CreatedAt = existing . UpdatedAt
if existing . Poll != nil {
// Poll only set if existing contained them.
edit . PollOptions = existing . Poll . Options
if ! * existing . Poll . HideCounts || pollChanged {
// If the counts are allowed to be
// shown, or poll has changed, then
// include poll vote counts in edit.
edit . PollVotes = existing . Poll . Votes
}
}
// Insert this new edit of existing status into database.
if err := d . state . DB . PutStatusEdit ( ctx , & edit ) ; err != nil {
return nil , gtserror . Newf ( "error putting edit in database: %w" , err )
}
// Add edit to list of edits on the status.
status . EditIDs = append ( status . EditIDs , edit . ID )
status . Edits = append ( status . Edits , & edit )
// Add updated_at and edits to list of cols.
cols = append ( cols , "updated_at" , "edits" )
}
return cols , nil
}
2024-09-10 14:33:32 +02:00
// getPopulatedMention tries to populate the given
2024-06-26 17:01:16 +02:00
// mention with the correct TargetAccount and (if not
// yet set) TargetAccountURI, returning the populated
// mention.
//
// Will check on the existing status if the mention
// is already there and populated; if so, existing
// mention will be returned along with `true`.
//
// Otherwise, this function will try to parse first
// the Href of the mention, and then the namestring,
// to see who it targets, and go fetch that account.
2024-09-10 14:33:32 +02:00
func ( d * Dereferencer ) getPopulatedMention (
2024-06-26 17:01:16 +02:00
ctx context . Context ,
requestUser string ,
existing * gtsmodel . Status ,
mention * gtsmodel . Mention ,
) (
* gtsmodel . Mention ,
bool , // True if mention already exists in the DB.
error ,
) {
// Mentions can be created using Name or Href.
// Prefer Href (TargetAccountURI), fall back to Name.
if mention . TargetAccountURI != "" {
2024-09-10 14:33:32 +02:00
// Look for existing mention with target account's URI, if so use this.
2024-06-26 17:01:16 +02:00
existingMention , ok := existing . GetMentionByTargetURI ( mention . TargetAccountURI )
if ok && existingMention . ID != "" {
return existingMention , true , nil
}
// Ensure that mention account URI is parseable.
accountURI , err := url . Parse ( mention . TargetAccountURI )
if err != nil {
2024-09-10 14:33:32 +02:00
err := gtserror . Newf ( "invalid account uri %q: %w" , mention . TargetAccountURI , err )
2024-06-26 17:01:16 +02:00
return nil , false , err
}
2024-09-10 14:33:32 +02:00
// Ensure we have account of the mention target dereferenced.
mention . TargetAccount , _ , err = d . getAccountByURI ( ctx ,
requestUser ,
accountURI ,
)
2024-06-26 17:01:16 +02:00
if err != nil {
2024-09-10 14:33:32 +02:00
err := gtserror . Newf ( "failed to dereference account %s: %w" , accountURI , err )
2024-06-26 17:01:16 +02:00
return nil , false , err
}
} else {
2024-09-10 14:33:32 +02:00
// Href wasn't set, extract the username and domain parts from namestring.
2024-06-26 17:01:16 +02:00
username , domain , err := util . ExtractNamestringParts ( mention . NameString )
if err != nil {
2024-09-10 14:33:32 +02:00
err := gtserror . Newf ( "failed to parse namestring %s: %w" , mention . NameString , err )
2024-06-26 17:01:16 +02:00
return nil , false , err
}
2024-09-10 14:33:32 +02:00
// Look for existing mention with username domain target, if so use this.
existingMention , ok := existing . GetMentionByUsernameDomain ( username , domain )
if ok && existingMention . ID != "" {
return existingMention , true , nil
}
// Ensure we have the account of the mention target dereferenced.
mention . TargetAccount , _ , err = d . getAccountByUsernameDomain ( ctx ,
requestUser ,
username ,
domain ,
)
2024-06-26 17:01:16 +02:00
if err != nil {
2024-09-10 14:33:32 +02:00
err := gtserror . Newf ( "failed to dereference account %s: %w" , mention . NameString , err )
2024-06-26 17:01:16 +02:00
return nil , false , err
}
2024-09-10 14:33:32 +02:00
// Look for existing mention with target account's URI, if so use this.
existingMention , ok = existing . GetMentionByTargetURI ( mention . TargetAccountURI )
2024-06-26 17:01:16 +02:00
if ok && existingMention . ID != "" {
return existingMention , true , nil
}
}
// At this point, mention.TargetAccountURI
// and mention.TargetAccount must be set.
return mention , false , nil
}