mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2026-01-06 03:03:16 -06:00
add more of status edit migrations, fill in more of the necessary edit delete functionality
This commit is contained in:
parent
20e20feae0
commit
305c50b037
9 changed files with 318 additions and 27 deletions
|
|
@ -19,8 +19,9 @@ package migrations
|
|||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
|
||||
gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20241113152126_add_status_edits_table"
|
||||
gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20241113152126_add_status_edits"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
|
@ -28,7 +29,26 @@ import (
|
|||
func init() {
|
||||
up := func(ctx context.Context, db *bun.DB) error {
|
||||
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||
_, err := tx.NewCreateTable().
|
||||
statusType := reflect.TypeOf((*gtsmodel.Status)(nil))
|
||||
|
||||
// Generate new Status.EditIDs column definition from bun.
|
||||
colDef, err := getBunColumnDef(tx, statusType, "EditIDs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add EditIDs column to Status table.
|
||||
_, err = tx.NewAddColumn().
|
||||
IfNotExists().
|
||||
Model((*gtsmodel.Status)(nil)).
|
||||
ColumnExpr(colDef).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create the main StatusEdits table.
|
||||
_, err = tx.NewCreateTable().
|
||||
IfNotExists().
|
||||
Model((*gtsmodel.StatusEdit)(nil)).
|
||||
Exec(ctx)
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
// 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/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
// 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
|
||||
Attachments []*gtsmodel.MediaAttachment `bun:"attached_media,rel:has-many"` // Attachments corresponding to attachmentIDs
|
||||
TagIDs []string `bun:"tags,array"` // Database IDs of any tags used in this status
|
||||
Tags []*gtsmodel.Tag `bun:"attached_tags,m2m:status_to_tags"` // Tags corresponding to tagIDs. https://bun.uptrace.dev/guide/relations.html#many-to-many-relation
|
||||
MentionIDs []string `bun:"mentions,array"` // Database IDs of any mentions in this status
|
||||
Mentions []*gtsmodel.Mention `bun:"attached_mentions,rel:has-many"` // Mentions corresponding to mentionIDs
|
||||
EmojiIDs []string `bun:"emojis,array"` // Database IDs of any emojis used in this status
|
||||
Emojis []*gtsmodel.Emoji `bun:"attached_emojis,m2m:status_to_emojis"` // Emojis corresponding to emojiIDs. https://bun.uptrace.dev/guide/relations.html#many-to-many-relation
|
||||
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?
|
||||
Account *gtsmodel.Account `bun:"rel:belongs-to"` // account corresponding to accountID
|
||||
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
|
||||
InReplyToAccount *gtsmodel.Account `bun:"rel:belongs-to"` // account corresponding to inReplyToAccountID
|
||||
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
|
||||
BoostOfAccount *gtsmodel.Account `bun:"rel:belongs-to"` // account that corresponds to boostOfAccountID
|
||||
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:",array"` //
|
||||
Edits []*StatusEdit `bun:"-"` //
|
||||
PollID string `bun:"type:CHAR(26),nullzero"` //
|
||||
Poll *gtsmodel.Poll `bun:"-"` //
|
||||
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?
|
||||
CreatedWithApplication *gtsmodel.Application `bun:"rel:belongs-to"` // application corresponding to createdWithApplicationID
|
||||
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 *gtsmodel.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.
|
||||
}
|
||||
|
||||
// Visibility represents the visibility granularity of a status.
|
||||
type Visibility string
|
||||
|
||||
const (
|
||||
// VisibilityNone means nobody can see this.
|
||||
// It's only used for web status visibility.
|
||||
VisibilityNone Visibility = "none"
|
||||
// VisibilityPublic means this status will be visible to everyone on all timelines.
|
||||
VisibilityPublic Visibility = "public"
|
||||
// VisibilityUnlocked means this status will be visible to everyone, but will only show on home timeline to followers, and in lists.
|
||||
VisibilityUnlocked Visibility = "unlocked"
|
||||
// VisibilityFollowersOnly means this status is viewable to followers only.
|
||||
VisibilityFollowersOnly Visibility = "followers_only"
|
||||
// VisibilityMutualsOnly means this status is visible to mutual followers only.
|
||||
VisibilityMutualsOnly Visibility = "mutuals_only"
|
||||
// VisibilityDirect means this status is visible only to mentioned recipients.
|
||||
VisibilityDirect Visibility = "direct"
|
||||
// VisibilityDefault is used when no other setting can be found.
|
||||
VisibilityDefault Visibility = VisibilityUnlocked
|
||||
)
|
||||
|
|
@ -19,11 +19,115 @@ package migrations
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"codeberg.org/gruf/go-byteutil"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect"
|
||||
"github.com/uptrace/bun/dialect/feature"
|
||||
"github.com/uptrace/bun/dialect/sqltype"
|
||||
"github.com/uptrace/bun/schema"
|
||||
)
|
||||
|
||||
// getBunColumnDef generates a column definition string for the SQL table represented by
|
||||
// Go type, with the SQL column represented by the given Go field name. This ensures when
|
||||
// adding a new column for table by migration that it will end up as bun would create it.
|
||||
//
|
||||
// NOTE: this function must stay in sync with (*bun.CreateTableQuery{}).AppendQuery(),
|
||||
// specifically where it loops over table fields appending each column definition.
|
||||
func getBunColumnDef(db bun.IDB, rtype reflect.Type, fieldName string) (string, error) {
|
||||
d := db.Dialect()
|
||||
f := d.Features()
|
||||
|
||||
// Get bun schema definitions for Go type and its field.
|
||||
field, table, err := getModelField(db, rtype, fieldName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Start with reasonable buf.
|
||||
buf := make([]byte, 0, 64)
|
||||
|
||||
// Start with the SQL column name.
|
||||
buf = append(buf, field.SQLName...)
|
||||
buf = append(buf, " "...)
|
||||
|
||||
// Append the SQL
|
||||
// type information.
|
||||
switch {
|
||||
|
||||
// Most of the time these two will match, but for the cases where DiscoveredSQLType is dialect-specific,
|
||||
// e.g. pgdialect would change sqltype.SmallInt to pgTypeSmallSerial for columns that have `bun:",autoincrement"`
|
||||
case !strings.EqualFold(field.CreateTableSQLType, field.DiscoveredSQLType):
|
||||
buf = append(buf, field.CreateTableSQLType...)
|
||||
|
||||
// For all common SQL types except VARCHAR, both UserDefinedSQLType and DiscoveredSQLType specify the correct type,
|
||||
// and we needn't modify it. For VARCHAR columns, we will stop to check if a valid length has been set in .Varchar(int).
|
||||
case !strings.EqualFold(field.CreateTableSQLType, sqltype.VarChar):
|
||||
buf = append(buf, field.CreateTableSQLType...)
|
||||
|
||||
// All else falls back
|
||||
// to a default varchar.
|
||||
default:
|
||||
if d.Name() == dialect.Oracle {
|
||||
buf = append(buf, "VARCHAR2"...)
|
||||
} else {
|
||||
buf = append(buf, sqltype.VarChar...)
|
||||
}
|
||||
buf = append(buf, sqltype.VarChar+"("...)
|
||||
buf = strconv.AppendInt(buf, int64(d.DefaultVarcharLen()), 10)
|
||||
buf = append(buf, ")"...)
|
||||
}
|
||||
|
||||
// Append not null definition if field requires.
|
||||
if field.NotNull && d.Name() != dialect.Oracle {
|
||||
buf = append(buf, " NOT NULL"...)
|
||||
}
|
||||
|
||||
// Append autoincrement definition if field requires.
|
||||
if field.Identity && f.Has(feature.GeneratedIdentity) ||
|
||||
(field.AutoIncrement && (f.Has(feature.AutoIncrement) || f.Has(feature.Identity))) {
|
||||
buf = d.AppendSequence(buf, table, field)
|
||||
}
|
||||
|
||||
// Append any default value.
|
||||
if field.SQLDefault != "" {
|
||||
buf = append(buf, " DEFAULT "...)
|
||||
buf = append(buf, field.SQLDefault...)
|
||||
}
|
||||
|
||||
return byteutil.B2S(buf), nil
|
||||
}
|
||||
|
||||
// getModelField returns the uptrace/bun schema details for given Go type and field name.
|
||||
func getModelField(db bun.IDB, rtype reflect.Type, fieldName string) (*schema.Field, *schema.Table, error) {
|
||||
|
||||
// Get the associated table for Go type.
|
||||
table := db.Dialect().Tables().Get(rtype)
|
||||
if table == nil {
|
||||
return nil, nil, fmt.Errorf("no table found for type: %s", rtype)
|
||||
}
|
||||
|
||||
var field *schema.Field
|
||||
|
||||
// Look for field matching Go name.
|
||||
for i := range table.Fields {
|
||||
if table.Fields[i].GoName == fieldName {
|
||||
field = table.Fields[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if field == nil {
|
||||
return nil, nil, fmt.Errorf("no bun field found on %s with name: %s", rtype, fieldName)
|
||||
}
|
||||
|
||||
return field, table, nil
|
||||
}
|
||||
|
||||
// doesColumnExist safely checks whether given column exists on table, handling both SQLite and PostgreSQL appropriately.
|
||||
func doesColumnExist(ctx context.Context, tx bun.Tx, table, col string) (bool, error) {
|
||||
var n int
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
|
|
@ -99,7 +99,7 @@ func (s *statusEditDB) GetStatusEditsByIDs(ctx context.Context, ids []string) ([
|
|||
// Reorder the edits by their
|
||||
// IDs to ensure in correct order.
|
||||
getID := func(e *gtsmodel.StatusEdit) string { return e.ID }
|
||||
util.OrderBy(edits, ids, getID)
|
||||
xslices.OrderBy(edits, ids, getID)
|
||||
|
||||
if gtscontext.Barebones(ctx) {
|
||||
// no need to fully populate.
|
||||
|
|
@ -149,10 +149,6 @@ func (s *statusEditDB) PutStatusEdit(ctx context.Context, edit *gtsmodel.StatusE
|
|||
}
|
||||
|
||||
func (s *statusEditDB) DeleteStatusEdits(ctx context.Context, ids []string) error {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Gather necessary fields from
|
||||
// deleted for cache invalidation.
|
||||
var deleted []*gtsmodel.StatusEdit
|
||||
|
|
@ -169,24 +165,34 @@ func (s *statusEditDB) DeleteStatusEdits(ctx context.Context, ids []string) erro
|
|||
return err
|
||||
}
|
||||
|
||||
// Check for no deletes.
|
||||
if len(deleted) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Invalidate all the cached status edits with IDs.
|
||||
s.state.Caches.DB.StatusEdit.InvalidateIDs("ID", ids)
|
||||
|
||||
// Make sure we only end up calling
|
||||
// the invalidate hook for each status
|
||||
// once. This should just be the one,
|
||||
// but we double check to save cycles.
|
||||
m := make(map[string]struct{}, 1)
|
||||
for _, edit := range deleted {
|
||||
// With each invalidate hook mark status ID of
|
||||
// edit we just called for. We only want to call
|
||||
// invalidate hooks of edits from unique statuses.
|
||||
invalidated := make(map[string]struct{}, 1)
|
||||
|
||||
// Invalidate the first delete manually, this
|
||||
// opt negates need for initial hashmap lookup.
|
||||
s.state.Caches.OnInvalidateStatusEdit(deleted[0])
|
||||
invalidated[deleted[0].StatusID] = struct{}{}
|
||||
|
||||
for _, edit := range deleted {
|
||||
// Check not already called for status.
|
||||
if _, ok := m[edit.StatusID]; ok {
|
||||
_, ok := invalidated[edit.StatusID]
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Manually call status edit invalidate hook.
|
||||
s.state.Caches.OnInvalidateStatusEdit(edit)
|
||||
m[edit.StatusID] = struct{}{}
|
||||
invalidated[edit.StatusID] = struct{}{}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -42,8 +42,9 @@ func (suite *StatusEditTestSuite) TestGetStatusEditBy() {
|
|||
// Sentinel error to mark avoiding a test case.
|
||||
sentinelErr := errors.New("sentinel")
|
||||
|
||||
// isEqual checks if 2 account models are equal.
|
||||
// isEqual checks if 2 status edit models are equal.
|
||||
isEqual := func(e1, e2 gtsmodel.StatusEdit) bool {
|
||||
|
||||
// Clear populated sub-models.
|
||||
e1.Attachments = nil
|
||||
e2.Attachments = nil
|
||||
|
|
@ -86,6 +87,10 @@ func (suite *StatusEditTestSuite) TestGetStatusEditBy() {
|
|||
}
|
||||
}
|
||||
|
||||
func (suite *StatusEditTestSuite) TestDeleteStatusEdits() {
|
||||
|
||||
}
|
||||
|
||||
func TestStatusEditTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(StatusEditTestSuite))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,18 +25,19 @@ import (
|
|||
|
||||
type StatusEdit interface {
|
||||
|
||||
// GetStatusEditByID ...
|
||||
// GetStatusEditByID fetches the StatusEdit with given ID from the database.
|
||||
GetStatusEditByID(ctx context.Context, id string) (*gtsmodel.StatusEdit, error)
|
||||
|
||||
// GetStatusEditsByIDs ...
|
||||
// GetStatusEditsByIDs fetches all StatusEdits with given IDs from database,
|
||||
// this is optimized and faster than multiple calls to GetStatusEditByID.
|
||||
GetStatusEditsByIDs(ctx context.Context, ids []string) ([]*gtsmodel.StatusEdit, error)
|
||||
|
||||
// PopulateStatusEdit ...
|
||||
// PopulateStatusEdit ensures the given StatusEdit's sub-models are populated.
|
||||
PopulateStatusEdit(ctx context.Context, edit *gtsmodel.StatusEdit) error
|
||||
|
||||
// PutStatusEdit ...
|
||||
// PutStatusEdit inserts the given new StatusEdit into the database.
|
||||
PutStatusEdit(ctx context.Context, edit *gtsmodel.StatusEdit) error
|
||||
|
||||
// DeleteStatusEdits ...
|
||||
// DeleteStatusEdits deletes the StatusEdits with given IDs from the database.
|
||||
DeleteStatusEdits(ctx context.Context, ids []string) error
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue