diff options
Diffstat (limited to 'internal/db/bundb')
7 files changed, 476 insertions, 62 deletions
diff --git a/internal/db/bundb/admin.go b/internal/db/bundb/admin.go index dcf51c6a5..39b2c848f 100644 --- a/internal/db/bundb/admin.go +++ b/internal/db/bundb/admin.go @@ -106,12 +106,7 @@ func (a *adminDB) NewSignup(ctx context.Context, newSignup gtsmodel.NewSignup) ( // with this username, create one now. if account == nil { uris := uris.GenerateURIsForAccount(newSignup.Username) - - accountID, err := id.NewRandomULID() - if err != nil { - err := gtserror.Newf("error creating new account id: %w", err) - return nil, err - } + accountID := id.NewRandomULID() privKey, err := rsa.GenerateKey(rand.Reader, rsaKeyBits) if err != nil { @@ -174,12 +169,9 @@ func (a *adminDB) NewSignup(ctx context.Context, newSignup gtsmodel.NewSignup) ( return user, nil } - // Had no user for this account, time to create one! - newUserID, err := id.NewRandomULID() - if err != nil { - err := gtserror.Newf("error creating new user id: %w", err) - return nil, err - } + // Had no user for this + // account, time to create one! + newUserID := id.NewRandomULID() encryptedPassword, err := bcrypt.GenerateFromPassword( []byte(newSignup.Password), @@ -273,14 +265,9 @@ func (a *adminDB) CreateInstanceAccount(ctx context.Context) error { return err } - aID, err := id.NewRandomULID() - if err != nil { - return err - } - newAccountURIs := uris.GenerateURIsForAccount(username) acct := >smodel.Account{ - ID: aID, + ID: id.NewRandomULID(), Username: username, DisplayName: username, URL: newAccountURIs.UserURL, @@ -325,13 +312,8 @@ func (a *adminDB) CreateInstanceInstance(ctx context.Context) error { return nil } - iID, err := id.NewRandomULID() - if err != nil { - return err - } - i := >smodel.Instance{ - ID: iID, + ID: id.NewRandomULID(), Domain: host, Title: host, URI: fmt.Sprintf("%s://%s", protocol, host), diff --git a/internal/db/bundb/interaction.go b/internal/db/bundb/interaction.go index b42eb46f6..2128a7aa6 100644 --- a/internal/db/bundb/interaction.go +++ b/internal/db/bundb/interaction.go @@ -58,31 +58,45 @@ func (i *interactionDB) GetInteractionRequestByID(ctx context.Context, id string ) } -func (i *interactionDB) GetInteractionRequestByInteractionURI(ctx context.Context, uri string) (*gtsmodel.InteractionRequest, error) { +func (i *interactionDB) GetInteractionRequestByInteractionURI(ctx context.Context, intURI string) (*gtsmodel.InteractionRequest, error) { return i.getInteractionRequest( ctx, "InteractionURI", func(request *gtsmodel.InteractionRequest) error { return i. newInteractionRequestQ(request). - Where("? = ?", bun.Ident("interaction_request.interaction_uri"), uri). + Where("? = ?", bun.Ident("interaction_request.interaction_uri"), intURI). Scan(ctx) }, - uri, + intURI, ) } -func (i *interactionDB) GetInteractionRequestByURI(ctx context.Context, uri string) (*gtsmodel.InteractionRequest, error) { +func (i *interactionDB) GetInteractionRequestByResponseURI(ctx context.Context, respURI string) (*gtsmodel.InteractionRequest, error) { return i.getInteractionRequest( ctx, - "URI", + "ResponseURI", func(request *gtsmodel.InteractionRequest) error { return i. newInteractionRequestQ(request). - Where("? = ?", bun.Ident("interaction_request.uri"), uri). + Where("? = ?", bun.Ident("interaction_request.response_uri"), respURI). Scan(ctx) }, - uri, + respURI, + ) +} + +func (i *interactionDB) GetInteractionRequestByAuthorizationURI(ctx context.Context, authURI string) (*gtsmodel.InteractionRequest, error) { + return i.getInteractionRequest( + ctx, + "AuthorizationURI", + func(request *gtsmodel.InteractionRequest) error { + return i. + newInteractionRequestQ(request). + Where("? = ?", bun.Ident("interaction_request.authorization_uri"), authURI). + Scan(ctx) + }, + authURI, ) } @@ -173,11 +187,11 @@ func (i *interactionDB) PopulateInteractionRequest(ctx context.Context, req *gts errs = gtserror.NewMultiError(4) ) - if req.Status == nil { + if req.TargetStatus == nil { // Target status is not set, fetch from the database. - req.Status, err = i.state.DB.GetStatusByID( + req.TargetStatus, err = i.state.DB.GetStatusByID( gtscontext.SetBarebones(ctx), - req.StatusID, + req.TargetStatusID, ) if err != nil { errs.Appendf("error populating interactionRequest target: %w", err) diff --git a/internal/db/bundb/interaction_test.go b/internal/db/bundb/interaction_test.go index 564b3a3f2..6a753dca8 100644 --- a/internal/db/bundb/interaction_test.go +++ b/internal/db/bundb/interaction_test.go @@ -57,9 +57,8 @@ func (suite *InteractionTestSuite) markInteractionsPending( suite.FailNow(err.Error()) } - // Put an interaction request - // in the DB for this reply. - req := typeutils.StatusToInteractionRequest(reply) + // Put an impolite interaction request in the DB for this reply. + req := typeutils.StatusToImpoliteInteractionRequest(reply) if err := suite.state.DB.PutInteractionRequest(ctx, req); err != nil { suite.FailNow(err.Error()) } @@ -84,9 +83,8 @@ func (suite *InteractionTestSuite) markInteractionsPending( suite.FailNow(err.Error()) } - // Put an interaction request - // in the DB for this boost. - req := typeutils.StatusToInteractionRequest(boost) + // Put an impolite interaction request in the DB for this boost. + req := typeutils.StatusToImpoliteInteractionRequest(boost) if err := suite.state.DB.PutInteractionRequest(ctx, req); err != nil { suite.FailNow(err.Error()) } @@ -111,9 +109,8 @@ func (suite *InteractionTestSuite) markInteractionsPending( suite.FailNow(err.Error()) } - // Put an interaction request - // in the DB for this fave. - req := typeutils.StatusFaveToInteractionRequest(fave) + // Put an impolite interaction request in the DB for this fave. + req := typeutils.StatusFaveToImpoliteInteractionRequest(fave) if err := suite.state.DB.PutInteractionRequest(ctx, req); err != nil { suite.FailNow(err.Error()) } @@ -229,8 +226,8 @@ func (suite *InteractionTestSuite) TestInteractionRejected() { // Update the interaction request to mark it rejected. req.RejectedAt = time.Now() - req.URI = "https://some.reject.uri" - if err := suite.state.DB.UpdateInteractionRequest(ctx, req, "uri", "rejected_at"); err != nil { + req.ResponseURI = "https://some.reject.uri" + if err := suite.state.DB.UpdateInteractionRequest(ctx, req, "response_uri", "rejected_at"); err != nil { suite.FailNow(err.Error()) } 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) + } +} diff --git a/internal/db/bundb/migrations/20250715095446_int_pols_forward_compat/new/interactionrequest.go b/internal/db/bundb/migrations/20250715095446_int_pols_forward_compat/new/interactionrequest.go new file mode 100644 index 000000000..bdd2d5811 --- /dev/null +++ b/internal/db/bundb/migrations/20250715095446_int_pols_forward_compat/new/interactionrequest.go @@ -0,0 +1,68 @@ +// 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 gtsmodel + +import ( + "time" + + "github.com/uptrace/bun" +) + +type InteractionRequest struct { + // Used only for migration. + bun.BaseModel `bun:"table:new_interaction_requests"` + + ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` + + // Removed in new model. + // CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` + + // Renamed from "StatusID" to "TargetStatusID" in new model. + TargetStatusID string `bun:"type:CHAR(26),nullzero,notnull"` + + TargetAccountID string `bun:"type:CHAR(26),nullzero,notnull"` + + InteractingAccountID string `bun:"type:CHAR(26),nullzero,notnull"` + + // Added in new model. + InteractionRequestURI string `bun:",nullzero,notnull,unique"` + + InteractionURI string `bun:",nullzero,notnull,unique"` + + // Changed type from int to int16 in new model. + InteractionType int16 `bun:",notnull"` + + // Added in new model. + Polite *bool `bun:",nullzero,notnull,default:false"` + + AcceptedAt time.Time `bun:"type:timestamptz,nullzero"` + + RejectedAt time.Time `bun:"type:timestamptz,nullzero"` + + // Renamed from "URI" to "ResponseURI" in new model. + ResponseURI string `bun:",nullzero,unique"` + + // Added in new model. + AuthorizationURI string `bun:",nullzero,unique"` +} + +const ( + LikeRequestSuffix = "#LikeRequest" + ReplyRequestSuffix = "#ReplyRequest" + AnnounceRequestSuffix = "#AnnounceRequest" +) diff --git a/internal/db/bundb/migrations/20250715095446_int_pols_forward_compat/old/interactionrequest.go b/internal/db/bundb/migrations/20250715095446_int_pols_forward_compat/old/interactionrequest.go new file mode 100644 index 000000000..a341f4d5b --- /dev/null +++ b/internal/db/bundb/migrations/20250715095446_int_pols_forward_compat/old/interactionrequest.go @@ -0,0 +1,39 @@ +// 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 gtsmodel + +import "time" + +type InteractionRequest struct { + ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` + CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` + StatusID string `bun:"type:CHAR(26),nullzero,notnull"` + TargetAccountID string `bun:"type:CHAR(26),nullzero,notnull"` + InteractingAccountID string `bun:"type:CHAR(26),nullzero,notnull"` + InteractionURI string `bun:",nullzero,notnull,unique"` + InteractionType int `bun:",notnull"` + AcceptedAt time.Time `bun:"type:timestamptz,nullzero"` + RejectedAt time.Time `bun:"type:timestamptz,nullzero"` + URI string `bun:",nullzero,unique"` +} + +const ( + InteractionLike int = 0 + InteractionReply int = 1 + InteractionAnnounce int = 2 +) diff --git a/internal/db/bundb/notification_test.go b/internal/db/bundb/notification_test.go index 10b82b7ce..5b67d4f58 100644 --- a/internal/db/bundb/notification_test.go +++ b/internal/db/bundb/notification_test.go @@ -46,21 +46,7 @@ func (suite *NotificationTestSuite) spamNotifs() { if i%2 == 0 { targetAccountID = zork.ID } else { - randomAssID, err := id.NewRandomULID() - if err != nil { - panic(err) - } - targetAccountID = randomAssID - } - - statusID, err := id.NewRandomULID() - if err != nil { - panic(err) - } - - originAccountID, err := id.NewRandomULID() - if err != nil { - panic(err) + targetAccountID = id.NewRandomULID() } notif := >smodel.Notification{ @@ -68,8 +54,8 @@ func (suite *NotificationTestSuite) spamNotifs() { NotificationType: gtsmodel.NotificationFavourite, CreatedAt: time.Now(), TargetAccountID: targetAccountID, - OriginAccountID: originAccountID, - StatusOrEditID: statusID, + OriginAccountID: id.NewRandomULID(), + StatusOrEditID: id.NewRandomULID(), Read: util.Ptr(false), } |
