mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-12-12 02:06:31 +00:00
[feature] add support for receiving federated status edits (#3597)
* add support for extracting Updated field from Statusable implementers * add support for status edits in the database, and update status dereferencer to handle them * remove unused AdditionalInfo{}.CreatedAt * remove unused AdditionalEmojiInfo{}.CreatedAt * update new mention creation to use status.UpdatedAt * remove mention.UpdatedAt, fixes related to NewULIDFromTime() change * add migration to remove Mention{}.UpdatedAt field * add migration to add the StatusEdit{} table * start adding tests, add delete function for status edits * add more of status edit migrations, fill in more of the necessary edit delete functionality * remove unused function * allow generating gotosocial compatible ulid via CLI with `go run ./cmd/gen-ulid` * add StatusEdit{} test models * fix new statusedits sql * use model instead of table name * actually remove the Mention.UpdatedAt field... * fix tests now new models are added, add more status edit DB tests * fix panic wording * add test for deleting status edits * don't automatically set `updated_at` field on updated statuses * flesh out more of the dereferencer status edit tests, ensure updated at field set on outgoing AS statuses * remove media_attachments.updated_at column * fix up more tests, further complete the dereferencer status edit tests * update more status serialization tests not expecting 'updated' AS property * gah!! json serialization tests!! * undo some gtscontext wrapping changes * more serialization test fixing 🥲 * more test fixing, ensure the edit.status_id field is actually set 🤦 * fix status edit test * grrr linter * add edited_at field to apimodel status * remove the choice of paging on the timeline public filtered test (otherwise it needs updating every time you add statuses ...) * ensure that status.updated_at always fits chronologically * fix more serialization tests ... * add more code comments * fix envparsing * update swagger file * properly handle media description changes during status edits * slight formatting tweak * code comment
This commit is contained in:
parent
3e18d97a6e
commit
23fc70f4e6
86 changed files with 2557 additions and 651 deletions
22
cmd/gen-ulid/main.go
Normal file
22
cmd/gen-ulid/main.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
// 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 main
|
||||
|
||||
import "github.com/superseriousbusiness/gotosocial/internal/id"
|
||||
|
||||
func main() { println(id.NewULID()) }
|
|
@ -2692,6 +2692,11 @@ definitions:
|
|||
example: "2021-07-30T09:20:25+00:00"
|
||||
type: string
|
||||
x-go-name: CreatedAt
|
||||
edited_at:
|
||||
description: Timestamp of when the status was last edited (ISO 8601 Datetime).
|
||||
example: "2021-07-30T09:20:25+00:00"
|
||||
type: string
|
||||
x-go-name: EditedAt
|
||||
emojis:
|
||||
description: Custom emoji to be used when rendering status content.
|
||||
items:
|
||||
|
@ -2889,6 +2894,11 @@ definitions:
|
|||
example: "2021-07-30T09:20:25+00:00"
|
||||
type: string
|
||||
x-go-name: CreatedAt
|
||||
edited_at:
|
||||
description: Timestamp of when the status was last edited (ISO 8601 Datetime).
|
||||
example: "2021-07-30T09:20:25+00:00"
|
||||
type: string
|
||||
x-go-name: EditedAt
|
||||
emojis:
|
||||
description: Custom emoji to be used when rendering status content.
|
||||
items:
|
||||
|
|
|
@ -25,8 +25,11 @@ import (
|
|||
|
||||
// IsActivityable returns whether AS vocab type name is acceptable as Activityable.
|
||||
func IsActivityable(typeName string) bool {
|
||||
return isActivity(typeName) ||
|
||||
isIntransitiveActivity(typeName)
|
||||
return isActivity(typeName)
|
||||
// See interfaces_test.go comment
|
||||
// about intransitive activities:
|
||||
//
|
||||
// || isIntransitiveActivity(typeName)
|
||||
}
|
||||
|
||||
// ToActivityable safely tries to cast vocab.Type as Activityable, also checking for expected AS type names.
|
||||
|
@ -184,6 +187,7 @@ type Accountable interface {
|
|||
WithEndpoints
|
||||
WithTag
|
||||
WithPublished
|
||||
WithUpdated
|
||||
}
|
||||
|
||||
// Statusable represents the minimum activitypub interface for representing a 'status'.
|
||||
|
@ -196,6 +200,7 @@ type Statusable interface {
|
|||
WithName
|
||||
WithInReplyTo
|
||||
WithPublished
|
||||
WithUpdated
|
||||
WithURL
|
||||
WithAttributedTo
|
||||
WithTo
|
||||
|
|
93
internal/ap/interfaces_test.go
Normal file
93
internal/ap/interfaces_test.go
Normal file
|
@ -0,0 +1,93 @@
|
|||
// 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 ap_test
|
||||
|
||||
import (
|
||||
"github.com/superseriousbusiness/activity/streams/vocab"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||
)
|
||||
|
||||
var (
|
||||
// NOTE: the below aren't actually tests that are run,
|
||||
// we just move them into an _test.go file to declutter
|
||||
// the main interfaces.go file, which is already long.
|
||||
|
||||
// Compile-time checks for Activityable interface methods.
|
||||
_ ap.Activityable = (vocab.ActivityStreamsAccept)(nil)
|
||||
_ ap.Activityable = (vocab.ActivityStreamsTentativeAccept)(nil)
|
||||
_ ap.Activityable = (vocab.ActivityStreamsAdd)(nil)
|
||||
_ ap.Activityable = (vocab.ActivityStreamsCreate)(nil)
|
||||
_ ap.Activityable = (vocab.ActivityStreamsDelete)(nil)
|
||||
_ ap.Activityable = (vocab.ActivityStreamsFollow)(nil)
|
||||
_ ap.Activityable = (vocab.ActivityStreamsIgnore)(nil)
|
||||
_ ap.Activityable = (vocab.ActivityStreamsJoin)(nil)
|
||||
_ ap.Activityable = (vocab.ActivityStreamsLeave)(nil)
|
||||
_ ap.Activityable = (vocab.ActivityStreamsLike)(nil)
|
||||
_ ap.Activityable = (vocab.ActivityStreamsOffer)(nil)
|
||||
_ ap.Activityable = (vocab.ActivityStreamsInvite)(nil)
|
||||
_ ap.Activityable = (vocab.ActivityStreamsReject)(nil)
|
||||
_ ap.Activityable = (vocab.ActivityStreamsTentativeReject)(nil)
|
||||
_ ap.Activityable = (vocab.ActivityStreamsRemove)(nil)
|
||||
_ ap.Activityable = (vocab.ActivityStreamsUndo)(nil)
|
||||
_ ap.Activityable = (vocab.ActivityStreamsUpdate)(nil)
|
||||
_ ap.Activityable = (vocab.ActivityStreamsView)(nil)
|
||||
_ ap.Activityable = (vocab.ActivityStreamsListen)(nil)
|
||||
_ ap.Activityable = (vocab.ActivityStreamsRead)(nil)
|
||||
_ ap.Activityable = (vocab.ActivityStreamsMove)(nil)
|
||||
_ ap.Activityable = (vocab.ActivityStreamsAnnounce)(nil)
|
||||
_ ap.Activityable = (vocab.ActivityStreamsBlock)(nil)
|
||||
_ ap.Activityable = (vocab.ActivityStreamsFlag)(nil)
|
||||
_ ap.Activityable = (vocab.ActivityStreamsDislike)(nil)
|
||||
|
||||
// the below intransitive activities don't fit the interface definition because they're
|
||||
// missing an attached object (as the activity itself contains the details), but we don't
|
||||
// actually end up using them so it's simpler to just comment them out and not have to do
|
||||
// a WithObject{} interface check on every single incoming activity:
|
||||
//
|
||||
// _ Activityable = (vocab.ActivityStreamsArrive)(nil)
|
||||
// _ Activityable = (vocab.ActivityStreamsTravel)(nil)
|
||||
// _ Activityable = (vocab.ActivityStreamsQuestion)(nil)
|
||||
|
||||
// Compile-time checks for Accountable interface methods.
|
||||
_ ap.Accountable = (vocab.ActivityStreamsPerson)(nil)
|
||||
_ ap.Accountable = (vocab.ActivityStreamsApplication)(nil)
|
||||
_ ap.Accountable = (vocab.ActivityStreamsOrganization)(nil)
|
||||
_ ap.Accountable = (vocab.ActivityStreamsService)(nil)
|
||||
_ ap.Accountable = (vocab.ActivityStreamsGroup)(nil)
|
||||
|
||||
// Compile-time checks for Statusable interface methods.
|
||||
_ ap.Statusable = (vocab.ActivityStreamsArticle)(nil)
|
||||
_ ap.Statusable = (vocab.ActivityStreamsDocument)(nil)
|
||||
_ ap.Statusable = (vocab.ActivityStreamsImage)(nil)
|
||||
_ ap.Statusable = (vocab.ActivityStreamsVideo)(nil)
|
||||
_ ap.Statusable = (vocab.ActivityStreamsNote)(nil)
|
||||
_ ap.Statusable = (vocab.ActivityStreamsPage)(nil)
|
||||
_ ap.Statusable = (vocab.ActivityStreamsEvent)(nil)
|
||||
_ ap.Statusable = (vocab.ActivityStreamsPlace)(nil)
|
||||
_ ap.Statusable = (vocab.ActivityStreamsProfile)(nil)
|
||||
_ ap.Statusable = (vocab.ActivityStreamsQuestion)(nil)
|
||||
|
||||
// Compile-time checks for Pollable interface methods.
|
||||
_ ap.Pollable = (vocab.ActivityStreamsQuestion)(nil)
|
||||
|
||||
// Compile-time checks for PollOptionable interface methods.
|
||||
_ ap.PollOptionable = (vocab.ActivityStreamsNote)(nil)
|
||||
|
||||
// Compile-time checks for Acceptable interface methods.
|
||||
_ ap.Acceptable = (vocab.ActivityStreamsAccept)(nil)
|
||||
)
|
|
@ -408,6 +408,25 @@ func SetPublished(with WithPublished, published time.Time) {
|
|||
publishProp.Set(published)
|
||||
}
|
||||
|
||||
// GetUpdated returns the time contained in the Updated property of 'with'.
|
||||
func GetUpdated(with WithUpdated) time.Time {
|
||||
updateProp := with.GetActivityStreamsUpdated()
|
||||
if updateProp == nil || !updateProp.IsXMLSchemaDateTime() {
|
||||
return time.Time{}
|
||||
}
|
||||
return updateProp.Get()
|
||||
}
|
||||
|
||||
// SetUpdated sets the given time on the Updated property of 'with'.
|
||||
func SetUpdated(with WithUpdated, updated time.Time) {
|
||||
updateProp := with.GetActivityStreamsUpdated()
|
||||
if updateProp == nil {
|
||||
updateProp = streams.NewActivityStreamsUpdatedProperty()
|
||||
with.SetActivityStreamsUpdated(updateProp)
|
||||
}
|
||||
updateProp.Set(updated)
|
||||
}
|
||||
|
||||
// GetEndTime returns the time contained in the EndTime property of 'with'.
|
||||
func GetEndTime(with WithEndTime) time.Time {
|
||||
endTimeProp := with.GetActivityStreamsEndTime()
|
||||
|
|
|
@ -82,7 +82,7 @@ func (suite *OutboxGetTestSuite) TestGetOutbox() {
|
|||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"first": "http://localhost:8080/users/the_mighty_zork/outbox?limit=40",
|
||||
"id": "http://localhost:8080/users/the_mighty_zork/outbox",
|
||||
"totalItems": 8,
|
||||
"totalItems": 9,
|
||||
"type": "OrderedCollection"
|
||||
}`, dst.String())
|
||||
|
||||
|
@ -142,6 +142,14 @@ func (suite *OutboxGetTestSuite) TestGetOutboxFirstPage() {
|
|||
"id": "http://localhost:8080/users/the_mighty_zork/outbox?limit=40",
|
||||
"next": "http://localhost:8080/users/the_mighty_zork/outbox?limit=40\u0026max_id=01F8MHAMCHF6Y650WCRSCP4WMY",
|
||||
"orderedItems": [
|
||||
{
|
||||
"actor": "http://localhost:8080/users/the_mighty_zork",
|
||||
"cc": "http://localhost:8080/users/the_mighty_zork/followers",
|
||||
"id": "http://localhost:8080/users/the_mighty_zork/statuses/01JDPZC707CKDN8N4QVWM4Z1NR/activity#Create",
|
||||
"object": "http://localhost:8080/users/the_mighty_zork/statuses/01JDPZC707CKDN8N4QVWM4Z1NR",
|
||||
"to": "https://www.w3.org/ns/activitystreams#Public",
|
||||
"type": "Create"
|
||||
},
|
||||
{
|
||||
"actor": "http://localhost:8080/users/the_mighty_zork",
|
||||
"cc": "http://localhost:8080/users/the_mighty_zork/followers",
|
||||
|
@ -160,8 +168,8 @@ func (suite *OutboxGetTestSuite) TestGetOutboxFirstPage() {
|
|||
}
|
||||
],
|
||||
"partOf": "http://localhost:8080/users/the_mighty_zork/outbox",
|
||||
"prev": "http://localhost:8080/users/the_mighty_zork/outbox?limit=40\u0026min_id=01HH9KYNQPA416TNJ53NSATP40",
|
||||
"totalItems": 8,
|
||||
"prev": "http://localhost:8080/users/the_mighty_zork/outbox?limit=40\u0026min_id=01JDPZC707CKDN8N4QVWM4Z1NR",
|
||||
"totalItems": 9,
|
||||
"type": "OrderedCollectionPage"
|
||||
}`, dst.String())
|
||||
|
||||
|
@ -224,7 +232,7 @@ func (suite *OutboxGetTestSuite) TestGetOutboxNextPage() {
|
|||
"id": "http://localhost:8080/users/the_mighty_zork/outbox?limit=40&max_id=01F8MHAMCHF6Y650WCRSCP4WMY",
|
||||
"orderedItems": [],
|
||||
"partOf": "http://localhost:8080/users/the_mighty_zork/outbox",
|
||||
"totalItems": 8,
|
||||
"totalItems": 9,
|
||||
"type": "OrderedCollectionPage"
|
||||
}`, dst.String())
|
||||
|
||||
|
|
|
@ -97,7 +97,7 @@ func (suite *AccountVerifyTestSuite) TestAccountVerifyGet() {
|
|||
suite.Equal("http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.webp", apimodelAccount.HeaderStatic)
|
||||
suite.Equal(2, apimodelAccount.FollowersCount)
|
||||
suite.Equal(2, apimodelAccount.FollowingCount)
|
||||
suite.Equal(8, apimodelAccount.StatusesCount)
|
||||
suite.Equal(9, apimodelAccount.StatusesCount)
|
||||
suite.EqualValues(apimodel.VisibilityPublic, apimodelAccount.Source.Privacy)
|
||||
suite.Equal(testAccount.Settings.Language, apimodelAccount.Source.Language)
|
||||
suite.Equal(testAccount.NoteRaw, apimodelAccount.Source.Note)
|
||||
|
|
|
@ -99,8 +99,8 @@ func (suite *AccountsGetTestSuite) TestAccountsGetFromTop() {
|
|||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 1,
|
||||
"following_count": 1,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2021-07-28",
|
||||
"statuses_count": 9,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": [
|
||||
{
|
||||
|
@ -262,8 +262,8 @@ func (suite *AccountsGetTestSuite) TestAccountsGetFromTop() {
|
|||
"header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q",
|
||||
"followers_count": 2,
|
||||
"following_count": 2,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2024-01-10",
|
||||
"statuses_count": 9,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": [],
|
||||
"enable_rss": true
|
||||
|
@ -403,8 +403,8 @@ func (suite *AccountsGetTestSuite) TestAccountsGetFromTop() {
|
|||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 3,
|
||||
"last_status_at": "2021-09-11",
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
|
|
|
@ -186,8 +186,8 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() {
|
|||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 3,
|
||||
"last_status_at": "2021-09-11",
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
|
@ -232,8 +232,8 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() {
|
|||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 1,
|
||||
"following_count": 1,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2021-07-28",
|
||||
"statuses_count": 9,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": [
|
||||
{
|
||||
|
@ -414,8 +414,8 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() {
|
|||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 1,
|
||||
"following_count": 1,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2021-07-28",
|
||||
"statuses_count": 9,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": [
|
||||
{
|
||||
|
@ -473,8 +473,8 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() {
|
|||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 3,
|
||||
"last_status_at": "2021-09-11",
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
|
@ -485,6 +485,7 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() {
|
|||
{
|
||||
"id": "01FVW7JHQFSFK166WWKR8CBA6M",
|
||||
"created_at": "2021-09-20T10:40:37.000Z",
|
||||
"edited_at": null,
|
||||
"in_reply_to_id": null,
|
||||
"in_reply_to_account_id": null,
|
||||
"sensitive": false,
|
||||
|
@ -521,8 +522,8 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() {
|
|||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 3,
|
||||
"last_status_at": "2021-09-11",
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
},
|
||||
|
@ -667,8 +668,8 @@ func (suite *ReportsGetTestSuite) TestReportsGetCreatedByAccount() {
|
|||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 1,
|
||||
"following_count": 1,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2021-07-28",
|
||||
"statuses_count": 9,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": [
|
||||
{
|
||||
|
@ -726,8 +727,8 @@ func (suite *ReportsGetTestSuite) TestReportsGetCreatedByAccount() {
|
|||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 3,
|
||||
"last_status_at": "2021-09-11",
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
|
@ -738,6 +739,7 @@ func (suite *ReportsGetTestSuite) TestReportsGetCreatedByAccount() {
|
|||
{
|
||||
"id": "01FVW7JHQFSFK166WWKR8CBA6M",
|
||||
"created_at": "2021-09-20T10:40:37.000Z",
|
||||
"edited_at": null,
|
||||
"in_reply_to_id": null,
|
||||
"in_reply_to_account_id": null,
|
||||
"sensitive": false,
|
||||
|
@ -774,8 +776,8 @@ func (suite *ReportsGetTestSuite) TestReportsGetCreatedByAccount() {
|
|||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 3,
|
||||
"last_status_at": "2021-09-11",
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
},
|
||||
|
@ -920,8 +922,8 @@ func (suite *ReportsGetTestSuite) TestReportsGetTargetAccount() {
|
|||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 1,
|
||||
"following_count": 1,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2021-07-28",
|
||||
"statuses_count": 9,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": [
|
||||
{
|
||||
|
@ -979,8 +981,8 @@ func (suite *ReportsGetTestSuite) TestReportsGetTargetAccount() {
|
|||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 3,
|
||||
"last_status_at": "2021-09-11",
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
|
@ -991,6 +993,7 @@ func (suite *ReportsGetTestSuite) TestReportsGetTargetAccount() {
|
|||
{
|
||||
"id": "01FVW7JHQFSFK166WWKR8CBA6M",
|
||||
"created_at": "2021-09-20T10:40:37.000Z",
|
||||
"edited_at": null,
|
||||
"in_reply_to_id": null,
|
||||
"in_reply_to_account_id": null,
|
||||
"sensitive": false,
|
||||
|
@ -1027,8 +1030,8 @@ func (suite *ReportsGetTestSuite) TestReportsGetTargetAccount() {
|
|||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 3,
|
||||
"last_status_at": "2021-09-11",
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
},
|
||||
|
|
|
@ -229,7 +229,7 @@ Cool Ass Posters From This Instance,admin@localhost:8080
|
|||
"media_storage": "",
|
||||
"followers_count": 2,
|
||||
"following_count": 2,
|
||||
"statuses_count": 8,
|
||||
"statuses_count": 9,
|
||||
"lists_count": 1,
|
||||
"blocks_count": 0,
|
||||
"mutes_count": 0
|
||||
|
|
|
@ -155,7 +155,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch1() {
|
|||
},
|
||||
"stats": {
|
||||
"domain_count": 2,
|
||||
"status_count": 19,
|
||||
"status_count": 21,
|
||||
"user_count": 4
|
||||
},
|
||||
"thumbnail": "http://localhost:8080/assets/logo.webp",
|
||||
|
@ -296,7 +296,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch2() {
|
|||
},
|
||||
"stats": {
|
||||
"domain_count": 2,
|
||||
"status_count": 19,
|
||||
"status_count": 21,
|
||||
"user_count": 4
|
||||
},
|
||||
"thumbnail": "http://localhost:8080/assets/logo.webp",
|
||||
|
@ -437,7 +437,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() {
|
|||
},
|
||||
"stats": {
|
||||
"domain_count": 2,
|
||||
"status_count": 19,
|
||||
"status_count": 21,
|
||||
"user_count": 4
|
||||
},
|
||||
"thumbnail": "http://localhost:8080/assets/logo.webp",
|
||||
|
@ -629,7 +629,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch6() {
|
|||
},
|
||||
"stats": {
|
||||
"domain_count": 2,
|
||||
"status_count": 19,
|
||||
"status_count": 21,
|
||||
"user_count": 4
|
||||
},
|
||||
"thumbnail": "http://localhost:8080/assets/logo.webp",
|
||||
|
@ -792,7 +792,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch8() {
|
|||
},
|
||||
"stats": {
|
||||
"domain_count": 2,
|
||||
"status_count": 19,
|
||||
"status_count": 21,
|
||||
"user_count": 4
|
||||
},
|
||||
"thumbnail": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/attachment/original/`+instanceAccount.AvatarMediaAttachment.ID+`.gif",`+`
|
||||
|
@ -974,7 +974,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch9() {
|
|||
},
|
||||
"stats": {
|
||||
"domain_count": 2,
|
||||
"status_count": 19,
|
||||
"status_count": 21,
|
||||
"user_count": 4
|
||||
},
|
||||
"thumbnail": "http://localhost:8080/assets/logo.webp",
|
||||
|
|
|
@ -148,7 +148,7 @@ func (suite *MutesTestSuite) TestIndefinitelyMutedAccountSerializesMuteExpiratio
|
|||
|
||||
// Fetch all muted accounts for the logged-in account.
|
||||
// The expected body contains `"mute_expires_at":null`.
|
||||
_, err = suite.getMutedAccounts(http.StatusOK, `[{"id":"01F8MH5ZK5VRH73AKHQM6Y9VNX","username":"foss_satan","acct":"foss_satan@fossbros-anonymous.io","display_name":"big gerald","locked":false,"discoverable":true,"bot":false,"created_at":"2021-09-26T10:52:36.000Z","note":"i post about like, i dunno, stuff, or whatever!!!!","url":"http://fossbros-anonymous.io/@foss_satan","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.webp","header_static":"http://localhost:8080/assets/default_header.webp","header_description":"Flat gray background (default header).","followers_count":0,"following_count":0,"statuses_count":3,"last_status_at":"2021-09-11","emojis":[],"fields":[],"mute_expires_at":null}]`)
|
||||
_, err = suite.getMutedAccounts(http.StatusOK, `[{"id":"01F8MH5ZK5VRH73AKHQM6Y9VNX","username":"foss_satan","acct":"foss_satan@fossbros-anonymous.io","display_name":"big gerald","locked":false,"discoverable":true,"bot":false,"created_at":"2021-09-26T10:52:36.000Z","note":"i post about like, i dunno, stuff, or whatever!!!!","url":"http://fossbros-anonymous.io/@foss_satan","avatar":"","avatar_static":"","header":"http://localhost:8080/assets/default_header.webp","header_static":"http://localhost:8080/assets/default_header.webp","header_description":"Flat gray background (default header).","followers_count":0,"following_count":0,"statuses_count":4,"last_status_at":"2024-11-01","emojis":[],"fields":[],"mute_expires_at":null}]`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
|
@ -130,8 +130,8 @@ func (suite *ReportGetTestSuite) TestGetReport1() {
|
|||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 3,
|
||||
"last_status_at": "2021-09-11",
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
|
|
|
@ -156,8 +156,8 @@ func (suite *ReportsGetTestSuite) TestGetReports() {
|
|||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 3,
|
||||
"last_status_at": "2021-09-11",
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
|
@ -247,8 +247,8 @@ func (suite *ReportsGetTestSuite) TestGetReports4() {
|
|||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 3,
|
||||
"last_status_at": "2021-09-11",
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
|
@ -322,8 +322,8 @@ func (suite *ReportsGetTestSuite) TestGetReports6() {
|
|||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 3,
|
||||
"last_status_at": "2021-09-11",
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
|
@ -381,8 +381,8 @@ func (suite *ReportsGetTestSuite) TestGetReports7() {
|
|||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 3,
|
||||
"last_status_at": "2021-09-11",
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
|
|
|
@ -916,7 +916,7 @@ func (suite *SearchGetTestSuite) TestSearchAAny() {
|
|||
}
|
||||
|
||||
suite.Len(searchResult.Accounts, 5)
|
||||
suite.Len(searchResult.Statuses, 7)
|
||||
suite.Len(searchResult.Statuses, 8)
|
||||
suite.Len(searchResult.Hashtags, 0)
|
||||
}
|
||||
|
||||
|
@ -959,7 +959,7 @@ func (suite *SearchGetTestSuite) TestSearchAAnyFollowingOnly() {
|
|||
}
|
||||
|
||||
suite.Len(searchResult.Accounts, 2)
|
||||
suite.Len(searchResult.Statuses, 7)
|
||||
suite.Len(searchResult.Statuses, 8)
|
||||
suite.Len(searchResult.Hashtags, 0)
|
||||
}
|
||||
|
||||
|
@ -1002,7 +1002,7 @@ func (suite *SearchGetTestSuite) TestSearchAStatuses() {
|
|||
}
|
||||
|
||||
suite.Len(searchResult.Accounts, 0)
|
||||
suite.Len(searchResult.Statuses, 7)
|
||||
suite.Len(searchResult.Statuses, 8)
|
||||
suite.Len(searchResult.Hashtags, 0)
|
||||
}
|
||||
|
||||
|
|
|
@ -100,6 +100,7 @@ func (suite *StatusBoostTestSuite) TestPostBoost() {
|
|||
"card": null,
|
||||
"content": "",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": true,
|
||||
"favourites_count": 0,
|
||||
|
@ -145,6 +146,7 @@ func (suite *StatusBoostTestSuite) TestPostBoost() {
|
|||
"card": null,
|
||||
"content": "hello world! #welcome ! first post on the instance :rainbow: !",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [
|
||||
{
|
||||
"category": "reactions",
|
||||
|
@ -280,6 +282,7 @@ func (suite *StatusBoostTestSuite) TestPostBoostOwnFollowersOnly() {
|
|||
"card": null,
|
||||
"content": "",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": false,
|
||||
"favourites_count": 0,
|
||||
|
@ -329,6 +332,7 @@ func (suite *StatusBoostTestSuite) TestPostBoostOwnFollowersOnly() {
|
|||
"card": null,
|
||||
"content": "hi!",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": false,
|
||||
"favourites_count": 0,
|
||||
|
@ -494,6 +498,7 @@ func (suite *StatusBoostTestSuite) TestPostBoostImplicitAccept() {
|
|||
"card": null,
|
||||
"content": "",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": false,
|
||||
"favourites_count": 0,
|
||||
|
@ -539,6 +544,7 @@ func (suite *StatusBoostTestSuite) TestPostBoostImplicitAccept() {
|
|||
"card": null,
|
||||
"content": "<p>Hi <span class=\"h-card\"><a href=\"http://localhost:8080/@1happyturtle\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>1happyturtle</span></a></span>, can I reply?</p>",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": false,
|
||||
"favourites_count": 0,
|
||||
|
|
|
@ -102,6 +102,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatus() {
|
|||
"card": null,
|
||||
"content": "<p>this is a brand new status! <a href=\"http://localhost:8080/tags/helloworld\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>helloworld</span></a></p>",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": false,
|
||||
"favourites_count": 0,
|
||||
|
@ -187,6 +188,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusIntPolicy() {
|
|||
"card": null,
|
||||
"content": "<p>this is a brand new status! <a href=\"http://localhost:8080/tags/helloworld\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>helloworld</span></a></p>",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": false,
|
||||
"favourites_count": 0,
|
||||
|
@ -282,6 +284,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusIntPolicyJSON() {
|
|||
"card": null,
|
||||
"content": "<p>this is a brand new status! <a href=\"http://localhost:8080/tags/helloworld\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>helloworld</span></a></p>",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": false,
|
||||
"favourites_count": 0,
|
||||
|
@ -407,6 +410,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusMarkdown() {
|
|||
"card": null,
|
||||
"content": "<h1>Title</h1><h2>Smaller title</h2><p>This is a post written in <a href=\"https://www.markdownguide.org/\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">markdown</a></p>",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": false,
|
||||
"favourites_count": 0,
|
||||
|
@ -490,6 +494,7 @@ func (suite *StatusCreateTestSuite) TestMentionUnknownAccount() {
|
|||
"card": null,
|
||||
"content": "<p>hello <span class=\"h-card\"><a href=\"https://unknown-instance.com/@brand_new_person\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>brand_new_person</span></a></span></p>",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": false,
|
||||
"favourites_count": 0,
|
||||
|
@ -567,6 +572,7 @@ func (suite *StatusCreateTestSuite) TestPostStatusWithLinksAndTags() {
|
|||
"card": null,
|
||||
"content": "<p><a href=\"http://localhost:8080/tags/test\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>test</span></a> alright, should be able to post <a href=\"http://localhost:8080/tags/links\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>links</span></a> with fragments in them now, let's see........<br><br><a href=\"https://docs.gotosocial.org/en/latest/user_guide/posts/#links\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">https://docs.gotosocial.org/en/latest/user_guide/posts/#links</a><br><br><a href=\"http://localhost:8080/tags/gotosocial\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>gotosocial</span></a><br><br>(tobi remember to pull the docker image challenge)</p>",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": false,
|
||||
"favourites_count": 0,
|
||||
|
@ -650,6 +656,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusWithEmoji() {
|
|||
"card": null,
|
||||
"content": "<p>here is a rainbow emoji a few times! :rainbow: :rainbow: :rainbow:<br>here's an emoji that isn't in the db: :test_emoji:</p>",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [
|
||||
{
|
||||
"category": "reactions",
|
||||
|
@ -747,6 +754,7 @@ func (suite *StatusCreateTestSuite) TestReplyToLocalStatus() {
|
|||
"card": null,
|
||||
"content": "<p>hello <span class=\"h-card\"><a href=\"http://localhost:8080/@1happyturtle\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>1happyturtle</span></a></span> this reply should work!</p>",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": false,
|
||||
"favourites_count": 0,
|
||||
|
@ -829,6 +837,7 @@ func (suite *StatusCreateTestSuite) TestAttachNewMediaSuccess() {
|
|||
"card": null,
|
||||
"content": "<p>here's an image attachment</p>",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": false,
|
||||
"favourites_count": 0,
|
||||
|
@ -933,6 +942,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusWithNoncanonicalLanguageTag
|
|||
"card": null,
|
||||
"content": "<p>English? what's English? i speak American</p>",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": false,
|
||||
"favourites_count": 0,
|
||||
|
@ -1007,6 +1017,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusWithPollForm() {
|
|||
"card": null,
|
||||
"content": "<p>this is a status with a poll!</p>",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": false,
|
||||
"favourites_count": 0,
|
||||
|
@ -1103,6 +1114,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusWithPollJSON() {
|
|||
"card": null,
|
||||
"content": "<p>this is a status with a poll!</p>",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": false,
|
||||
"favourites_count": 0,
|
||||
|
|
|
@ -105,6 +105,7 @@ func (suite *StatusFaveTestSuite) TestPostFave() {
|
|||
"card": null,
|
||||
"content": "🐕🐕🐕🐕🐕",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": true,
|
||||
"favourites_count": 1,
|
||||
|
@ -228,6 +229,7 @@ func (suite *StatusFaveTestSuite) TestPostFaveImplicitAccept() {
|
|||
"card": null,
|
||||
"content": "<p>Hi <span class=\"h-card\"><a href=\"http://localhost:8080/@1happyturtle\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>1happyturtle</span></a></span>, can I reply?</p>",
|
||||
"created_at": "right the hell just now babyee",
|
||||
"edited_at": null,
|
||||
"emojis": [],
|
||||
"favourited": true,
|
||||
"favourites_count": 1,
|
||||
|
|
|
@ -116,8 +116,8 @@ func (suite *StatusHistoryTestSuite) TestGetHistory() {
|
|||
"header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q",
|
||||
"followers_count": 2,
|
||||
"following_count": 2,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2024-01-10",
|
||||
"statuses_count": 9,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": [],
|
||||
"enable_rss": true
|
||||
|
|
|
@ -91,6 +91,7 @@ func (suite *StatusMuteTestSuite) TestMuteUnmuteStatus() {
|
|||
suite.Equal(`{
|
||||
"id": "01F8MHAMCHF6Y650WCRSCP4WMY",
|
||||
"created_at": "2021-10-20T10:40:37.000Z",
|
||||
"edited_at": null,
|
||||
"in_reply_to_id": null,
|
||||
"in_reply_to_account_id": null,
|
||||
"sensitive": true,
|
||||
|
@ -134,8 +135,8 @@ func (suite *StatusMuteTestSuite) TestMuteUnmuteStatus() {
|
|||
"header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q",
|
||||
"followers_count": 2,
|
||||
"following_count": 2,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2024-01-10",
|
||||
"statuses_count": 9,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": [],
|
||||
"enable_rss": true
|
||||
|
@ -178,6 +179,7 @@ func (suite *StatusMuteTestSuite) TestMuteUnmuteStatus() {
|
|||
suite.Equal(`{
|
||||
"id": "01F8MHAMCHF6Y650WCRSCP4WMY",
|
||||
"created_at": "2021-10-20T10:40:37.000Z",
|
||||
"edited_at": null,
|
||||
"in_reply_to_id": null,
|
||||
"in_reply_to_account_id": null,
|
||||
"sensitive": true,
|
||||
|
@ -221,8 +223,8 @@ func (suite *StatusMuteTestSuite) TestMuteUnmuteStatus() {
|
|||
"header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q",
|
||||
"followers_count": 2,
|
||||
"following_count": 2,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2024-01-10",
|
||||
"statuses_count": 9,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": [],
|
||||
"enable_rss": true
|
||||
|
|
|
@ -19,7 +19,6 @@ package model
|
|||
|
||||
import (
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/storage"
|
||||
)
|
||||
|
@ -30,8 +29,6 @@ type Content struct {
|
|||
ContentType string
|
||||
// ContentLength in bytes
|
||||
ContentLength int64
|
||||
// Time when the content was last updated.
|
||||
ContentUpdated time.Time
|
||||
// Actual content
|
||||
Content io.ReadCloser
|
||||
// Resource URL to forward to if the file can be fetched from the storage directly (e.g signed S3 URL)
|
||||
|
|
|
@ -29,6 +29,10 @@ type Status struct {
|
|||
// The date when this status was created (ISO 8601 Datetime).
|
||||
// example: 2021-07-30T09:20:25+00:00
|
||||
CreatedAt string `json:"created_at"`
|
||||
// Timestamp of when the status was last edited (ISO 8601 Datetime).
|
||||
// example: 2021-07-30T09:20:25+00:00
|
||||
// nullable: true
|
||||
EditedAt *string `json:"edited_at"`
|
||||
// ID of the status being replied to.
|
||||
// example: 01FBVD42CQ3ZEEVMW180SBX03B
|
||||
// nullable: true
|
||||
|
|
1
internal/cache/cache.go
vendored
1
internal/cache/cache.go
vendored
|
@ -105,6 +105,7 @@ func (c *Caches) Init() {
|
|||
c.initStatus()
|
||||
c.initStatusBookmark()
|
||||
c.initStatusBookmarkIDs()
|
||||
c.initStatusEdit()
|
||||
c.initStatusFave()
|
||||
c.initStatusFaveIDs()
|
||||
c.initTag()
|
||||
|
|
35
internal/cache/db.go
vendored
35
internal/cache/db.go
vendored
|
@ -226,6 +226,9 @@ type DBCaches struct {
|
|||
// StatusBookmarkIDs provides access to the status bookmark IDs list database cache.
|
||||
StatusBookmarkIDs SliceCache[string]
|
||||
|
||||
// StatusEdit provides access to the gtsmodel StatusEdit database cache.
|
||||
StatusEdit StructCache[*gtsmodel.StatusEdit]
|
||||
|
||||
// StatusFave provides access to the gtsmodel StatusFave database cache.
|
||||
StatusFave StructCache[*gtsmodel.StatusFave]
|
||||
|
||||
|
@ -1385,6 +1388,38 @@ func (c *Caches) initStatusBookmarkIDs() {
|
|||
c.DB.StatusBookmarkIDs.Init(0, cap)
|
||||
}
|
||||
|
||||
func (c *Caches) initStatusEdit() {
|
||||
// Calculate maximum cache size.
|
||||
cap := calculateResultCacheMax(
|
||||
sizeofStatusEdit(), // model in-mem size.
|
||||
config.GetCacheStatusEditMemRatio(),
|
||||
)
|
||||
|
||||
log.Infof(nil, "cache size = %d", cap)
|
||||
|
||||
copyF := func(s1 *gtsmodel.StatusEdit) *gtsmodel.StatusEdit {
|
||||
s2 := new(gtsmodel.StatusEdit)
|
||||
*s2 = *s1
|
||||
|
||||
// Don't include ptr fields that
|
||||
// will be populated separately.
|
||||
s2.Attachments = nil
|
||||
|
||||
return s2
|
||||
}
|
||||
|
||||
c.DB.StatusEdit.Init(structr.CacheConfig[*gtsmodel.StatusEdit]{
|
||||
Indices: []structr.IndexConfig{
|
||||
{Fields: "ID"},
|
||||
{Fields: "StatusID", Multiple: true},
|
||||
},
|
||||
MaxSize: cap,
|
||||
IgnoreErr: ignoreErrors,
|
||||
Copy: copyF,
|
||||
Invalidate: c.OnInvalidateStatusEdit,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Caches) initStatusFave() {
|
||||
// Calculate maximum cache size.
|
||||
cap := calculateResultCacheMax(
|
||||
|
|
5
internal/cache/invalidate.go
vendored
5
internal/cache/invalidate.go
vendored
|
@ -273,6 +273,11 @@ func (c *Caches) OnInvalidateStatusBookmark(bookmark *gtsmodel.StatusBookmark) {
|
|||
c.DB.StatusBookmarkIDs.Invalidate(bookmark.StatusID)
|
||||
}
|
||||
|
||||
func (c *Caches) OnInvalidateStatusEdit(edit *gtsmodel.StatusEdit) {
|
||||
// Invalidate cache of related status model.
|
||||
c.DB.Status.Invalidate("ID", edit.StatusID)
|
||||
}
|
||||
|
||||
func (c *Caches) OnInvalidateStatusFave(fave *gtsmodel.StatusFave) {
|
||||
// Invalidate status fave ID list for this status.
|
||||
c.DB.StatusFaveIDs.Invalidate(fave.StatusID)
|
||||
|
|
19
internal/cache/size.go
vendored
19
internal/cache/size.go
vendored
|
@ -505,7 +505,6 @@ func sizeofMedia() uintptr {
|
|||
URL: exampleURI,
|
||||
RemoteURL: exampleURI,
|
||||
CreatedAt: exampleTime,
|
||||
UpdatedAt: exampleTime,
|
||||
Type: gtsmodel.FileTypeImage,
|
||||
AccountID: exampleID,
|
||||
Description: exampleText,
|
||||
|
@ -532,7 +531,6 @@ func sizeofMention() uintptr {
|
|||
ID: exampleURI,
|
||||
StatusID: exampleURI,
|
||||
CreatedAt: exampleTime,
|
||||
UpdatedAt: exampleTime,
|
||||
OriginAccountID: exampleURI,
|
||||
OriginAccountURI: exampleURI,
|
||||
TargetAccountID: exampleID,
|
||||
|
@ -674,6 +672,23 @@ func sizeofStatusBookmark() uintptr {
|
|||
}))
|
||||
}
|
||||
|
||||
func sizeofStatusEdit() uintptr {
|
||||
return uintptr(size.Of(>smodel.StatusEdit{
|
||||
ID: exampleID,
|
||||
Content: exampleText,
|
||||
ContentWarning: exampleUsername, // similar length
|
||||
Text: exampleText,
|
||||
Language: "en",
|
||||
Sensitive: func() *bool { ok := false; return &ok }(),
|
||||
AttachmentIDs: []string{exampleID, exampleID, exampleID},
|
||||
Attachments: nil,
|
||||
PollOptions: []string{exampleTextSmall, exampleTextSmall, exampleTextSmall, exampleTextSmall},
|
||||
PollVotes: []int{69, 420, 1337, 1969},
|
||||
StatusID: exampleID,
|
||||
CreatedAt: exampleTime,
|
||||
}))
|
||||
}
|
||||
|
||||
func sizeofStatusFave() uintptr {
|
||||
return uintptr(size.Of(>smodel.StatusFave{
|
||||
ID: exampleID,
|
||||
|
|
|
@ -238,6 +238,7 @@ type CacheConfiguration struct {
|
|||
StatusMemRatio float64 `name:"status-mem-ratio"`
|
||||
StatusBookmarkMemRatio float64 `name:"status-bookmark-mem-ratio"`
|
||||
StatusBookmarkIDsMemRatio float64 `name:"status-bookmark-ids-mem-ratio"`
|
||||
StatusEditMemRatio float64 `name:"status-edit-mem-ratio"`
|
||||
StatusFaveMemRatio float64 `name:"status-fave-mem-ratio"`
|
||||
StatusFaveIDsMemRatio float64 `name:"status-fave-ids-mem-ratio"`
|
||||
TagMemRatio float64 `name:"tag-mem-ratio"`
|
||||
|
|
|
@ -199,6 +199,7 @@ var Defaults = Configuration{
|
|||
StatusMemRatio: 5,
|
||||
StatusBookmarkMemRatio: 0.5,
|
||||
StatusBookmarkIDsMemRatio: 2,
|
||||
StatusEditMemRatio: 2,
|
||||
StatusFaveMemRatio: 2,
|
||||
StatusFaveIDsMemRatio: 3,
|
||||
TagMemRatio: 2,
|
||||
|
|
|
@ -3912,6 +3912,31 @@ func GetCacheStatusBookmarkIDsMemRatio() float64 { return global.GetCacheStatusB
|
|||
// SetCacheStatusBookmarkIDsMemRatio safely sets the value for global configuration 'Cache.StatusBookmarkIDsMemRatio' field
|
||||
func SetCacheStatusBookmarkIDsMemRatio(v float64) { global.SetCacheStatusBookmarkIDsMemRatio(v) }
|
||||
|
||||
// GetCacheStatusEditMemRatio safely fetches the Configuration value for state's 'Cache.StatusEditMemRatio' field
|
||||
func (st *ConfigState) GetCacheStatusEditMemRatio() (v float64) {
|
||||
st.mutex.RLock()
|
||||
v = st.config.Cache.StatusEditMemRatio
|
||||
st.mutex.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
// SetCacheStatusEditMemRatio safely sets the Configuration value for state's 'Cache.StatusEditMemRatio' field
|
||||
func (st *ConfigState) SetCacheStatusEditMemRatio(v float64) {
|
||||
st.mutex.Lock()
|
||||
defer st.mutex.Unlock()
|
||||
st.config.Cache.StatusEditMemRatio = v
|
||||
st.reloadToViper()
|
||||
}
|
||||
|
||||
// CacheStatusEditMemRatioFlag returns the flag name for the 'Cache.StatusEditMemRatio' field
|
||||
func CacheStatusEditMemRatioFlag() string { return "cache-status-edit-mem-ratio" }
|
||||
|
||||
// GetCacheStatusEditMemRatio safely fetches the value for global configuration 'Cache.StatusEditMemRatio' field
|
||||
func GetCacheStatusEditMemRatio() float64 { return global.GetCacheStatusEditMemRatio() }
|
||||
|
||||
// SetCacheStatusEditMemRatio safely sets the value for global configuration 'Cache.StatusEditMemRatio' field
|
||||
func SetCacheStatusEditMemRatio(v float64) { global.SetCacheStatusEditMemRatio(v) }
|
||||
|
||||
// GetCacheStatusFaveMemRatio safely fetches the Configuration value for state's 'Cache.StatusFaveMemRatio' field
|
||||
func (st *ConfigState) GetCacheStatusFaveMemRatio() (v float64) {
|
||||
st.mutex.RLock()
|
||||
|
|
|
@ -46,7 +46,7 @@ type AccountTestSuite struct {
|
|||
func (suite *AccountTestSuite) TestGetAccountStatuses() {
|
||||
statuses, err := suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 20, false, false, "", "", false, false)
|
||||
suite.NoError(err)
|
||||
suite.Len(statuses, 8)
|
||||
suite.Len(statuses, 9)
|
||||
}
|
||||
|
||||
func (suite *AccountTestSuite) TestGetAccountStatusesPageDown() {
|
||||
|
@ -69,7 +69,7 @@ func (suite *AccountTestSuite) TestGetAccountStatusesPageDown() {
|
|||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
suite.Len(statuses, 2)
|
||||
suite.Len(statuses, 3)
|
||||
|
||||
// try to get the last page (should be empty)
|
||||
statuses, err = suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 3, false, false, statuses[len(statuses)-1].ID, "", false, false)
|
||||
|
@ -80,13 +80,13 @@ func (suite *AccountTestSuite) TestGetAccountStatusesPageDown() {
|
|||
func (suite *AccountTestSuite) TestGetAccountStatusesExcludeRepliesAndReblogs() {
|
||||
statuses, err := suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 20, true, true, "", "", false, false)
|
||||
suite.NoError(err)
|
||||
suite.Len(statuses, 7)
|
||||
suite.Len(statuses, 8)
|
||||
}
|
||||
|
||||
func (suite *AccountTestSuite) TestGetAccountStatusesExcludeRepliesAndReblogsPublicOnly() {
|
||||
statuses, err := suite.db.GetAccountStatuses(context.Background(), suite.testAccounts["local_account_1"].ID, 20, true, true, "", "", false, true)
|
||||
suite.NoError(err)
|
||||
suite.Len(statuses, 3)
|
||||
suite.Len(statuses, 4)
|
||||
}
|
||||
|
||||
// populateTestStatus adds mandatory fields to a partially populated status.
|
||||
|
@ -173,7 +173,7 @@ func (suite *AccountTestSuite) TestGetAccountStatusesExcludeRepliesExcludesSelfR
|
|||
testAccount := suite.testAccounts["local_account_1"]
|
||||
statuses, err := suite.db.GetAccountStatuses(context.Background(), testAccount.ID, 20, true, true, "", "", false, false)
|
||||
suite.NoError(err)
|
||||
suite.Len(statuses, 8)
|
||||
suite.Len(statuses, 9)
|
||||
for _, status := range statuses {
|
||||
if status.InReplyToID != "" && status.InReplyToAccountID != testAccount.ID {
|
||||
suite.FailNowf("", "Status with ID %s is a non-self reply and should have been excluded", status.ID)
|
||||
|
|
|
@ -114,7 +114,7 @@ func (suite *BasicTestSuite) TestGetAllStatuses() {
|
|||
s := []*gtsmodel.Status{}
|
||||
err := suite.db.GetAll(context.Background(), &s)
|
||||
suite.NoError(err)
|
||||
suite.Len(s, 25)
|
||||
suite.Len(s, 28)
|
||||
}
|
||||
|
||||
func (suite *BasicTestSuite) TestGetAllNotNull() {
|
||||
|
|
|
@ -81,6 +81,7 @@ type DBService struct {
|
|||
db.SinBinStatus
|
||||
db.Status
|
||||
db.StatusBookmark
|
||||
db.StatusEdit
|
||||
db.StatusFave
|
||||
db.Tag
|
||||
db.Thread
|
||||
|
@ -272,6 +273,10 @@ func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) {
|
|||
db: db,
|
||||
state: state,
|
||||
},
|
||||
StatusEdit: &statusEditDB{
|
||||
db: db,
|
||||
state: state,
|
||||
},
|
||||
StatusFave: &statusFaveDB{
|
||||
db: db,
|
||||
state: state,
|
||||
|
|
|
@ -57,6 +57,7 @@ type BunDBStandardTestSuite struct {
|
|||
testPolls map[string]*gtsmodel.Poll
|
||||
testPollVotes map[string]*gtsmodel.PollVote
|
||||
testInteractionRequests map[string]*gtsmodel.InteractionRequest
|
||||
testStatusEdits map[string]*gtsmodel.StatusEdit
|
||||
}
|
||||
|
||||
func (suite *BunDBStandardTestSuite) SetupSuite() {
|
||||
|
@ -83,6 +84,7 @@ func (suite *BunDBStandardTestSuite) SetupSuite() {
|
|||
suite.testPolls = testrig.NewTestPolls()
|
||||
suite.testPollVotes = testrig.NewTestPollVotes()
|
||||
suite.testInteractionRequests = testrig.NewTestInteractionRequests()
|
||||
suite.testStatusEdits = testrig.NewTestStatusEdits()
|
||||
}
|
||||
|
||||
func (suite *BunDBStandardTestSuite) SetupTest() {
|
||||
|
|
|
@ -47,13 +47,13 @@ func (suite *InstanceTestSuite) TestCountInstanceUsersRemote() {
|
|||
func (suite *InstanceTestSuite) TestCountInstanceStatuses() {
|
||||
count, err := suite.db.CountInstanceStatuses(context.Background(), config.GetHost())
|
||||
suite.NoError(err)
|
||||
suite.Equal(19, count)
|
||||
suite.Equal(21, count)
|
||||
}
|
||||
|
||||
func (suite *InstanceTestSuite) TestCountInstanceStatusesRemote() {
|
||||
count, err := suite.db.CountInstanceStatuses(context.Background(), "fossbros-anonymous.io")
|
||||
suite.NoError(err)
|
||||
suite.Equal(3, count)
|
||||
suite.Equal(4, count)
|
||||
}
|
||||
|
||||
func (suite *InstanceTestSuite) TestCountInstanceDomains() {
|
||||
|
|
|
@ -59,11 +59,7 @@ func (suite *InteractionTestSuite) markInteractionsPending(
|
|||
|
||||
// Put an interaction request
|
||||
// in the DB for this reply.
|
||||
req, err := typeutils.StatusToInteractionRequest(ctx, reply)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
req := typeutils.StatusToInteractionRequest(reply)
|
||||
if err := suite.state.DB.PutInteractionRequest(ctx, req); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
@ -90,11 +86,7 @@ func (suite *InteractionTestSuite) markInteractionsPending(
|
|||
|
||||
// Put an interaction request
|
||||
// in the DB for this boost.
|
||||
req, err := typeutils.StatusToInteractionRequest(ctx, boost)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
req := typeutils.StatusToInteractionRequest(boost)
|
||||
if err := suite.state.DB.PutInteractionRequest(ctx, req); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
@ -121,11 +113,7 @@ func (suite *InteractionTestSuite) markInteractionsPending(
|
|||
|
||||
// Put an interaction request
|
||||
// in the DB for this fave.
|
||||
req, err := typeutils.StatusFaveToInteractionRequest(ctx, fave)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
req := typeutils.StatusFaveToInteractionRequest(fave)
|
||||
if err := suite.state.DB.PutInteractionRequest(ctx, req); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
|
|
@ -104,12 +104,6 @@ func (m *mediaDB) PutAttachment(ctx context.Context, media *gtsmodel.MediaAttach
|
|||
}
|
||||
|
||||
func (m *mediaDB) UpdateAttachment(ctx context.Context, media *gtsmodel.MediaAttachment, columns ...string) error {
|
||||
media.UpdatedAt = time.Now()
|
||||
if len(columns) > 0 {
|
||||
// If we're updating by column, ensure "updated_at" is included.
|
||||
columns = append(columns, "updated_at")
|
||||
}
|
||||
|
||||
return m.state.Caches.DB.Media.Store(media, func() error {
|
||||
_, err := m.db.NewUpdate().
|
||||
Model(media).
|
||||
|
|
|
@ -93,11 +93,7 @@ func init() {
|
|||
// For each currently pending status, check whether it's a reply or
|
||||
// a boost, and insert a corresponding interaction request into the db.
|
||||
for _, pendingStatus := range pendingStatuses {
|
||||
req, err := typeutils.StatusToInteractionRequest(ctx, pendingStatus)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := typeutils.StatusToInteractionRequest(pendingStatus)
|
||||
if _, err := tx.
|
||||
NewInsert().
|
||||
Model(req).
|
||||
|
@ -125,10 +121,7 @@ func init() {
|
|||
}
|
||||
|
||||
for _, pendingFave := range pendingFaves {
|
||||
req, err := typeutils.StatusFaveToInteractionRequest(ctx, pendingFave)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req := typeutils.StatusFaveToInteractionRequest(pendingFave)
|
||||
|
||||
if _, err := tx.
|
||||
NewInsert().
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
// 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"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
func init() {
|
||||
up := func(ctx context.Context, db *bun.DB) error {
|
||||
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||
|
||||
// Check for 'updated_at' column on mentions table, else return.
|
||||
exists, err := doesColumnExist(ctx, tx, "mentions", "updated_at")
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove 'updated_at' column.
|
||||
_, err = tx.NewDropColumn().
|
||||
Model((*gtsmodel.Mention)(nil)).
|
||||
Column("updated_at").
|
||||
Exec(ctx)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
// 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"
|
||||
"reflect"
|
||||
|
||||
gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20241113152126_add_status_edits"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
func init() {
|
||||
up := func(ctx context.Context, db *bun.DB) error {
|
||||
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||
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().
|
||||
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)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -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:"edits,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
|
||||
)
|
|
@ -0,0 +1,48 @@
|
|||
// 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"
|
||||
)
|
||||
|
||||
// StatusEdit represents a **historical** view of a Status
|
||||
// after a received edit. The Status itself will always
|
||||
// contain the latest up-to-date information.
|
||||
//
|
||||
// Note that stored status edits may not exactly match that
|
||||
// of the origin server, they are a best-effort by receiver
|
||||
// to store version history. There is no AP history endpoint.
|
||||
type StatusEdit struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // ID of this item in the database.
|
||||
Content string `bun:""` // Content of status at time of edit; likely html-formatted but not guaranteed.
|
||||
ContentWarning string `bun:",nullzero"` // Content warning of status at time of edit.
|
||||
Text string `bun:""` // Original status text, without formatting, at time of edit.
|
||||
Language string `bun:",nullzero"` // Status language at time of edit.
|
||||
Sensitive *bool `bun:",nullzero,notnull,default:false"` // Status sensitive flag at time of edit.
|
||||
AttachmentIDs []string `bun:"attachments,array"` // Database IDs of media attachments associated with status at time of edit.
|
||||
AttachmentDescriptions []string `bun:",array"` // Previous media descriptions of media attachments associated with status at time of edit.
|
||||
PollOptions []string `bun:",array"` // Poll options of status at time of edit, only set if status contains a poll.
|
||||
PollVotes []int `bun:",array"` // Poll vote count at time of status edit, only set if poll votes were reset.
|
||||
StatusID string `bun:"type:CHAR(26),nullzero,notnull"` // The originating status ID this is a historical edit of.
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // The creation time of this version of the status content (according to receiving server).
|
||||
|
||||
// We don't bother having a *gtsmodel.Status model here
|
||||
// as the StatusEdit is always just attached to a Status,
|
||||
// so it doesn't need a self-reference back to it.
|
||||
}
|
|
@ -19,12 +19,9 @@ package migrations
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
old_gtsmodel "github.com/superseriousbusiness/gotosocial/internal/db/bundb/migrations/20241121121623_enum_strings_to_ints"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
new_gtsmodel "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
|
||||
"github.com/uptrace/bun"
|
||||
|
@ -128,97 +125,6 @@ func init() {
|
|||
}
|
||||
}
|
||||
|
||||
// convertEnums performs a transaction that converts
|
||||
// a table's column of our old-style enums (strings) to
|
||||
// more performant and space-saving integer types.
|
||||
func convertEnums[OldType ~string, NewType ~int16](
|
||||
ctx context.Context,
|
||||
tx bun.Tx,
|
||||
table string,
|
||||
column string,
|
||||
mapping map[OldType]NewType,
|
||||
defaultValue *NewType,
|
||||
) error {
|
||||
if len(mapping) == 0 {
|
||||
return errors.New("empty mapping")
|
||||
}
|
||||
|
||||
// Generate new column name.
|
||||
newColumn := column + "_new"
|
||||
|
||||
log.Infof(ctx, "converting %s.%s enums; "+
|
||||
"this may take a while, please don't interrupt!",
|
||||
table, column,
|
||||
)
|
||||
|
||||
// Ensure a default value.
|
||||
if defaultValue == nil {
|
||||
var zero NewType
|
||||
defaultValue = &zero
|
||||
}
|
||||
|
||||
// Add new column to database.
|
||||
if _, err := tx.NewAddColumn().
|
||||
Table(table).
|
||||
ColumnExpr("? SMALLINT NOT NULL DEFAULT ?",
|
||||
bun.Ident(newColumn),
|
||||
*defaultValue).
|
||||
Exec(ctx); err != nil {
|
||||
return gtserror.Newf("error adding new column: %w", err)
|
||||
}
|
||||
|
||||
// Get a count of all in table.
|
||||
total, err := tx.NewSelect().
|
||||
Table(table).
|
||||
Count(ctx)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error selecting total count: %w", err)
|
||||
}
|
||||
|
||||
var updated int
|
||||
for old, new := range mapping {
|
||||
|
||||
// Update old to new values.
|
||||
res, err := tx.NewUpdate().
|
||||
Table(table).
|
||||
Where("? = ?", bun.Ident(column), old).
|
||||
Set("? = ?", bun.Ident(newColumn), new).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error updating old column values: %w", err)
|
||||
}
|
||||
|
||||
// Count number items updated.
|
||||
n, _ := res.RowsAffected()
|
||||
updated += int(n)
|
||||
}
|
||||
|
||||
// Check total updated.
|
||||
if total != updated {
|
||||
log.Warnf(ctx, "total=%d does not match updated=%d", total, updated)
|
||||
}
|
||||
|
||||
// Drop the old column from table.
|
||||
if _, err := tx.NewDropColumn().
|
||||
Table(table).
|
||||
ColumnExpr("?", bun.Ident(column)).
|
||||
Exec(ctx); err != nil {
|
||||
return gtserror.Newf("error dropping old column: %w", err)
|
||||
}
|
||||
|
||||
// Rename new to old name.
|
||||
if _, err := tx.NewRaw(
|
||||
"ALTER TABLE ? RENAME COLUMN ? TO ?",
|
||||
bun.Ident(table),
|
||||
bun.Ident(newColumn),
|
||||
bun.Ident(column),
|
||||
).Exec(ctx); err != nil {
|
||||
return gtserror.Newf("error renaming new column: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// visibilityEnumMapping maps old Visibility enum values to their newer integer type.
|
||||
func visibilityEnumMapping[T ~string]() map[T]new_gtsmodel.Visibility {
|
||||
return map[T]new_gtsmodel.Visibility{
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
// 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"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
func init() {
|
||||
up := func(ctx context.Context, db *bun.DB) error {
|
||||
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||
|
||||
// Check for 'updated_at' column on media attachments table, else return.
|
||||
exists, err := doesColumnExist(ctx, tx, "media_attachments", "updated_at")
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove 'updated_at' column.
|
||||
_, err = tx.NewDropColumn().
|
||||
Model((*gtsmodel.MediaAttachment)(nil)).
|
||||
Column("updated_at").
|
||||
Exec(ctx)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -19,11 +19,209 @@ package migrations
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"codeberg.org/gruf/go-byteutil"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"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"
|
||||
)
|
||||
|
||||
// convertEnums performs a transaction that converts
|
||||
// a table's column of our old-style enums (strings) to
|
||||
// more performant and space-saving integer types.
|
||||
func convertEnums[OldType ~string, NewType ~int16](
|
||||
ctx context.Context,
|
||||
tx bun.Tx,
|
||||
table string,
|
||||
column string,
|
||||
mapping map[OldType]NewType,
|
||||
defaultValue *NewType,
|
||||
) error {
|
||||
if len(mapping) == 0 {
|
||||
return errors.New("empty mapping")
|
||||
}
|
||||
|
||||
// Generate new column name.
|
||||
newColumn := column + "_new"
|
||||
|
||||
log.Infof(ctx, "converting %s.%s enums; "+
|
||||
"this may take a while, please don't interrupt!",
|
||||
table, column,
|
||||
)
|
||||
|
||||
// Ensure a default value.
|
||||
if defaultValue == nil {
|
||||
var zero NewType
|
||||
defaultValue = &zero
|
||||
}
|
||||
|
||||
// Add new column to database.
|
||||
if _, err := tx.NewAddColumn().
|
||||
Table(table).
|
||||
ColumnExpr("? SMALLINT NOT NULL DEFAULT ?",
|
||||
bun.Ident(newColumn),
|
||||
*defaultValue).
|
||||
Exec(ctx); err != nil {
|
||||
return gtserror.Newf("error adding new column: %w", err)
|
||||
}
|
||||
|
||||
// Get a count of all in table.
|
||||
total, err := tx.NewSelect().
|
||||
Table(table).
|
||||
Count(ctx)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error selecting total count: %w", err)
|
||||
}
|
||||
|
||||
var updated int
|
||||
for old, new := range mapping {
|
||||
|
||||
// Update old to new values.
|
||||
res, err := tx.NewUpdate().
|
||||
Table(table).
|
||||
Where("? = ?", bun.Ident(column), old).
|
||||
Set("? = ?", bun.Ident(newColumn), new).
|
||||
Exec(ctx)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error updating old column values: %w", err)
|
||||
}
|
||||
|
||||
// Count number items updated.
|
||||
n, _ := res.RowsAffected()
|
||||
updated += int(n)
|
||||
}
|
||||
|
||||
// Check total updated.
|
||||
if total != updated {
|
||||
log.Warnf(ctx, "total=%d does not match updated=%d", total, updated)
|
||||
}
|
||||
|
||||
// Drop the old column from table.
|
||||
if _, err := tx.NewDropColumn().
|
||||
Table(table).
|
||||
ColumnExpr("?", bun.Ident(column)).
|
||||
Exec(ctx); err != nil {
|
||||
return gtserror.Newf("error dropping old column: %w", err)
|
||||
}
|
||||
|
||||
// Rename new to old name.
|
||||
if _, err := tx.NewRaw(
|
||||
"ALTER TABLE ? RENAME COLUMN ? TO ?",
|
||||
bun.Ident(table),
|
||||
bun.Ident(newColumn),
|
||||
bun.Ident(column),
|
||||
).Exec(ctx); err != nil {
|
||||
return gtserror.Newf("error renaming new column: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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, "("...)
|
||||
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
|
||||
|
|
|
@ -21,7 +21,6 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
|
@ -181,7 +180,7 @@ func (s *statusDB) getStatus(ctx context.Context, lookup string, dbQuery func(*g
|
|||
func (s *statusDB) PopulateStatus(ctx context.Context, status *gtsmodel.Status) error {
|
||||
var (
|
||||
err error
|
||||
errs = gtserror.NewMultiError(9)
|
||||
errs gtserror.MultiError
|
||||
)
|
||||
|
||||
if status.Account == nil {
|
||||
|
@ -257,7 +256,7 @@ func (s *statusDB) PopulateStatus(ctx context.Context, status *gtsmodel.Status)
|
|||
if !status.AttachmentsPopulated() {
|
||||
// Status attachments are out-of-date with IDs, repopulate.
|
||||
status.Attachments, err = s.state.DB.GetAttachmentsByIDs(
|
||||
ctx, // these are already barebones
|
||||
gtscontext.SetBarebones(ctx),
|
||||
status.AttachmentIDs,
|
||||
)
|
||||
if err != nil {
|
||||
|
@ -268,7 +267,7 @@ func (s *statusDB) PopulateStatus(ctx context.Context, status *gtsmodel.Status)
|
|||
if !status.TagsPopulated() {
|
||||
// Status tags are out-of-date with IDs, repopulate.
|
||||
status.Tags, err = s.state.DB.GetTags(
|
||||
ctx,
|
||||
gtscontext.SetBarebones(ctx),
|
||||
status.TagIDs,
|
||||
)
|
||||
if err != nil {
|
||||
|
@ -279,7 +278,7 @@ func (s *statusDB) PopulateStatus(ctx context.Context, status *gtsmodel.Status)
|
|||
if !status.MentionsPopulated() {
|
||||
// Status mentions are out-of-date with IDs, repopulate.
|
||||
status.Mentions, err = s.state.DB.GetMentions(
|
||||
ctx, // leave fully populated for now
|
||||
ctx, // TODO: manually populate mentions for places expecting these populated
|
||||
status.MentionIDs,
|
||||
)
|
||||
if err != nil {
|
||||
|
@ -290,7 +289,7 @@ func (s *statusDB) PopulateStatus(ctx context.Context, status *gtsmodel.Status)
|
|||
if !status.EmojisPopulated() {
|
||||
// Status emojis are out-of-date with IDs, repopulate.
|
||||
status.Emojis, err = s.state.DB.GetEmojisByIDs(
|
||||
ctx, // these are already barebones
|
||||
gtscontext.SetBarebones(ctx),
|
||||
status.EmojiIDs,
|
||||
)
|
||||
if err != nil {
|
||||
|
@ -298,10 +297,21 @@ func (s *statusDB) PopulateStatus(ctx context.Context, status *gtsmodel.Status)
|
|||
}
|
||||
}
|
||||
|
||||
if !status.EditsPopulated() {
|
||||
// Status edits are out-of-date with IDs, repopulate.
|
||||
status.Edits, err = s.state.DB.GetStatusEditsByIDs(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
status.EditIDs,
|
||||
)
|
||||
if err != nil {
|
||||
errs.Appendf("error populating status edits: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if status.CreatedWithApplicationID != "" && status.CreatedWithApplication == nil {
|
||||
// Populate the status' expected CreatedWithApplication (not always set).
|
||||
status.CreatedWithApplication, err = s.state.DB.GetApplicationByID(
|
||||
ctx, // these are already barebones
|
||||
gtscontext.SetBarebones(ctx),
|
||||
status.CreatedWithApplicationID,
|
||||
)
|
||||
if err != nil {
|
||||
|
@ -350,14 +360,14 @@ func (s *statusDB) PutStatus(ctx context.Context, status *gtsmodel.Status) error
|
|||
}
|
||||
}
|
||||
|
||||
// change the status ID of the media attachments to the new status
|
||||
// change the status ID of the media
|
||||
// attachments to the current status
|
||||
for _, a := range status.Attachments {
|
||||
a.StatusID = status.ID
|
||||
a.UpdatedAt = time.Now()
|
||||
if _, err := tx.
|
||||
NewUpdate().
|
||||
Model(a).
|
||||
Column("status_id", "updated_at").
|
||||
Column("status_id").
|
||||
Where("? = ?", bun.Ident("media_attachment.id"), a.ID).
|
||||
Exec(ctx); err != nil {
|
||||
if !errors.Is(err, db.ErrAlreadyExists) {
|
||||
|
@ -384,19 +394,15 @@ func (s *statusDB) PutStatus(ctx context.Context, status *gtsmodel.Status) error
|
|||
}
|
||||
|
||||
// Finally, insert the status
|
||||
_, err := tx.NewInsert().Model(status).Exec(ctx)
|
||||
_, err := tx.NewInsert().
|
||||
Model(status).
|
||||
Exec(ctx)
|
||||
return err
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (s *statusDB) UpdateStatus(ctx context.Context, status *gtsmodel.Status, columns ...string) error {
|
||||
status.UpdatedAt = time.Now()
|
||||
if len(columns) > 0 {
|
||||
// If we're updating by column, ensure "updated_at" is included.
|
||||
columns = append(columns, "updated_at")
|
||||
}
|
||||
|
||||
return s.state.Caches.DB.Status.Store(status, func() error {
|
||||
// It is safe to run this database transaction within cache.Store
|
||||
// as the cache does not attempt a mutex lock until AFTER hook.
|
||||
|
@ -434,13 +440,14 @@ func (s *statusDB) UpdateStatus(ctx context.Context, status *gtsmodel.Status, co
|
|||
}
|
||||
}
|
||||
|
||||
// change the status ID of the media attachments to the new status
|
||||
// change the status ID of the media
|
||||
// attachments to the current status.
|
||||
for _, a := range status.Attachments {
|
||||
a.StatusID = status.ID
|
||||
a.UpdatedAt = time.Now()
|
||||
if _, err := tx.
|
||||
NewUpdate().
|
||||
Model(a).
|
||||
Column("status_id").
|
||||
Where("? = ?", bun.Ident("media_attachment.id"), a.ID).
|
||||
Exec(ctx); err != nil {
|
||||
if !errors.Is(err, db.ErrAlreadyExists) {
|
||||
|
@ -467,8 +474,7 @@ func (s *statusDB) UpdateStatus(ctx context.Context, status *gtsmodel.Status, co
|
|||
}
|
||||
|
||||
// Finally, update the status
|
||||
_, err := tx.
|
||||
NewUpdate().
|
||||
_, err := tx.NewUpdate().
|
||||
Model(status).
|
||||
Column(columns...).
|
||||
Where("? = ?", bun.Ident("status.id"), status.ID).
|
||||
|
|
198
internal/db/bundb/statusedit.go
Normal file
198
internal/db/bundb/statusedit.go
Normal file
|
@ -0,0 +1,198 @@
|
|||
// 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 bundb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type statusEditDB struct {
|
||||
db *bun.DB
|
||||
state *state.State
|
||||
}
|
||||
|
||||
func (s *statusEditDB) GetStatusEditByID(ctx context.Context, id string) (*gtsmodel.StatusEdit, error) {
|
||||
// Fetch edit from database cache with loader callback.
|
||||
edit, err := s.state.Caches.DB.StatusEdit.LoadOne("ID",
|
||||
func() (*gtsmodel.StatusEdit, error) {
|
||||
var edit gtsmodel.StatusEdit
|
||||
|
||||
// Not cached, load edit
|
||||
// from database by its ID.
|
||||
if err := s.db.NewSelect().
|
||||
Model(&edit).
|
||||
Where("? = ?", bun.Ident("id"), id).
|
||||
Scan(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &edit, nil
|
||||
}, id,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if gtscontext.Barebones(ctx) {
|
||||
// no need to fully populate.
|
||||
return edit, nil
|
||||
}
|
||||
|
||||
// Further populate the edit fields where applicable.
|
||||
if err := s.PopulateStatusEdit(ctx, edit); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return edit, nil
|
||||
}
|
||||
|
||||
func (s *statusEditDB) GetStatusEditsByIDs(ctx context.Context, ids []string) ([]*gtsmodel.StatusEdit, error) {
|
||||
// Load status edits for IDs via cache loader callbacks.
|
||||
edits, err := s.state.Caches.DB.StatusEdit.LoadIDs("ID",
|
||||
ids,
|
||||
func(uncached []string) ([]*gtsmodel.StatusEdit, error) {
|
||||
// Preallocate expected length of uncached edits.
|
||||
edits := make([]*gtsmodel.StatusEdit, 0, len(uncached))
|
||||
|
||||
// Perform database query scanning
|
||||
// the remaining (uncached) edit IDs.
|
||||
if err := s.db.NewSelect().
|
||||
Model(&edits).
|
||||
Where("? IN (?)", bun.Ident("id"), bun.In(uncached)).
|
||||
Scan(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return edits, nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Reorder the edits by their
|
||||
// IDs to ensure in correct order.
|
||||
getID := func(e *gtsmodel.StatusEdit) string { return e.ID }
|
||||
xslices.OrderBy(edits, ids, getID)
|
||||
|
||||
if gtscontext.Barebones(ctx) {
|
||||
// no need to fully populate.
|
||||
return edits, nil
|
||||
}
|
||||
|
||||
// Populate all loaded edits, removing those we fail to
|
||||
// populate (removes needing so many nil checks everywhere).
|
||||
edits = slices.DeleteFunc(edits, func(edit *gtsmodel.StatusEdit) bool {
|
||||
if err := s.PopulateStatusEdit(ctx, edit); err != nil {
|
||||
log.Errorf(ctx, "error populating edit %s: %v", edit.ID, err)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
return edits, nil
|
||||
}
|
||||
|
||||
func (s *statusEditDB) PopulateStatusEdit(ctx context.Context, edit *gtsmodel.StatusEdit) error {
|
||||
var err error
|
||||
var errs gtserror.MultiError
|
||||
|
||||
// For sub-models we only want
|
||||
// barebones versions of them.
|
||||
ctx = gtscontext.SetBarebones(ctx)
|
||||
|
||||
if !edit.AttachmentsPopulated() {
|
||||
// Fetch all attachments for status edit's IDs.
|
||||
edit.Attachments, err = s.state.DB.GetAttachmentsByIDs(
|
||||
ctx,
|
||||
edit.AttachmentIDs,
|
||||
)
|
||||
if err != nil {
|
||||
errs.Appendf("error populating edit attachments: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return errs.Combine()
|
||||
}
|
||||
|
||||
func (s *statusEditDB) PutStatusEdit(ctx context.Context, edit *gtsmodel.StatusEdit) error {
|
||||
return s.state.Caches.DB.StatusEdit.Store(edit, func() error {
|
||||
_, err := s.db.NewInsert().Model(edit).Exec(ctx)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (s *statusEditDB) DeleteStatusEdits(ctx context.Context, ids []string) error {
|
||||
// Gather necessary fields from
|
||||
// deleted for cache invalidation.
|
||||
deleted := make([]*gtsmodel.StatusEdit, 0, len(ids))
|
||||
|
||||
// Delete all edits with IDs pertaining
|
||||
// to given slice, returning status IDs.
|
||||
if _, err := s.db.NewDelete().
|
||||
Model(&deleted).
|
||||
Where("? IN (?)", bun.Ident("id"), bun.In(ids)).
|
||||
Returning("?", bun.Ident("status_id")).
|
||||
Exec(ctx); err != nil &&
|
||||
!errors.Is(err, db.ErrNoEntries) {
|
||||
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)
|
||||
|
||||
// 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.
|
||||
_, ok := invalidated[edit.StatusID]
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Manually call status edit invalidate hook.
|
||||
s.state.Caches.OnInvalidateStatusEdit(edit)
|
||||
invalidated[edit.StatusID] = struct{}{}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
168
internal/db/bundb/statusedit_test.go
Normal file
168
internal/db/bundb/statusedit_test.go
Normal file
|
@ -0,0 +1,168 @@
|
|||
// 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 bundb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
type StatusEditTestSuite struct {
|
||||
BunDBStandardTestSuite
|
||||
}
|
||||
|
||||
func (suite *StatusEditTestSuite) TestGetStatusEditBy() {
|
||||
t := suite.T()
|
||||
|
||||
// Create a new context for this test.
|
||||
ctx, cncl := context.WithCancel(context.Background())
|
||||
defer cncl()
|
||||
|
||||
// Sentinel error to mark avoiding a test case.
|
||||
sentinelErr := errors.New("sentinel")
|
||||
|
||||
for _, edit := range suite.testStatusEdits {
|
||||
for lookup, dbfunc := range map[string]func() (*gtsmodel.StatusEdit, error){
|
||||
"id": func() (*gtsmodel.StatusEdit, error) {
|
||||
return suite.db.GetStatusEditByID(ctx, edit.ID)
|
||||
},
|
||||
} {
|
||||
// Clear database caches.
|
||||
suite.state.Caches.Init()
|
||||
|
||||
t.Logf("checking database lookup %q", lookup)
|
||||
|
||||
// Perform database function.
|
||||
checkEdit, err := dbfunc()
|
||||
if err != nil {
|
||||
if err == sentinelErr {
|
||||
continue
|
||||
}
|
||||
|
||||
t.Errorf("error encountered for database lookup %q: %v", lookup, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check received account data.
|
||||
if !areEditsEqual(edit, checkEdit) {
|
||||
t.Errorf("edit does not contain expected data: %+v", checkEdit)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *StatusEditTestSuite) TestGetStatusEditsByIDs() {
|
||||
t := suite.T()
|
||||
|
||||
// Create a new context for this test.
|
||||
ctx, cncl := context.WithCancel(context.Background())
|
||||
defer cncl()
|
||||
|
||||
// editsByStatus returns all test edits by the given status with ID.
|
||||
editsByStatus := func(status *gtsmodel.Status) []*gtsmodel.StatusEdit {
|
||||
var edits []*gtsmodel.StatusEdit
|
||||
for _, edit := range suite.testStatusEdits {
|
||||
if edit.StatusID == status.ID {
|
||||
edits = append(edits, edit)
|
||||
}
|
||||
}
|
||||
return edits
|
||||
}
|
||||
|
||||
for _, status := range suite.testStatuses {
|
||||
// Get test status edit models
|
||||
// that should be found for status.
|
||||
check := editsByStatus(status)
|
||||
|
||||
// Fetch edits for the slice of IDs attached to status from database.
|
||||
edits, err := suite.state.DB.GetStatusEditsByIDs(ctx, status.EditIDs)
|
||||
suite.NoError(err)
|
||||
|
||||
// Ensure both slices
|
||||
// sorted the same.
|
||||
sortEdits(check)
|
||||
sortEdits(edits)
|
||||
|
||||
// Check whether slices of status edits match.
|
||||
if !slices.EqualFunc(check, edits, areEditsEqual) {
|
||||
t.Error("status edit slices do not match")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *StatusEditTestSuite) TestDeleteStatusEdits() {
|
||||
// Create a new context for this test.
|
||||
ctx, cncl := context.WithCancel(context.Background())
|
||||
defer cncl()
|
||||
|
||||
for _, status := range suite.testStatuses {
|
||||
// Delete all edits for status with given IDs from database.
|
||||
err := suite.state.DB.DeleteStatusEdits(ctx, status.EditIDs)
|
||||
suite.NoError(err)
|
||||
|
||||
// Now attempt to fetch these edits from database, should be empty.
|
||||
edits, err := suite.state.DB.GetStatusEditsByIDs(ctx, status.EditIDs)
|
||||
suite.NoError(err)
|
||||
suite.Empty(edits)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusEditTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(StatusEditTestSuite))
|
||||
}
|
||||
|
||||
func areEditsEqual(e1, e2 *gtsmodel.StatusEdit) bool {
|
||||
// Clone the 1st status edit.
|
||||
e1Copy := new(gtsmodel.StatusEdit)
|
||||
*e1Copy = *e1
|
||||
e1 = e1Copy
|
||||
|
||||
// Clone the 2nd status edit.
|
||||
e2Copy := new(gtsmodel.StatusEdit)
|
||||
*e2Copy = *e2
|
||||
e2 = e2Copy
|
||||
|
||||
// Clear populated sub-models.
|
||||
e1.Attachments = nil
|
||||
e2.Attachments = nil
|
||||
|
||||
// Clear database-set fields.
|
||||
e1.CreatedAt = time.Time{}
|
||||
e2.CreatedAt = time.Time{}
|
||||
|
||||
return reflect.DeepEqual(*e1, *e2)
|
||||
}
|
||||
|
||||
func sortEdits(edits []*gtsmodel.StatusEdit) {
|
||||
slices.SortFunc(edits, func(a, b *gtsmodel.StatusEdit) int {
|
||||
if a.CreatedAt.Before(b.CreatedAt) {
|
||||
return +1
|
||||
} else if b.CreatedAt.Before(a.CreatedAt) {
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
}
|
|
@ -123,13 +123,8 @@ func (t *timelineDB) GetHomeTimeline(ctx context.Context, accountID string, maxI
|
|||
if maxID == "" || maxID >= id.Highest {
|
||||
const future = 24 * time.Hour
|
||||
|
||||
var err error
|
||||
|
||||
// don't return statuses more than 24hr in the future
|
||||
maxID, err = id.NewULIDFromTime(time.Now().Add(future))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
maxID = id.NewULIDFromTime(time.Now().Add(future))
|
||||
}
|
||||
|
||||
// return only statuses LOWER (ie., older) than maxID
|
||||
|
@ -223,13 +218,8 @@ func (t *timelineDB) GetPublicTimeline(ctx context.Context, maxID string, sinceI
|
|||
if maxID == "" || maxID >= id.Highest {
|
||||
const future = 24 * time.Hour
|
||||
|
||||
var err error
|
||||
|
||||
// don't return statuses more than 24hr in the future
|
||||
maxID, err = id.NewULIDFromTime(time.Now().Add(future))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
maxID = id.NewULIDFromTime(time.Now().Add(future))
|
||||
}
|
||||
|
||||
// return only statuses LOWER (ie., older) than maxID
|
||||
|
@ -409,13 +399,8 @@ func (t *timelineDB) GetListTimeline(
|
|||
if maxID == "" || maxID >= id.Highest {
|
||||
const future = 24 * time.Hour
|
||||
|
||||
var err error
|
||||
|
||||
// don't return statuses more than 24hr in the future
|
||||
maxID, err = id.NewULIDFromTime(time.Now().Add(future))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
maxID = id.NewULIDFromTime(time.Now().Add(future))
|
||||
}
|
||||
|
||||
// return only statuses LOWER (ie., older) than maxID
|
||||
|
@ -508,13 +493,8 @@ func (t *timelineDB) GetTagTimeline(
|
|||
if maxID == "" || maxID >= id.Highest {
|
||||
const future = 24 * time.Hour
|
||||
|
||||
var err error
|
||||
|
||||
// don't return statuses more than 24hr in the future
|
||||
maxID, err = id.NewULIDFromTime(time.Now().Add(future))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
maxID = id.NewULIDFromTime(time.Now().Add(future))
|
||||
}
|
||||
|
||||
// return only statuses LOWER (ie., older) than maxID
|
||||
|
|
|
@ -37,10 +37,7 @@ type TimelineTestSuite struct {
|
|||
|
||||
func getFutureStatus() *gtsmodel.Status {
|
||||
theDistantFuture := time.Now().Add(876600 * time.Hour)
|
||||
id, err := id.NewULIDFromTime(theDistantFuture)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
id := id.NewULIDFromTime(theDistantFuture)
|
||||
|
||||
return >smodel.Status{
|
||||
ID: id,
|
||||
|
@ -182,7 +179,7 @@ func (suite *TimelineTestSuite) TestGetHomeTimelineIgnoreExclusive() {
|
|||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
suite.checkStatuses(s, id.Highest, id.Lowest, 8)
|
||||
suite.checkStatuses(s, id.Highest, id.Lowest, 9)
|
||||
|
||||
// Remove admin account from the exclusive list.
|
||||
listEntry := suite.testListEntries["local_account_1_list_1_entry_2"]
|
||||
|
@ -196,7 +193,7 @@ func (suite *TimelineTestSuite) TestGetHomeTimelineIgnoreExclusive() {
|
|||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
suite.checkStatuses(s, id.Highest, id.Lowest, 12)
|
||||
suite.checkStatuses(s, id.Highest, id.Lowest, 13)
|
||||
}
|
||||
|
||||
func (suite *TimelineTestSuite) TestGetHomeTimelineNoFollowing() {
|
||||
|
@ -228,7 +225,7 @@ func (suite *TimelineTestSuite) TestGetHomeTimelineNoFollowing() {
|
|||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.checkStatuses(s, id.Highest, id.Lowest, 8)
|
||||
suite.checkStatuses(s, id.Highest, id.Lowest, 9)
|
||||
}
|
||||
|
||||
func (suite *TimelineTestSuite) TestGetHomeTimelineWithFutureStatus() {
|
||||
|
@ -281,8 +278,8 @@ func (suite *TimelineTestSuite) TestGetHomeTimelineFromHighest() {
|
|||
}
|
||||
|
||||
suite.checkStatuses(s, id.Highest, id.Lowest, 5)
|
||||
suite.Equal("01J2M1HPFSS54S60Y0KYV23KJE", s[0].ID)
|
||||
suite.Equal("01G36SF3V6Y6V5BF9P4R7PQG7G", s[len(s)-1].ID)
|
||||
suite.Equal("01JDPZEZ77X1NX0TY9M10BK1HM", s[0].ID)
|
||||
suite.Equal("01HEN2RZ8BG29Y5Z9VJC73HZW7", s[len(s)-1].ID)
|
||||
}
|
||||
|
||||
func (suite *TimelineTestSuite) TestGetListTimelineNoParams() {
|
||||
|
@ -296,7 +293,7 @@ func (suite *TimelineTestSuite) TestGetListTimelineNoParams() {
|
|||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.checkStatuses(s, id.Highest, id.Lowest, 12)
|
||||
suite.checkStatuses(s, id.Highest, id.Lowest, 13)
|
||||
}
|
||||
|
||||
func (suite *TimelineTestSuite) TestGetListTimelineMaxID() {
|
||||
|
@ -311,8 +308,8 @@ func (suite *TimelineTestSuite) TestGetListTimelineMaxID() {
|
|||
}
|
||||
|
||||
suite.checkStatuses(s, id.Highest, id.Lowest, 5)
|
||||
suite.Equal("01HEN2PRXT0TF4YDRA64FZZRN7", s[0].ID)
|
||||
suite.Equal("01FF25D5Q0DH7CHD57CTRS6WK0", s[len(s)-1].ID)
|
||||
suite.Equal("01JDPZEZ77X1NX0TY9M10BK1HM", s[0].ID)
|
||||
suite.Equal("01FN3VJGFH10KR7S2PB0GFJZYG", s[len(s)-1].ID)
|
||||
}
|
||||
|
||||
func (suite *TimelineTestSuite) TestGetListTimelineMinID() {
|
||||
|
|
|
@ -51,6 +51,7 @@ type DB interface {
|
|||
SinBinStatus
|
||||
Status
|
||||
StatusBookmark
|
||||
StatusEdit
|
||||
StatusFave
|
||||
Tag
|
||||
Thread
|
||||
|
|
43
internal/db/statusedit.go
Normal file
43
internal/db/statusedit.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
// 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 db
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
type StatusEdit interface {
|
||||
|
||||
// GetStatusEditByID fetches the StatusEdit with given ID from the database.
|
||||
GetStatusEditByID(ctx context.Context, id string) (*gtsmodel.StatusEdit, error)
|
||||
|
||||
// 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 ensures the given StatusEdit's sub-models are populated.
|
||||
PopulateStatusEdit(ctx context.Context, edit *gtsmodel.StatusEdit) error
|
||||
|
||||
// PutStatusEdit inserts the given new StatusEdit into the database.
|
||||
PutStatusEdit(ctx context.Context, edit *gtsmodel.StatusEdit) error
|
||||
|
||||
// DeleteStatusEdits deletes the StatusEdits with given IDs from the database.
|
||||
DeleteStatusEdits(ctx context.Context, ids []string) error
|
||||
}
|
|
@ -87,7 +87,7 @@ func (d *Dereferencer) EnrichAnnounce(
|
|||
boost.Federated = target.Federated
|
||||
|
||||
// Ensure this Announce is permitted by the Announcee.
|
||||
permit, err := d.isPermittedStatus(ctx, requestUser, nil, boost)
|
||||
permit, err := d.isPermittedStatus(ctx, requestUser, nil, boost, true)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("error checking permitted status %s: %w", boost.URI, err)
|
||||
}
|
||||
|
@ -99,10 +99,7 @@ func (d *Dereferencer) EnrichAnnounce(
|
|||
}
|
||||
|
||||
// Generate an ID for the boost wrapper status.
|
||||
boost.ID, err = id.NewULIDFromTime(boost.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("error generating id: %w", err)
|
||||
}
|
||||
boost.ID = id.NewULIDFromTime(boost.CreatedAt)
|
||||
|
||||
// Store the boost wrapper status in database.
|
||||
switch err = d.state.DB.PutStatus(ctx, boost); {
|
||||
|
|
|
@ -128,6 +128,7 @@ func (d *Dereferencer) RefreshMedia(
|
|||
// Check emoji is up-to-date
|
||||
// with provided extra info.
|
||||
switch {
|
||||
case force:
|
||||
case info.Blurhash != nil &&
|
||||
*info.Blurhash != attach.Blurhash:
|
||||
attach.Blurhash = *info.Blurhash
|
||||
|
|
|
@ -302,6 +302,7 @@ func (d *Dereferencer) enrichStatusSafely(
|
|||
uri,
|
||||
status,
|
||||
statusable,
|
||||
isNew,
|
||||
)
|
||||
|
||||
// Check for a returned HTTP code via error.
|
||||
|
@ -374,6 +375,7 @@ func (d *Dereferencer) enrichStatus(
|
|||
uri *url.URL,
|
||||
status *gtsmodel.Status,
|
||||
statusable ap.Statusable,
|
||||
isNew bool,
|
||||
) (
|
||||
*gtsmodel.Status,
|
||||
ap.Statusable,
|
||||
|
@ -476,8 +478,7 @@ func (d *Dereferencer) enrichStatus(
|
|||
|
||||
// Ensure the final parsed status URI or URL matches
|
||||
// the input URI we fetched (or received) it as.
|
||||
matches, err := util.URIMatches(
|
||||
uri,
|
||||
matches, err := util.URIMatches(uri,
|
||||
append(
|
||||
ap.GetURL(statusable), // status URL(s)
|
||||
ap.GetJSONLDId(statusable), // status URI
|
||||
|
@ -497,19 +498,10 @@ func (d *Dereferencer) enrichStatus(
|
|||
)
|
||||
}
|
||||
|
||||
var isNew bool
|
||||
|
||||
// Based on the original provided
|
||||
// status model, determine whether
|
||||
// this is a new insert / update.
|
||||
if isNew = (status.ID == ""); isNew {
|
||||
if isNew {
|
||||
|
||||
// Generate new status ID from the provided creation date.
|
||||
latestStatus.ID, err = id.NewULIDFromTime(latestStatus.CreatedAt)
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "invalid created at date (falling back to 'now'): %v", err)
|
||||
latestStatus.ID = id.NewULID() // just use "now"
|
||||
}
|
||||
latestStatus.ID = id.NewULIDFromTime(latestStatus.CreatedAt)
|
||||
} else {
|
||||
|
||||
// Reuse existing status ID.
|
||||
|
@ -519,7 +511,6 @@ func (d *Dereferencer) enrichStatus(
|
|||
// Set latest fetch time and carry-
|
||||
// over some values from "old" status.
|
||||
latestStatus.FetchedAt = time.Now()
|
||||
latestStatus.UpdatedAt = status.UpdatedAt
|
||||
latestStatus.Local = status.Local
|
||||
latestStatus.PinnedAt = status.PinnedAt
|
||||
|
||||
|
@ -538,8 +529,9 @@ func (d *Dereferencer) enrichStatus(
|
|||
}
|
||||
|
||||
// Check if this is a permitted status we should accept.
|
||||
// Function also sets "PendingApproval" bool as necessary.
|
||||
permit, err := d.isPermittedStatus(ctx, requestUser, status, latestStatus)
|
||||
// Function also sets "PendingApproval" bool as necessary,
|
||||
// and handles removal of existing statuses no longer permitted.
|
||||
permit, err := d.isPermittedStatus(ctx, requestUser, status, latestStatus, isNew)
|
||||
if err != nil {
|
||||
return nil, nil, gtserror.Newf("error checking permissibility for status %s: %w", uri, err)
|
||||
}
|
||||
|
@ -550,59 +542,113 @@ func (d *Dereferencer) enrichStatus(
|
|||
return nil, nil, gtserror.SetNotPermitted(err)
|
||||
}
|
||||
|
||||
// Ensure the status' mentions are populated, and pass in existing to check for changes.
|
||||
if err := d.fetchStatusMentions(ctx, requestUser, status, latestStatus); err != nil {
|
||||
// Insert / update any attached status poll.
|
||||
pollChanged, err := d.handleStatusPoll(ctx,
|
||||
status,
|
||||
latestStatus,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, gtserror.Newf("error handling poll for status %s: %w", uri, err)
|
||||
}
|
||||
|
||||
// Populate mentions associated with status, passing
|
||||
// in existing status to reuse old where possible.
|
||||
// (especially important here to reduce need to dereference).
|
||||
mentionsChanged, err := d.fetchStatusMentions(ctx,
|
||||
requestUser,
|
||||
status,
|
||||
latestStatus,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, gtserror.Newf("error populating mentions for status %s: %w", uri, err)
|
||||
}
|
||||
|
||||
// Ensure the status' poll remains consistent, else reset the poll.
|
||||
if err := d.fetchStatusPoll(ctx, status, latestStatus); err != nil {
|
||||
return nil, nil, gtserror.Newf("error populating poll for status %s: %w", uri, err)
|
||||
// Ensure status in a thread is connected.
|
||||
threadChanged, err := d.threadStatus(ctx,
|
||||
status,
|
||||
latestStatus,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, gtserror.Newf("error handling threading for status %s: %w", uri, err)
|
||||
}
|
||||
|
||||
// Now that we know who this status replies to (handled by ASStatusToStatus)
|
||||
// and who it mentions, we can add a ThreadID to it if necessary.
|
||||
if err := d.threadStatus(ctx, latestStatus); err != nil {
|
||||
return nil, nil, gtserror.Newf("error checking / creating threadID for status %s: %w", uri, err)
|
||||
}
|
||||
|
||||
// Ensure the status' tags are populated, (changes are expected / okay).
|
||||
if err := d.fetchStatusTags(ctx, status, latestStatus); err != nil {
|
||||
// Populate tags associated with status, passing
|
||||
// in existing status to reuse old where possible.
|
||||
tagsChanged, err := d.fetchStatusTags(ctx,
|
||||
status,
|
||||
latestStatus,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, gtserror.Newf("error populating tags for status %s: %w", uri, err)
|
||||
}
|
||||
|
||||
// Ensure the status' media attachments are populated, passing in existing to check for changes.
|
||||
if err := d.fetchStatusAttachments(ctx, requestUser, status, latestStatus); err != nil {
|
||||
// Populate media attachments associated with status,
|
||||
// passing in existing status to reuse old where possible
|
||||
// (especially important here to reduce need to dereference).
|
||||
mediaChanged, err := d.fetchStatusAttachments(ctx,
|
||||
requestUser,
|
||||
status,
|
||||
latestStatus,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, gtserror.Newf("error populating attachments for status %s: %w", uri, err)
|
||||
}
|
||||
|
||||
// Ensure the status' emoji attachments are populated, passing in existing to check for changes.
|
||||
if err := d.fetchStatusEmojis(ctx, status, latestStatus); err != nil {
|
||||
// Populate emoji associated with status, passing
|
||||
// in existing status to reuse old where possible
|
||||
// (especially important here to reduce need to dereference).
|
||||
emojiChanged, err := d.fetchStatusEmojis(ctx,
|
||||
status,
|
||||
latestStatus,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, gtserror.Newf("error populating emojis for status %s: %w", uri, err)
|
||||
}
|
||||
|
||||
if isNew {
|
||||
// This is new, put the status in the database.
|
||||
err := d.state.DB.PutStatus(ctx, latestStatus)
|
||||
if err != nil {
|
||||
return nil, nil, gtserror.Newf("error putting in database: %w", err)
|
||||
// Simplest case, insert this new status into the database.
|
||||
if err := d.state.DB.PutStatus(ctx, latestStatus); err != nil {
|
||||
return nil, nil, gtserror.Newf("error inserting new status %s: %w", uri, err)
|
||||
}
|
||||
} else {
|
||||
// This is an existing status, update the model in the database.
|
||||
if err := d.state.DB.UpdateStatus(ctx, latestStatus); err != nil {
|
||||
return nil, nil, gtserror.Newf("error updating database: %w", err)
|
||||
// Check for and handle any edits to status, inserting
|
||||
// historical edit if necessary. Also determines status
|
||||
// columns that need updating in below query.
|
||||
cols, err := d.handleStatusEdit(ctx,
|
||||
status,
|
||||
latestStatus,
|
||||
pollChanged,
|
||||
mentionsChanged,
|
||||
threadChanged,
|
||||
tagsChanged,
|
||||
mediaChanged,
|
||||
emojiChanged,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, gtserror.Newf("error handling edit for status %s: %w", uri, err)
|
||||
}
|
||||
|
||||
// With returned changed columns, now update the existing status entry.
|
||||
if err := d.state.DB.UpdateStatus(ctx, latestStatus, cols...); err != nil {
|
||||
return nil, nil, gtserror.Newf("error updating existing status %s: %w", uri, err)
|
||||
}
|
||||
}
|
||||
|
||||
return latestStatus, statusable, nil
|
||||
}
|
||||
|
||||
// fetchStatusMentions populates the mentions on 'status', creating
|
||||
// new where needed, or using unchanged mentions from 'existing' status.
|
||||
func (d *Dereferencer) fetchStatusMentions(
|
||||
ctx context.Context,
|
||||
requestUser string,
|
||||
existing *gtsmodel.Status,
|
||||
status *gtsmodel.Status,
|
||||
) error {
|
||||
) (
|
||||
changed bool,
|
||||
err error,
|
||||
) {
|
||||
|
||||
// Allocate new slice to take the yet-to-be created mention IDs.
|
||||
status.MentionIDs = make([]string, len(status.Mentions))
|
||||
|
||||
|
@ -610,7 +656,6 @@ func (d *Dereferencer) fetchStatusMentions(
|
|||
var (
|
||||
mention = status.Mentions[i]
|
||||
alreadyExists bool
|
||||
err error
|
||||
)
|
||||
|
||||
// Search existing status for a mention already stored,
|
||||
|
@ -633,19 +678,16 @@ func (d *Dereferencer) fetchStatusMentions(
|
|||
continue
|
||||
}
|
||||
|
||||
// Mark status as
|
||||
// having changed.
|
||||
changed = true
|
||||
|
||||
// This mention didn't exist yet.
|
||||
// Generate new ID according to status creation.
|
||||
// TODO: update this to use "edited_at" when we add
|
||||
// support for edited status revision history.
|
||||
mention.ID, err = id.NewULIDFromTime(status.CreatedAt)
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "invalid created at date (falling back to 'now'): %v", err)
|
||||
mention.ID = id.NewULID() // just use "now"
|
||||
}
|
||||
// Generate new ID according to latest update.
|
||||
mention.ID = id.NewULIDFromTime(status.UpdatedAt)
|
||||
|
||||
// Set known further mention details.
|
||||
mention.CreatedAt = status.CreatedAt
|
||||
mention.UpdatedAt = status.UpdatedAt
|
||||
mention.CreatedAt = status.UpdatedAt
|
||||
mention.OriginAccount = status.Account
|
||||
mention.OriginAccountID = status.AccountID
|
||||
mention.OriginAccountURI = status.AccountURI
|
||||
|
@ -657,7 +699,7 @@ func (d *Dereferencer) fetchStatusMentions(
|
|||
|
||||
// Place the new mention into the database.
|
||||
if err := d.state.DB.PutMention(ctx, mention); err != nil {
|
||||
return gtserror.Newf("error putting mention in database: %w", err)
|
||||
return changed, gtserror.Newf("error putting mention in database: %w", err)
|
||||
}
|
||||
|
||||
// Set the *new* mention and ID.
|
||||
|
@ -678,17 +720,42 @@ func (d *Dereferencer) fetchStatusMentions(
|
|||
i++
|
||||
}
|
||||
|
||||
return nil
|
||||
return changed, nil
|
||||
}
|
||||
|
||||
func (d *Dereferencer) threadStatus(ctx context.Context, status *gtsmodel.Status) error {
|
||||
if status.InReplyTo != nil {
|
||||
if parentThreadID := status.InReplyTo.ThreadID; parentThreadID != "" {
|
||||
// Simplest case: parent status
|
||||
// is threaded, so inherit threadID.
|
||||
status.ThreadID = parentThreadID
|
||||
return nil
|
||||
// threadStatus ensures that given status is threaded correctly
|
||||
// where necessary. that is it will inherit a thread ID from the
|
||||
// existing copy if it is threaded correctly, else it will inherit
|
||||
// a thread ID from a parent with existing thread, else it will
|
||||
// generate a new thread ID if status mentions a local account.
|
||||
func (d *Dereferencer) threadStatus(
|
||||
ctx context.Context,
|
||||
existing *gtsmodel.Status,
|
||||
status *gtsmodel.Status,
|
||||
) (
|
||||
changed bool,
|
||||
err error,
|
||||
) {
|
||||
|
||||
// Check for existing status
|
||||
// that is already threaded.
|
||||
if existing.ThreadID != "" {
|
||||
|
||||
// Existing is threaded correctly.
|
||||
if existing.InReplyTo == nil ||
|
||||
existing.InReplyTo.ThreadID == existing.ThreadID {
|
||||
status.ThreadID = existing.ThreadID
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// TODO: delete incorrect thread
|
||||
}
|
||||
|
||||
// Check for existing parent to inherit threading from.
|
||||
if inReplyTo := status.InReplyTo; inReplyTo != nil &&
|
||||
inReplyTo.ThreadID != "" {
|
||||
status.ThreadID = inReplyTo.ThreadID
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Parent wasn't threaded. If this
|
||||
|
@ -711,7 +778,7 @@ func (d *Dereferencer) threadStatus(ctx context.Context, status *gtsmodel.Status
|
|||
// Status doesn't mention a
|
||||
// local account, so we don't
|
||||
// need to thread it.
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Status mentions a local account.
|
||||
|
@ -719,24 +786,30 @@ func (d *Dereferencer) threadStatus(ctx context.Context, status *gtsmodel.Status
|
|||
// it to the status.
|
||||
threadID := id.NewULID()
|
||||
|
||||
if err := d.state.DB.PutThread(
|
||||
ctx,
|
||||
>smodel.Thread{
|
||||
ID: threadID,
|
||||
},
|
||||
// Insert new thread model into db.
|
||||
if err := d.state.DB.PutThread(ctx,
|
||||
>smodel.Thread{ID: threadID},
|
||||
); err != nil {
|
||||
return gtserror.Newf("error inserting new thread in db: %w", err)
|
||||
return false, gtserror.Newf("error inserting new thread in db: %w", err)
|
||||
}
|
||||
|
||||
// Set thread on latest status.
|
||||
status.ThreadID = threadID
|
||||
return nil
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// fetchStatusTags populates the tags on 'status', fetching existing
|
||||
// from the database and creating new where needed. 'existing' is used
|
||||
// to fetch tags that have not changed since previous stored status.
|
||||
func (d *Dereferencer) fetchStatusTags(
|
||||
ctx context.Context,
|
||||
existing *gtsmodel.Status,
|
||||
status *gtsmodel.Status,
|
||||
) error {
|
||||
) (
|
||||
changed bool,
|
||||
err error,
|
||||
) {
|
||||
|
||||
// Allocate new slice to take the yet-to-be determined tag IDs.
|
||||
status.TagIDs = make([]string, len(status.Tags))
|
||||
|
||||
|
@ -751,10 +824,14 @@ func (d *Dereferencer) fetchStatusTags(
|
|||
continue
|
||||
}
|
||||
|
||||
// Mark status as
|
||||
// having changed.
|
||||
changed = true
|
||||
|
||||
// Look for existing tag with name in the database.
|
||||
existing, err := d.state.DB.GetTagByName(ctx, tag.Name)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
return gtserror.Newf("db error getting tag %s: %w", tag.Name, err)
|
||||
return changed, gtserror.Newf("db error getting tag %s: %w", tag.Name, err)
|
||||
} else if existing != nil {
|
||||
status.Tags[i] = existing
|
||||
status.TagIDs[i] = existing.ID
|
||||
|
@ -788,106 +865,21 @@ func (d *Dereferencer) fetchStatusTags(
|
|||
i++
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Dereferencer) fetchStatusPoll(
|
||||
ctx context.Context,
|
||||
existing *gtsmodel.Status,
|
||||
status *gtsmodel.Status,
|
||||
) error {
|
||||
var (
|
||||
// insertStatusPoll generates ID and inserts the poll attached to status into the database.
|
||||
insertStatusPoll = func(ctx context.Context, status *gtsmodel.Status) error {
|
||||
var err error
|
||||
|
||||
// Generate new ID for poll from the status CreatedAt.
|
||||
// TODO: update this to use "edited_at" when we add
|
||||
// support for edited status revision history.
|
||||
status.Poll.ID, err = id.NewULIDFromTime(status.CreatedAt)
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "invalid created at date (falling back to 'now'): %v", err)
|
||||
status.Poll.ID = id.NewULID() // just use "now"
|
||||
}
|
||||
|
||||
// Update the status<->poll links.
|
||||
status.PollID = status.Poll.ID
|
||||
status.Poll.StatusID = status.ID
|
||||
status.Poll.Status = status
|
||||
|
||||
// Insert this latest poll into the database.
|
||||
err = d.state.DB.PutPoll(ctx, status.Poll)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error putting in database: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteStatusPoll deletes the poll with ID, and all attached votes, from the database.
|
||||
deleteStatusPoll = func(ctx context.Context, pollID string) error {
|
||||
if err := d.state.DB.DeletePollByID(ctx, pollID); err != nil {
|
||||
return gtserror.Newf("error deleting existing poll from database: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
)
|
||||
|
||||
switch {
|
||||
case existing.Poll == nil && status.Poll == nil:
|
||||
// no poll before or after, nothing to do.
|
||||
return nil
|
||||
|
||||
case existing.Poll == nil && status.Poll != nil:
|
||||
// no previous poll, insert new poll!
|
||||
return insertStatusPoll(ctx, status)
|
||||
|
||||
case status.Poll == nil:
|
||||
// existing poll has been deleted, remove this.
|
||||
return deleteStatusPoll(ctx, existing.PollID)
|
||||
|
||||
case pollChanged(existing.Poll, status.Poll):
|
||||
// poll has changed since original, delete and reinsert new.
|
||||
if err := deleteStatusPoll(ctx, existing.PollID); err != nil {
|
||||
return err
|
||||
}
|
||||
return insertStatusPoll(ctx, status)
|
||||
|
||||
case pollUpdated(existing.Poll, status.Poll):
|
||||
// Since we last saw it, the poll has updated!
|
||||
// Whether that be stats, or close time.
|
||||
poll := existing.Poll
|
||||
poll.Closing = pollJustClosed(existing.Poll, status.Poll)
|
||||
poll.ClosedAt = status.Poll.ClosedAt
|
||||
poll.Voters = status.Poll.Voters
|
||||
poll.Votes = status.Poll.Votes
|
||||
|
||||
// Update poll model in the database (specifically only the possible changed columns).
|
||||
if err := d.state.DB.UpdatePoll(ctx, poll, "closed_at", "voters", "votes"); err != nil {
|
||||
return gtserror.Newf("error updating poll: %w", err)
|
||||
}
|
||||
|
||||
// Update poll on status.
|
||||
status.PollID = poll.ID
|
||||
status.Poll = poll
|
||||
return nil
|
||||
|
||||
default:
|
||||
// latest and existing
|
||||
// polls are up to date.
|
||||
poll := existing.Poll
|
||||
status.PollID = poll.ID
|
||||
status.Poll = poll
|
||||
return nil
|
||||
}
|
||||
return changed, nil
|
||||
}
|
||||
|
||||
// fetchStatusAttachments populates the attachments on 'status', creating new database
|
||||
// entries where needed and dereferencing it, or using unchanged from 'existing' status.
|
||||
func (d *Dereferencer) fetchStatusAttachments(
|
||||
ctx context.Context,
|
||||
requestUser string,
|
||||
existing *gtsmodel.Status,
|
||||
status *gtsmodel.Status,
|
||||
) error {
|
||||
) (
|
||||
changed bool,
|
||||
err error,
|
||||
) {
|
||||
|
||||
// Allocate new slice to take the yet-to-be fetched attachment IDs.
|
||||
status.AttachmentIDs = make([]string, len(status.Attachments))
|
||||
|
||||
|
@ -897,9 +889,26 @@ func (d *Dereferencer) fetchStatusAttachments(
|
|||
// Look for existing media attachment with remote URL first.
|
||||
existing, ok := existing.GetAttachmentByRemoteURL(placeholder.RemoteURL)
|
||||
if ok && existing.ID != "" {
|
||||
var info media.AdditionalMediaInfo
|
||||
|
||||
// Ensure the existing media attachment is up-to-date and cached.
|
||||
existing, err := d.updateAttachment(ctx, requestUser, existing, placeholder)
|
||||
// Look for any difference in stored media description.
|
||||
diff := (existing.Description != placeholder.Description)
|
||||
if diff {
|
||||
info.Description = &placeholder.Description
|
||||
}
|
||||
|
||||
// If description changed,
|
||||
// we mark media as changed.
|
||||
changed = changed || diff
|
||||
|
||||
// Store any attachment updates and
|
||||
// ensure media is locally cached.
|
||||
existing, err := d.RefreshMedia(ctx,
|
||||
requestUser,
|
||||
existing,
|
||||
info,
|
||||
diff,
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf(ctx, "error updating existing attachment: %v", err)
|
||||
|
||||
|
@ -915,9 +924,12 @@ func (d *Dereferencer) fetchStatusAttachments(
|
|||
continue
|
||||
}
|
||||
|
||||
// Mark status as
|
||||
// having changed.
|
||||
changed = true
|
||||
|
||||
// Load this new media attachment.
|
||||
attachment, err := d.GetMedia(
|
||||
ctx,
|
||||
attachment, err := d.GetMedia(ctx,
|
||||
requestUser,
|
||||
status.AccountID,
|
||||
placeholder.RemoteURL,
|
||||
|
@ -955,28 +967,34 @@ func (d *Dereferencer) fetchStatusAttachments(
|
|||
i++
|
||||
}
|
||||
|
||||
return nil
|
||||
return changed, nil
|
||||
}
|
||||
|
||||
// fetchStatusEmojis populates the emojis on 'status', creating new database entries
|
||||
// where needed and dereferencing it, or using unchanged from 'existing' status.
|
||||
func (d *Dereferencer) fetchStatusEmojis(
|
||||
ctx context.Context,
|
||||
existing *gtsmodel.Status,
|
||||
status *gtsmodel.Status,
|
||||
) error {
|
||||
) (
|
||||
changed bool,
|
||||
err error,
|
||||
) {
|
||||
|
||||
// Fetch the updated emojis for our status.
|
||||
emojis, changed, err := d.fetchEmojis(ctx,
|
||||
existing.Emojis,
|
||||
status.Emojis,
|
||||
)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error fetching emojis: %w", err)
|
||||
return changed, gtserror.Newf("error fetching emojis: %w", err)
|
||||
}
|
||||
|
||||
if !changed {
|
||||
// Use existing status emoji objects.
|
||||
status.EmojiIDs = existing.EmojiIDs
|
||||
status.Emojis = existing.Emojis
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Set latest emojis.
|
||||
|
@ -988,9 +1006,254 @@ func (d *Dereferencer) fetchStatusEmojis(
|
|||
status.EmojiIDs[i] = emoji.ID
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// handleStatusPoll handles both inserting of new status poll or the
|
||||
// update of an existing poll. this handles the case of simple vote
|
||||
// count updates (without being classified as a change of the poll
|
||||
// itself), as well as full poll changes that delete existing instance.
|
||||
func (d *Dereferencer) handleStatusPoll(
|
||||
ctx context.Context,
|
||||
existing *gtsmodel.Status,
|
||||
status *gtsmodel.Status,
|
||||
) (
|
||||
changed bool,
|
||||
err error,
|
||||
) {
|
||||
switch {
|
||||
case existing.Poll == nil && status.Poll == nil:
|
||||
// no poll before or after, nothing to do.
|
||||
return false, nil
|
||||
|
||||
case existing.Poll == nil && status.Poll != nil:
|
||||
// no previous poll, insert new status poll!
|
||||
return true, d.insertStatusPoll(ctx, status)
|
||||
|
||||
case status.Poll == nil:
|
||||
// existing status poll has been deleted, remove this from the database.
|
||||
if err = d.state.DB.DeletePollByID(ctx, existing.Poll.ID); err != nil {
|
||||
err = gtserror.Newf("error deleting poll from database: %w", err)
|
||||
}
|
||||
return true, err
|
||||
|
||||
case pollChanged(existing.Poll, status.Poll):
|
||||
// existing status poll has been changed, remove this from the database.
|
||||
if err = d.state.DB.DeletePollByID(ctx, existing.Poll.ID); err != nil {
|
||||
return true, gtserror.Newf("error deleting poll from database: %w", err)
|
||||
}
|
||||
|
||||
// insert latest poll version into database.
|
||||
return true, d.insertStatusPoll(ctx, status)
|
||||
|
||||
case pollStateUpdated(existing.Poll, status.Poll):
|
||||
// Since we last saw it, the poll has updated!
|
||||
// Whether that be stats, or close time.
|
||||
poll := existing.Poll
|
||||
poll.Closing = pollJustClosed(existing.Poll, status.Poll)
|
||||
poll.ClosedAt = status.Poll.ClosedAt
|
||||
poll.Voters = status.Poll.Voters
|
||||
poll.Votes = status.Poll.Votes
|
||||
|
||||
// Update poll model in the database (specifically only the possible changed columns).
|
||||
if err = d.state.DB.UpdatePoll(ctx, poll, "closed_at", "voters", "votes"); err != nil {
|
||||
return false, gtserror.Newf("error updating poll: %w", err)
|
||||
}
|
||||
|
||||
// Update poll on status.
|
||||
status.PollID = poll.ID
|
||||
status.Poll = poll
|
||||
return false, nil
|
||||
|
||||
default:
|
||||
// latest and existing
|
||||
// polls are up to date.
|
||||
poll := existing.Poll
|
||||
status.PollID = poll.ID
|
||||
status.Poll = poll
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// insertStatusPoll inserts an assumed new poll attached to status into the database, this
|
||||
// also handles generating new ID for the poll and setting necessary fields on the status.
|
||||
func (d *Dereferencer) insertStatusPoll(ctx context.Context, status *gtsmodel.Status) error {
|
||||
var err error
|
||||
|
||||
// Generate new ID for poll from latest updated time.
|
||||
status.Poll.ID = id.NewULIDFromTime(status.UpdatedAt)
|
||||
|
||||
// Update the status<->poll links.
|
||||
status.PollID = status.Poll.ID
|
||||
status.Poll.StatusID = status.ID
|
||||
status.Poll.Status = status
|
||||
|
||||
// Insert this latest poll into the database.
|
||||
err = d.state.DB.PutPoll(ctx, status.Poll)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error putting poll in database: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleStatusEdit compiles a list of changed status table columns between
|
||||
// existing and latest status model, and where necessary inserts a historic
|
||||
// edit of the status into the database to store its previous state. the
|
||||
// returned slice is a list of columns requiring updating in the database.
|
||||
func (d *Dereferencer) handleStatusEdit(
|
||||
ctx context.Context,
|
||||
existing *gtsmodel.Status,
|
||||
status *gtsmodel.Status,
|
||||
pollChanged bool,
|
||||
mentionsChanged bool,
|
||||
threadChanged bool,
|
||||
tagsChanged bool,
|
||||
mediaChanged bool,
|
||||
emojiChanged bool,
|
||||
) (
|
||||
cols []string,
|
||||
err error,
|
||||
) {
|
||||
var edited bool
|
||||
|
||||
// Preallocate max slice length.
|
||||
cols = make([]string, 0, 13)
|
||||
|
||||
// Always update `fetched_at`.
|
||||
cols = append(cols, "fetched_at")
|
||||
|
||||
// Check for edited status content.
|
||||
if existing.Content != status.Content {
|
||||
cols = append(cols, "content")
|
||||
edited = true
|
||||
}
|
||||
|
||||
// Check for edited status content warning.
|
||||
if existing.ContentWarning != status.ContentWarning {
|
||||
cols = append(cols, "content_warning")
|
||||
edited = true
|
||||
}
|
||||
|
||||
// Check for edited status sensitive flag.
|
||||
if *existing.Sensitive != *status.Sensitive {
|
||||
cols = append(cols, "sensitive")
|
||||
edited = true
|
||||
}
|
||||
|
||||
// Check for edited status language tag.
|
||||
if existing.Language != status.Language {
|
||||
cols = append(cols, "language")
|
||||
edited = true
|
||||
}
|
||||
|
||||
if pollChanged {
|
||||
// Attached poll was changed.
|
||||
cols = append(cols, "poll_id")
|
||||
edited = true
|
||||
}
|
||||
|
||||
if mentionsChanged {
|
||||
cols = append(cols, "mentions") // i.e. MentionIDs
|
||||
|
||||
// Mentions changed doesn't necessarily
|
||||
// indicate an edit, it may just not have
|
||||
// been previously populated properly.
|
||||
}
|
||||
|
||||
if threadChanged {
|
||||
cols = append(cols, "thread_id")
|
||||
|
||||
// Thread changed doesn't necessarily
|
||||
// indicate an edit, it may just now
|
||||
// actually be included in a thread.
|
||||
}
|
||||
|
||||
if tagsChanged {
|
||||
cols = append(cols, "tags") // i.e. TagIDs
|
||||
|
||||
// Tags changed doesn't necessarily
|
||||
// indicate an edit, it may just not have
|
||||
// been previously populated properly.
|
||||
}
|
||||
|
||||
if mediaChanged {
|
||||
// Attached media was changed.
|
||||
cols = append(cols, "attachments") // i.e. AttachmentIDs
|
||||
edited = true
|
||||
}
|
||||
|
||||
if emojiChanged {
|
||||
// Attached emojis changed.
|
||||
cols = append(cols, "emojis") // i.e. EmojiIDs
|
||||
|
||||
// Emojis changed doesn't necessarily
|
||||
// indicate an edit, it may just not have
|
||||
// been previously populated properly.
|
||||
}
|
||||
|
||||
if edited {
|
||||
// We prefer to use provided 'upated_at', but ensure
|
||||
// it fits chronologically with creation / last update.
|
||||
if !status.UpdatedAt.After(status.CreatedAt) ||
|
||||
!status.UpdatedAt.After(existing.UpdatedAt) {
|
||||
|
||||
// Else fallback to now as update time.
|
||||
status.UpdatedAt = status.FetchedAt
|
||||
}
|
||||
|
||||
// Status has been editted since last
|
||||
// we saw it, take snapshot of existing.
|
||||
var edit gtsmodel.StatusEdit
|
||||
edit.ID = id.NewULIDFromTime(status.UpdatedAt)
|
||||
edit.Content = existing.Content
|
||||
edit.ContentWarning = existing.ContentWarning
|
||||
edit.Text = existing.Text
|
||||
edit.Language = existing.Language
|
||||
edit.Sensitive = existing.Sensitive
|
||||
edit.StatusID = status.ID
|
||||
|
||||
// Copy existing attachments and descriptions.
|
||||
edit.AttachmentIDs = existing.AttachmentIDs
|
||||
edit.Attachments = existing.Attachments
|
||||
if l := len(existing.Attachments); l > 0 {
|
||||
edit.AttachmentDescriptions = make([]string, l)
|
||||
for i, attach := range existing.Attachments {
|
||||
edit.AttachmentDescriptions[i] = attach.Description
|
||||
}
|
||||
}
|
||||
|
||||
// Edit creation is last update time.
|
||||
edit.CreatedAt = existing.UpdatedAt
|
||||
|
||||
if existing.Poll != nil {
|
||||
// Poll only set if existing contained them.
|
||||
edit.PollOptions = existing.Poll.Options
|
||||
|
||||
if !*existing.Poll.HideCounts || pollChanged {
|
||||
// If the counts are allowed to be
|
||||
// shown, or poll has changed, then
|
||||
// include poll vote counts in edit.
|
||||
edit.PollVotes = existing.Poll.Votes
|
||||
}
|
||||
}
|
||||
|
||||
// Insert this new edit of existing status into database.
|
||||
if err := d.state.DB.PutStatusEdit(ctx, &edit); err != nil {
|
||||
return nil, gtserror.Newf("error putting edit in database: %w", err)
|
||||
}
|
||||
|
||||
// Add edit to list of edits on the status.
|
||||
status.EditIDs = append(status.EditIDs, edit.ID)
|
||||
status.Edits = append(status.Edits, &edit)
|
||||
|
||||
// Add updated_at and edits to list of cols.
|
||||
cols = append(cols, "updated_at", "edits")
|
||||
}
|
||||
|
||||
return cols, nil
|
||||
}
|
||||
|
||||
// getPopulatedMention tries to populate the given
|
||||
// mention with the correct TargetAccount and (if not
|
||||
// yet set) TargetAccountURI, returning the populated
|
||||
|
|
|
@ -62,6 +62,7 @@ func (d *Dereferencer) isPermittedStatus(
|
|||
requestUser string,
|
||||
existing *gtsmodel.Status,
|
||||
status *gtsmodel.Status,
|
||||
isNew bool,
|
||||
) (
|
||||
permitted bool, // is permitted?
|
||||
err error,
|
||||
|
@ -98,7 +99,7 @@ func (d *Dereferencer) isPermittedStatus(
|
|||
permitted = true
|
||||
}
|
||||
|
||||
if !permitted && existing != nil {
|
||||
if !permitted && !isNew {
|
||||
log.Infof(ctx, "deleting unpermitted: %s", existing.URI)
|
||||
|
||||
// Delete existing status from database as it's no longer permitted.
|
||||
|
@ -110,11 +111,13 @@ func (d *Dereferencer) isPermittedStatus(
|
|||
return
|
||||
}
|
||||
|
||||
// isPermittedReply ...
|
||||
func (d *Dereferencer) isPermittedReply(
|
||||
ctx context.Context,
|
||||
requestUser string,
|
||||
reply *gtsmodel.Status,
|
||||
) (bool, error) {
|
||||
|
||||
var (
|
||||
replyURI = reply.URI // Definitely set.
|
||||
inReplyToURI = reply.InReplyToURI // Definitely set.
|
||||
|
@ -149,8 +152,7 @@ func (d *Dereferencer) isPermittedReply(
|
|||
// If this status's parent was rejected,
|
||||
// implicitly this reply should be too;
|
||||
// there's nothing more to check here.
|
||||
return false, d.unpermittedByParent(
|
||||
ctx,
|
||||
return false, d.unpermittedByParent(ctx,
|
||||
reply,
|
||||
thisReq,
|
||||
parentReq,
|
||||
|
@ -164,6 +166,7 @@ func (d *Dereferencer) isPermittedReply(
|
|||
// be approved, then we should just reject it
|
||||
// again, as nothing's changed since last time.
|
||||
if thisRejected && acceptIRI == "" {
|
||||
|
||||
// Nothing changed,
|
||||
// still rejected.
|
||||
return false, nil
|
||||
|
@ -174,6 +177,7 @@ func (d *Dereferencer) isPermittedReply(
|
|||
// to be approved. Continue permission checks.
|
||||
|
||||
if inReplyTo == nil {
|
||||
|
||||
// If we didn't have the replied-to status
|
||||
// in our database (yet), we can't check
|
||||
// right now if this reply is permitted.
|
||||
|
|
|
@ -21,14 +21,21 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/activity/streams"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
// instantFreshness is the shortest possible freshness window.
|
||||
var instantFreshness = util.Ptr(dereferencing.FreshnessWindow(0))
|
||||
|
||||
type StatusTestSuite struct {
|
||||
DereferencerStandardTestSuite
|
||||
}
|
||||
|
@ -229,6 +236,219 @@ func (suite *StatusTestSuite) TestDereferenceStatusWithNonMatchingURI() {
|
|||
suite.Nil(fetchedStatus)
|
||||
}
|
||||
|
||||
func (suite *StatusTestSuite) TestDereferencerRefreshStatusUpdated() {
|
||||
// Create a new context for this test.
|
||||
ctx, cncl := context.WithCancel(context.Background())
|
||||
defer cncl()
|
||||
|
||||
// The local account we will be fetching statuses as.
|
||||
fetchingAccount := suite.testAccounts["local_account_1"]
|
||||
|
||||
// The test status in question that we will be dereferencing from "remote".
|
||||
testURIStr := "https://unknown-instance.com/users/brand_new_person/statuses/01FE4NTHKWW7THT67EF10EB839"
|
||||
testURI := testrig.URLMustParse(testURIStr)
|
||||
testStatusable := suite.client.TestRemoteStatuses[testURIStr]
|
||||
|
||||
// Fetch the remote status first to load it into instance.
|
||||
testStatus, statusable, err := suite.dereferencer.GetStatusByURI(ctx,
|
||||
fetchingAccount.Username,
|
||||
testURI,
|
||||
)
|
||||
suite.NotNil(statusable)
|
||||
suite.NoError(err)
|
||||
|
||||
// Run through multiple possible edits.
|
||||
for _, testCase := range []struct {
|
||||
editedContent string
|
||||
editedContentWarning string
|
||||
editedLanguage string
|
||||
editedSensitive bool
|
||||
editedAttachmentIDs []string
|
||||
editedPollOptions []string
|
||||
editedPollVotes []int
|
||||
editedAt time.Time
|
||||
}{
|
||||
{
|
||||
editedContent: "updated status content!",
|
||||
editedContentWarning: "CW: edited status content",
|
||||
editedLanguage: testStatus.Language, // no change
|
||||
editedSensitive: *testStatus.Sensitive, // no change
|
||||
editedAttachmentIDs: testStatus.AttachmentIDs, // no change
|
||||
editedPollOptions: getPollOptions(testStatus), // no change
|
||||
editedPollVotes: getPollVotes(testStatus), // no change
|
||||
editedAt: time.Now(),
|
||||
},
|
||||
} {
|
||||
// Take a snapshot of current
|
||||
// state of the test status.
|
||||
testStatus = copyStatus(testStatus)
|
||||
|
||||
// Edit the "remote" statusable obj.
|
||||
suite.editStatusable(testStatusable,
|
||||
testCase.editedContent,
|
||||
testCase.editedContentWarning,
|
||||
testCase.editedLanguage,
|
||||
testCase.editedSensitive,
|
||||
testCase.editedAttachmentIDs,
|
||||
testCase.editedPollOptions,
|
||||
testCase.editedPollVotes,
|
||||
testCase.editedAt,
|
||||
)
|
||||
|
||||
// Refresh with a given statusable to updated to edited copy.
|
||||
latest, statusable, err := suite.dereferencer.RefreshStatus(ctx,
|
||||
fetchingAccount.Username,
|
||||
testStatus,
|
||||
nil, // NOTE: can provide testStatusable here to test as being received (not deref'd)
|
||||
instantFreshness,
|
||||
)
|
||||
suite.NotNil(statusable)
|
||||
suite.NoError(err)
|
||||
|
||||
// verify updated status details.
|
||||
suite.verifyEditedStatusUpdate(
|
||||
|
||||
// the original status
|
||||
// before any changes.
|
||||
testStatus,
|
||||
|
||||
// latest status
|
||||
// being tested.
|
||||
latest,
|
||||
|
||||
// expected current state.
|
||||
>smodel.StatusEdit{
|
||||
Content: testCase.editedContent,
|
||||
ContentWarning: testCase.editedContentWarning,
|
||||
Language: testCase.editedLanguage,
|
||||
Sensitive: &testCase.editedSensitive,
|
||||
AttachmentIDs: testCase.editedAttachmentIDs,
|
||||
PollOptions: testCase.editedPollOptions,
|
||||
PollVotes: testCase.editedPollVotes,
|
||||
// createdAt never changes
|
||||
},
|
||||
|
||||
// expected historic edit.
|
||||
>smodel.StatusEdit{
|
||||
Content: testStatus.Content,
|
||||
ContentWarning: testStatus.ContentWarning,
|
||||
Language: testStatus.Language,
|
||||
Sensitive: testStatus.Sensitive,
|
||||
AttachmentIDs: testStatus.AttachmentIDs,
|
||||
PollOptions: getPollOptions(testStatus),
|
||||
PollVotes: getPollVotes(testStatus),
|
||||
CreatedAt: testStatus.UpdatedAt,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// editStatusable updates the given statusable attributes.
|
||||
// note that this acts on the original object, no copying.
|
||||
func (suite *StatusTestSuite) editStatusable(
|
||||
statusable ap.Statusable,
|
||||
content string,
|
||||
contentWarning string,
|
||||
language string,
|
||||
sensitive bool,
|
||||
attachmentIDs []string, // TODO: this will require some thinking as to how ...
|
||||
pollOptions []string, // TODO: this will require changing statusable type to question
|
||||
pollVotes []int, // TODO: this will require changing statusable type to question
|
||||
editedAt time.Time,
|
||||
) {
|
||||
// simply reset all mentions / emojis / tags
|
||||
statusable.SetActivityStreamsTag(nil)
|
||||
|
||||
// Update the statusable content property + language (if set).
|
||||
contentProp := streams.NewActivityStreamsContentProperty()
|
||||
statusable.SetActivityStreamsContent(contentProp)
|
||||
contentProp.AppendXMLSchemaString(content)
|
||||
if language != "" {
|
||||
contentProp.AppendRDFLangString(map[string]string{
|
||||
language: content,
|
||||
})
|
||||
}
|
||||
|
||||
// Update the statusable content-warning property.
|
||||
summaryProp := streams.NewActivityStreamsSummaryProperty()
|
||||
statusable.SetActivityStreamsSummary(summaryProp)
|
||||
summaryProp.AppendXMLSchemaString(contentWarning)
|
||||
|
||||
// Update the statusable sensitive property.
|
||||
sensitiveProp := streams.NewActivityStreamsSensitiveProperty()
|
||||
statusable.SetActivityStreamsSensitive(sensitiveProp)
|
||||
sensitiveProp.AppendXMLSchemaBoolean(sensitive)
|
||||
|
||||
// Update the statusable updated property.
|
||||
ap.SetUpdated(statusable, editedAt)
|
||||
}
|
||||
|
||||
// verifyEditedStatusUpdate verifies that a given status has
|
||||
// the expected number of historic edits, the 'current' status
|
||||
// attributes (encapsulated as an edit for minimized no. args),
|
||||
// and the last given 'historic' status edit attributes.
|
||||
func (suite *StatusTestSuite) verifyEditedStatusUpdate(
|
||||
testStatus *gtsmodel.Status, // the original model
|
||||
status *gtsmodel.Status, // the status to check
|
||||
current *gtsmodel.StatusEdit, // expected current state
|
||||
historic *gtsmodel.StatusEdit, // historic edit we expect to have
|
||||
) {
|
||||
// don't use this func
|
||||
// name in error msgs.
|
||||
suite.T().Helper()
|
||||
|
||||
// Check we have expected number of edits.
|
||||
previousEdits := len(testStatus.Edits)
|
||||
suite.Len(status.Edits, previousEdits+1)
|
||||
suite.Len(status.EditIDs, previousEdits+1)
|
||||
|
||||
// Check current state of status.
|
||||
suite.Equal(current.Content, status.Content)
|
||||
suite.Equal(current.ContentWarning, status.ContentWarning)
|
||||
suite.Equal(current.Language, status.Language)
|
||||
suite.Equal(*current.Sensitive, *status.Sensitive)
|
||||
suite.Equal(current.AttachmentIDs, status.AttachmentIDs)
|
||||
suite.Equal(current.PollOptions, getPollOptions(status))
|
||||
suite.Equal(current.PollVotes, getPollVotes(status))
|
||||
|
||||
// Check the latest historic edit matches expected.
|
||||
latestEdit := status.Edits[len(status.Edits)-1]
|
||||
suite.Equal(historic.Content, latestEdit.Content)
|
||||
suite.Equal(historic.ContentWarning, latestEdit.ContentWarning)
|
||||
suite.Equal(historic.Language, latestEdit.Language)
|
||||
suite.Equal(*historic.Sensitive, *latestEdit.Sensitive)
|
||||
suite.Equal(historic.AttachmentIDs, latestEdit.AttachmentIDs)
|
||||
suite.Equal(historic.PollOptions, latestEdit.PollOptions)
|
||||
suite.Equal(historic.PollVotes, latestEdit.PollVotes)
|
||||
suite.Equal(historic.CreatedAt, latestEdit.CreatedAt)
|
||||
|
||||
// The status creation date should never change.
|
||||
suite.Equal(testStatus.CreatedAt, status.CreatedAt)
|
||||
}
|
||||
|
||||
func TestStatusTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(StatusTestSuite))
|
||||
}
|
||||
|
||||
// copyStatus returns a copy of the given status model (not including sub-structs).
|
||||
func copyStatus(status *gtsmodel.Status) *gtsmodel.Status {
|
||||
copy := new(gtsmodel.Status)
|
||||
*copy = *status
|
||||
return copy
|
||||
}
|
||||
|
||||
// getPollOptions extracts poll option strings from status (if poll is set).
|
||||
func getPollOptions(status *gtsmodel.Status) []string {
|
||||
if status.Poll != nil {
|
||||
return status.Poll.Options
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getPollVotes extracts poll vote counts from status (if poll is set).
|
||||
func getPollVotes(status *gtsmodel.Status) []int {
|
||||
if status.Poll != nil {
|
||||
return status.Poll.Votes
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -52,15 +52,15 @@ func emojiChanged(existing, latest *gtsmodel.Emoji) bool {
|
|||
|
||||
// pollChanged returns whether a poll has changed in way that
|
||||
// indicates that this should be an entirely new poll. i.e. if
|
||||
// the available options have changed, or the expiry has increased.
|
||||
// the available options have changed, or the expiry has changed.
|
||||
func pollChanged(existing, latest *gtsmodel.Poll) bool {
|
||||
return !slices.Equal(existing.Options, latest.Options) ||
|
||||
!existing.ExpiresAt.Equal(latest.ExpiresAt)
|
||||
}
|
||||
|
||||
// pollUpdated returns whether a poll has updated, i.e. if the
|
||||
// pollStateUpdated returns whether a poll has updated, i.e. if
|
||||
// vote counts have changed, or if it has expired / been closed.
|
||||
func pollUpdated(existing, latest *gtsmodel.Poll) bool {
|
||||
func pollStateUpdated(existing, latest *gtsmodel.Poll) bool {
|
||||
return *existing.Voters != *latest.Voters ||
|
||||
!slices.Equal(existing.Votes, latest.Votes) ||
|
||||
!existing.ClosedAt.Equal(latest.ClosedAt)
|
||||
|
|
|
@ -79,7 +79,7 @@ func (suite *AnnounceTestSuite) TestAnnounceTwice() {
|
|||
|
||||
// Insert the boost-of status into the
|
||||
// DB cache to emulate processor handling
|
||||
boost.ID, _ = id.NewULIDFromTime(boost.CreatedAt)
|
||||
boost.ID = id.NewULIDFromTime(boost.CreatedAt)
|
||||
suite.state.Caches.DB.Status.Put(boost)
|
||||
|
||||
// only the URI will be set for the boosted status
|
||||
|
|
|
@ -26,7 +26,6 @@ import (
|
|||
type MediaAttachment 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
|
||||
StatusID string `bun:"type:CHAR(26),nullzero"` // ID of the status to which this is attached
|
||||
URL string `bun:",nullzero"` // Where can the attachment be retrieved on *this* server
|
||||
RemoteURL string `bun:",nullzero"` // Where can the attachment be retrieved on a remote server (empty for local media)
|
||||
|
|
|
@ -26,7 +26,6 @@ import (
|
|||
type Mention 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
|
||||
StatusID string `bun:"type:CHAR(26),nullzero,notnull"` // ID of the status this mention originates from
|
||||
Status *Status `bun:"rel:belongs-to"` // status referred to by statusID
|
||||
OriginAccountID string `bun:"type:CHAR(26),nullzero,notnull"` // ID of the mention creator account
|
||||
|
|
|
@ -20,6 +20,8 @@ package gtsmodel
|
|||
import (
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util/xslices"
|
||||
)
|
||||
|
||||
// Status represents a user-created 'post' or 'status' in the database, either remote or local
|
||||
|
@ -55,6 +57,8 @@ type Status struct {
|
|||
BoostOf *Status `bun:"-"` // status that corresponds to boostOfID
|
||||
BoostOfAccount *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:"edits,array"` //
|
||||
Edits []*StatusEdit `bun:"-"` //
|
||||
PollID string `bun:"type:CHAR(26),nullzero"` //
|
||||
Poll *Poll `bun:"-"` //
|
||||
ContentWarning string `bun:",nullzero"` // cw string for this status
|
||||
|
@ -92,7 +96,8 @@ func (s *Status) GetBoostOfAccountID() string {
|
|||
return s.BoostOfAccountID
|
||||
}
|
||||
|
||||
// AttachmentsPopulated returns whether media attachments are populated according to current AttachmentIDs.
|
||||
// AttachmentsPopulated returns whether media attachments
|
||||
// are populated according to current AttachmentIDs.
|
||||
func (s *Status) AttachmentsPopulated() bool {
|
||||
if len(s.AttachmentIDs) != len(s.Attachments) {
|
||||
// this is the quickest indicator.
|
||||
|
@ -106,7 +111,8 @@ func (s *Status) AttachmentsPopulated() bool {
|
|||
return true
|
||||
}
|
||||
|
||||
// TagsPopulated returns whether tags are populated according to current TagIDs.
|
||||
// TagsPopulated returns whether tags are
|
||||
// populated according to current TagIDs.
|
||||
func (s *Status) TagsPopulated() bool {
|
||||
if len(s.TagIDs) != len(s.Tags) {
|
||||
// this is the quickest indicator.
|
||||
|
@ -120,7 +126,8 @@ func (s *Status) TagsPopulated() bool {
|
|||
return true
|
||||
}
|
||||
|
||||
// MentionsPopulated returns whether mentions are populated according to current MentionIDs.
|
||||
// MentionsPopulated returns whether mentions are
|
||||
// populated according to current MentionIDs.
|
||||
func (s *Status) MentionsPopulated() bool {
|
||||
if len(s.MentionIDs) != len(s.Mentions) {
|
||||
// this is the quickest indicator.
|
||||
|
@ -134,7 +141,8 @@ func (s *Status) MentionsPopulated() bool {
|
|||
return true
|
||||
}
|
||||
|
||||
// EmojisPopulated returns whether emojis are populated according to current EmojiIDs.
|
||||
// EmojisPopulated returns whether emojis are
|
||||
// populated according to current EmojiIDs.
|
||||
func (s *Status) EmojisPopulated() bool {
|
||||
if len(s.EmojiIDs) != len(s.Emojis) {
|
||||
// this is the quickest indicator.
|
||||
|
@ -148,6 +156,21 @@ func (s *Status) EmojisPopulated() bool {
|
|||
return true
|
||||
}
|
||||
|
||||
// EditsPopulated returns whether edits are
|
||||
// populated according to current EditIDs.
|
||||
func (s *Status) EditsPopulated() bool {
|
||||
if len(s.EditIDs) != len(s.Edits) {
|
||||
// this is quickest indicator.
|
||||
return false
|
||||
}
|
||||
for i, id := range s.EditIDs {
|
||||
if s.Edits[i].ID != id {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// EmojissUpToDate returns whether status emoji attachments of receiving status are up-to-date
|
||||
// according to emoji attachments of the passed status, by comparing their emoji URIs. We don't
|
||||
// use IDs as this is used to determine whether there are new emojis to fetch.
|
||||
|
@ -247,6 +270,35 @@ func (s *Status) IsLocalOnly() bool {
|
|||
return s.Federated == nil || !*s.Federated
|
||||
}
|
||||
|
||||
// AllAttachmentIDs gathers ALL media attachment IDs from both the
|
||||
// receiving Status{}, and any historical Status{}.Edits. Note that
|
||||
// this function will panic if Status{}.Edits is not populated.
|
||||
func (s *Status) AllAttachmentIDs() []string {
|
||||
var total int
|
||||
|
||||
if len(s.EditIDs) != len(s.Edits) {
|
||||
panic("status edits not populated")
|
||||
}
|
||||
|
||||
// Get count of attachment IDs.
|
||||
total += len(s.Attachments)
|
||||
for _, edit := range s.Edits {
|
||||
total += len(edit.AttachmentIDs)
|
||||
}
|
||||
|
||||
// Start gathering of all IDs with *current* attachment IDs.
|
||||
attachmentIDs := make([]string, len(s.AttachmentIDs), total)
|
||||
copy(attachmentIDs, s.AttachmentIDs)
|
||||
|
||||
// Append IDs of historical edits.
|
||||
for _, edit := range s.Edits {
|
||||
attachmentIDs = append(attachmentIDs, edit.AttachmentIDs...)
|
||||
}
|
||||
|
||||
// Deduplicate these IDs in case of shared media.
|
||||
return xslices.Deduplicate(attachmentIDs)
|
||||
}
|
||||
|
||||
// StatusToTag is an intermediate struct to facilitate the many2many relationship between a status and one or more tags.
|
||||
type StatusToTag struct {
|
||||
StatusID string `bun:"type:CHAR(26),unique:statustag,nullzero,notnull"`
|
||||
|
|
62
internal/gtsmodel/statusedit.go
Normal file
62
internal/gtsmodel/statusedit.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
// 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"
|
||||
|
||||
// StatusEdit represents a **historical** view of a Status
|
||||
// after a received edit. The Status itself will always
|
||||
// contain the latest up-to-date information.
|
||||
//
|
||||
// Note that stored status edits may not exactly match that
|
||||
// of the origin server, they are a best-effort by receiver
|
||||
// to store version history. There is no AP history endpoint.
|
||||
type StatusEdit struct {
|
||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // ID of this item in the database.
|
||||
Content string `bun:""` // Content of status at time of edit; likely html-formatted but not guaranteed.
|
||||
ContentWarning string `bun:",nullzero"` // Content warning of status at time of edit.
|
||||
Text string `bun:""` // Original status text, without formatting, at time of edit.
|
||||
Language string `bun:",nullzero"` // Status language at time of edit.
|
||||
Sensitive *bool `bun:",nullzero,notnull,default:false"` // Status sensitive flag at time of edit.
|
||||
AttachmentIDs []string `bun:"attachments,array"` // Database IDs of media attachments associated with status at time of edit.
|
||||
AttachmentDescriptions []string `bun:",array"` // Previous media descriptions of media attachments associated with status at time of edit.
|
||||
Attachments []*MediaAttachment `bun:"-"` // Media attachments relating to .AttachmentIDs field (not always populated).
|
||||
PollOptions []string `bun:",array"` // Poll options of status at time of edit, only set if status contains a poll.
|
||||
PollVotes []int `bun:",array"` // Poll vote count at time of status edit, only set if poll votes were reset.
|
||||
StatusID string `bun:"type:CHAR(26),nullzero,notnull"` // The originating status ID this is a historical edit of.
|
||||
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // The creation time of this version of the status content (according to receiving server).
|
||||
|
||||
// We don't bother having a *gtsmodel.Status model here
|
||||
// as the StatusEdit is always just attached to a Status,
|
||||
// so it doesn't need a self-reference back to it.
|
||||
}
|
||||
|
||||
// AttachmentsPopulated returns whether media attachments
|
||||
// are populated according to current AttachmentIDs.
|
||||
func (e *StatusEdit) AttachmentsPopulated() bool {
|
||||
if len(e.AttachmentIDs) != len(e.Attachments) {
|
||||
// this is the quickest indicator.
|
||||
return false
|
||||
}
|
||||
for i, id := range e.AttachmentIDs {
|
||||
if e.Attachments[i].ID != id {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
|
@ -22,7 +22,9 @@ import (
|
|||
"math/big"
|
||||
"time"
|
||||
|
||||
"codeberg.org/gruf/go-kv"
|
||||
"github.com/oklog/ulid"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -45,13 +47,19 @@ func NewULID() string {
|
|||
return ulid.String()
|
||||
}
|
||||
|
||||
// NewULIDFromTime returns a new ULID string using the given time, or an error if something goes wrong.
|
||||
func NewULIDFromTime(t time.Time) (string, error) {
|
||||
newUlid, err := ulid.New(ulid.Timestamp(t), rand.Reader)
|
||||
if err != nil {
|
||||
return "", err
|
||||
// NewULIDFromTime returns a new ULID string using
|
||||
// given time, or from current time on any error.
|
||||
func NewULIDFromTime(t time.Time) string {
|
||||
ts := ulid.Timestamp(t)
|
||||
if ts > ulid.MaxTime() {
|
||||
log.WarnKVs(nil, kv.Fields{
|
||||
{K: "caller", V: log.Caller(2)},
|
||||
{K: "value", V: t},
|
||||
{K: "msg", V: "invalid ulid time"},
|
||||
}...)
|
||||
ts = ulid.Now()
|
||||
}
|
||||
return newUlid.String(), nil
|
||||
return ulid.MustNew(ts, rand.Reader).String()
|
||||
}
|
||||
|
||||
// NewRandomULID returns a new ULID string using a random time in an ~80 year range around the current datetime, or an error if something goes wrong.
|
||||
|
|
|
@ -118,15 +118,11 @@ func (m *Manager) CreateMedia(
|
|||
Header: util.Ptr(false),
|
||||
Cached: util.Ptr(false),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
// Check if we were provided additional info
|
||||
// to add to the attachment, and overwrite
|
||||
// some of the attachment fields if so.
|
||||
if info.CreatedAt != nil {
|
||||
attachment.CreatedAt = *info.CreatedAt
|
||||
}
|
||||
if info.StatusID != nil {
|
||||
attachment.StatusID = *info.StatusID
|
||||
}
|
||||
|
@ -372,9 +368,6 @@ func (m *Manager) createOrUpdateEmoji(
|
|||
if info.URI != nil {
|
||||
emoji.URI = *info.URI
|
||||
}
|
||||
if info.CreatedAt != nil {
|
||||
emoji.CreatedAt = *info.CreatedAt
|
||||
}
|
||||
if info.Domain != nil {
|
||||
emoji.Domain = *info.Domain
|
||||
}
|
||||
|
|
|
@ -109,7 +109,6 @@ func (suite *ManagerTestSuite) TestEmojiProcessRefresh() {
|
|||
emojiToUpdate,
|
||||
data,
|
||||
media.AdditionalEmojiInfo{
|
||||
CreatedAt: &emojiToUpdate.CreatedAt,
|
||||
Domain: &emojiToUpdate.Domain,
|
||||
ImageRemoteURL: &newImageRemoteURL,
|
||||
},
|
||||
|
|
|
@ -20,7 +20,6 @@ package media
|
|||
import (
|
||||
"context"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Size string
|
||||
|
@ -44,10 +43,6 @@ const (
|
|||
// should be added to attachment when processing a piece of media.
|
||||
type AdditionalMediaInfo struct {
|
||||
|
||||
// Time that this media was
|
||||
// created; defaults to time.Now().
|
||||
CreatedAt *time.Time
|
||||
|
||||
// ID of the status to which this
|
||||
// media is attached; defaults to "".
|
||||
StatusID *string
|
||||
|
@ -93,10 +88,6 @@ type AdditionalEmojiInfo struct {
|
|||
// this remote emoji.
|
||||
URI *string
|
||||
|
||||
// Time that this emoji was
|
||||
// created; defaults to time.Now().
|
||||
CreatedAt *time.Time
|
||||
|
||||
// Domain the emoji originated from. Blank
|
||||
// for this instance's domain. Defaults to "".
|
||||
Domain *string
|
||||
|
|
|
@ -70,7 +70,7 @@ func (suite *GetRSSTestSuite) TestGetAccountRSSAdmin() {
|
|||
func (suite *GetRSSTestSuite) TestGetAccountRSSZork() {
|
||||
getFeed, lastModified, err := suite.accountProcessor.GetRSSFeedForUsername(context.Background(), "the_mighty_zork")
|
||||
suite.NoError(err)
|
||||
suite.EqualValues(1704878640, lastModified.Unix())
|
||||
suite.EqualValues(1730451600, lastModified.Unix())
|
||||
|
||||
feed, err := getFeed()
|
||||
suite.NoError(err)
|
||||
|
@ -79,13 +79,23 @@ func (suite *GetRSSTestSuite) TestGetAccountRSSZork() {
|
|||
<title>Posts from @the_mighty_zork@localhost:8080</title>
|
||||
<link>http://localhost:8080/@the_mighty_zork</link>
|
||||
<description>Posts from @the_mighty_zork@localhost:8080</description>
|
||||
<pubDate>Wed, 10 Jan 2024 09:24:00 +0000</pubDate>
|
||||
<lastBuildDate>Wed, 10 Jan 2024 09:24:00 +0000</lastBuildDate>
|
||||
<pubDate>Fri, 01 Nov 2024 09:00:00 +0000</pubDate>
|
||||
<lastBuildDate>Fri, 01 Nov 2024 09:00:00 +0000</lastBuildDate>
|
||||
<image>
|
||||
<url>http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.webp</url>
|
||||
<title>Avatar for @the_mighty_zork@localhost:8080</title>
|
||||
<link>http://localhost:8080/@the_mighty_zork</link>
|
||||
</image>
|
||||
<item>
|
||||
<title>edited status</title>
|
||||
<link>http://localhost:8080/@the_mighty_zork/statuses/01JDPZC707CKDN8N4QVWM4Z1NR</link>
|
||||
<description>@the_mighty_zork@localhost:8080 made a new post: "this is the latest revision of the status, with a content-warning"</description>
|
||||
<content:encoded><![CDATA[<p>this is the latest revision of the status, with a content-warning</p>]]></content:encoded>
|
||||
<author>@the_mighty_zork@localhost:8080</author>
|
||||
<guid isPermaLink="true">http://localhost:8080/@the_mighty_zork/statuses/01JDPZC707CKDN8N4QVWM4Z1NR</guid>
|
||||
<pubDate>Fri, 01 Nov 2024 09:00:00 +0000</pubDate>
|
||||
<source>http://localhost:8080/@the_mighty_zork/feed.rss</source>
|
||||
</item>
|
||||
<item>
|
||||
<title>HTML in post</title>
|
||||
<link>http://localhost:8080/@the_mighty_zork/statuses/01HH9KYNQPA416TNJ53NSATP40</link>
|
||||
|
|
|
@ -177,9 +177,7 @@ func (p *Processor) getAttachmentContent(
|
|||
}
|
||||
|
||||
// Start preparing API content model.
|
||||
apiContent := &apimodel.Content{
|
||||
ContentUpdated: attach.UpdatedAt,
|
||||
}
|
||||
apiContent := &apimodel.Content{}
|
||||
|
||||
// Retrieve appropriate
|
||||
// size file from storage.
|
||||
|
|
|
@ -20,7 +20,6 @@ package media_test
|
|||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
@ -42,8 +41,6 @@ func (suite *UnattachTestSuite) TestUnattachMedia() {
|
|||
|
||||
dbAttachment, errWithCode := suite.db.GetAttachmentByID(ctx, a.ID)
|
||||
suite.NoError(errWithCode)
|
||||
|
||||
suite.WithinDuration(dbAttachment.UpdatedAt, time.Now(), 1*time.Minute)
|
||||
suite.Empty(dbAttachment.StatusID)
|
||||
}
|
||||
|
||||
|
|
|
@ -67,7 +67,6 @@ func (p *Processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account
|
|||
if errWithCode != nil {
|
||||
return nil, errWithCode
|
||||
}
|
||||
|
||||
return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
|
||||
}
|
||||
|
||||
|
@ -106,5 +105,6 @@ func (p *Processor) SourceGet(ctx context.Context, requestingAccount *gtsmodel.A
|
|||
err = gtserror.Newf("error converting status: %w", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
return statusSource, nil
|
||||
}
|
||||
|
|
|
@ -79,8 +79,8 @@ func (suite *NotificationTestSuite) TestStreamNotification() {
|
|||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 3,
|
||||
"last_status_at": "2021-09-11",
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
|
|
|
@ -54,6 +54,7 @@ func (suite *StatusUpdateTestSuite) TestStreamNotification() {
|
|||
suite.Equal(`{
|
||||
"id": "01FVW7JHQFSFK166WWKR8CBA6M",
|
||||
"created_at": "2021-09-20T10:40:37.000Z",
|
||||
"edited_at": null,
|
||||
"in_reply_to_id": null,
|
||||
"in_reply_to_account_id": null,
|
||||
"sensitive": false,
|
||||
|
@ -90,8 +91,8 @@ func (suite *StatusUpdateTestSuite) TestStreamNotification() {
|
|||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 3,
|
||||
"last_status_at": "2021-09-11",
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
},
|
||||
|
|
|
@ -102,8 +102,8 @@ func (suite *PublicTestSuite) TestPublicTimelineGetHideFiltered() {
|
|||
requester = suite.testAccounts["local_account_1"]
|
||||
maxID = ""
|
||||
sinceID = ""
|
||||
minID = "01F8MHAAY43M6RJ473VQFCVH36" // 1 before filteredStatus
|
||||
limit = 10
|
||||
minID = ""
|
||||
limit = 100
|
||||
local = false
|
||||
filteredStatus = suite.testStatuses["admin_account_status_2"]
|
||||
filteredStatusFound = false
|
||||
|
|
|
@ -75,6 +75,21 @@ func (u *utils) wipeStatus(
|
|||
}
|
||||
}
|
||||
|
||||
// Before handling media, ensure
|
||||
// historic edits are populated.
|
||||
if !status.EditsPopulated() {
|
||||
var err error
|
||||
|
||||
// Fetch all historical edits of status from database.
|
||||
status.Edits, err = u.state.DB.GetStatusEditsByIDs(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
status.EditIDs,
|
||||
)
|
||||
if err != nil {
|
||||
errs.Appendf("error getting status edits from database: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Either delete all attachments for this status,
|
||||
// or simply detach + clean them separately later.
|
||||
//
|
||||
|
@ -83,20 +98,27 @@ func (u *utils) wipeStatus(
|
|||
// status immediately (in case of delete + redraft).
|
||||
if deleteAttachments {
|
||||
// todo:u.state.DB.DeleteAttachmentsForStatus
|
||||
for _, id := range status.AttachmentIDs {
|
||||
for _, id := range status.AllAttachmentIDs() {
|
||||
if err := u.media.Delete(ctx, id); err != nil {
|
||||
errs.Appendf("error deleting media: %w", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// todo:u.state.DB.UnattachAttachmentsForStatus
|
||||
for _, id := range status.AttachmentIDs {
|
||||
for _, id := range status.AllAttachmentIDs() {
|
||||
if _, err := u.media.Unattach(ctx, status.Account, id); err != nil {
|
||||
errs.Appendf("error unattaching media: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete all historical edits of status.
|
||||
if ids := status.EditIDs; len(ids) > 0 {
|
||||
if err := u.state.DB.DeleteStatusEdits(ctx, ids); err != nil {
|
||||
errs.Appendf("error deleting status edits: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Delete all mentions generated by this status.
|
||||
// todo:u.state.DB.DeleteMentionsForStatus
|
||||
for _, id := range status.MentionIDs {
|
||||
|
@ -120,19 +142,20 @@ func (u *utils) wipeStatus(
|
|||
errs.Appendf("error deleting status faves: %w", err)
|
||||
}
|
||||
|
||||
if pollID := status.PollID; pollID != "" {
|
||||
if id := status.PollID; id != "" {
|
||||
// Delete this poll by ID from the database.
|
||||
if err := u.state.DB.DeletePollByID(ctx, pollID); err != nil {
|
||||
if err := u.state.DB.DeletePollByID(ctx, id); err != nil {
|
||||
errs.Appendf("error deleting status poll: %w", err)
|
||||
}
|
||||
|
||||
// Cancel any scheduled expiry task for poll.
|
||||
_ = u.state.Workers.Scheduler.Cancel(pollID)
|
||||
_ = u.state.Workers.Scheduler.Cancel(id)
|
||||
}
|
||||
|
||||
// Get all boost of this status so that we can
|
||||
// delete those boosts + remove them from timelines.
|
||||
boosts, err := u.state.DB.GetStatusBoosts(
|
||||
|
||||
// We MUST set a barebones context here,
|
||||
// as depending on where it came from the
|
||||
// original BoostOf may already be gone.
|
||||
|
@ -537,11 +560,7 @@ func (u *utils) requestFave(
|
|||
}
|
||||
|
||||
// Create + store new interaction request.
|
||||
req, err = typeutils.StatusFaveToInteractionRequest(ctx, fave)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error creating interaction request: %w", err)
|
||||
}
|
||||
|
||||
req = typeutils.StatusFaveToInteractionRequest(fave)
|
||||
if err := u.state.DB.PutInteractionRequest(ctx, req); err != nil {
|
||||
return gtserror.Newf("db error storing interaction request: %w", err)
|
||||
}
|
||||
|
@ -584,11 +603,7 @@ func (u *utils) requestReply(
|
|||
}
|
||||
|
||||
// Create + store interaction request.
|
||||
req, err = typeutils.StatusToInteractionRequest(ctx, reply)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error creating interaction request: %w", err)
|
||||
}
|
||||
|
||||
req = typeutils.StatusToInteractionRequest(reply)
|
||||
if err := u.state.DB.PutInteractionRequest(ctx, req); err != nil {
|
||||
return gtserror.Newf("db error storing interaction request: %w", err)
|
||||
}
|
||||
|
@ -631,11 +646,7 @@ func (u *utils) requestAnnounce(
|
|||
}
|
||||
|
||||
// Create + store interaction request.
|
||||
req, err = typeutils.StatusToInteractionRequest(ctx, boost)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error creating interaction request: %w", err)
|
||||
}
|
||||
|
||||
req = typeutils.StatusToInteractionRequest(boost)
|
||||
if err := u.state.DB.PutInteractionRequest(ctx, req); err != nil {
|
||||
return gtserror.Newf("db error storing interaction request: %w", err)
|
||||
}
|
||||
|
|
|
@ -228,7 +228,7 @@ func (suite *GetTestSuite) TestGetNewTimelineMoreThanPossible() {
|
|||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
suite.checkStatuses(statuses, id.Highest, id.Lowest, 20)
|
||||
suite.checkStatuses(statuses, id.Highest, id.Lowest, 22)
|
||||
}
|
||||
|
||||
func (suite *GetTestSuite) TestGetNewTimelineMoreThanPossiblePageUp() {
|
||||
|
@ -255,7 +255,7 @@ func (suite *GetTestSuite) TestGetNewTimelineMoreThanPossiblePageUp() {
|
|||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
suite.checkStatuses(statuses, id.Highest, id.Lowest, 20)
|
||||
suite.checkStatuses(statuses, id.Highest, id.Lowest, 22)
|
||||
}
|
||||
|
||||
func (suite *GetTestSuite) TestGetNewTimelineNoFollowing() {
|
||||
|
@ -284,7 +284,7 @@ func (suite *GetTestSuite) TestGetNewTimelineNoFollowing() {
|
|||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
suite.checkStatuses(statuses, id.Highest, id.Lowest, 8)
|
||||
suite.checkStatuses(statuses, id.Highest, id.Lowest, 9)
|
||||
|
||||
for _, s := range statuses {
|
||||
if s.GetAccountID() != testAccount.ID {
|
||||
|
|
|
@ -40,7 +40,7 @@ func (suite *PruneTestSuite) TestPrune() {
|
|||
|
||||
pruned, err := suite.state.Timelines.Home.Prune(ctx, testAccountID, desiredPreparedItemsLength, desiredIndexedItemsLength)
|
||||
suite.NoError(err)
|
||||
suite.Equal(20, pruned)
|
||||
suite.Equal(23, pruned)
|
||||
suite.Equal(5, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID))
|
||||
}
|
||||
|
||||
|
@ -56,7 +56,7 @@ func (suite *PruneTestSuite) TestPruneTwice() {
|
|||
|
||||
pruned, err := suite.state.Timelines.Home.Prune(ctx, testAccountID, desiredPreparedItemsLength, desiredIndexedItemsLength)
|
||||
suite.NoError(err)
|
||||
suite.Equal(20, pruned)
|
||||
suite.Equal(23, pruned)
|
||||
suite.Equal(5, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID))
|
||||
|
||||
// Prune same again, nothing should be pruned this time.
|
||||
|
@ -78,7 +78,7 @@ func (suite *PruneTestSuite) TestPruneTo0() {
|
|||
|
||||
pruned, err := suite.state.Timelines.Home.Prune(ctx, testAccountID, desiredPreparedItemsLength, desiredIndexedItemsLength)
|
||||
suite.NoError(err)
|
||||
suite.Equal(25, pruned)
|
||||
suite.Equal(28, pruned)
|
||||
suite.Equal(0, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID))
|
||||
}
|
||||
|
||||
|
@ -95,7 +95,7 @@ func (suite *PruneTestSuite) TestPruneToInfinityAndBeyond() {
|
|||
pruned, err := suite.state.Timelines.Home.Prune(ctx, testAccountID, desiredPreparedItemsLength, desiredIndexedItemsLength)
|
||||
suite.NoError(err)
|
||||
suite.Equal(0, pruned)
|
||||
suite.Equal(25, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID))
|
||||
suite.Equal(28, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID))
|
||||
}
|
||||
|
||||
func TestPruneTestSuite(t *testing.T) {
|
||||
|
|
|
@ -111,6 +111,13 @@ func (c *Converter) ASRepresentationToAccount(
|
|||
acct.UpdatedAt = pub
|
||||
}
|
||||
|
||||
// Extract updated time if possible, i.e. last edited.
|
||||
if upd := ap.GetUpdated(accountable); !upd.IsZero() {
|
||||
acct.UpdatedAt = upd
|
||||
} else {
|
||||
acct.UpdatedAt = acct.CreatedAt
|
||||
}
|
||||
|
||||
// Extract a preferred name (display name), fallback to username.
|
||||
if displayName := ap.ExtractName(accountable); displayName != "" {
|
||||
acct.DisplayName = displayName
|
||||
|
@ -348,18 +355,25 @@ func (c *Converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusab
|
|||
// zero-time will fall back to db defaults.
|
||||
if pub := ap.GetPublished(statusable); !pub.IsZero() {
|
||||
status.CreatedAt = pub
|
||||
status.UpdatedAt = pub
|
||||
} else {
|
||||
log.Warnf(ctx, "unusable published property on %s", uri)
|
||||
}
|
||||
|
||||
// status.Updated
|
||||
//
|
||||
// Extract updated time for status, defaults to Published.
|
||||
if upd := ap.GetUpdated(statusable); !upd.IsZero() {
|
||||
status.UpdatedAt = upd
|
||||
} else {
|
||||
status.UpdatedAt = status.CreatedAt
|
||||
}
|
||||
|
||||
// status.AccountURI
|
||||
// status.AccountID
|
||||
// status.Account
|
||||
//
|
||||
// Account that created the status. Assume we have
|
||||
// this in the db by the time this function is called,
|
||||
// error if we don't.
|
||||
// Account that created the status. Assume we have this
|
||||
// in the db by the time this function is called, else error.
|
||||
status.Account, err = c.getASAttributedToAccount(ctx,
|
||||
status.URI,
|
||||
statusable,
|
||||
|
|
|
@ -104,14 +104,8 @@ func (c *Converter) StatusToBoost(
|
|||
return boost, nil
|
||||
}
|
||||
|
||||
func StatusToInteractionRequest(
|
||||
ctx context.Context,
|
||||
status *gtsmodel.Status,
|
||||
) (*gtsmodel.InteractionRequest, error) {
|
||||
reqID, err := id.NewULIDFromTime(status.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("error generating ID: %w", err)
|
||||
}
|
||||
func StatusToInteractionRequest(status *gtsmodel.Status) *gtsmodel.InteractionRequest {
|
||||
reqID := id.NewULIDFromTime(status.CreatedAt)
|
||||
|
||||
var (
|
||||
targetID string
|
||||
|
@ -154,17 +148,11 @@ func StatusToInteractionRequest(
|
|||
InteractionType: interactionType,
|
||||
Reply: reply,
|
||||
Announce: announce,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func StatusFaveToInteractionRequest(
|
||||
ctx context.Context,
|
||||
fave *gtsmodel.StatusFave,
|
||||
) (*gtsmodel.InteractionRequest, error) {
|
||||
reqID, err := id.NewULIDFromTime(fave.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("error generating ID: %w", err)
|
||||
}
|
||||
func StatusFaveToInteractionRequest(fave *gtsmodel.StatusFave) *gtsmodel.InteractionRequest {
|
||||
reqID := id.NewULIDFromTime(fave.CreatedAt)
|
||||
|
||||
return >smodel.InteractionRequest{
|
||||
ID: reqID,
|
||||
|
@ -178,7 +166,7 @@ func StatusFaveToInteractionRequest(
|
|||
InteractionURI: fave.URI,
|
||||
InteractionType: gtsmodel.InteractionLike,
|
||||
Like: fave,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Converter) StatusToSinBinStatus(
|
||||
|
|
|
@ -484,10 +484,9 @@ func (c *Converter) StatusToAS(ctx context.Context, s *gtsmodel.Status) (ap.Stat
|
|||
status.SetActivityStreamsInReplyTo(inReplyToProp)
|
||||
}
|
||||
|
||||
// published
|
||||
publishedProp := streams.NewActivityStreamsPublishedProperty()
|
||||
publishedProp.Set(s.CreatedAt)
|
||||
status.SetActivityStreamsPublished(publishedProp)
|
||||
// Set created / updated at properties.
|
||||
ap.SetPublished(status, s.CreatedAt)
|
||||
ap.SetUpdated(status, s.UpdatedAt)
|
||||
|
||||
// url
|
||||
if s.URL != "" {
|
||||
|
|
|
@ -499,6 +499,7 @@ func (suite *InternalToASTestSuite) TestStatusToAS() {
|
|||
"tag": [],
|
||||
"to": "https://www.w3.org/ns/activitystreams#Public",
|
||||
"type": "Note",
|
||||
"updated": "2021-10-20T12:40:37+02:00",
|
||||
"url": "http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY"
|
||||
}`, string(bytes))
|
||||
}
|
||||
|
@ -598,6 +599,7 @@ func (suite *InternalToASTestSuite) TestStatusWithTagsToASWithIDs() {
|
|||
],
|
||||
"to": "https://www.w3.org/ns/activitystreams#Public",
|
||||
"type": "Note",
|
||||
"updated": "2021-10-20T11:36:45Z",
|
||||
"url": "http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R"
|
||||
}`, string(bytes))
|
||||
}
|
||||
|
@ -698,6 +700,7 @@ func (suite *InternalToASTestSuite) TestStatusWithTagsToASFromDB() {
|
|||
],
|
||||
"to": "https://www.w3.org/ns/activitystreams#Public",
|
||||
"type": "Note",
|
||||
"updated": "2021-10-20T11:36:45Z",
|
||||
"url": "http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R"
|
||||
}`, string(bytes))
|
||||
}
|
||||
|
@ -778,6 +781,7 @@ func (suite *InternalToASTestSuite) TestStatusToASWithMentions() {
|
|||
},
|
||||
"to": "https://www.w3.org/ns/activitystreams#Public",
|
||||
"type": "Note",
|
||||
"updated": "2021-11-20T13:32:16Z",
|
||||
"url": "http://localhost:8080/@admin/statuses/01FF25D5Q0DH7CHD57CTRS6WK0"
|
||||
}`, string(bytes))
|
||||
}
|
||||
|
|
|
@ -1399,17 +1399,13 @@ func (c *Converter) baseStatusToFrontend(
|
|||
}
|
||||
|
||||
// Nullable fields.
|
||||
if s.InReplyToID != "" {
|
||||
apiStatus.InReplyToID = util.Ptr(s.InReplyToID)
|
||||
}
|
||||
|
||||
if s.InReplyToAccountID != "" {
|
||||
apiStatus.InReplyToAccountID = util.Ptr(s.InReplyToAccountID)
|
||||
}
|
||||
|
||||
if s.Language != "" {
|
||||
apiStatus.Language = util.Ptr(s.Language)
|
||||
if !s.UpdatedAt.Equal(s.CreatedAt) {
|
||||
timestamp := util.FormatISO8601(s.UpdatedAt)
|
||||
apiStatus.EditedAt = util.Ptr(timestamp)
|
||||
}
|
||||
apiStatus.InReplyToID = util.PtrIf(s.InReplyToID)
|
||||
apiStatus.InReplyToAccountID = util.PtrIf(s.InReplyToAccountID)
|
||||
apiStatus.Language = util.PtrIf(s.Language)
|
||||
|
||||
if app := s.CreatedWithApplication; app != nil {
|
||||
apiStatus.Application, err = c.AppToAPIAppPublic(ctx, app)
|
||||
|
|
|
@ -67,8 +67,8 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontend() {
|
|||
"header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q",
|
||||
"followers_count": 2,
|
||||
"following_count": 2,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2024-01-10",
|
||||
"statuses_count": 9,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": [],
|
||||
"enable_rss": true
|
||||
|
@ -119,8 +119,8 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendAliasedAndMoved()
|
|||
"header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q",
|
||||
"followers_count": 2,
|
||||
"following_count": 2,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2024-01-10",
|
||||
"statuses_count": 9,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": [],
|
||||
"source": {
|
||||
|
@ -162,8 +162,8 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendAliasedAndMoved()
|
|||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 1,
|
||||
"following_count": 1,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2021-07-28",
|
||||
"statuses_count": 9,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": [
|
||||
{
|
||||
|
@ -217,8 +217,8 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendWithEmojiStruct()
|
|||
"header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q",
|
||||
"followers_count": 2,
|
||||
"following_count": 2,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2024-01-10",
|
||||
"statuses_count": 9,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [
|
||||
{
|
||||
"shortcode": "rainbow",
|
||||
|
@ -266,8 +266,8 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendWithEmojiIDs() {
|
|||
"header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q",
|
||||
"followers_count": 2,
|
||||
"following_count": 2,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2024-01-10",
|
||||
"statuses_count": 9,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [
|
||||
{
|
||||
"shortcode": "rainbow",
|
||||
|
@ -311,8 +311,8 @@ func (suite *InternalToFrontendTestSuite) TestAccountToFrontendSensitive() {
|
|||
"header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q",
|
||||
"followers_count": 2,
|
||||
"following_count": 2,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2024-01-10",
|
||||
"statuses_count": 9,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": [],
|
||||
"source": {
|
||||
|
@ -463,6 +463,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontend() {
|
|||
suite.Equal(`{
|
||||
"id": "01F8MH75CBF9JFX4ZAD54N0W0R",
|
||||
"created_at": "2021-10-20T11:36:45.000Z",
|
||||
"edited_at": null,
|
||||
"in_reply_to_id": null,
|
||||
"in_reply_to_account_id": null,
|
||||
"sensitive": false,
|
||||
|
@ -641,6 +642,7 @@ func (suite *InternalToFrontendTestSuite) TestWarnFilteredStatusToFrontend() {
|
|||
suite.Equal(`{
|
||||
"id": "01F8MH75CBF9JFX4ZAD54N0W0R",
|
||||
"created_at": "2021-10-20T11:36:45.000Z",
|
||||
"edited_at": null,
|
||||
"in_reply_to_id": null,
|
||||
"in_reply_to_account_id": null,
|
||||
"sensitive": false,
|
||||
|
@ -807,6 +809,7 @@ func (suite *InternalToFrontendTestSuite) TestWarnFilteredBoostToFrontend() {
|
|||
suite.Equal(`{
|
||||
"id": "01G36SF3V6Y6V5BF9P4R7PQG7G",
|
||||
"created_at": "2021-10-20T10:41:37.000Z",
|
||||
"edited_at": null,
|
||||
"in_reply_to_id": null,
|
||||
"in_reply_to_account_id": null,
|
||||
"sensitive": false,
|
||||
|
@ -827,6 +830,7 @@ func (suite *InternalToFrontendTestSuite) TestWarnFilteredBoostToFrontend() {
|
|||
"reblog": {
|
||||
"id": "01F8MH75CBF9JFX4ZAD54N0W0R",
|
||||
"created_at": "2021-10-20T11:36:45.000Z",
|
||||
"edited_at": null,
|
||||
"in_reply_to_id": null,
|
||||
"in_reply_to_account_id": null,
|
||||
"sensitive": false,
|
||||
|
@ -870,8 +874,8 @@ func (suite *InternalToFrontendTestSuite) TestWarnFilteredBoostToFrontend() {
|
|||
"header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q",
|
||||
"followers_count": 2,
|
||||
"following_count": 2,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2024-01-10",
|
||||
"statuses_count": 9,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": [],
|
||||
"enable_rss": true
|
||||
|
@ -1218,6 +1222,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendUnknownAttachments
|
|||
suite.Equal(`{
|
||||
"id": "01HE7XJ1CG84TBKH5V9XKBVGF5",
|
||||
"created_at": "2023-11-02T10:44:25.000Z",
|
||||
"edited_at": null,
|
||||
"in_reply_to_id": "01F8MH75CBF9JFX4ZAD54N0W0R",
|
||||
"in_reply_to_account_id": "01F8MH17FWEB39HZJ76B6VXSKF",
|
||||
"sensitive": true,
|
||||
|
@ -1350,6 +1355,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToWebStatus() {
|
|||
suite.Equal(`{
|
||||
"id": "01HE7XJ1CG84TBKH5V9XKBVGF5",
|
||||
"created_at": "2023-11-02T10:44:25.000Z",
|
||||
"edited_at": null,
|
||||
"in_reply_to_id": "01F8MH75CBF9JFX4ZAD54N0W0R",
|
||||
"in_reply_to_account_id": "01F8MH17FWEB39HZJ76B6VXSKF",
|
||||
"sensitive": true,
|
||||
|
@ -1511,6 +1517,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendUnknownLanguage()
|
|||
suite.Equal(`{
|
||||
"id": "01F8MH75CBF9JFX4ZAD54N0W0R",
|
||||
"created_at": "2021-10-20T11:36:45.000Z",
|
||||
"edited_at": null,
|
||||
"in_reply_to_id": null,
|
||||
"in_reply_to_account_id": null,
|
||||
"sensitive": false,
|
||||
|
@ -1654,6 +1661,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendPartialInteraction
|
|||
suite.Equal(`{
|
||||
"id": "01F8MHBBN8120SYH7D5S050MGK",
|
||||
"created_at": "2021-10-20T10:40:37.000Z",
|
||||
"edited_at": null,
|
||||
"in_reply_to_id": null,
|
||||
"in_reply_to_account_id": null,
|
||||
"sensitive": false,
|
||||
|
@ -1697,8 +1705,8 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendPartialInteraction
|
|||
"header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q",
|
||||
"followers_count": 2,
|
||||
"following_count": 2,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2024-01-10",
|
||||
"statuses_count": 9,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": [],
|
||||
"enable_rss": true
|
||||
|
@ -1764,6 +1772,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToAPIStatusPendingApproval()
|
|||
suite.Equal(`{
|
||||
"id": "01J5QVB9VC76NPPRQ207GG4DRZ",
|
||||
"created_at": "2024-02-20T10:41:37.000Z",
|
||||
"edited_at": null,
|
||||
"in_reply_to_id": "01F8MHC8VWDRBQR0N1BATDDEM5",
|
||||
"in_reply_to_account_id": "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||
"sensitive": false,
|
||||
|
@ -1993,7 +2002,7 @@ func (suite *InternalToFrontendTestSuite) TestInstanceV1ToFrontend() {
|
|||
},
|
||||
"stats": {
|
||||
"domain_count": 2,
|
||||
"status_count": 19,
|
||||
"status_count": 21,
|
||||
"user_count": 4
|
||||
},
|
||||
"thumbnail": "http://localhost:8080/assets/logo.webp",
|
||||
|
@ -2277,8 +2286,8 @@ func (suite *InternalToFrontendTestSuite) TestReportToFrontend1() {
|
|||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 3,
|
||||
"last_status_at": "2021-09-11",
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
|
@ -2321,8 +2330,8 @@ func (suite *InternalToFrontendTestSuite) TestReportToFrontend2() {
|
|||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 1,
|
||||
"following_count": 1,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2021-07-28",
|
||||
"statuses_count": 9,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": [
|
||||
{
|
||||
|
@ -2398,8 +2407,8 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend1() {
|
|||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 3,
|
||||
"last_status_at": "2021-09-11",
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
|
@ -2444,8 +2453,8 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend1() {
|
|||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 1,
|
||||
"following_count": 1,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2021-07-28",
|
||||
"statuses_count": 9,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": [
|
||||
{
|
||||
|
@ -2636,8 +2645,8 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend2() {
|
|||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 1,
|
||||
"following_count": 1,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2021-07-28",
|
||||
"statuses_count": 9,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": [
|
||||
{
|
||||
|
@ -2695,8 +2704,8 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend2() {
|
|||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 3,
|
||||
"last_status_at": "2021-09-11",
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
|
@ -2707,6 +2716,7 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend2() {
|
|||
{
|
||||
"id": "01FVW7JHQFSFK166WWKR8CBA6M",
|
||||
"created_at": "2021-09-20T10:40:37.000Z",
|
||||
"edited_at": null,
|
||||
"in_reply_to_id": null,
|
||||
"in_reply_to_account_id": null,
|
||||
"sensitive": false,
|
||||
|
@ -2743,8 +2753,8 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend2() {
|
|||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 3,
|
||||
"last_status_at": "2021-09-11",
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
},
|
||||
|
@ -2902,8 +2912,8 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontendSuspendedLoca
|
|||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 0,
|
||||
"following_count": 0,
|
||||
"statuses_count": 3,
|
||||
"last_status_at": "2021-09-11",
|
||||
"statuses_count": 4,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
}
|
||||
|
@ -3214,6 +3224,7 @@ func (suite *InternalToFrontendTestSuite) TestIntReqToAPI() {
|
|||
"status": {
|
||||
"id": "01F8MHC8VWDRBQR0N1BATDDEM5",
|
||||
"created_at": "2021-10-20T10:40:37.000Z",
|
||||
"edited_at": null,
|
||||
"in_reply_to_id": null,
|
||||
"in_reply_to_account_id": null,
|
||||
"sensitive": true,
|
||||
|
@ -3254,8 +3265,8 @@ func (suite *InternalToFrontendTestSuite) TestIntReqToAPI() {
|
|||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 1,
|
||||
"following_count": 1,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2021-07-28",
|
||||
"statuses_count": 9,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": [
|
||||
{
|
||||
|
@ -3307,6 +3318,7 @@ func (suite *InternalToFrontendTestSuite) TestIntReqToAPI() {
|
|||
"reply": {
|
||||
"id": "01J5QVB9VC76NPPRQ207GG4DRZ",
|
||||
"created_at": "2024-02-20T10:41:37.000Z",
|
||||
"edited_at": null,
|
||||
"in_reply_to_id": "01F8MHC8VWDRBQR0N1BATDDEM5",
|
||||
"in_reply_to_account_id": "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||
"sensitive": false,
|
||||
|
@ -3464,8 +3476,8 @@ func (suite *InternalToFrontendTestSuite) TestConversationToAPISelfConvo() {
|
|||
"header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q",
|
||||
"followers_count": 2,
|
||||
"following_count": 2,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2024-01-10",
|
||||
"statuses_count": 9,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": [],
|
||||
"enable_rss": true
|
||||
|
@ -3474,6 +3486,7 @@ func (suite *InternalToFrontendTestSuite) TestConversationToAPISelfConvo() {
|
|||
"last_status": {
|
||||
"id": "01F8MHAMCHF6Y650WCRSCP4WMY",
|
||||
"created_at": "2021-10-20T10:40:37.000Z",
|
||||
"edited_at": null,
|
||||
"in_reply_to_id": null,
|
||||
"in_reply_to_account_id": null,
|
||||
"sensitive": true,
|
||||
|
@ -3517,8 +3530,8 @@ func (suite *InternalToFrontendTestSuite) TestConversationToAPISelfConvo() {
|
|||
"header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q",
|
||||
"followers_count": 2,
|
||||
"following_count": 2,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2024-01-10",
|
||||
"statuses_count": 9,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": [],
|
||||
"enable_rss": true
|
||||
|
@ -3619,8 +3632,8 @@ func (suite *InternalToFrontendTestSuite) TestConversationToAPI() {
|
|||
"header_description": "Flat gray background (default header).",
|
||||
"followers_count": 1,
|
||||
"following_count": 1,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2021-07-28",
|
||||
"statuses_count": 9,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": [
|
||||
{
|
||||
|
@ -3640,6 +3653,7 @@ func (suite *InternalToFrontendTestSuite) TestConversationToAPI() {
|
|||
"last_status": {
|
||||
"id": "01F8MHAMCHF6Y650WCRSCP4WMY",
|
||||
"created_at": "2021-10-20T10:40:37.000Z",
|
||||
"edited_at": null,
|
||||
"in_reply_to_id": null,
|
||||
"in_reply_to_account_id": null,
|
||||
"sensitive": true,
|
||||
|
@ -3683,8 +3697,8 @@ func (suite *InternalToFrontendTestSuite) TestConversationToAPI() {
|
|||
"header_media_id": "01PFPMWK2FF0D9WMHEJHR07C3Q",
|
||||
"followers_count": 2,
|
||||
"following_count": 2,
|
||||
"statuses_count": 8,
|
||||
"last_status_at": "2024-01-10",
|
||||
"statuses_count": 9,
|
||||
"last_status_at": "2024-11-01",
|
||||
"emojis": [],
|
||||
"fields": [],
|
||||
"enable_rss": true
|
||||
|
|
|
@ -131,6 +131,7 @@ func (suite *WrapTestSuite) TestWrapNoteInCreate() {
|
|||
"tag": [],
|
||||
"to": "https://www.w3.org/ns/activitystreams#Public",
|
||||
"type": "Note",
|
||||
"updated": "2021-10-20T12:40:37+02:00",
|
||||
"url": "http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY"
|
||||
},
|
||||
"published": "2021-10-20T12:40:37+02:00",
|
||||
|
|
|
@ -64,6 +64,7 @@ EXPECT=$(cat << "EOF"
|
|||
"sin-bin-status-mem-ratio": 0.5,
|
||||
"status-bookmark-ids-mem-ratio": 2,
|
||||
"status-bookmark-mem-ratio": 0.5,
|
||||
"status-edit-mem-ratio": 2,
|
||||
"status-fave-ids-mem-ratio": 3,
|
||||
"status-fave-mem-ratio": 2,
|
||||
"status-mem-ratio": 5,
|
||||
|
|
|
@ -52,6 +52,7 @@ var testModels = []interface{}{
|
|||
>smodel.Status{},
|
||||
>smodel.StatusToEmoji{},
|
||||
>smodel.StatusToTag{},
|
||||
>smodel.StatusEdit{},
|
||||
>smodel.StatusFave{},
|
||||
>smodel.StatusBookmark{},
|
||||
>smodel.Tag{},
|
||||
|
@ -101,7 +102,7 @@ func CreateTestTables(db db.DB) {
|
|||
ctx := context.Background()
|
||||
for _, m := range testModels {
|
||||
if err := db.CreateTable(ctx, m); err != nil {
|
||||
log.Panicf(nil, "error creating table for %+v: %s", m, err)
|
||||
log.Panicf(ctx, "error creating table for %+v: %s", m, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -125,243 +126,249 @@ func StandardDBSetup(db db.DB, accounts map[string]*gtsmodel.Account) {
|
|||
|
||||
for _, v := range NewTestTokens() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestClients() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestApplications() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestBlocks() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestReports() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestRules() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestDomainBlocks() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestInstances() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestUsers() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
if accounts == nil {
|
||||
for _, v := range NewTestAccounts() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, v := range accounts {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestAccountSettings() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestAttachments() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestStatuses() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestEmojis() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestEmojiCategories() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestStatusToEmojis() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestTags() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestStatusToTags() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestMentions() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestFaves() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestFollows() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestLists() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestListEntries() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestNotifications() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestTombstones() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestBookmarks() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestAccountNotes() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestMarkers() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestThreads() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestThreadToStatus() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestPolls() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestPollVotes() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestFilters() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestFilterKeywords() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestFilterStatuses() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestUserMutes() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestInteractionRequests() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range NewTestStatusEdits() {
|
||||
if err := db.Put(ctx, v); err != nil {
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := db.CreateInstanceAccount(ctx); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
|
||||
if err := db.CreateInstanceInstance(ctx); err != nil {
|
||||
log.Panic(nil, err)
|
||||
log.Panic(ctx, err)
|
||||
}
|
||||
|
||||
log.Debug(nil, "testing db setup complete")
|
||||
log.Debug(ctx, "testing db setup complete")
|
||||
}
|
||||
|
||||
// StandardDBTeardown drops all the standard testing tables/models from the database to ensure it's clean for the next test.
|
||||
|
|
|
@ -718,7 +718,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
|||
URL: "http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg",
|
||||
RemoteURL: "",
|
||||
CreatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
|
||||
UpdatedAt: TimeMustParse("2022-06-04T13:12:00Z"),
|
||||
Type: gtsmodel.FileTypeImage,
|
||||
FileMeta: gtsmodel.FileMeta{
|
||||
Original: gtsmodel.Original{
|
||||
|
@ -761,7 +760,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
|||
URL: "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01F8MH7TDVANYKWVE8VVKFPJTJ.gif",
|
||||
RemoteURL: "",
|
||||
CreatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
|
||||
UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
|
||||
Type: gtsmodel.FileTypeImage,
|
||||
FileMeta: gtsmodel.FileMeta{
|
||||
Original: gtsmodel.Original{
|
||||
|
@ -808,7 +806,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
|||
URL: "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01CDR64G398ADCHXK08WWTHEZ5.mp4",
|
||||
RemoteURL: "",
|
||||
CreatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
|
||||
UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
|
||||
Type: gtsmodel.FileTypeVideo,
|
||||
FileMeta: gtsmodel.FileMeta{
|
||||
Original: gtsmodel.Original{
|
||||
|
@ -858,7 +855,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
|||
URL: "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01F8MH8RMYQ6MSNY3JM2XT1CQ5.jpg",
|
||||
RemoteURL: "",
|
||||
CreatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
|
||||
UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
|
||||
Type: gtsmodel.FileTypeImage,
|
||||
FileMeta: gtsmodel.FileMeta{
|
||||
Original: gtsmodel.Original{
|
||||
|
@ -905,7 +901,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
|||
URL: "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
||||
RemoteURL: "",
|
||||
CreatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
|
||||
UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
|
||||
Type: gtsmodel.FileTypeImage,
|
||||
FileMeta: gtsmodel.FileMeta{
|
||||
Original: gtsmodel.Original{
|
||||
|
@ -952,7 +947,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
|||
URL: "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||
RemoteURL: "",
|
||||
CreatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
|
||||
UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
|
||||
Type: gtsmodel.FileTypeImage,
|
||||
FileMeta: gtsmodel.FileMeta{
|
||||
Original: gtsmodel.Original{
|
||||
|
@ -999,7 +993,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
|||
URL: "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/attachment/original/01J2M20K6K9XQC4WSB961YJHV6.mp3",
|
||||
RemoteURL: "",
|
||||
CreatedAt: TimeMustParse("2024-01-10T11:24:00+02:00"),
|
||||
UpdatedAt: TimeMustParse("2024-01-10T11:24:00+02:00"),
|
||||
Type: gtsmodel.FileTypeAudio,
|
||||
FileMeta: gtsmodel.FileMeta{
|
||||
Original: gtsmodel.Original{
|
||||
|
@ -1043,13 +1036,30 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
|||
Header: util.Ptr(false),
|
||||
Cached: util.Ptr(true),
|
||||
},
|
||||
"local_account_2_status_9_attachment_1": {
|
||||
ID: "01JDQ164HM08SGJ7ZEK9003Z4B",
|
||||
StatusID: "01JDPZEZ77X1NX0TY9M10BK1HM",
|
||||
URL: "http://localhost:8080/fileserver/01FHMQX3GAABWSM0S2VZEC2SWC/attachment/original/01HE88YG74PVAB81PX2XA9F3FG.mp3",
|
||||
RemoteURL: "http://example.org/fileserver/01HE7Y659ZWZ02JM4AWYJZ176Q/attachment/original/01HE892Y8ZS68TQCNPX7J888P3.mp3",
|
||||
CreatedAt: TimeMustParse("2024-11-01T10:01:00+02:00"),
|
||||
Type: gtsmodel.FileTypeUnknown,
|
||||
FileMeta: gtsmodel.FileMeta{},
|
||||
AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||
Description: "Jolly salsa song, public domain.",
|
||||
Blurhash: "",
|
||||
Processing: gtsmodel.ProcessingStatusProcessed,
|
||||
File: gtsmodel.File{},
|
||||
Thumbnail: gtsmodel.Thumbnail{RemoteURL: ""},
|
||||
Avatar: util.Ptr(false),
|
||||
Header: util.Ptr(false),
|
||||
Cached: util.Ptr(false),
|
||||
},
|
||||
"remote_account_1_status_1_attachment_1": {
|
||||
ID: "01FVW7RXPQ8YJHTEXYPE7Q8ZY0",
|
||||
StatusID: "01FVW7JHQFSFK166WWKR8CBA6M",
|
||||
URL: "http://localhost:8080/fileserver/01F8MH5ZK5VRH73AKHQM6Y9VNX/attachment/original/01FVW7RXPQ8YJHTEXYPE7Q8ZY0.jpg",
|
||||
RemoteURL: "http://fossbros-anonymous.io/attachments/original/13bbc3f8-2b5e-46ea-9531-40b4974d9912.jpg",
|
||||
CreatedAt: TimeMustParse("2021-09-20T12:40:37+02:00"),
|
||||
UpdatedAt: TimeMustParse("2021-09-20T12:40:37+02:00"),
|
||||
Type: gtsmodel.FileTypeImage,
|
||||
FileMeta: gtsmodel.FileMeta{
|
||||
Original: gtsmodel.Original{
|
||||
|
@ -1095,7 +1105,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
|||
URL: "http://localhost:8080/fileserver/062G5WYKY35KKD12EMSM3F8PJ8/header/original/01PFPMWK2FF0D9WMHEJHR07C3R.jpg",
|
||||
RemoteURL: "http://fossbros-anonymous.io/attachments/small/a499f55b-2d1e-4acd-98d2-1ac2ba6d79b9.jpg",
|
||||
CreatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
|
||||
UpdatedAt: TimeMustParse("2022-06-09T13:12:00Z"),
|
||||
Type: gtsmodel.FileTypeImage,
|
||||
FileMeta: gtsmodel.FileMeta{
|
||||
Original: gtsmodel.Original{
|
||||
|
@ -1141,7 +1150,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
|||
URL: "http://localhost:8080/fileserver/01FHMQX3GAABWSM0S2VZEC2SWC/attachment/original/01HE7Y3C432WRSNS10EZM86SA5.jpg",
|
||||
RemoteURL: "http://example.org/fileserver/01HE7Y659ZWZ02JM4AWYJZ176Q/attachment/original/01HE7Y6G0EMCKST3Q0914WW0MS.jpg",
|
||||
CreatedAt: TimeMustParse("2023-11-02T12:44:25+02:00"),
|
||||
UpdatedAt: TimeMustParse("2023-11-02T12:44:25+02:00"),
|
||||
Type: gtsmodel.FileTypeImage,
|
||||
FileMeta: gtsmodel.FileMeta{
|
||||
Original: gtsmodel.Original{
|
||||
|
@ -1186,7 +1194,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
|||
URL: "http://localhost:8080/fileserver/01FHMQX3GAABWSM0S2VZEC2SWC/attachment/original/01HE7ZFX9GKA5ZZVD4FACABSS9.svg",
|
||||
RemoteURL: "http://example.org/fileserver/01HE7Y659ZWZ02JM4AWYJZ176Q/attachment/original/01HE7ZGJYTSYMXF927GF9353KR.svg",
|
||||
CreatedAt: TimeMustParse("2023-11-02T12:44:25+02:00"),
|
||||
UpdatedAt: TimeMustParse("2023-11-02T12:44:25+02:00"),
|
||||
Type: gtsmodel.FileTypeUnknown,
|
||||
FileMeta: gtsmodel.FileMeta{},
|
||||
AccountID: "01FHMQX3GAABWSM0S2VZEC2SWC",
|
||||
|
@ -1205,7 +1212,6 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment {
|
|||
URL: "http://localhost:8080/fileserver/01FHMQX3GAABWSM0S2VZEC2SWC/attachment/original/01HE88YG74PVAB81PX2XA9F3FG.mp3",
|
||||
RemoteURL: "http://example.org/fileserver/01HE7Y659ZWZ02JM4AWYJZ176Q/attachment/original/01HE892Y8ZS68TQCNPX7J888P3.mp3",
|
||||
CreatedAt: TimeMustParse("2023-11-02T12:44:25+02:00"),
|
||||
UpdatedAt: TimeMustParse("2023-11-02T12:44:25+02:00"),
|
||||
Type: gtsmodel.FileTypeUnknown,
|
||||
FileMeta: gtsmodel.FileMeta{},
|
||||
AccountID: "01FHMQX3GAABWSM0S2VZEC2SWC",
|
||||
|
@ -1739,6 +1745,32 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
|||
Federated: util.Ptr(true),
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
},
|
||||
"local_account_1_status_9": {
|
||||
ID: "01JDPZC707CKDN8N4QVWM4Z1NR",
|
||||
URI: "http://localhost:8080/users/the_mighty_zork/statuses/01JDPZC707CKDN8N4QVWM4Z1NR",
|
||||
URL: "http://localhost:8080/@the_mighty_zork/statuses/01JDPZC707CKDN8N4QVWM4Z1NR",
|
||||
Content: "<p>this is the latest revision of the status, with a content-warning</p>",
|
||||
Text: "this is the latest revision of the status, with a content-warning",
|
||||
ContentWarning: "edited status",
|
||||
AttachmentIDs: nil,
|
||||
CreatedAt: TimeMustParse("2024-11-01T11:00:00+02:00"),
|
||||
UpdatedAt: TimeMustParse("2024-11-01T11:02:00+02:00"),
|
||||
Local: util.Ptr(true),
|
||||
AccountURI: "http://localhost:8080/users/the_mighty_zork",
|
||||
AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
||||
InReplyToID: "",
|
||||
InReplyToAccountID: "",
|
||||
InReplyToURI: "",
|
||||
BoostOfID: "",
|
||||
ThreadID: "",
|
||||
EditIDs: []string{"01JDPZCZ2Y9KSGZW0R7ZG8T8Y2", "01JDPZDADMD1T9HKF94RECF7PP"},
|
||||
Visibility: gtsmodel.VisibilityPublic,
|
||||
Sensitive: util.Ptr(false),
|
||||
Language: "en",
|
||||
CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG",
|
||||
Federated: util.Ptr(true),
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
},
|
||||
"local_account_2_status_1": {
|
||||
ID: "01F8MHBQCBTDKN6X5VHGMMN4MA",
|
||||
URI: "http://localhost:8080/users/1happyturtle/statuses/01F8MHBQCBTDKN6X5VHGMMN4MA",
|
||||
|
@ -1967,6 +1999,32 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
|||
PollID: "01HEN2QB5NR4NCEHGYC3HN84K6",
|
||||
PendingApproval: util.Ptr(false),
|
||||
},
|
||||
"local_account_2_status_9": {
|
||||
ID: "01JDPZEZ77X1NX0TY9M10BK1HM",
|
||||
URI: "http://localhost:8080/users/1happyturtle/statuses/01JDPZEZ77X1NX0TY9M10BK1HM",
|
||||
URL: "http://localhost:8080/@1happyturtle/statuses/01JDPZEZ77X1NX0TY9M10BK1HM",
|
||||
Content: "<p>now edited to bring back the previous edit's media!</p>",
|
||||
Text: "now edited to bring back the previous edit's media!",
|
||||
ContentWarning: "edit with media attachments",
|
||||
AttachmentIDs: []string{"01JDQ164HM08SGJ7ZEK9003Z4B"},
|
||||
CreatedAt: TimeMustParse("2024-11-01T10:00:00+02:00"),
|
||||
UpdatedAt: TimeMustParse("2024-11-01T10:03:00+02:00"),
|
||||
Local: util.Ptr(true),
|
||||
AccountURI: "http://localhost:8080/users/the_mighty_zork",
|
||||
AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||
InReplyToID: "",
|
||||
InReplyToAccountID: "",
|
||||
InReplyToURI: "",
|
||||
BoostOfID: "",
|
||||
ThreadID: "",
|
||||
EditIDs: []string{"01JDPZPBXAX0M02YSEPB21KX4R", "01JDPZPJHKP7E3M0YQXEXPS1YT", "01JDPZPY3F85Y7B78ETRXEMWD9"},
|
||||
Visibility: gtsmodel.VisibilityPublic,
|
||||
Sensitive: util.Ptr(false),
|
||||
Language: "en",
|
||||
CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ",
|
||||
Federated: util.Ptr(true),
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
},
|
||||
"remote_account_1_status_1": {
|
||||
ID: "01FVW7JHQFSFK166WWKR8CBA6M",
|
||||
URI: "http://fossbros-anonymous.io/users/foss_satan/statuses/01FVW7JHQFSFK166WWKR8CBA6M",
|
||||
|
@ -2042,6 +2100,33 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
|||
PollID: "01HEWV1GW2D49R919NPEDXPTZ5",
|
||||
PendingApproval: util.Ptr(false),
|
||||
},
|
||||
"remote_account_1_status_4": {
|
||||
ID: "01JDQ07JZTX9CMDJP67CNA71YD",
|
||||
URI: "http://fossbros-anonymous.io/users/foss_satan/statuses/______",
|
||||
URL: "http://fossbros-anonymous.io/@foss_satan/statuses/______",
|
||||
Content: "<p>this is the latest status edit without poll change</p>",
|
||||
Text: "this is the latest status edit without poll change",
|
||||
ContentWarning: "",
|
||||
AttachmentIDs: nil,
|
||||
CreatedAt: TimeMustParse("2024-11-01T09:00:00+02:00"),
|
||||
UpdatedAt: TimeMustParse("2024-11-01T09:02:00+02:00"),
|
||||
Local: util.Ptr(false),
|
||||
AccountURI: "http://fossbros-anonymous.io/users/foss_satan",
|
||||
AccountID: "01F8MH5ZK5VRH73AKHQM6Y9VNX",
|
||||
InReplyToID: "",
|
||||
InReplyToAccountID: "",
|
||||
InReplyToURI: "",
|
||||
BoostOfID: "",
|
||||
ThreadID: "",
|
||||
EditIDs: []string{"01JDQ07ZZ4FGP13YN8TF63P5A6", "01JDQ08AYQC0G6413VAHA51CV9"},
|
||||
PollID: "01JDQ0EZ5HM9T4WXRQ5WSVD40J",
|
||||
Visibility: gtsmodel.VisibilityPublic,
|
||||
Sensitive: util.Ptr(false),
|
||||
Language: "en",
|
||||
CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ",
|
||||
Federated: util.Ptr(true),
|
||||
ActivityStreamsType: ap.ObjectNote,
|
||||
},
|
||||
"remote_account_2_status_1": {
|
||||
ID: "01HE7XJ1CG84TBKH5V9XKBVGF5",
|
||||
URI: "http://example.org/users/Some_User/statuses/01HE7XJ1CG84TBKH5V9XKBVGF5",
|
||||
|
@ -2125,6 +2210,19 @@ func NewTestPolls() map[string]*gtsmodel.Poll {
|
|||
ClosedAt: time.Time{},
|
||||
Closing: false,
|
||||
},
|
||||
"remote_account_1_status_4_poll": {
|
||||
ID: "01JDQ0EZ5HM9T4WXRQ5WSVD40J",
|
||||
Multiple: util.Ptr(false),
|
||||
HideCounts: util.Ptr(false),
|
||||
Options: []string{"yes", "no", "maybe", "i don't know", "can you repeat the question"},
|
||||
Votes: []int{0, 0, 0, 0, 2},
|
||||
Voters: util.Ptr(2),
|
||||
StatusID: "01JDQ07JZTX9CMDJP67CNA71YD",
|
||||
// empty expiry AND closed date, i.e. no end
|
||||
ExpiresAt: time.Time{},
|
||||
ClosedAt: time.Time{},
|
||||
Closing: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2184,6 +2282,24 @@ func NewTestPollVotes() map[string]*gtsmodel.PollVote {
|
|||
Poll: nil,
|
||||
CreatedAt: TimeMustParse("2021-09-11T11:47:37+02:00"),
|
||||
},
|
||||
"remote_account_1_status_4_poll_vote_local_account_1": {
|
||||
ID: "01JDQ0SX9QVVFHS7P8M1PA3SVG",
|
||||
Choices: []int{4},
|
||||
AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
||||
Account: nil,
|
||||
PollID: "01JDQ0EZ5HM9T4WXRQ5WSVD40J",
|
||||
Poll: nil,
|
||||
CreatedAt: TimeMustParse("2024-11-01T09:01:30+02:00"),
|
||||
},
|
||||
"remote_account_1_status_4_poll_vote_local_account_2": {
|
||||
ID: "01JDQ0T3EEDN7SAVBQMQP4PR12",
|
||||
Choices: []int{4},
|
||||
AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||
Account: nil,
|
||||
PollID: "01JDQ0EZ5HM9T4WXRQ5WSVD40J",
|
||||
Poll: nil,
|
||||
CreatedAt: TimeMustParse("2024-11-01T09:02:30+02:00"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2341,7 +2457,6 @@ func NewTestMentions() map[string]*gtsmodel.Mention {
|
|||
ID: "01FCTA2Y6FGHXQA4ZE6N5NMNEX",
|
||||
StatusID: "01FCTA44PW9H1TB328S9AQXKDS",
|
||||
CreatedAt: TimeMustParse("2022-05-14T13:21:09+02:00"),
|
||||
UpdatedAt: TimeMustParse("2022-05-14T13:21:09+02:00"),
|
||||
OriginAccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
||||
OriginAccountURI: "http://localhost:8080/users/the_mighty_zork",
|
||||
TargetAccountID: "01F8MH5ZK5VRH73AKHQM6Y9VNX",
|
||||
|
@ -2353,7 +2468,6 @@ func NewTestMentions() map[string]*gtsmodel.Mention {
|
|||
ID: "01FDF2HM2NF6FSRZCDEDV451CN",
|
||||
StatusID: "01FCQSQ667XHJ9AV9T27SJJSX5",
|
||||
CreatedAt: TimeMustParse("2022-05-14T13:21:09+02:00"),
|
||||
UpdatedAt: TimeMustParse("2022-05-14T13:21:09+02:00"),
|
||||
OriginAccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||
OriginAccountURI: "http://localhost:8080/users/1happyturtle",
|
||||
TargetAccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
||||
|
@ -2365,7 +2479,6 @@ func NewTestMentions() map[string]*gtsmodel.Mention {
|
|||
ID: "01FN3VKDEF4CN2W9TKX339BEHB",
|
||||
StatusID: "01FN3VJGFH10KR7S2PB0GFJZYG",
|
||||
CreatedAt: TimeMustParse("2022-05-14T13:21:09+02:00"),
|
||||
UpdatedAt: TimeMustParse("2022-05-14T13:21:09+02:00"),
|
||||
OriginAccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||
OriginAccountURI: "http://localhost:8080/users/1happyturtle",
|
||||
TargetAccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
||||
|
@ -2377,7 +2490,6 @@ func NewTestMentions() map[string]*gtsmodel.Mention {
|
|||
ID: "01FF26A6BGEKCZFWNEHXB2ZZ6M",
|
||||
StatusID: "01FF25D5Q0DH7CHD57CTRS6WK0",
|
||||
CreatedAt: TimeMustParse("2022-05-14T13:21:09+02:00"),
|
||||
UpdatedAt: TimeMustParse("2022-05-14T13:21:09+02:00"),
|
||||
OriginAccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
|
||||
OriginAccountURI: "http://localhost:8080/users/admin",
|
||||
TargetAccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
||||
|
@ -2389,7 +2501,6 @@ func NewTestMentions() map[string]*gtsmodel.Mention {
|
|||
ID: "01J5QVP69ANF1K4WHES6GA4WXP",
|
||||
StatusID: "01J5QVB9VC76NPPRQ207GG4DRZ",
|
||||
CreatedAt: TimeMustParse("2024-02-20T12:41:37+02:00"),
|
||||
UpdatedAt: TimeMustParse("2024-02-20T12:41:37+02:00"),
|
||||
OriginAccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
|
||||
OriginAccountURI: "http://localhost:8080/users/admin",
|
||||
TargetAccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||
|
@ -2401,7 +2512,6 @@ func NewTestMentions() map[string]*gtsmodel.Mention {
|
|||
ID: "01HE7XQNMKTVC8MNPCE1JGK4J3",
|
||||
StatusID: "01HE7XJ1CG84TBKH5V9XKBVGF5",
|
||||
CreatedAt: TimeMustParse("2023-11-02T12:44:25+02:00"),
|
||||
UpdatedAt: TimeMustParse("2023-11-02T12:44:25+02:00"),
|
||||
OriginAccountID: "01FHMQX3GAABWSM0S2VZEC2SWC",
|
||||
OriginAccountURI: "http://example.org/users/Some_User",
|
||||
TargetAccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
|
||||
|
@ -3490,6 +3600,102 @@ func NewTestInteractionRequests() map[string]*gtsmodel.InteractionRequest {
|
|||
}
|
||||
}
|
||||
|
||||
func NewTestStatusEdits() map[string]*gtsmodel.StatusEdit {
|
||||
return map[string]*gtsmodel.StatusEdit{
|
||||
"local_account_1_status_9_edit_1": {
|
||||
ID: "01JDPZCZ2Y9KSGZW0R7ZG8T8Y2",
|
||||
Content: "<p>this is the original status</p>",
|
||||
ContentWarning: "",
|
||||
Text: "this is the original status",
|
||||
Language: "en",
|
||||
Sensitive: util.Ptr(false),
|
||||
AttachmentIDs: nil,
|
||||
PollOptions: nil,
|
||||
PollVotes: nil,
|
||||
StatusID: "01JDPZC707CKDN8N4QVWM4Z1NR",
|
||||
CreatedAt: TimeMustParse("2024-11-01T11:00:00+02:00"),
|
||||
},
|
||||
"local_account_1_status_9_edit_2": {
|
||||
ID: "01JDPZDADMD1T9HKF94RECF7PP",
|
||||
Content: "<p>this is the first status edit! now with content-warning</p>",
|
||||
ContentWarning: "edited status",
|
||||
Text: "this is the first status edit! now with content-warning",
|
||||
Language: "en",
|
||||
Sensitive: util.Ptr(false),
|
||||
AttachmentIDs: nil,
|
||||
PollOptions: nil,
|
||||
PollVotes: nil,
|
||||
StatusID: "01JDPZC707CKDN8N4QVWM4Z1NR",
|
||||
CreatedAt: TimeMustParse("2024-11-01T11:01:00+02:00"),
|
||||
},
|
||||
"local_account_2_status_9_edit_1": {
|
||||
ID: "01JDPZPBXAX0M02YSEPB21KX4R",
|
||||
Content: "<p>this is the original status</p>",
|
||||
ContentWarning: "",
|
||||
Text: "this is the original status",
|
||||
Language: "en",
|
||||
Sensitive: util.Ptr(false),
|
||||
AttachmentIDs: nil,
|
||||
PollOptions: nil,
|
||||
PollVotes: nil,
|
||||
StatusID: "01JDPZEZ77X1NX0TY9M10BK1HM",
|
||||
CreatedAt: TimeMustParse("2024-11-01T10:00:00+02:00"),
|
||||
},
|
||||
"local_account_2_status_9_edit_2": {
|
||||
ID: "01JDPZPJHKP7E3M0YQXEXPS1YT",
|
||||
Content: "<p>now edited to have some media!</p>",
|
||||
ContentWarning: "edit with media attachments",
|
||||
Text: "now edited to have some media!",
|
||||
Language: "en",
|
||||
Sensitive: util.Ptr(true),
|
||||
AttachmentIDs: []string{"01JDQ164HM08SGJ7ZEK9003Z4B"},
|
||||
PollOptions: nil,
|
||||
PollVotes: nil,
|
||||
StatusID: "01JDPZEZ77X1NX0TY9M10BK1HM",
|
||||
CreatedAt: TimeMustParse("2024-11-01T10:01:00+02:00"),
|
||||
},
|
||||
"local_account_2_status_9_edit_3": {
|
||||
ID: "01JDPZPY3F85Y7B78ETRXEMWD9",
|
||||
Content: "<p>now edited to remove the media</p>",
|
||||
ContentWarning: "edit missing previous media attachments",
|
||||
Text: "now edited to remove the media",
|
||||
Language: "en",
|
||||
Sensitive: util.Ptr(false),
|
||||
AttachmentIDs: nil,
|
||||
PollOptions: nil,
|
||||
PollVotes: nil,
|
||||
StatusID: "01JDPZEZ77X1NX0TY9M10BK1HM",
|
||||
CreatedAt: TimeMustParse("2024-11-01T10:02:00+02:00"),
|
||||
},
|
||||
"remote_account_1_status_4_edit_1": {
|
||||
ID: "01JDQ07ZZ4FGP13YN8TF63P5A6",
|
||||
Content: "<p>this is the original status, with a poll!</p>",
|
||||
ContentWarning: "",
|
||||
Text: "this is the original status, with a poll!",
|
||||
Language: "en",
|
||||
Sensitive: util.Ptr(false),
|
||||
AttachmentIDs: nil,
|
||||
PollOptions: []string{"yes", "no", "spiderman"},
|
||||
PollVotes: []int{42, 42, 69},
|
||||
StatusID: "01JDQ07JZTX9CMDJP67CNA71YD",
|
||||
CreatedAt: TimeMustParse("2024-11-01T09:00:00+02:00"),
|
||||
},
|
||||
"remote_account_1_status_4_edit_2": {
|
||||
ID: "01JDQ08AYQC0G6413VAHA51CV9",
|
||||
Content: "<p>this is the first status edit! now with a different poll!</p>",
|
||||
ContentWarning: "edited status",
|
||||
Text: "this is the first status edit! now with a different poll!",
|
||||
Language: "en",
|
||||
Sensitive: util.Ptr(false),
|
||||
AttachmentIDs: nil,
|
||||
PollOptions: []string{"yes", "no", "maybe", "i don't know", "can you repeat the question"},
|
||||
PollVotes: []int{0, 0, 0, 0, 1},
|
||||
StatusID: "01JDQ07JZTX9CMDJP67CNA71YD",
|
||||
CreatedAt: TimeMustParse("2024-11-01T09:01:00+02:00"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetSignatureForActivity prepares a mock HTTP request as if it were going to deliver activity to destination signed for privkey and pubKeyID, signs the request and returns the header values.
|
||||
func GetSignatureForActivity(activity pub.Activity, pubKeyID string, privkey *rsa.PrivateKey, destination *url.URL) (signatureHeader string, digestHeader string, dateHeader string) {
|
||||
// convert the activity into json bytes
|
||||
|
|
Loading…
Reference in a new issue