diff options
Diffstat (limited to 'internal/db')
6 files changed, 317 insertions, 14 deletions
| diff --git a/internal/db/bundb/migrations/20250106114512_replace_statuses_updatedat_with_editedat.go b/internal/db/bundb/migrations/20250106114512_replace_statuses_updatedat_with_editedat.go new file mode 100644 index 000000000..fa28c7ce3 --- /dev/null +++ b/internal/db/bundb/migrations/20250106114512_replace_statuses_updatedat_with_editedat.go @@ -0,0 +1,104 @@ +// 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" +	"fmt" +	"reflect" + +	oldmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20250106114512_replace_statuses_updatedat_with_editedat" +	newmodel "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/log" +	"github.com/uptrace/bun" +	"github.com/uptrace/bun/dialect" +) + +func init() { +	up := func(ctx context.Context, db *bun.DB) error { +		return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { +			var newStatus *newmodel.Status +			newStatusType := reflect.TypeOf(newStatus) + +			// Generate new Status.EditedAt column definition from bun. +			colDef, err := getBunColumnDef(tx, newStatusType, "EditedAt") +			if err != nil { +				return fmt.Errorf("error making column def: %w", err) +			} + +			log.Info(ctx, "adding statuses.edited_at column...") +			_, err = tx.NewAddColumn().Model(newStatus). +				ColumnExpr(colDef). +				Exec(ctx) +			if err != nil { +				return fmt.Errorf("error adding column: %w", err) +			} + +			var whereSQL string +			var whereArg []any + +			// Check for an empty length +			// EditIDs JSON array, with different +			// SQL depending on connected database. +			switch tx.Dialect().Name() { +			case dialect.SQLite: +				whereSQL = "NOT (json_array_length(?) = 0 OR ? IS NULL)" +				whereArg = []any{bun.Ident("edits"), bun.Ident("edits")} +			case dialect.PG: +				whereSQL = "NOT (CARDINALITY(?) = 0 OR ? IS NULL)" +				whereArg = []any{bun.Ident("edits"), bun.Ident("edits")} +			default: +				panic("unsupported db type") +			} + +			log.Info(ctx, "setting edited_at = updated_at where not empty(edits)...") +			res, err := tx.NewUpdate().Model(newStatus).Where(whereSQL, whereArg...). +				Set("? = ?", +					bun.Ident("edited_at"), +					bun.Ident("updated_at"), +				). +				Exec(ctx) +			if err != nil { +				return fmt.Errorf("error updating columns: %w", err) +			} + +			count, _ := res.RowsAffected() +			log.Infof(ctx, "updated %d statuses", count) + +			log.Info(ctx, "removing statuses.updated_at column...") +			_, err = tx.NewDropColumn().Model((*oldmodel.Status)(nil)). +				Column("updated_at"). +				Exec(ctx) +			if err != nil { +				return fmt.Errorf("error dropping column: %w", err) +			} + +			return nil +		}) +	} + +	down := func(ctx context.Context, db *bun.DB) error { +		return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { +			return nil +		}) +	} + +	if err := Migrations.Register(up, down); err != nil { +		panic(err) +	} +} diff --git a/internal/db/bundb/migrations/20250106114512_replace_statuses_updatedat_with_editedat/interactionpolicy.go b/internal/db/bundb/migrations/20250106114512_replace_statuses_updatedat_with_editedat/interactionpolicy.go new file mode 100644 index 000000000..9895acc22 --- /dev/null +++ b/internal/db/bundb/migrations/20250106114512_replace_statuses_updatedat_with_editedat/interactionpolicy.go @@ -0,0 +1,99 @@ +// 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 + +// A policy URI is GoToSocial's internal representation of +// one ActivityPub URI for an Actor or a Collection of Actors, +// specific to the domain of enforcing interaction policies. +// +// A PolicyValue can be stored in the database either as one +// of the Value constants defined below (to save space), OR as +// a full-fledged ActivityPub URI. +// +// A PolicyValue should be translated to the canonical string +// value of the represented URI when federating an item, or +// from the canonical string value of the URI when receiving +// or retrieving an item. +// +// For example, if the PolicyValue `followers` was being +// federated outwards in an interaction policy attached to an +// item created by the actor `https://example.org/users/someone`, +// then it should be translated to their followers URI when sent, +// eg., `https://example.org/users/someone/followers`. +// +// Likewise, if GoToSocial receives an item with an interaction +// policy containing `https://example.org/users/someone/followers`, +// and the item was created by `https://example.org/users/someone`, +// then the followers URI would be converted to `followers` +// for internal storage. +type PolicyValue string + +const ( +	// Stand-in for ActivityPub magic public URI, +	// which encompasses every possible Actor URI. +	PolicyValuePublic PolicyValue = "public" +	// Stand-in for the Followers Collection of +	// the item owner's Actor. +	PolicyValueFollowers PolicyValue = "followers" +	// Stand-in for the Following Collection of +	// the item owner's Actor. +	PolicyValueFollowing PolicyValue = "following" +	// Stand-in for the Mutuals Collection of +	// the item owner's Actor. +	// +	// (TODO: Reserved, currently unused). +	PolicyValueMutuals PolicyValue = "mutuals" +	// Stand-in for Actor URIs tagged in the item. +	PolicyValueMentioned PolicyValue = "mentioned" +	// Stand-in for the Actor URI of the item owner. +	PolicyValueAuthor PolicyValue = "author" +) + +type PolicyValues []PolicyValue + +// An InteractionPolicy determines which +// interactions will be accepted for an +// item, and according to what rules. +type InteractionPolicy struct { +	// Conditions in which a Like +	// interaction will be accepted +	// for an item with this policy. +	CanLike PolicyRules +	// Conditions in which a Reply +	// interaction will be accepted +	// for an item with this policy. +	CanReply PolicyRules +	// Conditions in which an Announce +	// interaction will be accepted +	// for an item with this policy. +	CanAnnounce PolicyRules +} + +// PolicyRules represents the rules according +// to which a certain interaction is permitted +// to various Actor and Actor Collection URIs. +type PolicyRules struct { +	// Always is for PolicyValues who are +	// permitted to do an interaction +	// without requiring approval. +	Always PolicyValues +	// WithApproval is for PolicyValues who +	// are conditionally permitted to do +	// an interaction, pending approval. +	WithApproval PolicyValues +} diff --git a/internal/db/bundb/migrations/20250106114512_replace_statuses_updatedat_with_editedat/status.go b/internal/db/bundb/migrations/20250106114512_replace_statuses_updatedat_with_editedat/status.go new file mode 100644 index 000000000..27f3e5046 --- /dev/null +++ b/internal/db/bundb/migrations/20250106114512_replace_statuses_updatedat_with_editedat/status.go @@ -0,0 +1,114 @@ +// 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" +) + +// Status represents a user-created 'post' or +// 'status' in the database, either remote or local +type Status struct { +	ID                       string             `bun:"type:CHAR(26),pk,nullzero,notnull,unique"`                    // id of this item in the database +	CreatedAt                time.Time          `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created +	UpdatedAt                time.Time          `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated +	FetchedAt                time.Time          `bun:"type:timestamptz,nullzero"`                                   // when was item (remote) last fetched. +	PinnedAt                 time.Time          `bun:"type:timestamptz,nullzero"`                                   // Status was pinned by owning account at this time. +	URI                      string             `bun:",unique,nullzero,notnull"`                                    // activitypub URI of this status +	URL                      string             `bun:",nullzero"`                                                   // web url for viewing this status +	Content                  string             `bun:""`                                                            // content of this status; likely html-formatted but not guaranteed +	AttachmentIDs            []string           `bun:"attachments,array"`                                           // Database IDs of any media attachments associated with this status +	TagIDs                   []string           `bun:"tags,array"`                                                  // Database IDs of any tags used in this status +	MentionIDs               []string           `bun:"mentions,array"`                                              // Database IDs of any mentions in this status +	EmojiIDs                 []string           `bun:"emojis,array"`                                                // Database IDs of any emojis used in this status +	Local                    *bool              `bun:",nullzero,notnull,default:false"`                             // is this status from a local account? +	AccountID                string             `bun:"type:CHAR(26),nullzero,notnull"`                              // which account posted this status? +	AccountURI               string             `bun:",nullzero,notnull"`                                           // activitypub uri of the owner of this status +	InReplyToID              string             `bun:"type:CHAR(26),nullzero"`                                      // id of the status this status replies to +	InReplyToURI             string             `bun:",nullzero"`                                                   // activitypub uri of the status this status is a reply to +	InReplyToAccountID       string             `bun:"type:CHAR(26),nullzero"`                                      // id of the account that this status replies to +	InReplyTo                *Status            `bun:"-"`                                                           // status corresponding to inReplyToID +	BoostOfID                string             `bun:"type:CHAR(26),nullzero"`                                      // id of the status this status is a boost of +	BoostOfURI               string             `bun:"-"`                                                           // URI of the status this status is a boost of; field not inserted in the db, just for dereferencing purposes. +	BoostOfAccountID         string             `bun:"type:CHAR(26),nullzero"`                                      // id of the account that owns the boosted status +	BoostOf                  *Status            `bun:"-"`                                                           // status that corresponds to boostOfID +	ThreadID                 string             `bun:"type:CHAR(26),nullzero"`                                      // id of the thread to which this status belongs; only set for remote statuses if a local account is involved at some point in the thread, otherwise null +	EditIDs                  []string           `bun:"edits,array"`                                                 // +	PollID                   string             `bun:"type:CHAR(26),nullzero"`                                      // +	ContentWarning           string             `bun:",nullzero"`                                                   // cw string for this status +	Visibility               Visibility         `bun:",nullzero,notnull"`                                           // visibility entry for this status +	Sensitive                *bool              `bun:",nullzero,notnull,default:false"`                             // mark the status as sensitive? +	Language                 string             `bun:",nullzero"`                                                   // what language is this status written in? +	CreatedWithApplicationID string             `bun:"type:CHAR(26),nullzero"`                                      // Which application was used to create this status? +	ActivityStreamsType      string             `bun:",nullzero,notnull"`                                           // What is the activitystreams type of this status? See: https://www.w3.org/TR/activitystreams-vocabulary/#object-types. Will probably almost always be Note but who knows!. +	Text                     string             `bun:""`                                                            // Original text of the status without formatting +	Federated                *bool              `bun:",notnull"`                                                    // This status will be federated beyond the local timeline(s) +	InteractionPolicy        *InteractionPolicy `bun:""`                                                            // InteractionPolicy for this status. If null then the default InteractionPolicy should be assumed for this status's Visibility. Always null for boost wrappers. +	PendingApproval          *bool              `bun:",nullzero,notnull,default:false"`                             // If true then status is a reply or boost wrapper that must be Approved by the reply-ee or boost-ee before being fully distributed. +	PreApproved              bool               `bun:"-"`                                                           // If true, then status is a reply to or boost wrapper of a status on our instance, has permission to do the interaction, and an Accept should be sent out for it immediately. Field not stored in the DB. +	ApprovedByURI            string             `bun:",nullzero"`                                                   // URI of an Accept Activity that approves the Announce or Create Activity that this status was/will be attached to. +} + +// GetID implements timeline.Timelineable{}. +func (s *Status) GetID() string { +	return s.ID +} + +// IsLocal returns true if this is a local +// status (ie., originating from this instance). +func (s *Status) IsLocal() bool { +	return s.Local != nil && *s.Local +} + +// enumType is the type we (at least, should) use +// for database enum types. it is the largest size +// supported by a PostgreSQL SMALLINT, since an +// SQLite SMALLINT is actually variable in size. +type enumType int16 + +// Visibility represents the +// visibility granularity of a status. +type Visibility enumType + +const ( +	// VisibilityNone means nobody can see this. +	// It's only used for web status visibility. +	VisibilityNone Visibility = 1 + +	// VisibilityPublic means this status will +	// be visible to everyone on all timelines. +	VisibilityPublic Visibility = 2 + +	// VisibilityUnlocked means this status will be visible to everyone, +	// but will only show on home timeline to followers, and in lists. +	VisibilityUnlocked Visibility = 3 + +	// VisibilityFollowersOnly means this status is viewable to followers only. +	VisibilityFollowersOnly Visibility = 4 + +	// VisibilityMutualsOnly means this status +	// is visible to mutual followers only. +	VisibilityMutualsOnly Visibility = 5 + +	// VisibilityDirect means this status is +	// visible only to mentioned recipients. +	VisibilityDirect Visibility = 6 + +	// VisibilityDefault is used when no other setting can be found. +	VisibilityDefault Visibility = VisibilityUnlocked +) diff --git a/internal/db/bundb/poll.go b/internal/db/bundb/poll.go index e8c3e7e54..5da9832f0 100644 --- a/internal/db/bundb/poll.go +++ b/internal/db/bundb/poll.go @@ -21,7 +21,6 @@ import (  	"context"  	"errors"  	"slices" -	"time"  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtscontext" @@ -158,17 +157,6 @@ func (p *pollDB) UpdatePoll(ctx context.Context, poll *gtsmodel.Poll, cols ...st  	return p.state.Caches.DB.Poll.Store(poll, func() error {  		return p.db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { -			// Update the status' "updated_at" field. -			if _, err := tx.NewUpdate(). -				Table("statuses"). -				Where("? = ?", bun.Ident("id"), poll.StatusID). -				SetColumn("updated_at", "?", time.Now()). -				Exec(ctx); err != nil { -				return err -			} - -			// Finally, update poll -			// columns in database.  			_, err := tx.NewUpdate().  				Model(poll).  				Column(cols...). diff --git a/internal/db/bundb/timeline_test.go b/internal/db/bundb/timeline_test.go index 75a335512..5b3268d36 100644 --- a/internal/db/bundb/timeline_test.go +++ b/internal/db/bundb/timeline_test.go @@ -50,7 +50,6 @@ func getFutureStatus() *gtsmodel.Status {  		MentionIDs:               []string{},  		EmojiIDs:                 []string{},  		CreatedAt:                theDistantFuture, -		UpdatedAt:                theDistantFuture,  		Local:                    util.Ptr(true),  		AccountURI:               "http://localhost:8080/users/admin",  		AccountID:                "01F8MH17FWEB39HZJ76B6VXSKF", diff --git a/internal/db/test/conversation.go b/internal/db/test/conversation.go index 95713927e..50bca5308 100644 --- a/internal/db/test/conversation.go +++ b/internal/db/test/conversation.go @@ -72,7 +72,6 @@ func (f *ConversationFactory) NewTestStatus(localAccount *gtsmodel.Account, thre  	status := >smodel.Status{  		ID:                  statusID,  		CreatedAt:           createdAt, -		UpdatedAt:           createdAt,  		URI:                 "http://localhost:8080/users/" + localAccount.Username + "/statuses/" + statusID,  		AccountID:           localAccount.ID,  		AccountURI:          localAccount.URI, | 
