summaryrefslogtreecommitdiff
path: root/internal/federation/dereferencing/account.go
diff options
context:
space:
mode:
authorLibravatar tobi <31960611+tsmethurst@users.noreply.github.com>2023-03-01 18:52:44 +0100
committerLibravatar GitHub <noreply@github.com>2023-03-01 17:52:44 +0000
commit24cec4e7aab33b6c44ba6d1ecf16895f254351b8 (patch)
treecf0107a34e0fa00ab1b68aed4b52afe502147393 /internal/federation/dereferencing/account.go
parent[chore/performance] simplify storage driver to use storage.Storage directly (... (diff)
downloadgotosocial-24cec4e7aab33b6c44ba6d1ecf16895f254351b8.tar.xz
[feature] Federate pinned posts (aka `featuredCollection`) in and out (#1560)
* start fiddling * the ol' fiddle + update * start working on fetching statuses * poopy doopy doo where r u uwu * further adventures in featuring statuses * finishing up * fmt * simply status unpin loop * move empty featured check back to caller function * remove unnecessary log.WithContext calls * remove unnecessary IsIRI() checks * add explanatory comment about status URIs * change log level to error * better test names
Diffstat (limited to 'internal/federation/dereferencing/account.go')
-rw-r--r--internal/federation/dereferencing/account.go160
1 files changed, 158 insertions, 2 deletions
diff --git a/internal/federation/dereferencing/account.go b/internal/federation/dereferencing/account.go
index 93e0e3549..041f34a2c 100644
--- a/internal/federation/dereferencing/account.go
+++ b/internal/federation/dereferencing/account.go
@@ -281,8 +281,7 @@ func (d *deref) enrichAccount(ctx context.Context, requestUser string, uri *url.
}
// Fetch the latest remote account emoji IDs used in account display name/bio.
- _, err = d.fetchRemoteAccountEmojis(ctx, latestAcc, requestUser)
- if err != nil {
+ if _, err = d.fetchRemoteAccountEmojis(ctx, latestAcc, requestUser); err != nil {
log.Errorf(ctx, "error fetching remote emojis for account %s: %v", uri, err)
}
@@ -312,6 +311,18 @@ func (d *deref) enrichAccount(ctx context.Context, requestUser string, uri *url.
}
}
+ if latestAcc.FeaturedCollectionURI != "" {
+ // Fetch this account's pinned statuses, now that the account is in the database.
+ //
+ // The order is important here: if we tried to fetch the pinned statuses before
+ // storing the account, the process might end up calling enrichAccount again,
+ // causing us to get stuck in a loop. By calling it now, we make sure this doesn't
+ // happen!
+ if err := d.fetchRemoteAccountFeatured(ctx, requestUser, latestAcc.FeaturedCollectionURI, latestAcc.ID); err != nil {
+ log.Errorf(ctx, "error fetching featured collection for account %s: %v", uri, err)
+ }
+ }
+
return latestAcc, nil
}
@@ -569,3 +580,148 @@ func (d *deref) fetchRemoteAccountEmojis(ctx context.Context, targetAccount *gts
return changed, nil
}
+
+// fetchRemoteAccountFeatured dereferences an account's featuredCollectionURI (if not empty).
+// For each discovered status, this status will be dereferenced (if necessary) and marked as
+// pinned (if necessary). Then, old pins will be removed if they're not included in new pins.
+func (d *deref) fetchRemoteAccountFeatured(ctx context.Context, requestingUsername string, featuredCollectionURI string, accountID string) error {
+ uri, err := url.Parse(featuredCollectionURI)
+ if err != nil {
+ return err
+ }
+
+ tsport, err := d.transportController.NewTransportForUsername(ctx, requestingUsername)
+ if err != nil {
+ return err
+ }
+
+ b, err := tsport.Dereference(ctx, uri)
+ if err != nil {
+ return err
+ }
+
+ m := make(map[string]interface{})
+ if err := json.Unmarshal(b, &m); err != nil {
+ return fmt.Errorf("error unmarshalling bytes into json: %w", err)
+ }
+
+ t, err := streams.ToType(ctx, m)
+ if err != nil {
+ return fmt.Errorf("error resolving json into ap vocab type: %w", err)
+ }
+
+ if t.GetTypeName() != ap.ObjectOrderedCollection {
+ return fmt.Errorf("%s was not an OrderedCollection", featuredCollectionURI)
+ }
+
+ collection, ok := t.(vocab.ActivityStreamsOrderedCollection)
+ if !ok {
+ return errors.New("couldn't coerce OrderedCollection")
+ }
+
+ items := collection.GetActivityStreamsOrderedItems()
+ if items == nil {
+ return errors.New("nil orderedItems")
+ }
+
+ // Get previous pinned statuses (we'll need these later).
+ wasPinned, err := d.db.GetAccountPinnedStatuses(ctx, accountID)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ return fmt.Errorf("error getting account pinned statuses: %w", err)
+ }
+
+ statusURIs := make([]*url.URL, 0, items.Len())
+ for iter := items.Begin(); iter != items.End(); iter = iter.Next() {
+ var statusURI *url.URL
+
+ switch {
+ case iter.IsActivityStreamsNote():
+ // We got a whole Note. Extract the URI.
+ if note := iter.GetActivityStreamsNote(); note != nil {
+ if id := note.GetJSONLDId(); id != nil {
+ statusURI = id.GetIRI()
+ }
+ }
+ case iter.IsActivityStreamsArticle():
+ // We got a whole Article. Extract the URI.
+ if article := iter.GetActivityStreamsArticle(); article != nil {
+ if id := article.GetJSONLDId(); id != nil {
+ statusURI = id.GetIRI()
+ }
+ }
+ default:
+ // Try to get just the URI.
+ statusURI = iter.GetIRI()
+ }
+
+ if statusURI == nil {
+ continue
+ }
+
+ if statusURI.Host != uri.Host {
+ // If this status doesn't share a host with its featured
+ // collection URI, we shouldn't trust it. Just move on.
+ continue
+ }
+
+ // Already append this status URI to our slice.
+ // We do this here so that even if we can't get
+ // the status in the next part for some reason,
+ // we still know it was *meant* to be pinned.
+ statusURIs = append(statusURIs, statusURI)
+
+ status, _, err := d.GetStatus(ctx, requestingUsername, statusURI, false, false)
+ if err != nil {
+ // We couldn't get the status, bummer.
+ // Just log + move on, we can try later.
+ log.Errorf(ctx, "error getting status from featured collection %s: %s", featuredCollectionURI, err)
+ continue
+ }
+
+ // If the status was already pinned, we don't need to do anything.
+ if !status.PinnedAt.IsZero() {
+ continue
+ }
+
+ if status.AccountID != accountID {
+ // Someone's pinned a status that doesn't
+ // belong to them, this doesn't work for us.
+ continue
+ }
+
+ if status.BoostOfID != "" {
+ // Someone's pinned a boost. This also
+ // doesn't work for us.
+ continue
+ }
+
+ // All conditions are met for this status to
+ // be pinned, so we can finally update it.
+ status.PinnedAt = time.Now()
+ if err := d.db.UpdateStatus(ctx, status, "pinned_at"); err != nil {
+ log.Errorf(ctx, "error updating status in featured collection %s: %s", featuredCollectionURI, err)
+ }
+ }
+
+ // Now that we know which statuses are pinned, we should
+ // *unpin* previous pinned statuses that aren't included.
+outerLoop:
+ for _, status := range wasPinned {
+ for _, statusURI := range statusURIs {
+ if status.URI == statusURI.String() {
+ // This status is included in most recent
+ // pinned uris. No need to keep checking.
+ continue outerLoop
+ }
+ }
+
+ // Status was pinned before, but is not included
+ // in most recent pinned uris, so unpin it now.
+ status.PinnedAt = time.Time{}
+ if err := d.db.UpdateStatus(ctx, status, "pinned_at"); err != nil {
+ return fmt.Errorf("error unpinning status: %w", err)
+ }
+ }
+
+ return nil
+}