summaryrefslogtreecommitdiff
path: root/internal/db/bundb/migrations/20250715095446_int_pols_forward_compat.go
diff options
context:
space:
mode:
authorLibravatar tobi <tobi.smethurst@protonmail.com>2025-09-14 15:37:35 +0200
committerLibravatar tobi <kipvandenbos@noreply.codeberg.org>2025-09-14 15:37:35 +0200
commit754b7be9cfd3f50e6e219177cdd4c992a2d9c38f (patch)
treef20e7d3c4aa12341289aada97542a756d2daed5d /internal/db/bundb/migrations/20250715095446_int_pols_forward_compat.go
parent[bugfix] set link header lo,hi values directly from returned slice, don't acc... (diff)
downloadgotosocial-754b7be9cfd3f50e6e219177cdd4c992a2d9c38f.tar.xz
[feature] Support new model of interaction flow for forward compat with v0.21.0 (#4394)
~~Still WIP!~~ This PR allows v0.20.0 of GtS to be forward-compatible with the interaction request / authorization flow that will fully replace the current flow in v0.21.0. Basically, this means we need to recognize LikeRequest, ReplyRequest, and AnnounceRequest, and in response to those requests, deliver either a Reject or an Accept, with the latter pointing towards a LikeAuthorization, ReplyAuthorization, or AnnounceAuthorization, respectively. This can then be used by the remote instance to prove to third parties that the interaction has been accepted by the interactee. These Authorization types need to be dereferencable to third parties, so we need to serve them. As well as recognizing the above "polite" interaction request types, we also need to still serve appropriate responses to "impolite" interaction request types, where an instance that's unaware of interaction policies tries to interact with a post by sending a reply, like, or boost directly, without wrapping it in a WhateverRequest type. Doesn't fully close https://codeberg.org/superseriousbusiness/gotosocial/issues/4026 but gets damn near (just gotta update the federating with GtS documentation). Migrations tested on both Postgres and SQLite. Co-authored-by: kim <grufwub@gmail.com> Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4394 Co-authored-by: tobi <tobi.smethurst@protonmail.com> Co-committed-by: tobi <tobi.smethurst@protonmail.com>
Diffstat (limited to 'internal/db/bundb/migrations/20250715095446_int_pols_forward_compat.go')
-rw-r--r--internal/db/bundb/migrations/20250715095446_int_pols_forward_compat.go328
1 files changed, 328 insertions, 0 deletions
diff --git a/internal/db/bundb/migrations/20250715095446_int_pols_forward_compat.go b/internal/db/bundb/migrations/20250715095446_int_pols_forward_compat.go
new file mode 100644
index 000000000..0df515082
--- /dev/null
+++ b/internal/db/bundb/migrations/20250715095446_int_pols_forward_compat.go
@@ -0,0 +1,328 @@
+// 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/>.
+
+package migrations
+
+import (
+ "context"
+ "database/sql"
+ "errors"
+ "net/url"
+ "strings"
+
+ "code.superseriousbusiness.org/gotosocial/internal/config"
+ "code.superseriousbusiness.org/gotosocial/internal/gtserror"
+ "code.superseriousbusiness.org/gotosocial/internal/id"
+ "code.superseriousbusiness.org/gotosocial/internal/log"
+ "code.superseriousbusiness.org/gotosocial/internal/util"
+ "github.com/uptrace/bun"
+ "github.com/uptrace/bun/dialect"
+
+ new_gtsmodel "code.superseriousbusiness.org/gotosocial/internal/db/bundb/migrations/20250715095446_int_pols_forward_compat/new"
+ old_gtsmodel "code.superseriousbusiness.org/gotosocial/internal/db/bundb/migrations/20250715095446_int_pols_forward_compat/old"
+)
+
+func init() {
+ up := func(ctx context.Context, db *bun.DB) error {
+ const tmpTableName = "new_interaction_requests"
+ const tableName = "interaction_requests"
+ var host = config.GetHost()
+ var accountDomain = config.GetAccountDomain()
+
+ // Count number of interaction
+ // requests we need to update.
+ total, err := db.NewSelect().
+ Table(tableName).
+ Count(ctx)
+ if err != nil {
+ return gtserror.Newf("error geting interaction requests table count: %w", err)
+ }
+
+ // Create new interaction_requests table and convert all existing into it.
+ if err := db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
+
+ log.Info(ctx, "creating new interaction_requests table")
+ if _, err := tx.NewCreateTable().
+ ModelTableExpr(tmpTableName).
+ Model((*new_gtsmodel.InteractionRequest)(nil)).
+ Exec(ctx); err != nil {
+ return gtserror.Newf("error creating new interaction requests table: %w", err)
+ }
+
+ // Conversion batch size.
+ const batchsz = 1000
+
+ var maxID string
+ var count int
+
+ // Start at largest
+ // possible ULID value.
+ maxID = id.Highest
+
+ // Preallocate interaction request slices to maximum possible size.
+ oldRequests := make([]*old_gtsmodel.InteractionRequest, 0, batchsz)
+ newRequests := make([]*new_gtsmodel.InteractionRequest, 0, batchsz)
+
+ log.Info(ctx, "migrating interaction requests to new table, this may take some time!")
+ outer:
+ for {
+ // Reset slices slices.
+ clear(oldRequests)
+ clear(newRequests)
+ oldRequests = oldRequests[:0]
+ newRequests = newRequests[:0]
+
+ // Select next batch of
+ // interaction requests.
+ if err := tx.NewSelect().
+ Model(&oldRequests).
+ Where("? < ?", bun.Ident("id"), maxID).
+ OrderExpr("? DESC", bun.Ident("id")).
+ Limit(batchsz).
+ Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) {
+ return gtserror.Newf("error selecting interaction requests: %w", err)
+ }
+
+ // Reached end of requests.
+ if len(oldRequests) == 0 {
+ break outer
+ }
+
+ // Set next maxID value from old requests.
+ maxID = oldRequests[len(oldRequests)-1].ID
+
+ inner:
+ // Convert old request models to new.
+ for _, oldRequest := range oldRequests {
+ newRequest := &new_gtsmodel.InteractionRequest{
+ ID: oldRequest.ID,
+ TargetStatusID: oldRequest.StatusID,
+ TargetAccountID: oldRequest.TargetAccountID,
+ InteractingAccountID: oldRequest.InteractingAccountID,
+ InteractionURI: oldRequest.InteractionURI,
+ InteractionType: int16(oldRequest.InteractionType), // #nosec G115
+ Polite: util.Ptr(false), // old requests were always impolite
+ AcceptedAt: oldRequest.AcceptedAt,
+ RejectedAt: oldRequest.RejectedAt,
+ ResponseURI: oldRequest.URI,
+ }
+
+ // Append new request to slice,
+ // though we continue operating on
+ // its ptr in the rest of this loop.
+ newRequests = append(newRequests,
+ newRequest)
+
+ // Re-use the original interaction URI to create
+ // a mock interaction request URI on the new model.
+ switch oldRequest.InteractionType {
+ case old_gtsmodel.InteractionLike:
+ newRequest.InteractionRequestURI = oldRequest.InteractionURI + new_gtsmodel.LikeRequestSuffix
+ case old_gtsmodel.InteractionReply:
+ newRequest.InteractionRequestURI = oldRequest.InteractionURI + new_gtsmodel.ReplyRequestSuffix
+ case old_gtsmodel.InteractionAnnounce:
+ newRequest.InteractionRequestURI = oldRequest.InteractionURI + new_gtsmodel.AnnounceRequestSuffix
+ }
+
+ // If the request was accepted by us, then generate an authorization
+ // URI for it, in order to be able to serve an Authorization if necessary.
+ if oldRequest.AcceptedAt.IsZero() || oldRequest.URI == "" {
+
+ // Wasn't accepted,
+ // nothing else to do.
+ continue inner
+ }
+
+ // Parse URI details of accept URI string.
+ acceptURI, err := url.Parse(oldRequest.URI)
+ if err != nil {
+ log.Warnf(ctx, "could not parse oldRequest.URI for interaction request %s,"+
+ " skipping forward-compat hack (don't worry, this is not a big deal): %v",
+ oldRequest.ID, err)
+ continue inner
+ }
+
+ // Check whether accept URI originated from this instance.
+ if !(acceptURI.Host == host || acceptURI.Host == accountDomain) {
+
+ // Not an accept from
+ // us, leave it alone.
+ continue inner
+ }
+
+ // Reuse the Accept URI to create an Authorization URI.
+ // Creates `https://example.org/users/aaa/authorizations/[ID]`
+ // from `https://example.org/users/aaa/accepts/[ID]`.
+ authorizationURI := strings.ReplaceAll(
+ oldRequest.URI,
+ "/accepts/"+oldRequest.ID,
+ "/authorizations/"+oldRequest.ID,
+ )
+ newRequest.AuthorizationURI = authorizationURI
+
+ var updateTableName string
+
+ // Determine which table will have corresponding approved_by_uri.
+ if oldRequest.InteractionType == old_gtsmodel.InteractionLike {
+ updateTableName = "status_faves"
+ } else {
+ updateTableName = "statuses"
+ }
+
+ // Update the corresponding interaction
+ // with generated authorization URI.
+ if _, err := tx.NewUpdate().
+ Table(updateTableName).
+ Set("? = ?", bun.Ident("approved_by_uri"), authorizationURI).
+ Where("? = ?", bun.Ident("uri"), oldRequest.InteractionURI).
+ Exec(ctx); err != nil {
+ return gtserror.Newf("error updating approved_by_uri: %w", err)
+ }
+ }
+
+ // Insert converted interaction
+ // request models to new table.
+ if _, err := tx.
+ NewInsert().
+ Model(&newRequests).
+ Exec(ctx); err != nil {
+ return gtserror.Newf("error inserting interaction requests: %w", err)
+ }
+
+ // Increment insert count.
+ count += len(newRequests)
+
+ log.Infof(ctx, "[%d of %d] converting interaction requests", count, total)
+ }
+
+ return nil
+ }); err != nil {
+ return err
+ }
+
+ // Ensure that the above transaction
+ // has gone ahead without issues.
+ //
+ // Also placing this here might make
+ // breaking this into piecemeal steps
+ // easier if turns out necessary.
+ newTotal, err := db.NewSelect().
+ Table(tmpTableName).
+ Count(ctx)
+ if err != nil {
+ return gtserror.Newf("error geting new interaction requests table count: %w", err)
+ } else if total != newTotal {
+ return gtserror.Newf("new interaction requests table contains unexpected count %d, want %d", newTotal, total)
+ }
+
+ // Attempt to merge any sqlite write-ahead-log.
+ if err := doWALCheckpoint(ctx, db); err != nil {
+ return err
+ }
+
+ // Drop the old interaction requests table and rename new one to replace it.
+ if err := db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
+
+ log.Info(ctx, "dropping old interaction_requests table")
+ if _, err := tx.NewDropTable().
+ Table(tableName).
+ Exec(ctx); err != nil {
+ return gtserror.Newf("error dropping old interaction requests table: %w", err)
+ }
+
+ log.Info(ctx, "renaming new interaction_requests table to old")
+ if _, err := tx.NewRaw("ALTER TABLE ? RENAME TO ?",
+ bun.Ident(tmpTableName),
+ bun.Ident(tableName),
+ ).Exec(ctx); err != nil {
+ return gtserror.Newf("error renaming interaction requests table: %w", err)
+ }
+
+ // Create necessary indices on the new table.
+ for index, columns := range map[string][]string{
+ "interaction_requests_target_status_id_idx": {"target_status_id"},
+ "interaction_requests_interacting_account_id_idx": {"interacting_account_id"},
+ "interaction_requests_target_account_id_idx": {"target_account_id"},
+ "interaction_requests_accepted_at_idx": {"accepted_at"},
+ "interaction_requests_rejected_at_idx": {"rejected_at"},
+ } {
+ log.Infof(ctx, "recreating %s index", index)
+ if _, err := tx.NewCreateIndex().
+ Table(tableName).
+ Index(index).
+ Column(columns...).
+ Exec(ctx); err != nil {
+ return err
+ }
+ }
+
+ if tx.Dialect().Name() == dialect.PG {
+ // Rename postgres uniqueness constraints:
+ // "new_interaction_requests_*" -> "interaction_requests_*"
+ log.Info(ctx, "renaming interaction_requests constraints on new table")
+ for _, spec := range []struct {
+ old string
+ new string
+ }{
+ {
+ old: "new_interaction_requests_pkey",
+ new: "interaction_requests_pkey",
+ },
+ {
+ old: "new_interaction_requests_interaction_request_uri_key",
+ new: "interaction_requests_interaction_request_uri_key",
+ },
+ {
+ old: "new_interaction_requests_interaction_uri_key",
+ new: "interaction_requests_interaction_uri_key",
+ },
+ {
+ old: "new_interaction_requests_response_uri_key",
+ new: "interaction_requests_response_uri_key",
+ },
+ {
+ old: "new_interaction_requests_authorization_uri_key",
+ new: "interaction_requests_authorization_uri_key",
+ },
+ } {
+ if _, err := tx.NewRaw("ALTER TABLE ? RENAME CONSTRAINT ? TO ?",
+ bun.Ident(tableName),
+ bun.Safe(spec.old),
+ bun.Safe(spec.new),
+ ).Exec(ctx); err != nil {
+ return gtserror.Newf("error renaming postgres interaction requests constraint %s: %w", spec.new, err)
+ }
+ }
+ }
+
+ return nil
+ }); err != nil {
+ return err
+ }
+
+ // Final sqlite write-ahead-log merge.
+ return doWALCheckpoint(ctx, db)
+ }
+
+ down := func(ctx context.Context, db *bun.DB) error {
+ return nil
+ }
+
+ if err := Migrations.Register(up, down); err != nil {
+ panic(err)
+ }
+}