summaryrefslogtreecommitdiff
path: root/internal/processing
diff options
context:
space:
mode:
authorLibravatar tobi <31960611+tsmethurst@users.noreply.github.com>2024-04-16 13:10:13 +0200
committerLibravatar GitHub <noreply@github.com>2024-04-16 13:10:13 +0200
commit3cceed11b28b5f42a653d85ed779d652fd8c26ad (patch)
tree0a7f0994e477609ca705a45f382dfb62056b196e /internal/processing
parent[performance] cached oauth database types (#2838) (diff)
downloadgotosocial-3cceed11b28b5f42a653d85ed779d652fd8c26ad.tar.xz
[feature/performance] Store account stats in separate table (#2831)
* [feature/performance] Store account stats in separate table, get stats from remote * test account stats * add some missing increment / decrement calls * change stats function signatures * rejig logging a bit * use lock when updating stats
Diffstat (limited to 'internal/processing')
-rw-r--r--internal/processing/account/delete.go5
-rw-r--r--internal/processing/account/move.go2
-rw-r--r--internal/processing/account/rss.go14
-rw-r--r--internal/processing/account/rss_test.go14
-rw-r--r--internal/processing/admin/accountapprove.go2
-rw-r--r--internal/processing/admin/accountreject.go2
-rw-r--r--internal/processing/fedi/collections.go26
-rw-r--r--internal/processing/status/pin.go56
-rw-r--r--internal/processing/workers/fromclientapi.go54
-rw-r--r--internal/processing/workers/fromclientapi_test.go66
-rw-r--r--internal/processing/workers/fromfediapi.go64
-rw-r--r--internal/processing/workers/fromfediapi_test.go74
-rw-r--r--internal/processing/workers/util.go255
13 files changed, 532 insertions, 102 deletions
diff --git a/internal/processing/account/delete.go b/internal/processing/account/delete.go
index 858e42d36..a900c566d 100644
--- a/internal/processing/account/delete.go
+++ b/internal/processing/account/delete.go
@@ -485,6 +485,11 @@ func (p *Processor) deleteAccountPeripheral(ctx context.Context, account *gtsmod
return gtserror.Newf("error deleting poll votes by account: %w", err)
}
+ // Delete account stats model.
+ if err := p.state.DB.DeleteAccountStats(ctx, account.ID); err != nil {
+ return gtserror.Newf("error deleting stats for account: %w", err)
+ }
+
return nil
}
diff --git a/internal/processing/account/move.go b/internal/processing/account/move.go
index a68c8f750..602e8c021 100644
--- a/internal/processing/account/move.go
+++ b/internal/processing/account/move.go
@@ -113,7 +113,7 @@ func (p *Processor) MoveSelf(
// in quick succession, so get a lock on
// this account.
lockKey := originAcct.URI
- unlock := p.state.ClientLocks.Lock(lockKey)
+ unlock := p.state.AccountLocks.Lock(lockKey)
defer unlock()
// Ensure we have a valid, up-to-date representation of the target account.
diff --git a/internal/processing/account/rss.go b/internal/processing/account/rss.go
index f2c6cba5e..cbbb4875b 100644
--- a/internal/processing/account/rss.go
+++ b/internal/processing/account/rss.go
@@ -69,14 +69,18 @@ func (p *Processor) GetRSSFeedForUsername(ctx context.Context, username string)
return nil, never, gtserror.NewErrorNotFound(err)
}
+ // Ensure account stats populated.
+ if account.Stats == nil {
+ if err := p.state.DB.PopulateAccountStats(ctx, account); err != nil {
+ err = gtserror.Newf("db error getting account stats %s: %w", username, err)
+ return nil, never, gtserror.NewErrorInternalError(err)
+ }
+ }
+
// LastModified time is needed by callers to check freshness for cacheing.
// This might be a zero time.Time if account has never posted a status that's
// eligible to appear in the RSS feed; that's fine.
- lastPostAt, err := p.state.DB.GetAccountLastPosted(ctx, account.ID, true)
- if err != nil && !errors.Is(err, db.ErrNoEntries) {
- err = gtserror.Newf("db error getting account %s last posted: %w", username, err)
- return nil, never, gtserror.NewErrorInternalError(err)
- }
+ lastPostAt := account.Stats.LastStatusAt
return func() (string, gtserror.WithCode) {
// Assemble author namestring once only.
diff --git a/internal/processing/account/rss_test.go b/internal/processing/account/rss_test.go
index 6ae285f9e..c08ef8874 100644
--- a/internal/processing/account/rss_test.go
+++ b/internal/processing/account/rss_test.go
@@ -19,7 +19,6 @@ package account_test
import (
"context"
- "fmt"
"testing"
"github.com/stretchr/testify/suite"
@@ -32,14 +31,11 @@ type GetRSSTestSuite struct {
func (suite *GetRSSTestSuite) TestGetAccountRSSAdmin() {
getFeed, lastModified, err := suite.accountProcessor.GetRSSFeedForUsername(context.Background(), "admin")
suite.NoError(err)
- suite.EqualValues(1634733405, lastModified.Unix())
+ suite.EqualValues(1634726497, lastModified.Unix())
feed, err := getFeed()
suite.NoError(err)
-
- fmt.Println(feed)
-
- suite.Equal("<?xml version=\"1.0\" encoding=\"UTF-8\"?><rss version=\"2.0\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">\n <channel>\n <title>Posts from @admin@localhost:8080</title>\n <link>http://localhost:8080/@admin</link>\n <description>Posts from @admin@localhost:8080</description>\n <pubDate>Wed, 20 Oct 2021 12:36:45 +0000</pubDate>\n <lastBuildDate>Wed, 20 Oct 2021 12:36:45 +0000</lastBuildDate>\n <item>\n <title>open to see some puppies</title>\n <link>http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37</link>\n <description>@admin@localhost:8080 made a new post: &#34;🐕🐕🐕🐕🐕&#34;</description>\n <content:encoded><![CDATA[🐕🐕🐕🐕🐕]]></content:encoded>\n <author>@admin@localhost:8080</author>\n <guid>http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37</guid>\n <pubDate>Wed, 20 Oct 2021 12:36:45 +0000</pubDate>\n <source>http://localhost:8080/@admin/feed.rss</source>\n </item>\n <item>\n <title>hello world! #welcome ! first post on the instance :rainbow: !</title>\n <link>http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R</link>\n <description>@admin@localhost:8080 posted 1 attachment: &#34;hello world! #welcome ! first post on the instance :rainbow: !&#34;</description>\n <content:encoded><![CDATA[hello world! #welcome ! first post on the instance <img src=\"http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png\" title=\":rainbow:\" alt=\":rainbow:\" width=\"25\" height=\"25\"/> !]]></content:encoded>\n <author>@admin@localhost:8080</author>\n <enclosure url=\"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg\" length=\"62529\" type=\"image/jpeg\"></enclosure>\n <guid>http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R</guid>\n <pubDate>Wed, 20 Oct 2021 11:36:45 +0000</pubDate>\n <source>http://localhost:8080/@admin/feed.rss</source>\n </item>\n </channel>\n</rss>", feed)
+ suite.Equal("<?xml version=\"1.0\" encoding=\"UTF-8\"?><rss version=\"2.0\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">\n <channel>\n <title>Posts from @admin@localhost:8080</title>\n <link>http://localhost:8080/@admin</link>\n <description>Posts from @admin@localhost:8080</description>\n <pubDate>Wed, 20 Oct 2021 10:41:37 +0000</pubDate>\n <lastBuildDate>Wed, 20 Oct 2021 10:41:37 +0000</lastBuildDate>\n <item>\n <title>open to see some puppies</title>\n <link>http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37</link>\n <description>@admin@localhost:8080 made a new post: &#34;🐕🐕🐕🐕🐕&#34;</description>\n <content:encoded><![CDATA[🐕🐕🐕🐕🐕]]></content:encoded>\n <author>@admin@localhost:8080</author>\n <guid>http://localhost:8080/@admin/statuses/01F8MHAAY43M6RJ473VQFCVH37</guid>\n <pubDate>Wed, 20 Oct 2021 12:36:45 +0000</pubDate>\n <source>http://localhost:8080/@admin/feed.rss</source>\n </item>\n <item>\n <title>hello world! #welcome ! first post on the instance :rainbow: !</title>\n <link>http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R</link>\n <description>@admin@localhost:8080 posted 1 attachment: &#34;hello world! #welcome ! first post on the instance :rainbow: !&#34;</description>\n <content:encoded><![CDATA[hello world! #welcome ! first post on the instance <img src=\"http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png\" title=\":rainbow:\" alt=\":rainbow:\" width=\"25\" height=\"25\"/> !]]></content:encoded>\n <author>@admin@localhost:8080</author>\n <enclosure url=\"http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg\" length=\"62529\" type=\"image/jpeg\"></enclosure>\n <guid>http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R</guid>\n <pubDate>Wed, 20 Oct 2021 11:36:45 +0000</pubDate>\n <source>http://localhost:8080/@admin/feed.rss</source>\n </item>\n </channel>\n</rss>", feed)
}
func (suite *GetRSSTestSuite) TestGetAccountRSSZork() {
@@ -49,9 +45,6 @@ func (suite *GetRSSTestSuite) TestGetAccountRSSZork() {
feed, err := getFeed()
suite.NoError(err)
-
- fmt.Println(feed)
-
suite.Equal("<?xml version=\"1.0\" encoding=\"UTF-8\"?><rss version=\"2.0\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">\n <channel>\n <title>Posts from @the_mighty_zork@localhost:8080</title>\n <link>http://localhost:8080/@the_mighty_zork</link>\n <description>Posts from @the_mighty_zork@localhost:8080</description>\n <pubDate>Sun, 10 Dec 2023 09:24:00 +0000</pubDate>\n <lastBuildDate>Sun, 10 Dec 2023 09:24:00 +0000</lastBuildDate>\n <image>\n <url>http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg</url>\n <title>Avatar for @the_mighty_zork@localhost:8080</title>\n <link>http://localhost:8080/@the_mighty_zork</link>\n </image>\n <item>\n <title>HTML in post</title>\n <link>http://localhost:8080/@the_mighty_zork/statuses/01HH9KYNQPA416TNJ53NSATP40</link>\n <description>@the_mighty_zork@localhost:8080 made a new post: &#34;Here&#39;s a bunch of HTML, read it and weep, weep then!&#xA;&#xA;```html&#xA;&lt;section class=&#34;about-user&#34;&gt;&#xA; &lt;div class=&#34;col-header&#34;&gt;&#xA; &lt;h2&gt;About&lt;/h2&gt;&#xA; &lt;/div&gt; &#xA; &lt;div class=&#34;fields&#34;&gt;&#xA; &lt;h3 class=&#34;sr-only&#34;&gt;Fields&lt;/h3&gt;&#xA; &lt;dl&gt;&#xA;...</description>\n <content:encoded><![CDATA[<p>Here's a bunch of HTML, read it and weep, weep then!</p><pre><code class=\"language-html\">&lt;section class=&#34;about-user&#34;&gt;\n &lt;div class=&#34;col-header&#34;&gt;\n &lt;h2&gt;About&lt;/h2&gt;\n &lt;/div&gt; \n &lt;div class=&#34;fields&#34;&gt;\n &lt;h3 class=&#34;sr-only&#34;&gt;Fields&lt;/h3&gt;\n &lt;dl&gt;\n &lt;div class=&#34;field&#34;&gt;\n &lt;dt&gt;should you follow me?&lt;/dt&gt;\n &lt;dd&gt;maybe!&lt;/dd&gt;\n &lt;/div&gt;\n &lt;div class=&#34;field&#34;&gt;\n &lt;dt&gt;age&lt;/dt&gt;\n &lt;dd&gt;120&lt;/dd&gt;\n &lt;/div&gt;\n &lt;/dl&gt;\n &lt;/div&gt;\n &lt;div class=&#34;bio&#34;&gt;\n &lt;h3 class=&#34;sr-only&#34;&gt;Bio&lt;/h3&gt;\n &lt;p&gt;i post about things that concern me&lt;/p&gt;\n &lt;/div&gt;\n &lt;div class=&#34;sr-only&#34; role=&#34;group&#34;&gt;\n &lt;h3 class=&#34;sr-only&#34;&gt;Stats&lt;/h3&gt;\n &lt;span&gt;Joined in Jun, 2022.&lt;/span&gt;\n &lt;span&gt;8 posts.&lt;/span&gt;\n &lt;span&gt;Followed by 1.&lt;/span&gt;\n &lt;span&gt;Following 1.&lt;/span&gt;\n &lt;/div&gt;\n &lt;div class=&#34;accountstats&#34; aria-hidden=&#34;true&#34;&gt;\n &lt;b&gt;Joined&lt;/b&gt;&lt;time datetime=&#34;2022-06-04T13:12:00.000Z&#34;&gt;Jun, 2022&lt;/time&gt;\n &lt;b&gt;Posts&lt;/b&gt;&lt;span&gt;8&lt;/span&gt;\n &lt;b&gt;Followed by&lt;/b&gt;&lt;span&gt;1&lt;/span&gt;\n &lt;b&gt;Following&lt;/b&gt;&lt;span&gt;1&lt;/span&gt;\n &lt;/div&gt;\n&lt;/section&gt;\n</code></pre><p>There, hope you liked that!</p>]]></content:encoded>\n <author>@the_mighty_zork@localhost:8080</author>\n <guid>http://localhost:8080/@the_mighty_zork/statuses/01HH9KYNQPA416TNJ53NSATP40</guid>\n <pubDate>Sun, 10 Dec 2023 09:24:00 +0000</pubDate>\n <source>http://localhost:8080/@the_mighty_zork/feed.rss</source>\n </item>\n <item>\n <title>introduction post</title>\n <link>http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY</link>\n <description>@the_mighty_zork@localhost:8080 made a new post: &#34;hello everyone!&#34;</description>\n <content:encoded><![CDATA[hello everyone!]]></content:encoded>\n <author>@the_mighty_zork@localhost:8080</author>\n <guid>http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY</guid>\n <pubDate>Wed, 20 Oct 2021 10:40:37 +0000</pubDate>\n <source>http://localhost:8080/@the_mighty_zork/feed.rss</source>\n </item>\n </channel>\n</rss>", feed)
}
@@ -77,9 +70,6 @@ func (suite *GetRSSTestSuite) TestGetAccountRSSZorkNoPosts() {
feed, err := getFeed()
suite.NoError(err)
-
- fmt.Println(feed)
-
suite.Equal("<?xml version=\"1.0\" encoding=\"UTF-8\"?><rss version=\"2.0\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">\n <channel>\n <title>Posts from @the_mighty_zork@localhost:8080</title>\n <link>http://localhost:8080/@the_mighty_zork</link>\n <description>Posts from @the_mighty_zork@localhost:8080</description>\n <pubDate>Fri, 20 May 2022 11:09:18 +0000</pubDate>\n <lastBuildDate>Fri, 20 May 2022 11:09:18 +0000</lastBuildDate>\n <image>\n <url>http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg</url>\n <title>Avatar for @the_mighty_zork@localhost:8080</title>\n <link>http://localhost:8080/@the_mighty_zork</link>\n </image>\n </channel>\n</rss>", feed)
}
diff --git a/internal/processing/admin/accountapprove.go b/internal/processing/admin/accountapprove.go
index e34cb18e3..c8a49e089 100644
--- a/internal/processing/admin/accountapprove.go
+++ b/internal/processing/admin/accountapprove.go
@@ -49,7 +49,7 @@ func (p *Processor) AccountApprove(
// Get a lock on the account URI,
// to ensure it's not also being
// rejected at the same time!
- unlock := p.state.ClientLocks.Lock(user.Account.URI)
+ unlock := p.state.AccountLocks.Lock(user.Account.URI)
defer unlock()
if !*user.Approved {
diff --git a/internal/processing/admin/accountreject.go b/internal/processing/admin/accountreject.go
index bc7a1c20a..eee2b2ff5 100644
--- a/internal/processing/admin/accountreject.go
+++ b/internal/processing/admin/accountreject.go
@@ -52,7 +52,7 @@ func (p *Processor) AccountReject(
// Get a lock on the account URI,
// since we're going to be deleting
// it and its associated user.
- unlock := p.state.ClientLocks.Lock(user.Account.URI)
+ unlock := p.state.AccountLocks.Lock(user.Account.URI)
defer unlock()
// Can't reject an account with a
diff --git a/internal/processing/fedi/collections.go b/internal/processing/fedi/collections.go
index 0eacf45da..7a6c99adb 100644
--- a/internal/processing/fedi/collections.go
+++ b/internal/processing/fedi/collections.go
@@ -126,11 +126,12 @@ func (p *Processor) FollowersGet(ctx context.Context, requestedUser string, page
return nil, gtserror.NewErrorInternalError(err)
}
- // Calculate total number of followers available for account.
- total, err := p.state.DB.CountAccountFollowers(ctx, receiver.ID)
- if err != nil {
- err := gtserror.Newf("error counting followers: %w", err)
- return nil, gtserror.NewErrorInternalError(err)
+ // Ensure we have stats for this account.
+ if receiver.Stats == nil {
+ if err := p.state.DB.PopulateAccountStats(ctx, receiver); err != nil {
+ err := gtserror.Newf("error getting stats for account %s: %w", receiver.ID, err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
}
var obj vocab.Type
@@ -138,7 +139,7 @@ func (p *Processor) FollowersGet(ctx context.Context, requestedUser string, page
// Start the AS collection params.
var params ap.CollectionParams
params.ID = collectionID
- params.Total = total
+ params.Total = *receiver.Stats.FollowersCount
switch {
@@ -235,11 +236,12 @@ func (p *Processor) FollowingGet(ctx context.Context, requestedUser string, page
return nil, gtserror.NewErrorInternalError(err)
}
- // Calculate total number of following available for account.
- total, err := p.state.DB.CountAccountFollows(ctx, receiver.ID)
- if err != nil {
- err := gtserror.Newf("error counting follows: %w", err)
- return nil, gtserror.NewErrorInternalError(err)
+ // Ensure we have stats for this account.
+ if receiver.Stats == nil {
+ if err := p.state.DB.PopulateAccountStats(ctx, receiver); err != nil {
+ err := gtserror.Newf("error getting stats for account %s: %w", receiver.ID, err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
}
var obj vocab.Type
@@ -247,7 +249,7 @@ func (p *Processor) FollowingGet(ctx context.Context, requestedUser string, page
// Start AS collection params.
var params ap.CollectionParams
params.ID = collectionID
- params.Total = total
+ params.Total = *receiver.Stats.FollowingCount
switch {
case receiver.IsInstance() ||
diff --git a/internal/processing/status/pin.go b/internal/processing/status/pin.go
index 9a4a4b266..d0688331b 100644
--- a/internal/processing/status/pin.go
+++ b/internal/processing/status/pin.go
@@ -82,18 +82,26 @@ func (p *Processor) PinCreate(ctx context.Context, requestingAccount *gtsmodel.A
return nil, errWithCode
}
+ // Get a lock on this account.
+ unlock := p.state.AccountLocks.Lock(requestingAccount.URI)
+ defer unlock()
+
if !targetStatus.PinnedAt.IsZero() {
err := errors.New("status already pinned")
return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
}
- pinnedCount, err := p.state.DB.CountAccountPinned(ctx, requestingAccount.ID)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking number of pinned statuses: %w", err))
+ // Ensure account stats populated.
+ if requestingAccount.Stats == nil {
+ if err := p.state.DB.PopulateAccountStats(ctx, requestingAccount); err != nil {
+ err = gtserror.Newf("db error getting account stats: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
}
+ pinnedCount := *requestingAccount.Stats.StatusesPinnedCount
if pinnedCount >= allowedPinnedCount {
- err = fmt.Errorf("status pin limit exceeded, you've already pinned %d status(es) out of %d", pinnedCount, allowedPinnedCount)
+ err := fmt.Errorf("status pin limit exceeded, you've already pinned %d status(es) out of %d", pinnedCount, allowedPinnedCount)
return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
}
@@ -103,6 +111,17 @@ func (p *Processor) PinCreate(ctx context.Context, requestingAccount *gtsmodel.A
return nil, gtserror.NewErrorInternalError(err)
}
+ // Update account stats.
+ *requestingAccount.Stats.StatusesPinnedCount++
+ if err := p.state.DB.UpdateAccountStats(
+ ctx,
+ requestingAccount.Stats,
+ "statuses_pinned_count",
+ ); err != nil {
+ err = gtserror.Newf("db error updating stats: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
if err := p.c.InvalidateTimelinedStatus(ctx, requestingAccount.ID, targetStatusID); err != nil {
err = gtserror.Newf("error invalidating status from timelines: %w", err)
return nil, gtserror.NewErrorInternalError(err)
@@ -128,16 +147,45 @@ func (p *Processor) PinRemove(ctx context.Context, requestingAccount *gtsmodel.A
return nil, errWithCode
}
+ // Get a lock on this account.
+ unlock := p.state.AccountLocks.Lock(requestingAccount.URI)
+ defer unlock()
+
if targetStatus.PinnedAt.IsZero() {
+ // Status already not pinned.
return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
}
+ // Ensure account stats populated.
+ if requestingAccount.Stats == nil {
+ if err := p.state.DB.PopulateAccountStats(ctx, requestingAccount); err != nil {
+ err = gtserror.Newf("db error getting account stats: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ }
+
targetStatus.PinnedAt = time.Time{}
if err := p.state.DB.UpdateStatus(ctx, targetStatus, "pinned_at"); err != nil {
err = gtserror.Newf("db error unpinning status: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
+ // Update account stats.
+ //
+ // Clamp to 0 to avoid funny business.
+ *requestingAccount.Stats.StatusesPinnedCount--
+ if *requestingAccount.Stats.StatusesPinnedCount < 0 {
+ *requestingAccount.Stats.StatusesPinnedCount = 0
+ }
+ if err := p.state.DB.UpdateAccountStats(
+ ctx,
+ requestingAccount.Stats,
+ "statuses_pinned_count",
+ ); err != nil {
+ err = gtserror.Newf("db error updating stats: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
if err := p.c.InvalidateTimelinedStatus(ctx, requestingAccount.ID, targetStatusID); err != nil {
err = gtserror.Newf("error invalidating status from timelines: %w", err)
return nil, gtserror.NewErrorInternalError(err)
diff --git a/internal/processing/workers/fromclientapi.go b/internal/processing/workers/fromclientapi.go
index 37c330cf0..1412ea003 100644
--- a/internal/processing/workers/fromclientapi.go
+++ b/internal/processing/workers/fromclientapi.go
@@ -247,6 +247,11 @@ func (p *clientAPI) CreateStatus(ctx context.Context, cMsg messages.FromClientAP
return gtserror.Newf("%T not parseable as *gtsmodel.Status", cMsg.GTSModel)
}
+ // Update stats for the actor account.
+ if err := p.utilF.incrementStatusesCount(ctx, cMsg.OriginAccount, status); err != nil {
+ log.Errorf(ctx, "error updating account stats: %v", err)
+ }
+
if err := p.surface.timelineAndNotifyStatus(ctx, status); err != nil {
log.Errorf(ctx, "error timelining and notifying status: %v", err)
}
@@ -311,6 +316,11 @@ func (p *clientAPI) CreateFollowReq(ctx context.Context, cMsg messages.FromClien
return gtserror.Newf("%T not parseable as *gtsmodel.FollowRequest", cMsg.GTSModel)
}
+ // Update stats for the target account.
+ if err := p.utilF.incrementFollowRequestsCount(ctx, cMsg.TargetAccount); err != nil {
+ log.Errorf(ctx, "error updating account stats: %v", err)
+ }
+
if err := p.surface.notifyFollowRequest(ctx, followRequest); err != nil {
log.Errorf(ctx, "error notifying follow request: %v", err)
}
@@ -360,6 +370,11 @@ func (p *clientAPI) CreateAnnounce(ctx context.Context, cMsg messages.FromClient
return gtserror.Newf("%T not parseable as *gtsmodel.Status", cMsg.GTSModel)
}
+ // Update stats for the actor account.
+ if err := p.utilF.incrementStatusesCount(ctx, cMsg.OriginAccount, boost); err != nil {
+ log.Errorf(ctx, "error updating account stats: %v", err)
+ }
+
// Timeline and notify the boost wrapper status.
if err := p.surface.timelineAndNotifyStatus(ctx, boost); err != nil {
log.Errorf(ctx, "error timelining and notifying status: %v", err)
@@ -485,6 +500,20 @@ func (p *clientAPI) AcceptFollow(ctx context.Context, cMsg messages.FromClientAP
return gtserror.Newf("%T not parseable as *gtsmodel.Follow", cMsg.GTSModel)
}
+ // Update stats for the target account.
+ if err := p.utilF.decrementFollowRequestsCount(ctx, cMsg.TargetAccount); err != nil {
+ log.Errorf(ctx, "error updating account stats: %v", err)
+ }
+
+ if err := p.utilF.incrementFollowersCount(ctx, cMsg.TargetAccount); err != nil {
+ log.Errorf(ctx, "error updating account stats: %v", err)
+ }
+
+ // Update stats for the origin account.
+ if err := p.utilF.incrementFollowingCount(ctx, cMsg.OriginAccount); err != nil {
+ log.Errorf(ctx, "error updating account stats: %v", err)
+ }
+
if err := p.surface.notifyFollow(ctx, follow); err != nil {
log.Errorf(ctx, "error notifying follow: %v", err)
}
@@ -502,6 +531,11 @@ func (p *clientAPI) RejectFollowRequest(ctx context.Context, cMsg messages.FromC
return gtserror.Newf("%T not parseable as *gtsmodel.FollowRequest", cMsg.GTSModel)
}
+ // Update stats for the target account.
+ if err := p.utilF.decrementFollowRequestsCount(ctx, cMsg.TargetAccount); err != nil {
+ log.Errorf(ctx, "error updating account stats: %v", err)
+ }
+
if err := p.federate.RejectFollow(
ctx,
p.converter.FollowRequestToFollow(ctx, followReq),
@@ -518,6 +552,16 @@ func (p *clientAPI) UndoFollow(ctx context.Context, cMsg messages.FromClientAPI)
return gtserror.Newf("%T not parseable as *gtsmodel.Follow", cMsg.GTSModel)
}
+ // Update stats for the origin account.
+ if err := p.utilF.decrementFollowingCount(ctx, cMsg.OriginAccount); err != nil {
+ log.Errorf(ctx, "error updating account stats: %v", err)
+ }
+
+ // Update stats for the target account.
+ if err := p.utilF.decrementFollowersCount(ctx, cMsg.TargetAccount); err != nil {
+ log.Errorf(ctx, "error updating account stats: %v", err)
+ }
+
if err := p.federate.UndoFollow(ctx, follow); err != nil {
log.Errorf(ctx, "error federating follow undo: %v", err)
}
@@ -565,6 +609,11 @@ func (p *clientAPI) UndoAnnounce(ctx context.Context, cMsg messages.FromClientAP
return gtserror.Newf("db error deleting status: %w", err)
}
+ // Update stats for the origin account.
+ if err := p.utilF.decrementStatusesCount(ctx, cMsg.OriginAccount); err != nil {
+ log.Errorf(ctx, "error updating account stats: %v", err)
+ }
+
if err := p.surface.deleteStatusFromTimelines(ctx, status.ID); err != nil {
log.Errorf(ctx, "error removing timelined status: %v", err)
}
@@ -603,6 +652,11 @@ func (p *clientAPI) DeleteStatus(ctx context.Context, cMsg messages.FromClientAP
log.Errorf(ctx, "error wiping status: %v", err)
}
+ // Update stats for the origin account.
+ if err := p.utilF.decrementStatusesCount(ctx, cMsg.OriginAccount); err != nil {
+ log.Errorf(ctx, "error updating account stats: %v", err)
+ }
+
if status.InReplyToID != "" {
// Interaction counts changed on the replied status;
// uncache the prepared version from all timelines.
diff --git a/internal/processing/workers/fromclientapi_test.go b/internal/processing/workers/fromclientapi_test.go
index 3d3630b11..5e294597d 100644
--- a/internal/processing/workers/fromclientapi_test.go
+++ b/internal/processing/workers/fromclientapi_test.go
@@ -182,11 +182,6 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithNotification() {
nil,
nil,
)
- statusJSON = suite.statusJSON(
- ctx,
- status,
- receivingAccount,
- )
)
// Update the follow from receiving account -> posting account so
@@ -212,6 +207,12 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithNotification() {
suite.FailNow(err.Error())
}
+ statusJSON := suite.statusJSON(
+ ctx,
+ status,
+ receivingAccount,
+ )
+
// Check message in home stream.
suite.checkStreamed(
homeStream,
@@ -285,11 +286,6 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusReply() {
suite.testStatuses["local_account_2_status_1"],
nil,
)
- statusJSON = suite.statusJSON(
- ctx,
- status,
- receivingAccount,
- )
)
// Process the new status.
@@ -305,6 +301,12 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusReply() {
suite.FailNow(err.Error())
}
+ statusJSON := suite.statusJSON(
+ ctx,
+ status,
+ receivingAccount,
+ )
+
// Check message in home stream.
suite.checkStreamed(
homeStream,
@@ -451,11 +453,6 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyLis
suite.testStatuses["local_account_2_status_1"],
nil,
)
- statusJSON = suite.statusJSON(
- ctx,
- status,
- receivingAccount,
- )
)
// Modify replies policy of test list to show replies
@@ -480,6 +477,12 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyLis
suite.FailNow(err.Error())
}
+ statusJSON := suite.statusJSON(
+ ctx,
+ status,
+ receivingAccount,
+ )
+
// Check message in home stream.
suite.checkStreamed(
homeStream,
@@ -518,11 +521,6 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyLis
suite.testStatuses["local_account_2_status_1"],
nil,
)
- statusJSON = suite.statusJSON(
- ctx,
- status,
- receivingAccount,
- )
)
// Modify replies policy of test list to show replies
@@ -552,6 +550,12 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyLis
suite.FailNow(err.Error())
}
+ statusJSON := suite.statusJSON(
+ ctx,
+ status,
+ receivingAccount,
+ )
+
// Check message in home stream.
suite.checkStreamed(
homeStream,
@@ -590,11 +594,6 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusReplyListRepliesPoli
suite.testStatuses["local_account_2_status_1"],
nil,
)
- statusJSON = suite.statusJSON(
- ctx,
- status,
- receivingAccount,
- )
)
// Modify replies policy of test list.
@@ -619,6 +618,12 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusReplyListRepliesPoli
suite.FailNow(err.Error())
}
+ statusJSON := suite.statusJSON(
+ ctx,
+ status,
+ receivingAccount,
+ )
+
// Check message in home stream.
suite.checkStreamed(
homeStream,
@@ -654,11 +659,6 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoost() {
nil,
suite.testStatuses["local_account_2_status_1"],
)
- statusJSON = suite.statusJSON(
- ctx,
- status,
- receivingAccount,
- )
)
// Process the new status.
@@ -674,6 +674,12 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoost() {
suite.FailNow(err.Error())
}
+ statusJSON := suite.statusJSON(
+ ctx,
+ status,
+ receivingAccount,
+ )
+
// Check message in home stream.
suite.checkStreamed(
homeStream,
diff --git a/internal/processing/workers/fromfediapi.go b/internal/processing/workers/fromfediapi.go
index 7b0e72490..0b1106a9e 100644
--- a/internal/processing/workers/fromfediapi.go
+++ b/internal/processing/workers/fromfediapi.go
@@ -122,7 +122,7 @@ func (p *Processor) ProcessFromFediAPI(ctx context.Context, fMsg messages.FromFe
// UPDATE SOMETHING
case ap.ActivityUpdate:
- switch fMsg.APObjectType { //nolint:gocritic
+ switch fMsg.APObjectType {
// UPDATE NOTE/STATUS
case ap.ObjectNote:
@@ -133,6 +133,15 @@ func (p *Processor) ProcessFromFediAPI(ctx context.Context, fMsg messages.FromFe
return p.fediAPI.UpdateAccount(ctx, fMsg)
}
+ // ACCEPT SOMETHING
+ case ap.ActivityAccept:
+ switch fMsg.APObjectType { //nolint:gocritic
+
+ // ACCEPT FOLLOW
+ case ap.ActivityFollow:
+ return p.fediAPI.AcceptFollow(ctx, fMsg)
+ }
+
// DELETE SOMETHING
case ap.ActivityDelete:
switch fMsg.APObjectType {
@@ -220,6 +229,11 @@ func (p *fediAPI) CreateStatus(ctx context.Context, fMsg messages.FromFediAPI) e
return nil
}
+ // Update stats for the remote account.
+ if err := p.utilF.incrementStatusesCount(ctx, fMsg.RequestingAccount, status); err != nil {
+ log.Errorf(ctx, "error updating account stats: %v", err)
+ }
+
if status.InReplyToID != "" {
// Interaction counts changed on the replied status; uncache the
// prepared version from all timelines. The status dereferencer
@@ -290,14 +304,20 @@ func (p *fediAPI) CreateFollowReq(ctx context.Context, fMsg messages.FromFediAPI
}
if *followRequest.TargetAccount.Locked {
- // Account on our instance is locked: just notify the follow request.
+ // Local account is locked: just notify the follow request.
if err := p.surface.notifyFollowRequest(ctx, followRequest); err != nil {
log.Errorf(ctx, "error notifying follow request: %v", err)
}
+
+ // And update stats for the local account.
+ if err := p.utilF.incrementFollowRequestsCount(ctx, fMsg.ReceivingAccount); err != nil {
+ log.Errorf(ctx, "error updating account stats: %v", err)
+ }
+
return nil
}
- // Account on our instance is not locked:
+ // Local account is not locked:
// Automatically accept the follow request
// and notify about the new follower.
follow, err := p.state.DB.AcceptFollowRequest(
@@ -309,6 +329,16 @@ func (p *fediAPI) CreateFollowReq(ctx context.Context, fMsg messages.FromFediAPI
return gtserror.Newf("error accepting follow request: %w", err)
}
+ // Update stats for the local account.
+ if err := p.utilF.incrementFollowersCount(ctx, fMsg.ReceivingAccount); err != nil {
+ log.Errorf(ctx, "error updating account stats: %v", err)
+ }
+
+ // Update stats for the remote account.
+ if err := p.utilF.incrementFollowingCount(ctx, fMsg.RequestingAccount); err != nil {
+ log.Errorf(ctx, "error updating account stats: %v", err)
+ }
+
if err := p.federate.AcceptFollow(ctx, follow); err != nil {
log.Errorf(ctx, "error federating follow request accept: %v", err)
}
@@ -369,6 +399,11 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg messages.FromFediAPI)
return gtserror.Newf("error dereferencing announce: %w", err)
}
+ // Update stats for the remote account.
+ if err := p.utilF.incrementStatusesCount(ctx, fMsg.RequestingAccount, boost); err != nil {
+ log.Errorf(ctx, "error updating account stats: %v", err)
+ }
+
// Timeline and notify the announce.
if err := p.surface.timelineAndNotifyStatus(ctx, boost); err != nil {
log.Errorf(ctx, "error timelining and notifying status: %v", err)
@@ -509,6 +544,24 @@ func (p *fediAPI) UpdateAccount(ctx context.Context, fMsg messages.FromFediAPI)
return nil
}
+func (p *fediAPI) AcceptFollow(ctx context.Context, fMsg messages.FromFediAPI) error {
+ // Update stats for the remote account.
+ if err := p.utilF.decrementFollowRequestsCount(ctx, fMsg.RequestingAccount); err != nil {
+ log.Errorf(ctx, "error updating account stats: %v", err)
+ }
+
+ if err := p.utilF.incrementFollowersCount(ctx, fMsg.RequestingAccount); err != nil {
+ log.Errorf(ctx, "error updating account stats: %v", err)
+ }
+
+ // Update stats for the local account.
+ if err := p.utilF.incrementFollowingCount(ctx, fMsg.ReceivingAccount); err != nil {
+ log.Errorf(ctx, "error updating account stats: %v", err)
+ }
+
+ return nil
+}
+
func (p *fediAPI) UpdateStatus(ctx context.Context, fMsg messages.FromFediAPI) error {
// Cast the existing Status model attached to msg.
existing, ok := fMsg.GTSModel.(*gtsmodel.Status)
@@ -567,6 +620,11 @@ func (p *fediAPI) DeleteStatus(ctx context.Context, fMsg messages.FromFediAPI) e
log.Errorf(ctx, "error wiping status: %v", err)
}
+ // Update stats for the remote account.
+ if err := p.utilF.decrementStatusesCount(ctx, fMsg.RequestingAccount); err != nil {
+ log.Errorf(ctx, "error updating account stats: %v", err)
+ }
+
if status.InReplyToID != "" {
// Interaction counts changed on the replied status;
// uncache the prepared version from all timelines.
diff --git a/internal/processing/workers/fromfediapi_test.go b/internal/processing/workers/fromfediapi_test.go
index 51f61bd12..eb3d73e0c 100644
--- a/internal/processing/workers/fromfediapi_test.go
+++ b/internal/processing/workers/fromfediapi_test.go
@@ -55,10 +55,11 @@ func (suite *FromFediAPITestSuite) TestProcessFederationAnnounce() {
announceStatus.Visibility = boostedStatus.Visibility
err := suite.processor.Workers().ProcessFromFediAPI(context.Background(), messages.FromFediAPI{
- APObjectType: ap.ActivityAnnounce,
- APActivityType: ap.ActivityCreate,
- GTSModel: announceStatus,
- ReceivingAccount: suite.testAccounts["local_account_1"],
+ APObjectType: ap.ActivityAnnounce,
+ APActivityType: ap.ActivityCreate,
+ GTSModel: announceStatus,
+ ReceivingAccount: suite.testAccounts["local_account_1"],
+ RequestingAccount: boostingAccount,
})
suite.NoError(err)
@@ -115,10 +116,11 @@ func (suite *FromFediAPITestSuite) TestProcessReplyMention() {
// Send the replied status off to the fedi worker to be further processed.
err = suite.processor.Workers().ProcessFromFediAPI(context.Background(), messages.FromFediAPI{
- APObjectType: ap.ObjectNote,
- APActivityType: ap.ActivityCreate,
- APObjectModel: replyingStatusable,
- ReceivingAccount: suite.testAccounts["local_account_1"],
+ APObjectType: ap.ObjectNote,
+ APActivityType: ap.ActivityCreate,
+ APObjectModel: replyingStatusable,
+ ReceivingAccount: repliedAccount,
+ RequestingAccount: replyingAccount,
})
suite.NoError(err)
@@ -178,10 +180,11 @@ func (suite *FromFediAPITestSuite) TestProcessFave() {
suite.NoError(err)
err = suite.processor.Workers().ProcessFromFediAPI(context.Background(), messages.FromFediAPI{
- APObjectType: ap.ActivityLike,
- APActivityType: ap.ActivityCreate,
- GTSModel: fave,
- ReceivingAccount: favedAccount,
+ APObjectType: ap.ActivityLike,
+ APActivityType: ap.ActivityCreate,
+ GTSModel: fave,
+ ReceivingAccount: favedAccount,
+ RequestingAccount: favingAccount,
})
suite.NoError(err)
@@ -247,10 +250,11 @@ func (suite *FromFediAPITestSuite) TestProcessFaveWithDifferentReceivingAccount(
suite.NoError(err)
err = suite.processor.Workers().ProcessFromFediAPI(context.Background(), messages.FromFediAPI{
- APObjectType: ap.ActivityLike,
- APActivityType: ap.ActivityCreate,
- GTSModel: fave,
- ReceivingAccount: receivingAccount,
+ APObjectType: ap.ActivityLike,
+ APActivityType: ap.ActivityCreate,
+ GTSModel: fave,
+ ReceivingAccount: receivingAccount,
+ RequestingAccount: favingAccount,
})
suite.NoError(err)
@@ -318,10 +322,11 @@ func (suite *FromFediAPITestSuite) TestProcessAccountDelete() {
// now they are mufos!
err = suite.processor.Workers().ProcessFromFediAPI(ctx, messages.FromFediAPI{
- APObjectType: ap.ObjectProfile,
- APActivityType: ap.ActivityDelete,
- GTSModel: deletedAccount,
- ReceivingAccount: receivingAccount,
+ APObjectType: ap.ObjectProfile,
+ APActivityType: ap.ActivityDelete,
+ GTSModel: deletedAccount,
+ ReceivingAccount: receivingAccount,
+ RequestingAccount: deletedAccount,
})
suite.NoError(err)
@@ -398,10 +403,11 @@ func (suite *FromFediAPITestSuite) TestProcessFollowRequestLocked() {
suite.NoError(err)
err = suite.processor.Workers().ProcessFromFediAPI(ctx, messages.FromFediAPI{
- APObjectType: ap.ActivityFollow,
- APActivityType: ap.ActivityCreate,
- GTSModel: satanFollowRequestTurtle,
- ReceivingAccount: targetAccount,
+ APObjectType: ap.ActivityFollow,
+ APActivityType: ap.ActivityCreate,
+ GTSModel: satanFollowRequestTurtle,
+ ReceivingAccount: targetAccount,
+ RequestingAccount: originAccount,
})
suite.NoError(err)
@@ -451,10 +457,11 @@ func (suite *FromFediAPITestSuite) TestProcessFollowRequestUnlocked() {
suite.NoError(err)
err = suite.processor.Workers().ProcessFromFediAPI(ctx, messages.FromFediAPI{
- APObjectType: ap.ActivityFollow,
- APActivityType: ap.ActivityCreate,
- GTSModel: satanFollowRequestTurtle,
- ReceivingAccount: targetAccount,
+ APObjectType: ap.ActivityFollow,
+ APActivityType: ap.ActivityCreate,
+ GTSModel: satanFollowRequestTurtle,
+ ReceivingAccount: targetAccount,
+ RequestingAccount: originAccount,
})
suite.NoError(err)
@@ -526,11 +533,12 @@ func (suite *FromFediAPITestSuite) TestCreateStatusFromIRI() {
statusCreator := suite.testAccounts["remote_account_2"]
err := suite.processor.Workers().ProcessFromFediAPI(ctx, messages.FromFediAPI{
- APObjectType: ap.ObjectNote,
- APActivityType: ap.ActivityCreate,
- GTSModel: nil, // gtsmodel is nil because this is a forwarded status -- we want to dereference it using the iri
- ReceivingAccount: receivingAccount,
- APIri: testrig.URLMustParse("http://example.org/users/Some_User/statuses/afaba698-5740-4e32-a702-af61aa543bc1"),
+ APObjectType: ap.ObjectNote,
+ APActivityType: ap.ActivityCreate,
+ GTSModel: nil, // gtsmodel is nil because this is a forwarded status -- we want to dereference it using the iri
+ ReceivingAccount: receivingAccount,
+ RequestingAccount: statusCreator,
+ APIri: testrig.URLMustParse("http://example.org/users/Some_User/statuses/afaba698-5740-4e32-a702-af61aa543bc1"),
})
suite.NoError(err)
diff --git a/internal/processing/workers/util.go b/internal/processing/workers/util.go
index a38ecd336..cd936f428 100644
--- a/internal/processing/workers/util.go
+++ b/internal/processing/workers/util.go
@@ -238,3 +238,258 @@ func (u *utilF) redirectFollowers(
return true
}
+
+func (u *utilF) incrementStatusesCount(
+ ctx context.Context,
+ account *gtsmodel.Account,
+ status *gtsmodel.Status,
+) error {
+ // Lock on this account since we're changing stats.
+ unlock := u.state.AccountLocks.Lock(account.URI)
+ defer unlock()
+
+ // Populate stats.
+ if account.Stats == nil {
+ if err := u.state.DB.PopulateAccountStats(ctx, account); err != nil {
+ return gtserror.Newf("db error getting account stats: %w", err)
+ }
+ }
+
+ // Update stats by incrementing status
+ // count by one and setting last posted.
+ *account.Stats.StatusesCount++
+ account.Stats.LastStatusAt = status.CreatedAt
+ if err := u.state.DB.UpdateAccountStats(
+ ctx,
+ account.Stats,
+ "statuses_count",
+ "last_status_at",
+ ); err != nil {
+ return gtserror.Newf("db error updating account stats: %w", err)
+ }
+
+ return nil
+}
+
+func (u *utilF) decrementStatusesCount(
+ ctx context.Context,
+ account *gtsmodel.Account,
+) error {
+ // Lock on this account since we're changing stats.
+ unlock := u.state.AccountLocks.Lock(account.URI)
+ defer unlock()
+
+ // Populate stats.
+ if account.Stats == nil {
+ if err := u.state.DB.PopulateAccountStats(ctx, account); err != nil {
+ return gtserror.Newf("db error getting account stats: %w", err)
+ }
+ }
+
+ // Update stats by decrementing
+ // status count by one.
+ //
+ // Clamp to 0 to avoid funny business.
+ *account.Stats.StatusesCount--
+ if *account.Stats.StatusesCount < 0 {
+ *account.Stats.StatusesCount = 0
+ }
+ if err := u.state.DB.UpdateAccountStats(
+ ctx,
+ account.Stats,
+ "statuses_count",
+ ); err != nil {
+ return gtserror.Newf("db error updating account stats: %w", err)
+ }
+
+ return nil
+}
+
+func (u *utilF) incrementFollowersCount(
+ ctx context.Context,
+ account *gtsmodel.Account,
+) error {
+ // Lock on this account since we're changing stats.
+ unlock := u.state.AccountLocks.Lock(account.URI)
+ defer unlock()
+
+ // Populate stats.
+ if account.Stats == nil {
+ if err := u.state.DB.PopulateAccountStats(ctx, account); err != nil {
+ return gtserror.Newf("db error getting account stats: %w", err)
+ }
+ }
+
+ // Update stats by incrementing followers
+ // count by one and setting last posted.
+ *account.Stats.FollowersCount++
+ if err := u.state.DB.UpdateAccountStats(
+ ctx,
+ account.Stats,
+ "followers_count",
+ ); err != nil {
+ return gtserror.Newf("db error updating account stats: %w", err)
+ }
+
+ return nil
+}
+
+func (u *utilF) decrementFollowersCount(
+ ctx context.Context,
+ account *gtsmodel.Account,
+) error {
+ // Lock on this account since we're changing stats.
+ unlock := u.state.AccountLocks.Lock(account.URI)
+ defer unlock()
+
+ // Populate stats.
+ if account.Stats == nil {
+ if err := u.state.DB.PopulateAccountStats(ctx, account); err != nil {
+ return gtserror.Newf("db error getting account stats: %w", err)
+ }
+ }
+
+ // Update stats by decrementing
+ // followers count by one.
+ //
+ // Clamp to 0 to avoid funny business.
+ *account.Stats.FollowersCount--
+ if *account.Stats.FollowersCount < 0 {
+ *account.Stats.FollowersCount = 0
+ }
+ if err := u.state.DB.UpdateAccountStats(
+ ctx,
+ account.Stats,
+ "followers_count",
+ ); err != nil {
+ return gtserror.Newf("db error updating account stats: %w", err)
+ }
+
+ return nil
+}
+
+func (u *utilF) incrementFollowingCount(
+ ctx context.Context,
+ account *gtsmodel.Account,
+) error {
+ // Lock on this account since we're changing stats.
+ unlock := u.state.AccountLocks.Lock(account.URI)
+ defer unlock()
+
+ // Populate stats.
+ if account.Stats == nil {
+ if err := u.state.DB.PopulateAccountStats(ctx, account); err != nil {
+ return gtserror.Newf("db error getting account stats: %w", err)
+ }
+ }
+
+ // Update stats by incrementing
+ // followers count by one.
+ *account.Stats.FollowingCount++
+ if err := u.state.DB.UpdateAccountStats(
+ ctx,
+ account.Stats,
+ "following_count",
+ ); err != nil {
+ return gtserror.Newf("db error updating account stats: %w", err)
+ }
+
+ return nil
+}
+
+func (u *utilF) decrementFollowingCount(
+ ctx context.Context,
+ account *gtsmodel.Account,
+) error {
+ // Lock on this account since we're changing stats.
+ unlock := u.state.AccountLocks.Lock(account.URI)
+ defer unlock()
+
+ // Populate stats.
+ if account.Stats == nil {
+ if err := u.state.DB.PopulateAccountStats(ctx, account); err != nil {
+ return gtserror.Newf("db error getting account stats: %w", err)
+ }
+ }
+
+ // Update stats by decrementing
+ // following count by one.
+ //
+ // Clamp to 0 to avoid funny business.
+ *account.Stats.FollowingCount--
+ if *account.Stats.FollowingCount < 0 {
+ *account.Stats.FollowingCount = 0
+ }
+ if err := u.state.DB.UpdateAccountStats(
+ ctx,
+ account.Stats,
+ "following_count",
+ ); err != nil {
+ return gtserror.Newf("db error updating account stats: %w", err)
+ }
+
+ return nil
+}
+
+func (u *utilF) incrementFollowRequestsCount(
+ ctx context.Context,
+ account *gtsmodel.Account,
+) error {
+ // Lock on this account since we're changing stats.
+ unlock := u.state.AccountLocks.Lock(account.URI)
+ defer unlock()
+
+ // Populate stats.
+ if account.Stats == nil {
+ if err := u.state.DB.PopulateAccountStats(ctx, account); err != nil {
+ return gtserror.Newf("db error getting account stats: %w", err)
+ }
+ }
+
+ // Update stats by incrementing
+ // follow requests count by one.
+ *account.Stats.FollowRequestsCount++
+ if err := u.state.DB.UpdateAccountStats(
+ ctx,
+ account.Stats,
+ "follow_requests_count",
+ ); err != nil {
+ return gtserror.Newf("db error updating account stats: %w", err)
+ }
+
+ return nil
+}
+
+func (u *utilF) decrementFollowRequestsCount(
+ ctx context.Context,
+ account *gtsmodel.Account,
+) error {
+ // Lock on this account since we're changing stats.
+ unlock := u.state.AccountLocks.Lock(account.URI)
+ defer unlock()
+
+ // Populate stats.
+ if account.Stats == nil {
+ if err := u.state.DB.PopulateAccountStats(ctx, account); err != nil {
+ return gtserror.Newf("db error getting account stats: %w", err)
+ }
+ }
+
+ // Update stats by decrementing
+ // follow requests count by one.
+ //
+ // Clamp to 0 to avoid funny business.
+ *account.Stats.FollowRequestsCount--
+ if *account.Stats.FollowRequestsCount < 0 {
+ *account.Stats.FollowRequestsCount = 0
+ }
+ if err := u.state.DB.UpdateAccountStats(
+ ctx,
+ account.Stats,
+ "follow_requests_count",
+ ); err != nil {
+ return gtserror.Newf("db error updating account stats: %w", err)
+ }
+
+ return nil
+}