diff --git a/.golangci.yml b/.golangci.yml
index 786cf3a40..332bf9d6b 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -14,7 +14,6 @@ run:
linters:
# enable some extra linters, see here for the list: https://golangci-lint.run/usage/linters/
enable:
- - forcetypeassert
- goconst
- gocritic
- gofmt
diff --git a/internal/ap/collections.go b/internal/ap/collections.go
index 471dae0a1..e86d989ff 100644
--- a/internal/ap/collections.go
+++ b/internal/ap/collections.go
@@ -32,10 +32,10 @@ import (
func ToCollectionPageIterator(t vocab.Type) (CollectionPageIterator, error) {
switch name := t.GetTypeName(); name {
case ObjectCollectionPage:
- t := t.(vocab.ActivityStreamsCollectionPage) //nolint:forcetypeassert
+ t := t.(vocab.ActivityStreamsCollectionPage)
return WrapCollectionPage(t), nil
case ObjectOrderedCollectionPage:
- t := t.(vocab.ActivityStreamsOrderedCollectionPage) //nolint:forcetypeassert
+ t := t.(vocab.ActivityStreamsOrderedCollectionPage)
return WrapOrderedCollectionPage(t), nil
default:
return nil, fmt.Errorf("%T(%s) was not CollectionPage-like", t, name)
@@ -74,7 +74,7 @@ func (iter *regularCollectionPageIterator) PrevPage() WithIRI {
return iter.GetActivityStreamsPrev()
}
-func (iter *regularCollectionPageIterator) NextItem() IteratorItemable {
+func (iter *regularCollectionPageIterator) NextItem() TypeOrIRI {
if !iter.initItems() {
return nil
}
@@ -83,7 +83,7 @@ func (iter *regularCollectionPageIterator) NextItem() IteratorItemable {
return cur
}
-func (iter *regularCollectionPageIterator) PrevItem() IteratorItemable {
+func (iter *regularCollectionPageIterator) PrevItem() TypeOrIRI {
if !iter.initItems() {
return nil
}
@@ -130,7 +130,7 @@ func (iter *orderedCollectionPageIterator) PrevPage() WithIRI {
return iter.GetActivityStreamsPrev()
}
-func (iter *orderedCollectionPageIterator) NextItem() IteratorItemable {
+func (iter *orderedCollectionPageIterator) NextItem() TypeOrIRI {
if !iter.initItems() {
return nil
}
@@ -139,7 +139,7 @@ func (iter *orderedCollectionPageIterator) NextItem() IteratorItemable {
return cur
}
-func (iter *orderedCollectionPageIterator) PrevItem() IteratorItemable {
+func (iter *orderedCollectionPageIterator) PrevItem() TypeOrIRI {
if !iter.initItems() {
return nil
}
diff --git a/internal/ap/extract.go b/internal/ap/extract.go
index 4cefd22dc..41cc5dcbc 100644
--- a/internal/ap/extract.go
+++ b/internal/ap/extract.go
@@ -35,39 +35,56 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/util"
)
-// ExtractObject will extract an object vocab.Type from given implementing interface.
-func ExtractObject(with WithObject) vocab.Type {
+// ExtractObjects will extract object vocab.Types from given implementing interface.
+func ExtractObjects(with WithObject) []TypeOrIRI {
// Extract the attached object (if any).
- obj := with.GetActivityStreamsObject()
- if obj == nil {
+ objProp := with.GetActivityStreamsObject()
+ if objProp == nil {
return nil
}
- // Only support single
- // objects (for now...)
- if obj.Len() != 1 {
+ // Check for zero len.
+ if objProp.Len() == 0 {
return nil
}
- // Extract object vocab.Type.
- return obj.At(0).GetType()
+ // Accumulate all of the objects into a slice.
+ objs := make([]TypeOrIRI, objProp.Len())
+ for i := 0; i < objProp.Len(); i++ {
+ objs[i] = objProp.At(i)
+ }
+
+ return objs
}
// ExtractActivityData will extract the usable data type (e.g. Note, Question, etc) and corresponding JSON, from activity.
-func ExtractActivityData(activity pub.Activity, rawJSON map[string]any) (vocab.Type, map[string]any, bool) {
+func ExtractActivityData(activity pub.Activity, rawJSON map[string]any) ([]TypeOrIRI, []any, bool) {
switch typeName := activity.GetTypeName(); {
// Activity (has "object").
case isActivity(typeName):
- objType := ExtractObject(activity)
- if objType == nil {
+ objTypes := ExtractObjects(activity)
+ if len(objTypes) == 0 {
return nil, nil, false
}
- objJSON, _ := rawJSON["object"].(map[string]any)
- return objType, objJSON, true
+
+ var objJSON []any
+ switch json := rawJSON["object"].(type) {
+ case nil:
+ // do nothing
+ case map[string]any:
+ // Wrap map in slice.
+ objJSON = []any{json}
+ case []any:
+ // Use existing slice.
+ objJSON = json
+ }
+
+ return objTypes, objJSON, true
// IntransitiveAcitivity (no "object").
case isIntransitiveActivity(typeName):
- return activity, rawJSON, false
+ asTypeOrIRI := _TypeOrIRI{activity} // wrap activity.
+ return []TypeOrIRI{&asTypeOrIRI}, []any{rawJSON}, true
// Unknown.
default:
diff --git a/internal/ap/interfaces.go b/internal/ap/interfaces.go
index 4538c476f..9e606db62 100644
--- a/internal/ap/interfaces.go
+++ b/internal/ap/interfaces.go
@@ -247,14 +247,8 @@ type CollectionPageIterator interface {
NextPage() WithIRI
PrevPage() WithIRI
- NextItem() IteratorItemable
- PrevItem() IteratorItemable
-}
-
-// IteratorItemable represents the minimum interface for an item in an iterator.
-type IteratorItemable interface {
- WithIRI
- WithType
+ NextItem() TypeOrIRI
+ PrevItem() TypeOrIRI
}
// Flaggable represents the minimum interface for an activitystreams 'Flag' activity.
@@ -267,6 +261,12 @@ type Flaggable interface {
WithObject
}
+// TypeOrIRI represents the minimum interface for something that may be a vocab.Type OR IRI.
+type TypeOrIRI interface {
+ WithIRI
+ WithType
+}
+
// WithJSONLDId represents an activity with JSONLDIdProperty.
type WithJSONLDId interface {
GetJSONLDId() vocab.JSONLDIdProperty
diff --git a/internal/ap/normalize.go b/internal/ap/normalize.go
index 52ada2848..192a2d740 100644
--- a/internal/ap/normalize.go
+++ b/internal/ap/normalize.go
@@ -39,60 +39,48 @@ import (
// This function is a noop if the type passed in is anything except a Create or Update with a Statusable or Accountable as its Object.
func NormalizeIncomingActivity(activity pub.Activity, rawJSON map[string]interface{}) {
// From the activity extract the data vocab.Type + its "raw" JSON.
- dataType, rawData, ok := ExtractActivityData(activity, rawJSON)
- if !ok {
+ dataIfaces, rawData, ok := ExtractActivityData(activity, rawJSON)
+ if !ok || len(dataIfaces) != len(rawData) {
+ // non-equal lengths *shouldn't* happen,
+ // but this is just an integrity check.
return
}
- switch dataType.GetTypeName() {
- // "Pollable" types.
- case ActivityQuestion:
- pollable, ok := dataType.(Pollable)
- if !ok {
- return
+ // Iterate over the available data.
+ for i, dataIface := range dataIfaces {
+ // Try to get as vocab.Type, else
+ // skip this entry for normalization.
+ dataType := dataIface.GetType()
+ if dataType == nil {
+ continue
}
- // Normalize the Pollable specific properties.
- NormalizeIncomingPollOptions(pollable, rawData)
-
- // Fallthrough to handle
- // the rest as Statusable.
- fallthrough
-
- // "Statusable" types.
- case ObjectArticle,
- ObjectDocument,
- ObjectImage,
- ObjectVideo,
- ObjectNote,
- ObjectPage,
- ObjectEvent,
- ObjectPlace,
- ObjectProfile:
- statusable, ok := dataType.(Statusable)
+ // Get the raw data map at index, else skip
+ // this entry due to impossible normalization.
+ rawData, ok := rawData[i].(map[string]any)
if !ok {
- return
+ continue
}
- // Normalize everything we can on the statusable.
- NormalizeIncomingContent(statusable, rawData)
- NormalizeIncomingAttachments(statusable, rawData)
- NormalizeIncomingSummary(statusable, rawData)
- NormalizeIncomingName(statusable, rawData)
+ if statusable, ok := ToStatusable(dataType); ok {
+ if pollable, ok := ToPollable(dataType); ok {
+ // Normalize the Pollable specific properties.
+ NormalizeIncomingPollOptions(pollable, rawData)
+ }
- // "Accountable" types.
- case ActorApplication,
- ActorGroup,
- ActorOrganization,
- ActorPerson,
- ActorService:
- accountable, ok := dataType.(Accountable)
- if !ok {
- return
+ // Normalize everything we can on the statusable.
+ NormalizeIncomingContent(statusable, rawData)
+ NormalizeIncomingAttachments(statusable, rawData)
+ NormalizeIncomingSummary(statusable, rawData)
+ NormalizeIncomingName(statusable, rawData)
+ continue
}
- // Normalize everything we can on the accountable.
- NormalizeIncomingSummary(accountable, rawData)
+ if accountable, ok := ToAccountable(dataType); ok {
+ // Normalize everything we can on the accountable.
+ NormalizeIncomingSummary(accountable, rawData)
+ continue
+ }
}
}
diff --git a/internal/ap/util.go b/internal/ap/util.go
new file mode 100644
index 000000000..c810b7985
--- /dev/null
+++ b/internal/ap/util.go
@@ -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 .
+
+package ap
+
+import (
+ "net/url"
+
+ "github.com/superseriousbusiness/activity/streams/vocab"
+)
+
+// _TypeOrIRI wraps a vocab.Type to implement TypeOrIRI.
+type _TypeOrIRI struct {
+ vocab.Type
+}
+
+func (t *_TypeOrIRI) GetType() vocab.Type {
+ return t.Type
+}
+
+func (t *_TypeOrIRI) GetIRI() *url.URL {
+ return nil
+}
+
+func (t *_TypeOrIRI) IsIRI() bool {
+ return false
+}
+
+func (t *_TypeOrIRI) SetIRI(*url.URL) {}
diff --git a/internal/api/client/admin/domainpermission.go b/internal/api/client/admin/domainpermission.go
index 80aa05041..bd6b83425 100644
--- a/internal/api/client/admin/domainpermission.go
+++ b/internal/api/client/admin/domainpermission.go
@@ -144,7 +144,6 @@ func (m *Module) createDomainPermissions(
if multiStatus.Metadata.Failure != 0 {
failures := make(map[string]any, multiStatus.Metadata.Failure)
for _, entry := range multiStatus.Data {
- // nolint:forcetypeassert
failures[entry.Resource.(string)] = entry.Message
}
diff --git a/internal/api/client/statuses/statuscreate_test.go b/internal/api/client/statuses/statuscreate_test.go
index 6238b111a..e945d9caf 100644
--- a/internal/api/client/statuses/statuscreate_test.go
+++ b/internal/api/client/statuses/statuscreate_test.go
@@ -290,7 +290,7 @@ func (suite *StatusCreateTestSuite) TestReplyToNonexistentStatus() {
defer result.Body.Close()
b, err := ioutil.ReadAll(result.Body)
suite.NoError(err)
- suite.Equal(`{"error":"Bad Request: status with id 3759e7ef-8ee1-4c0c-86f6-8b70b9ad3d50 not replyable because it doesn't exist"}`, string(b))
+ suite.Equal(`{"error":"Bad Request: cannot reply to status that does not exist"}`, string(b))
}
// Post a reply to the status of a local user that allows replies.
diff --git a/internal/federation/dereferencing/status.go b/internal/federation/dereferencing/status.go
index 5ec04175e..84316f3a9 100644
--- a/internal/federation/dereferencing/status.go
+++ b/internal/federation/dereferencing/status.go
@@ -288,8 +288,8 @@ func (d *deref) enrichStatus(
return nil, nil, gtserror.Newf("error populating mentions for status %s: %w", uri, err)
}
- // Ensure the status' tags are populated.
- if err := d.fetchStatusTags(ctx, requestUser, latestStatus); err != nil {
+ // Ensure the status' tags are populated, (changes are expected / okay).
+ if err := d.fetchStatusTags(ctx, latestStatus); err != nil {
return nil, nil, gtserror.Newf("error populating tags for status %s: %w", uri, err)
}
@@ -298,8 +298,8 @@ func (d *deref) enrichStatus(
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, requestUser, status, latestStatus); err != nil {
+ // Ensure the status' emoji attachments are populated, (changes are expected / okay).
+ if err := d.fetchStatusEmojis(ctx, requestUser, latestStatus); err != nil {
return nil, nil, gtserror.Newf("error populating emojis for status %s: %w", uri, err)
}
@@ -359,6 +359,8 @@ func (d *deref) fetchStatusMentions(ctx context.Context, requestUser string, exi
}
// 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: %v", err)
@@ -403,7 +405,7 @@ func (d *deref) fetchStatusMentions(ctx context.Context, requestUser string, exi
return nil
}
-func (d *deref) fetchStatusTags(ctx context.Context, requestUser string, status *gtsmodel.Status) error {
+func (d *deref) fetchStatusTags(ctx context.Context, status *gtsmodel.Status) error {
// Allocate new slice to take the yet-to-be determined tag IDs.
status.TagIDs = make([]string, len(status.Tags))
@@ -417,13 +419,14 @@ func (d *deref) fetchStatusTags(ctx context.Context, requestUser string, status
continue
}
- // No tag with this name yet, create it.
if tag == nil {
+ // Create new ID for tag name.
tag = >smodel.Tag{
ID: id.NewULID(),
Name: placeholder.Name,
}
+ // Insert this tag with new name into the database.
if err := d.state.DB.PutTag(ctx, tag); err != nil {
log.Errorf(ctx, "db error putting tag %s: %v", tag.Name, err)
continue
@@ -516,7 +519,7 @@ func (d *deref) fetchStatusAttachments(ctx context.Context, tsport transport.Tra
return nil
}
-func (d *deref) fetchStatusEmojis(ctx context.Context, requestUser string, existing, status *gtsmodel.Status) error {
+func (d *deref) fetchStatusEmojis(ctx context.Context, requestUser string, status *gtsmodel.Status) error {
// Fetch the full-fleshed-out emoji objects for our status.
emojis, err := d.populateEmojis(ctx, status.Emojis, requestUser)
if err != nil {
diff --git a/internal/federation/federatingdb/accept.go b/internal/federation/federatingdb/accept.go
index 1c514d035..38b6b9300 100644
--- a/internal/federation/federatingdb/accept.go
+++ b/internal/federation/federatingdb/accept.go
@@ -46,28 +46,29 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA
return nil // Already processed.
}
- acceptObject := accept.GetActivityStreamsObject()
- if acceptObject == nil {
- return errors.New("ACCEPT: no object set on vocab.ActivityStreamsAccept")
- }
+ // Iterate all provided objects in the activity.
+ for _, object := range ap.ExtractObjects(accept) {
- for iter := acceptObject.Begin(); iter != acceptObject.End(); iter = iter.Next() {
- // check if the object is an IRI
- if iter.IsIRI() {
- // we have just the URI of whatever is being accepted, so we need to find out what it is
- acceptedObjectIRI := iter.GetIRI()
- if uris.IsFollowPath(acceptedObjectIRI) {
- // ACCEPT FOLLOW
- followReq, err := f.state.DB.GetFollowRequestByURI(ctx, acceptedObjectIRI.String())
+ // Check and handle any vocab.Type objects.
+ if objType := object.GetType(); objType != nil {
+ switch objType.GetTypeName() { //nolint:gocritic
+
+ case ap.ActivityFollow:
+ // Cast the vocab.Type object to known AS type.
+ asFollow := objType.(vocab.ActivityStreamsFollow)
+
+ // convert the follow to something we can understand
+ gtsFollow, err := f.converter.ASFollowToFollow(ctx, asFollow)
if err != nil {
- return fmt.Errorf("ACCEPT: couldn't get follow request with id %s from the database: %s", acceptedObjectIRI.String(), err)
+ return fmt.Errorf("ACCEPT: error converting asfollow to gtsfollow: %s", err)
}
// make sure the addressee of the original follow is the same as whatever inbox this landed in
- if followReq.AccountID != receivingAccount.ID {
+ if gtsFollow.AccountID != receivingAccount.ID {
return errors.New("ACCEPT: follow object account and inbox account were not the same")
}
- follow, err := f.state.DB.AcceptFollowRequest(ctx, followReq.AccountID, followReq.TargetAccountID)
+
+ follow, err := f.state.DB.AcceptFollowRequest(ctx, gtsFollow.AccountID, gtsFollow.TargetAccountID)
if err != nil {
return err
}
@@ -78,31 +79,36 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA
GTSModel: follow,
ReceivingAccount: receivingAccount,
})
-
- return nil
}
- }
- // check if iter is an AP object / type
- if iter.GetType() == nil {
continue
}
- if iter.GetType().GetTypeName() == ap.ActivityFollow {
+
+ // Check and handle any
+ // IRI type objects.
+ if object.IsIRI() {
+
+ // Extract IRI from object.
+ iri := object.GetIRI()
+ if !uris.IsFollowPath(iri) {
+ continue
+ }
+
+ // Serialize IRI.
+ iriStr := iri.String()
+
// ACCEPT FOLLOW
- asFollow, ok := iter.GetType().(vocab.ActivityStreamsFollow)
- if !ok {
- return errors.New("ACCEPT: couldn't parse follow into vocab.ActivityStreamsFollow")
- }
- // convert the follow to something we can understand
- gtsFollow, err := f.converter.ASFollowToFollow(ctx, asFollow)
+ followReq, err := f.state.DB.GetFollowRequestByURI(ctx, iriStr)
if err != nil {
- return fmt.Errorf("ACCEPT: error converting asfollow to gtsfollow: %s", err)
+ return fmt.Errorf("ACCEPT: couldn't get follow request with id %s from the database: %s", iriStr, err)
}
+
// make sure the addressee of the original follow is the same as whatever inbox this landed in
- if gtsFollow.AccountID != receivingAccount.ID {
+ if followReq.AccountID != receivingAccount.ID {
return errors.New("ACCEPT: follow object account and inbox account were not the same")
}
- follow, err := f.state.DB.AcceptFollowRequest(ctx, gtsFollow.AccountID, gtsFollow.TargetAccountID)
+
+ follow, err := f.state.DB.AcceptFollowRequest(ctx, followReq.AccountID, followReq.TargetAccountID)
if err != nil {
return err
}
@@ -114,8 +120,9 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA
ReceivingAccount: receivingAccount,
})
- return nil
+ continue
}
+
}
return nil
diff --git a/internal/federation/federatingdb/create.go b/internal/federation/federatingdb/create.go
index 12e324166..6cb230589 100644
--- a/internal/federation/federatingdb/create.go
+++ b/internal/federation/federatingdb/create.go
@@ -81,6 +81,7 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
// FLAG / REPORT SOMETHING
return f.activityFlag(ctx, asType, receivingAccount, requestingAccount)
}
+
return nil
}
@@ -111,6 +112,7 @@ func (f *federatingDB) activityBlock(ctx context.Context, asType vocab.Type, rec
GTSModel: block,
ReceivingAccount: receiving,
})
+
return nil
}
@@ -132,37 +134,19 @@ func (f *federatingDB) activityCreate(
return gtserror.Newf("could not convert asType %T to ActivityStreamsCreate", asType)
}
- // Create must have an Object.
- objectProp := create.GetActivityStreamsObject()
- if objectProp == nil {
- return gtserror.New("create had no Object")
- }
-
- // Iterate through the Object property and process FIRST provided statusable.
- // todo: https://github.com/superseriousbusiness/gotosocial/issues/1905
- for iter := objectProp.Begin(); iter != objectProp.End(); iter = iter.Next() {
- object := iter.GetType()
- if object == nil {
- // Can't do Create with Object that's just a URI.
- // Warn log this because it's an AP error.
- log.Warn(ctx, "object entry was not a type: %[1]T%[1]+v", iter)
+ for _, object := range ap.ExtractObjects(create) {
+ // Try to get object as vocab.Type,
+ // else skip handling (likely) IRI.
+ objType := object.GetType()
+ if objType == nil {
continue
}
- // Ensure given object type is a statusable.
- statusable, ok := object.(ap.Statusable)
- if !ok {
- // Can't (currently) Create anything other than a Statusable. ([1] is a format arg index)
- log.Debugf(ctx, "object entry type (currently) unsupported: %[1]T%[1]+v", object)
- continue
+ if statusable, ok := ap.ToStatusable(objType); ok {
+ return f.createStatusable(ctx, statusable, receivingAccount, requestingAccount)
}
- // Handle creation of statusable.
- return f.createStatusable(ctx,
- statusable,
- receivingAccount,
- requestingAccount,
- )
+ // TODO: handle CREATE of other types?
}
return nil
diff --git a/internal/federation/federatingdb/db.go b/internal/federation/federatingdb/db.go
index 3f35a96c3..c412ba3f8 100644
--- a/internal/federation/federatingdb/db.go
+++ b/internal/federation/federatingdb/db.go
@@ -34,6 +34,7 @@ type DB interface {
Accept(ctx context.Context, accept vocab.ActivityStreamsAccept) error
Reject(ctx context.Context, reject vocab.ActivityStreamsReject) error
Announce(ctx context.Context, announce vocab.ActivityStreamsAnnounce) error
+ Question(ctx context.Context, question vocab.ActivityStreamsQuestion) error
}
// FederatingDB uses the underlying DB interface to implement the go-fed pub.Database interface.
diff --git a/internal/federation/federatingdb/question.go b/internal/federation/federatingdb/question.go
new file mode 100644
index 000000000..85226d9ed
--- /dev/null
+++ b/internal/federation/federatingdb/question.go
@@ -0,0 +1,32 @@
+// 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 .
+
+package federatingdb
+
+import (
+ "context"
+
+ "github.com/superseriousbusiness/activity/streams/vocab"
+)
+
+func (f *federatingDB) Question(ctx context.Context, question vocab.ActivityStreamsQuestion) error {
+ receivingAccount, requestingAccount, internal := extractFromCtx(ctx)
+ if internal {
+ return nil // Already processed.
+ }
+ return f.createStatusable(ctx, question, receivingAccount, requestingAccount)
+}
diff --git a/internal/federation/federatingdb/undo.go b/internal/federation/federatingdb/undo.go
index 84a5bdd47..a7a0f077a 100644
--- a/internal/federation/federatingdb/undo.go
+++ b/internal/federation/federatingdb/undo.go
@@ -27,6 +27,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/ap"
"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"
)
@@ -48,31 +49,31 @@ func (f *federatingDB) Undo(ctx context.Context, undo vocab.ActivityStreamsUndo)
return nil // Already processed.
}
- undoObject := undo.GetActivityStreamsObject()
- if undoObject == nil {
- return errors.New("UNDO: no object set on vocab.ActivityStreamsUndo")
- }
+ var errs gtserror.MultiError
- for iter := undoObject.Begin(); iter != undoObject.End(); iter = iter.Next() {
- t := iter.GetType()
- if t == nil {
+ for _, object := range ap.ExtractObjects(undo) {
+ // Try to get object as vocab.Type,
+ // else skip handling (likely) IRI.
+ objType := object.GetType()
+ if objType == nil {
continue
}
- switch t.GetTypeName() {
+ switch objType.GetTypeName() {
case ap.ActivityFollow:
- if err := f.undoFollow(ctx, receivingAccount, undo, t); err != nil {
- return err
+ if err := f.undoFollow(ctx, receivingAccount, undo, objType); err != nil {
+ errs.Appendf("error undoing follow: %w", err)
}
case ap.ActivityLike:
- if err := f.undoLike(ctx, receivingAccount, undo, t); err != nil {
- return err
+ if err := f.undoLike(ctx, receivingAccount, undo, objType); err != nil {
+ errs.Appendf("error undoing like: %w", err)
}
case ap.ActivityAnnounce:
- // todo: undo boost / reblog / announce
+ // TODO: actually handle this !
+ log.Warn(ctx, "skipped undo announce")
case ap.ActivityBlock:
- if err := f.undoBlock(ctx, receivingAccount, undo, t); err != nil {
- return err
+ if err := f.undoBlock(ctx, receivingAccount, undo, objType); err != nil {
+ errs.Appendf("error undoing block: %w", err)
}
}
}
diff --git a/internal/federation/federatingdb/update.go b/internal/federation/federatingdb/update.go
index 8e452eb3c..5d3d4a0ff 100644
--- a/internal/federation/federatingdb/update.go
+++ b/internal/federation/federatingdb/update.go
@@ -56,21 +56,18 @@ func (f *federatingDB) Update(ctx context.Context, asType vocab.Type) error {
return nil // Already processed.
}
- switch asType.GetTypeName() {
- case ap.ActorApplication, ap.ActorGroup, ap.ActorOrganization, ap.ActorPerson, ap.ActorService:
- return f.updateAccountable(ctx, receivingAccount, requestingAccount, asType)
+ if accountable, ok := ap.ToAccountable(asType); ok {
+ return f.updateAccountable(ctx, receivingAccount, requestingAccount, accountable)
+ }
+
+ if statusable, ok := ap.ToStatusable(asType); ok {
+ return f.updateStatusable(ctx, receivingAccount, requestingAccount, statusable)
}
return nil
}
-func (f *federatingDB) updateAccountable(ctx context.Context, receivingAcct *gtsmodel.Account, requestingAcct *gtsmodel.Account, asType vocab.Type) error {
- // Ensure delivered asType is a valid Accountable model.
- accountable, ok := asType.(ap.Accountable)
- if !ok {
- return gtserror.Newf("could not convert vocab.Type %T to Accountable", asType)
- }
-
+func (f *federatingDB) updateAccountable(ctx context.Context, receivingAcct *gtsmodel.Account, requestingAcct *gtsmodel.Account, accountable ap.Accountable) error {
// Extract AP URI of the updated Accountable model.
idProp := accountable.GetJSONLDId()
if idProp == nil || !idProp.IsIRI() {
@@ -103,3 +100,43 @@ func (f *federatingDB) updateAccountable(ctx context.Context, receivingAcct *gts
return nil
}
+
+func (f *federatingDB) updateStatusable(ctx context.Context, receivingAcct *gtsmodel.Account, requestingAcct *gtsmodel.Account, statusable ap.Statusable) error {
+ // Extract AP URI of the updated model.
+ idProp := statusable.GetJSONLDId()
+ if idProp == nil || !idProp.IsIRI() {
+ return gtserror.New("invalid id prop")
+ }
+
+ // Get the status URI string for lookups.
+ statusURI := idProp.GetIRI()
+ statusURIStr := statusURI.String()
+
+ // Don't try to update local statuses.
+ if statusURI.Host == config.GetHost() {
+ return nil
+ }
+
+ // Get the status we have on file for this URI string.
+ status, err := f.state.DB.GetStatusByURI(ctx, statusURIStr)
+ if err != nil {
+ return gtserror.Newf("error fetching status from db: %w", err)
+ }
+
+ // Check that update was by the status author.
+ if status.AccountID != requestingAcct.ID {
+ return gtserror.Newf("update for %s was not requested by author", statusURIStr)
+ }
+
+ // Queue an UPDATE NOTE activity to our fedi API worker,
+ // this will handle necessary database insertions, etc.
+ f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
+ APObjectType: ap.ObjectNote,
+ APActivityType: ap.ActivityUpdate,
+ GTSModel: status, // original status
+ APObjectModel: statusable,
+ ReceivingAccount: receivingAcct,
+ })
+
+ return nil
+}
diff --git a/internal/federation/federatingprotocol.go b/internal/federation/federatingprotocol.go
index fb4e5bfb9..ea19eb651 100644
--- a/internal/federation/federatingprotocol.go
+++ b/internal/federation/federatingprotocol.go
@@ -522,6 +522,9 @@ func (f *federator) FederatingCallbacks(ctx context.Context) (wrapped pub.Federa
func(ctx context.Context, announce vocab.ActivityStreamsAnnounce) error {
return f.FederatingDB().Announce(ctx, announce)
},
+ func(ctx context.Context, question vocab.ActivityStreamsQuestion) error {
+ return f.FederatingDB().Question(ctx, question)
+ },
}
return
diff --git a/internal/processing/status/create.go b/internal/processing/status/create.go
index 4d4f7c574..ee4466b1b 100644
--- a/internal/processing/status/create.go
+++ b/internal/processing/status/create.go
@@ -34,70 +34,75 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/text"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
"github.com/superseriousbusiness/gotosocial/internal/uris"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
)
// Create processes the given form to create a new status, returning the api model representation of that status if it's OK.
//
// Precondition: the form's fields should have already been validated and normalized by the caller.
-func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, application *gtsmodel.Application, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, gtserror.WithCode) {
- accountURIs := uris.GenerateURIsForAccount(account.Username)
- thisStatusID := id.NewULID()
- local := true
- sensitive := form.Sensitive
+func (p *Processor) Create(ctx context.Context, requestingAccount *gtsmodel.Account, application *gtsmodel.Application, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, gtserror.WithCode) {
+ // Generate new ID for status.
+ statusID := id.NewULID()
- newStatus := >smodel.Status{
- ID: thisStatusID,
- URI: accountURIs.StatusesURI + "/" + thisStatusID,
- URL: accountURIs.StatusesURL + "/" + thisStatusID,
- CreatedAt: time.Now(),
- UpdatedAt: time.Now(),
- Local: &local,
- AccountID: account.ID,
- AccountURI: account.URI,
- ContentWarning: text.SanitizeToPlaintext(form.SpoilerText),
+ // Generate necessary URIs for username, to build status URIs.
+ accountURIs := uris.GenerateURIsForAccount(requestingAccount.Username)
+
+ // Get current time.
+ now := time.Now()
+
+ status := >smodel.Status{
+ ID: statusID,
+ URI: accountURIs.StatusesURI + "/" + statusID,
+ URL: accountURIs.StatusesURL + "/" + statusID,
+ CreatedAt: now,
+ UpdatedAt: now,
+ Local: util.Ptr(true),
+ Account: requestingAccount,
+ AccountID: requestingAccount.ID,
+ AccountURI: requestingAccount.URI,
ActivityStreamsType: ap.ObjectNote,
- Sensitive: &sensitive,
+ Sensitive: &form.Sensitive,
CreatedWithApplicationID: application.ID,
Text: form.Status,
}
- if errWithCode := processReplyToID(ctx, p.state.DB, form, account.ID, newStatus); errWithCode != nil {
+ if errWithCode := p.processReplyToID(ctx, form, requestingAccount.ID, status); errWithCode != nil {
return nil, errWithCode
}
- if errWithCode := processMediaIDs(ctx, p.state.DB, form, account.ID, newStatus); errWithCode != nil {
+ if errWithCode := p.processMediaIDs(ctx, form, requestingAccount.ID, status); errWithCode != nil {
return nil, errWithCode
}
- if err := processVisibility(ctx, form, account.Privacy, newStatus); err != nil {
+ if err := processVisibility(form, requestingAccount.Privacy, status); err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
- if err := processLanguage(ctx, form, account.Language, newStatus); err != nil {
+ if err := processLanguage(form, requestingAccount.Language, status); err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
- if err := processContent(ctx, p.state.DB, p.formatter, p.parseMention, form, account.ID, newStatus); err != nil {
+ if err := p.processContent(ctx, p.parseMention, form, status); err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
- // put the new status in the database
- if err := p.state.DB.PutStatus(ctx, newStatus); err != nil {
+ // Insert this new status in the database.
+ if err := p.state.DB.PutStatus(ctx, status); err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
- // send it back to the processor for async processing
+ // send it back to the client API worker for async side-effects.
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityCreate,
- GTSModel: newStatus,
- OriginAccount: account,
+ GTSModel: status,
+ OriginAccount: requestingAccount,
})
- return p.apiStatus(ctx, newStatus, account)
+ return p.apiStatus(ctx, status, requestingAccount)
}
-func processReplyToID(ctx context.Context, dbService db.DB, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode {
+func (p *Processor) processReplyToID(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode {
if form.InReplyToID == "" {
return nil
}
@@ -109,78 +114,74 @@ func processReplyToID(ctx context.Context, dbService db.DB, form *apimodel.Advan
// 3. Does a block exist between either the current account or the account that posted the status it's replying to?
//
// If this is all OK, then we fetch the repliedStatus and the repliedAccount for later processing.
- repliedStatus := >smodel.Status{}
- repliedAccount := >smodel.Account{}
- if err := dbService.GetByID(ctx, form.InReplyToID, repliedStatus); err != nil {
- if err == db.ErrNoEntries {
- err := fmt.Errorf("status with id %s not replyable because it doesn't exist", form.InReplyToID)
- return gtserror.NewErrorBadRequest(err, err.Error())
- }
- err := fmt.Errorf("db error fetching status with id %s: %s", form.InReplyToID, err)
- return gtserror.NewErrorInternalError(err)
- }
- if !*repliedStatus.Replyable {
- err := fmt.Errorf("status with id %s is marked as not replyable", form.InReplyToID)
- return gtserror.NewErrorForbidden(err, err.Error())
- }
-
- if err := dbService.GetByID(ctx, repliedStatus.AccountID, repliedAccount); err != nil {
- if err == db.ErrNoEntries {
- err := fmt.Errorf("status with id %s not replyable because account id %s is not known", form.InReplyToID, repliedStatus.AccountID)
- return gtserror.NewErrorBadRequest(err, err.Error())
- }
- err := fmt.Errorf("db error fetching account with id %s: %s", repliedStatus.AccountID, err)
+ inReplyTo, err := p.state.DB.GetStatusByID(ctx, form.InReplyToID)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("error fetching status %s from db: %w", form.InReplyToID, err)
return gtserror.NewErrorInternalError(err)
}
- if blocked, err := dbService.IsEitherBlocked(ctx, thisAccountID, repliedAccount.ID); err != nil {
- err := fmt.Errorf("db error checking block: %s", err)
+ if inReplyTo == nil {
+ const text = "cannot reply to status that does not exist"
+ return gtserror.NewErrorBadRequest(errors.New(text), text)
+ }
+
+ if !*inReplyTo.Replyable {
+ text := fmt.Sprintf("status %s is marked as not replyable", form.InReplyToID)
+ return gtserror.NewErrorForbidden(errors.New(text), text)
+ }
+
+ if blocked, err := p.state.DB.IsEitherBlocked(ctx, thisAccountID, inReplyTo.AccountID); err != nil {
+ err := gtserror.Newf("error checking block in db: %w", err)
return gtserror.NewErrorInternalError(err)
} else if blocked {
- err := fmt.Errorf("status with id %s not replyable", form.InReplyToID)
- return gtserror.NewErrorNotFound(err)
+ text := fmt.Sprintf("status %s is not replyable", form.InReplyToID)
+ return gtserror.NewErrorNotFound(errors.New(text), text)
}
- status.InReplyToID = repliedStatus.ID
- status.InReplyToURI = repliedStatus.URI
- status.InReplyToAccountID = repliedAccount.ID
+ // Set status fields from inReplyTo.
+ status.InReplyToID = inReplyTo.ID
+ status.InReplyToURI = inReplyTo.URI
+ status.InReplyToAccountID = inReplyTo.AccountID
return nil
}
-func processMediaIDs(ctx context.Context, dbService db.DB, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode {
+func (p *Processor) processMediaIDs(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode {
if form.MediaIDs == nil {
return nil
}
+ // Get minimum allowed char descriptions.
+ minChars := config.GetMediaDescriptionMinChars()
+
attachments := []*gtsmodel.MediaAttachment{}
attachmentIDs := []string{}
for _, mediaID := range form.MediaIDs {
- attachment, err := dbService.GetAttachmentByID(ctx, mediaID)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- err = fmt.Errorf("ProcessMediaIDs: media not found for media id %s", mediaID)
- return gtserror.NewErrorBadRequest(err, err.Error())
- }
- err = fmt.Errorf("ProcessMediaIDs: db error for media id %s", mediaID)
+ attachment, err := p.state.DB.GetAttachmentByID(ctx, mediaID)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("error fetching media from db: %w", err)
return gtserror.NewErrorInternalError(err)
}
+ if attachment == nil {
+ text := fmt.Sprintf("media %s not found", mediaID)
+ return gtserror.NewErrorBadRequest(errors.New(text), text)
+ }
+
if attachment.AccountID != thisAccountID {
- err = fmt.Errorf("ProcessMediaIDs: media with id %s does not belong to account %s", mediaID, thisAccountID)
- return gtserror.NewErrorBadRequest(err, err.Error())
+ text := fmt.Sprintf("media %s does not belong to account", mediaID)
+ return gtserror.NewErrorBadRequest(errors.New(text), text)
}
if attachment.StatusID != "" || attachment.ScheduledStatusID != "" {
- err = fmt.Errorf("ProcessMediaIDs: media with id %s is already attached to a status", mediaID)
- return gtserror.NewErrorBadRequest(err, err.Error())
+ text := fmt.Sprintf("media %s already attached to status", mediaID)
+ return gtserror.NewErrorBadRequest(errors.New(text), text)
}
- minDescriptionChars := config.GetMediaDescriptionMinChars()
- if descriptionLength := len([]rune(attachment.Description)); descriptionLength < minDescriptionChars {
- err = fmt.Errorf("ProcessMediaIDs: description too short! media description of at least %d chararacters is required but %d was provided for media with id %s", minDescriptionChars, descriptionLength, mediaID)
- return gtserror.NewErrorBadRequest(err, err.Error())
+ if length := len([]rune(attachment.Description)); length < minChars {
+ text := fmt.Sprintf("media %s description too short, at least %d required", mediaID, minChars)
+ return gtserror.NewErrorBadRequest(errors.New(text), text)
}
attachments = append(attachments, attachment)
@@ -192,7 +193,7 @@ func processMediaIDs(ctx context.Context, dbService db.DB, form *apimodel.Advanc
return nil
}
-func processVisibility(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, accountDefaultVis gtsmodel.Visibility, status *gtsmodel.Status) error {
+func processVisibility(form *apimodel.AdvancedStatusCreateForm, accountDefaultVis gtsmodel.Visibility, status *gtsmodel.Status) error {
// by default all flags are set to true
federated := true
boostable := true
@@ -265,7 +266,7 @@ func processVisibility(ctx context.Context, form *apimodel.AdvancedStatusCreateF
return nil
}
-func processLanguage(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, accountDefaultLanguage string, status *gtsmodel.Status) error {
+func processLanguage(form *apimodel.AdvancedStatusCreateForm, accountDefaultLanguage string, status *gtsmodel.Status) error {
if form.Language != "" {
status.Language = form.Language
} else {
@@ -277,68 +278,80 @@ func processLanguage(ctx context.Context, form *apimodel.AdvancedStatusCreateFor
return nil
}
-func processContent(ctx context.Context, dbService db.DB, formatter *text.Formatter, parseMention gtsmodel.ParseMentionFunc, form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
- // if there's nothing in the status at all we can just return early
- if form.Status == "" {
- status.Content = ""
- return nil
- }
-
- // if content type wasn't specified we should try to figure out what content type this user prefers
+func (p *Processor) processContent(ctx context.Context, parseMention gtsmodel.ParseMentionFunc, form *apimodel.AdvancedStatusCreateForm, status *gtsmodel.Status) error {
if form.ContentType == "" {
- acct, err := dbService.GetAccountByID(ctx, accountID)
- if err != nil {
- return fmt.Errorf("error processing new content: couldn't retrieve account from db to check post format: %s", err)
- }
-
- switch acct.StatusContentType {
- case "text/plain":
- form.ContentType = apimodel.StatusContentTypePlain
- case "text/markdown":
- form.ContentType = apimodel.StatusContentTypeMarkdown
- default:
- form.ContentType = apimodel.StatusContentTypeDefault
- }
+ // If content type wasn't specified, use the author's preferred content-type.
+ contentType := apimodel.StatusContentType(status.Account.StatusContentType)
+ form.ContentType = contentType
+ }
+
+ // format is the currently set text formatting
+ // function, according to the provided content-type.
+ var format text.FormatFunc
+
+ // formatInput is a shorthand function to format the given input string with the
+ // currently set 'formatFunc', passing in all required args and returning result.
+ formatInput := func(formatFunc text.FormatFunc, input string) *text.FormatResult {
+ return formatFunc(ctx, parseMention, status.AccountID, status.ID, input)
}
- // parse content out of the status depending on what content type has been submitted
- var f text.FormatFunc
switch form.ContentType {
+ // None given / set,
+ // use default (plain).
+ case "":
+ fallthrough
+
+ // Format status according to text/plain.
case apimodel.StatusContentTypePlain:
- f = formatter.FromPlain
+ format = p.formatter.FromPlain
+
+ // Format status according to text/markdown.
case apimodel.StatusContentTypeMarkdown:
- f = formatter.FromMarkdown
+ format = p.formatter.FromMarkdown
+
+ // Unknown.
default:
- return fmt.Errorf("format %s not recognised as a valid status format", form.ContentType)
- }
- formatted := f(ctx, parseMention, accountID, status.ID, form.Status)
-
- // add full populated gts {mentions, tags, emojis} to the status for passing them around conveniently
- // add just their ids to the status for putting in the db
- status.Mentions = formatted.Mentions
- status.MentionIDs = make([]string, 0, len(formatted.Mentions))
- for _, gtsmention := range formatted.Mentions {
- status.MentionIDs = append(status.MentionIDs, gtsmention.ID)
+ return fmt.Errorf("invalid status format: %q", form.ContentType)
}
- status.Tags = formatted.Tags
- status.TagIDs = make([]string, 0, len(formatted.Tags))
- for _, gtstag := range formatted.Tags {
- status.TagIDs = append(status.TagIDs, gtstag.ID)
- }
+ // Sanitize status text and format.
+ contentRes := formatInput(format, form.Status)
- status.Emojis = formatted.Emojis
- status.EmojiIDs = make([]string, 0, len(formatted.Emojis))
- for _, gtsemoji := range formatted.Emojis {
- status.EmojiIDs = append(status.EmojiIDs, gtsemoji.ID)
- }
+ // Collect formatted results.
+ status.Content = contentRes.HTML
+ status.Mentions = append(status.Mentions, contentRes.Mentions...)
+ status.Emojis = append(status.Emojis, contentRes.Emojis...)
+ status.Tags = append(status.Tags, contentRes.Tags...)
- spoilerformatted := formatter.FromPlainEmojiOnly(ctx, parseMention, accountID, status.ID, form.SpoilerText)
- for _, gtsemoji := range spoilerformatted.Emojis {
- status.Emojis = append(status.Emojis, gtsemoji)
- status.EmojiIDs = append(status.EmojiIDs, gtsemoji.ID)
- }
+ // From here-on-out just use emoji-only
+ // plain-text formatting as the FormatFunc.
+ format = p.formatter.FromPlainEmojiOnly
+
+ // Sanitize content warning and format.
+ spoiler := text.SanitizeToPlaintext(form.SpoilerText)
+ warningRes := formatInput(format, spoiler)
+
+ // Collect formatted results.
+ status.ContentWarning = warningRes.HTML
+ status.Emojis = append(status.Emojis, warningRes.Emojis...)
+
+ // Gather all the database IDs from each of the gathered status mentions, tags, and emojis.
+ status.MentionIDs = gatherIDs(status.Mentions, func(mention *gtsmodel.Mention) string { return mention.ID })
+ status.TagIDs = gatherIDs(status.Tags, func(tag *gtsmodel.Tag) string { return tag.ID })
+ status.EmojiIDs = gatherIDs(status.Emojis, func(emoji *gtsmodel.Emoji) string { return emoji.ID })
- status.Content = formatted.HTML
return nil
}
+
+// gatherIDs is a small utility function to gather IDs from a slice of type T.
+func gatherIDs[T any](in []T, getID func(T) string) []string {
+ if getID == nil {
+ // move nil check out loop.
+ panic("nil getID function")
+ }
+ ids := make([]string, len(in))
+ for i, t := range in {
+ ids[i] = getID(t)
+ }
+ return ids
+}
diff --git a/internal/processing/status/create_test.go b/internal/processing/status/create_test.go
index 2c86e5a29..8d4267585 100644
--- a/internal/processing/status/create_test.go
+++ b/internal/processing/status/create_test.go
@@ -204,7 +204,7 @@ func (suite *StatusCreateTestSuite) TestProcessMediaDescriptionTooShort() {
}
apiStatus, err := suite.status.Create(ctx, creatingAccount, creatingApplication, statusCreateForm)
- suite.EqualError(err, "ProcessMediaIDs: description too short! media description of at least 100 chararacters is required but 15 was provided for media with id 01F8MH8RMYQ6MSNY3JM2XT1CQ5")
+ suite.EqualError(err, "media 01F8MH8RMYQ6MSNY3JM2XT1CQ5 description too short, at least 100 required")
suite.Nil(apiStatus)
}
diff --git a/internal/processing/stream/stream.go b/internal/processing/stream/stream.go
index 972173c7a..a5b3b9386 100644
--- a/internal/processing/stream/stream.go
+++ b/internal/processing/stream/stream.go
@@ -46,7 +46,7 @@ func (p *Processor) toAccount(payload string, event string, streamTypes []string
if !ok {
return nil // No entry = nothing to stream.
}
- streamsForAccount := v.(*stream.StreamsForAccount) //nolint:forcetypeassert
+ streamsForAccount := v.(*stream.StreamsForAccount)
streamsForAccount.Lock()
defer streamsForAccount.Unlock()
diff --git a/internal/processing/workers/federate.go b/internal/processing/workers/federate.go
index 4b2ca4de1..a87a89fd2 100644
--- a/internal/processing/workers/federate.go
+++ b/internal/processing/workers/federate.go
@@ -147,27 +147,27 @@ func (f *federate) CreateStatus(ctx context.Context, status *gtsmodel.Status) er
return nil
}
- // Populate model.
+ // Ensure the status model is fully populated.
if err := f.state.DB.PopulateStatus(ctx, status); err != nil {
return gtserror.Newf("error populating status: %w", err)
}
- // Parse relevant URI(s).
+ // Parse the outbox URI of the status author.
outboxIRI, err := parseURI(status.Account.OutboxURI)
if err != nil {
return err
}
- // Convert status to an ActivityStreams
- // Note, wrapped in a Create activity.
- asStatus, err := f.converter.StatusToAS(ctx, status)
+ // Convert status to ActivityStreams Statusable implementing type.
+ statusable, err := f.converter.StatusToAS(ctx, status)
if err != nil {
- return gtserror.Newf("error converting status to AS: %w", err)
+ return gtserror.Newf("error converting status to Statusable: %w", err)
}
- create, err := f.converter.WrapNoteInCreate(asStatus, false)
+ // Use ActivityStreams Statusable type as Object of Create.
+ create, err := f.converter.WrapStatusableInCreate(statusable, false)
if err != nil {
- return gtserror.Newf("error wrapping status in create: %w", err)
+ return gtserror.Newf("error wrapping Statusable in Create: %w", err)
}
// Send the Create via the Actor's outbox.
@@ -196,12 +196,12 @@ func (f *federate) DeleteStatus(ctx context.Context, status *gtsmodel.Status) er
return nil
}
- // Populate model.
+ // Ensure the status model is fully populated.
if err := f.state.DB.PopulateStatus(ctx, status); err != nil {
return gtserror.Newf("error populating status: %w", err)
}
- // Parse relevant URI(s).
+ // Parse the outbox URI of the status author.
outboxIRI, err := parseURI(status.Account.OutboxURI)
if err != nil {
return err
@@ -226,6 +226,50 @@ func (f *federate) DeleteStatus(ctx context.Context, status *gtsmodel.Status) er
return nil
}
+func (f *federate) UpdateStatus(ctx context.Context, status *gtsmodel.Status) error {
+ // Do nothing if the status
+ // shouldn't be federated.
+ if !*status.Federated {
+ return nil
+ }
+
+ // Do nothing if this
+ // isn't our status.
+ if !*status.Local {
+ return nil
+ }
+
+ // Ensure the status model is fully populated.
+ if err := f.state.DB.PopulateStatus(ctx, status); err != nil {
+ return gtserror.Newf("error populating status: %w", err)
+ }
+
+ // Parse the outbox URI of the status author.
+ outboxIRI, err := parseURI(status.Account.OutboxURI)
+ if err != nil {
+ return err
+ }
+
+ // Convert status to ActivityStreams Statusable implementing type.
+ statusable, err := f.converter.StatusToAS(ctx, status)
+ if err != nil {
+ return gtserror.Newf("error converting status to Statusable: %w", err)
+ }
+
+ // Use ActivityStreams Statusable type as Object of Update.
+ update, err := f.converter.WrapStatusableInUpdate(statusable, false)
+ if err != nil {
+ return gtserror.Newf("error wrapping Statusable in Update: %w", err)
+ }
+
+ // Send the Update activity with Statusable via the Actor's outbox.
+ if _, err := f.FederatingActor().Send(ctx, outboxIRI, update); err != nil {
+ return gtserror.Newf("error sending Update activity via outbox %s: %w", outboxIRI, err)
+ }
+
+ return nil
+}
+
func (f *federate) Follow(ctx context.Context, follow *gtsmodel.Follow) error {
// Populate model.
if err := f.state.DB.PopulateFollow(ctx, follow); err != nil {
diff --git a/internal/processing/workers/fromclientapi.go b/internal/processing/workers/fromclientapi.go
index 1c668db71..ff316b1f4 100644
--- a/internal/processing/workers/fromclientapi.go
+++ b/internal/processing/workers/fromclientapi.go
@@ -114,6 +114,10 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg messages.From
case ap.ActivityUpdate:
switch cMsg.APObjectType {
+ // UPDATE NOTE/STATUS
+ case ap.ObjectNote:
+ return p.clientAPI.UpdateStatus(ctx, cMsg)
+
// UPDATE PROFILE/ACCOUNT
case ap.ObjectProfile, ap.ActorPerson:
return p.clientAPI.UpdateAccount(ctx, cMsg)
@@ -332,10 +336,25 @@ func (p *clientAPI) CreateBlock(ctx context.Context, cMsg messages.FromClientAPI
return nil
}
+func (p *clientAPI) UpdateStatus(ctx context.Context, cMsg messages.FromClientAPI) error {
+ // Cast the updated Status model attached to msg.
+ status, ok := cMsg.GTSModel.(*gtsmodel.Status)
+ if !ok {
+ return gtserror.Newf("cannot cast %T -> *gtsmodel.Status", cMsg.GTSModel)
+ }
+
+ // Federate the updated status changes out remotely.
+ if err := p.federate.UpdateStatus(ctx, status); err != nil {
+ return gtserror.Newf("error federating status update: %w", err)
+ }
+
+ return nil
+}
+
func (p *clientAPI) UpdateAccount(ctx context.Context, cMsg messages.FromClientAPI) error {
account, ok := cMsg.GTSModel.(*gtsmodel.Account)
if !ok {
- return gtserror.Newf("%T not parseable as *gtsmodel.Account", cMsg.GTSModel)
+ return gtserror.Newf("cannot cast %T -> *gtsmodel.Account", cMsg.GTSModel)
}
if err := p.federate.UpdateAccount(ctx, account); err != nil {
diff --git a/internal/processing/workers/fromfediapi.go b/internal/processing/workers/fromfediapi.go
index 57e087499..598480cfb 100644
--- a/internal/processing/workers/fromfediapi.go
+++ b/internal/processing/workers/fromfediapi.go
@@ -119,6 +119,10 @@ func (p *Processor) ProcessFromFediAPI(ctx context.Context, fMsg messages.FromFe
case ap.ActivityUpdate:
switch fMsg.APObjectType { //nolint:gocritic
+ // UPDATE NOTE/STATUS
+ case ap.ObjectNote:
+ return p.fediAPI.UpdateStatus(ctx, fMsg)
+
// UPDATE PROFILE/ACCOUNT
case ap.ObjectProfile:
return p.fediAPI.UpdateAccount(ctx, fMsg)
@@ -485,13 +489,13 @@ func (p *fediAPI) UpdateAccount(ctx context.Context, fMsg messages.FromFediAPI)
// Parse the old/existing account model.
account, ok := fMsg.GTSModel.(*gtsmodel.Account)
if !ok {
- return gtserror.Newf("%T not parseable as *gtsmodel.Account", fMsg.GTSModel)
+ return gtserror.Newf("cannot cast %T -> *gtsmodel.Account", fMsg.GTSModel)
}
// Because this was an Update, the new Accountable should be set on the message.
apubAcc, ok := fMsg.APObjectModel.(ap.Accountable)
if !ok {
- return gtserror.Newf("%T not parseable as ap.Accountable", fMsg.APObjectModel)
+ return gtserror.Newf("cannot cast %T -> ap.Accountable", fMsg.APObjectModel)
}
// Fetch up-to-date bio, avatar, header, etc.
@@ -509,6 +513,34 @@ func (p *fediAPI) UpdateAccount(ctx context.Context, fMsg messages.FromFediAPI)
return nil
}
+func (p *fediAPI) UpdateStatus(ctx context.Context, fMsg messages.FromFediAPI) error {
+ // Cast the existing Status model attached to msg.
+ existing, ok := fMsg.GTSModel.(*gtsmodel.Status)
+ if !ok {
+ return gtserror.Newf("cannot cast %T -> *gtsmodel.Status", fMsg.GTSModel)
+ }
+
+ // Cast the updated ActivityPub statusable object .
+ apStatus, ok := fMsg.APObjectModel.(ap.Statusable)
+ if !ok {
+ return gtserror.Newf("cannot cast %T -> ap.Statusable", fMsg.APObjectModel)
+ }
+
+ // Fetch up-to-date attach status attachments, etc.
+ _, _, err := p.federate.RefreshStatus(
+ ctx,
+ fMsg.ReceivingAccount.Username,
+ existing,
+ apStatus,
+ false,
+ )
+ if err != nil {
+ return gtserror.Newf("error refreshing updated status: %w", err)
+ }
+
+ return nil
+}
+
func (p *fediAPI) DeleteStatus(ctx context.Context, fMsg messages.FromFediAPI) error {
// Delete attachments from this status, since this request
// comes from the federating API, and there's no way the
diff --git a/internal/processing/workers/wipestatus.go b/internal/processing/workers/wipestatus.go
index 0891d9e24..ab59f14be 100644
--- a/internal/processing/workers/wipestatus.go
+++ b/internal/processing/workers/wipestatus.go
@@ -38,7 +38,7 @@ func wipeStatusF(state *state.State, media *media.Processor, surface *surface) w
statusToDelete *gtsmodel.Status,
deleteAttachments bool,
) error {
- errs := new(gtserror.MultiError)
+ var errs gtserror.MultiError
// Either delete all attachments for this status,
// or simply unattach + clean them separately later.
@@ -48,15 +48,15 @@ func wipeStatusF(state *state.State, media *media.Processor, surface *surface) w
// status immediately (in case of delete + redraft)
if deleteAttachments {
// todo:state.DB.DeleteAttachmentsForStatus
- for _, a := range statusToDelete.AttachmentIDs {
- if err := media.Delete(ctx, a); err != nil {
+ for _, id := range statusToDelete.AttachmentIDs {
+ if err := media.Delete(ctx, id); err != nil {
errs.Appendf("error deleting media: %w", err)
}
}
} else {
// todo:state.DB.UnattachAttachmentsForStatus
- for _, a := range statusToDelete.AttachmentIDs {
- if _, err := media.Unattach(ctx, statusToDelete.Account, a); err != nil {
+ for _, id := range statusToDelete.AttachmentIDs {
+ if _, err := media.Unattach(ctx, statusToDelete.Account, id); err != nil {
errs.Appendf("error unattaching media: %w", err)
}
}
@@ -95,11 +95,12 @@ func wipeStatusF(state *state.State, media *media.Processor, surface *surface) w
if err != nil {
errs.Appendf("error fetching status boosts: %w", err)
}
- for _, b := range boosts {
- if err := surface.deleteStatusFromTimelines(ctx, b.ID); err != nil {
+
+ for _, boost := range boosts {
+ if err := surface.deleteStatusFromTimelines(ctx, boost.ID); err != nil {
errs.Appendf("error deleting boost from timelines: %w", err)
}
- if err := state.DB.DeleteStatusByID(ctx, b.ID); err != nil {
+ if err := state.DB.DeleteStatusByID(ctx, boost.ID); err != nil {
errs.Appendf("error deleting boost: %w", err)
}
}
diff --git a/internal/text/plain.go b/internal/text/plain.go
index 1456fd016..1935cec8c 100644
--- a/internal/text/plain.go
+++ b/internal/text/plain.go
@@ -21,6 +21,7 @@ import (
"bytes"
"context"
+ "codeberg.org/gruf/go-byteutil"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/yuin/goldmark"
@@ -103,11 +104,11 @@ func (f *Formatter) FromPlainEmojiOnly(
statusID string,
input string,
) *FormatResult {
- // Initialize standard block parser
- // that wraps result in
tags.
+ // Initialize block parser that
+ // doesn't wrap result in
tags.
plainTextParser := parser.NewParser(
parser.WithBlockParsers(
- util.Prioritized(newPlaintextParser(), 500),
+ util.Prioritized(newPlaintextParserNoParagraph(), 500),
),
)
@@ -161,17 +162,21 @@ func (f *Formatter) fromPlain(
),
)
+ // Convert input string to bytes
+ // without performing any allocs.
+ bInput := byteutil.S2B(input)
+
// Parse input into HTML.
var htmlBytes bytes.Buffer
if err := md.Convert(
- []byte(input),
+ bInput,
&htmlBytes,
); err != nil {
log.Errorf(ctx, "error formatting plaintext input to HTML: %s", err)
}
// Clean and shrink HTML.
- result.HTML = htmlBytes.String()
+ result.HTML = byteutil.B2S(htmlBytes.Bytes())
result.HTML = SanitizeToHTML(result.HTML)
result.HTML = MinifyHTML(result.HTML)
diff --git a/internal/timeline/get.go b/internal/timeline/get.go
index bc238c276..93c869e73 100644
--- a/internal/timeline/get.go
+++ b/internal/timeline/get.go
@@ -222,7 +222,7 @@ func (t *timeline) getXBetweenIDs(ctx context.Context, amount int, behindID stri
// a point where the items are out of the range
// we're interested in.
rangeF = func(e *list.Element) (bool, error) {
- entry := e.Value.(*indexedItemsEntry) //nolint:forcetypeassert
+ entry := e.Value.(*indexedItemsEntry)
if entry.itemID >= behindID {
// ID of this item is too high,
@@ -276,7 +276,6 @@ func (t *timeline) getXBetweenIDs(ctx context.Context, amount int, behindID stri
// Move the mark back one place each loop.
beforeIDMark = e
- //nolint:forcetypeassert
if entry := e.Value.(*indexedItemsEntry); entry.itemID <= beforeID {
// We've gone as far as we can through
// the list and reached entries that are
@@ -319,7 +318,7 @@ func (t *timeline) getXBetweenIDs(ctx context.Context, amount int, behindID stri
// To preserve ordering, we need to reverse the slice
// when we're finished.
for e := beforeIDMark; e != nil; e = e.Prev() {
- entry := e.Value.(*indexedItemsEntry) //nolint:forcetypeassert
+ entry := e.Value.(*indexedItemsEntry)
if entry.itemID == beforeID {
// Don't include the beforeID
diff --git a/internal/timeline/index.go b/internal/timeline/index.go
index 993f7dc5d..6abb6d28d 100644
--- a/internal/timeline/index.go
+++ b/internal/timeline/index.go
@@ -65,7 +65,7 @@ func (t *timeline) indexXBetweenIDs(ctx context.Context, amount int, behindID st
)
for e := t.items.data.Front(); e != nil; e = e.Next() {
- entry := e.Value.(*indexedItemsEntry) //nolint:forcetypeassert
+ entry := e.Value.(*indexedItemsEntry)
position++
@@ -174,7 +174,6 @@ func (t *timeline) grab(ctx context.Context, amount int, behindID string, before
// Don't grab more than we need to.
amount-grabbed,
)
-
if err != nil {
// Grab function already checks for
// db.ErrNoEntries, so if an error
@@ -280,5 +279,5 @@ func (t *timeline) OldestIndexedItemID() string {
return ""
}
- return e.Value.(*indexedItemsEntry).itemID //nolint:forcetypeassert
+ return e.Value.(*indexedItemsEntry).itemID
}
diff --git a/internal/timeline/indexeditems.go b/internal/timeline/indexeditems.go
index c2c7a19b6..9b75e7256 100644
--- a/internal/timeline/indexeditems.go
+++ b/internal/timeline/indexeditems.go
@@ -65,7 +65,7 @@ func (i *indexedItems) insertIndexed(ctx context.Context, newEntry *indexedItems
for e := i.data.Front(); e != nil; e = e.Next() {
currentPosition++
- currentEntry := e.Value.(*indexedItemsEntry) //nolint:forcetypeassert
+ currentEntry := e.Value.(*indexedItemsEntry)
// Check if we need to skip inserting this item based on
// the current item.
diff --git a/internal/timeline/manager.go b/internal/timeline/manager.go
index df0323cdb..b4f075138 100644
--- a/internal/timeline/manager.go
+++ b/internal/timeline/manager.go
@@ -219,7 +219,6 @@ func (m *manager) UnprepareItemFromAllTimelines(ctx context.Context, itemID stri
// Work through all timelines held by this
// manager, and call Unprepare for each.
m.timelines.Range(func(_ any, v any) bool {
- // nolint:forcetypeassert
if err := v.(Timeline).Unprepare(ctx, itemID); err != nil {
errs.Append(err)
}
@@ -248,7 +247,7 @@ func (m *manager) getOrCreateTimeline(ctx context.Context, timelineID string) Ti
i, ok := m.timelines.Load(timelineID)
if ok {
// Timeline already existed in sync.Map.
- return i.(Timeline) //nolint:forcetypeassert
+ return i.(Timeline)
}
// Timeline did not yet exist in sync.Map.
diff --git a/internal/timeline/prepare.go b/internal/timeline/prepare.go
index 3344a0e73..07bde79fa 100644
--- a/internal/timeline/prepare.go
+++ b/internal/timeline/prepare.go
@@ -63,7 +63,7 @@ func (t *timeline) prepareXBetweenIDs(ctx context.Context, amount int, behindID
if frontToBack {
// Paging forwards / down.
for e := t.items.data.Front(); e != nil; e = e.Next() {
- entry := e.Value.(*indexedItemsEntry) //nolint:forcetypeassert
+ entry := e.Value.(*indexedItemsEntry)
if entry.itemID > behindID {
l.Trace("item is too new, continuing")
@@ -91,7 +91,7 @@ func (t *timeline) prepareXBetweenIDs(ctx context.Context, amount int, behindID
} else {
// Paging backwards / up.
for e := t.items.data.Back(); e != nil; e = e.Prev() {
- entry := e.Value.(*indexedItemsEntry) //nolint:forcetypeassert
+ entry := e.Value.(*indexedItemsEntry)
if entry.itemID < beforeID {
l.Trace("item is too old, continuing")
diff --git a/internal/timeline/prune.go b/internal/timeline/prune.go
index a3a5bf9cb..5c7476956 100644
--- a/internal/timeline/prune.go
+++ b/internal/timeline/prune.go
@@ -63,7 +63,7 @@ func (t *timeline) Prune(desiredPreparedItemsLength int, desiredIndexedItemsLeng
continue
}
- entry := e.Value.(*indexedItemsEntry) //nolint:forcetypeassert
+ entry := e.Value.(*indexedItemsEntry)
if entry.prepared == nil {
// It's already unprepared (mood).
continue
diff --git a/internal/timeline/remove.go b/internal/timeline/remove.go
index 693c9f9b9..86352b9fa 100644
--- a/internal/timeline/remove.go
+++ b/internal/timeline/remove.go
@@ -42,7 +42,7 @@ func (t *timeline) Remove(ctx context.Context, statusID string) (int, error) {
var toRemove []*list.Element
for e := t.items.data.Front(); e != nil; e = e.Next() {
- entry := e.Value.(*indexedItemsEntry) // nolint:forcetypeassert
+ entry := e.Value.(*indexedItemsEntry)
if entry.itemID != statusID {
// Not relevant.
@@ -78,7 +78,7 @@ func (t *timeline) RemoveAllByOrBoosting(ctx context.Context, accountID string)
var toRemove []*list.Element
for e := t.items.data.Front(); e != nil; e = e.Next() {
- entry := e.Value.(*indexedItemsEntry) // nolint:forcetypeassert
+ entry := e.Value.(*indexedItemsEntry)
if entry.accountID != accountID && entry.boostOfAccountID != accountID {
// Not relevant.
diff --git a/internal/timeline/unprepare.go b/internal/timeline/unprepare.go
index 827b274d8..67a990287 100644
--- a/internal/timeline/unprepare.go
+++ b/internal/timeline/unprepare.go
@@ -31,7 +31,7 @@ func (t *timeline) Unprepare(ctx context.Context, itemID string) error {
}
for e := t.items.data.Front(); e != nil; e = e.Next() {
- entry := e.Value.(*indexedItemsEntry) // nolint:forcetypeassert
+ entry := e.Value.(*indexedItemsEntry)
if entry.itemID != itemID && entry.boostOfID != itemID {
// Not relevant.
diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go
index 81dbc6f40..92465c790 100644
--- a/internal/typeutils/astointernal.go
+++ b/internal/typeutils/astointernal.go
@@ -216,40 +216,10 @@ func (c *Converter) ASRepresentationToAccount(ctx context.Context, accountable a
return acct, nil
}
-func (c *Converter) extractAttachments(i ap.WithAttachment) []*gtsmodel.MediaAttachment {
- attachmentProp := i.GetActivityStreamsAttachment()
- if attachmentProp == nil {
- return nil
- }
-
- attachments := make([]*gtsmodel.MediaAttachment, 0, attachmentProp.Len())
-
- for iter := attachmentProp.Begin(); iter != attachmentProp.End(); iter = iter.Next() {
- t := iter.GetType()
- if t == nil {
- continue
- }
-
- attachmentable, ok := t.(ap.Attachmentable)
- if !ok {
- log.Error(nil, "ap attachment was not attachmentable")
- continue
- }
-
- attachment, err := ap.ExtractAttachment(attachmentable)
- if err != nil {
- log.Errorf(nil, "error extracting attachment: %s", err)
- continue
- }
-
- attachments = append(attachments, attachment)
- }
-
- return attachments
-}
-
// ASStatus converts a remote activitystreams 'status' representation into a gts model status.
func (c *Converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusable) (*gtsmodel.Status, error) {
+ var err error
+
status := new(gtsmodel.Status)
// status.URI
@@ -281,7 +251,19 @@ func (c *Converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusab
// status.Attachments
//
// Media attachments for later dereferencing.
- status.Attachments = c.extractAttachments(statusable)
+ status.Attachments, err = ap.ExtractAttachments(statusable)
+ if err != nil {
+ l.Warnf("error(s) extracting attachments: %v", err)
+ }
+
+ // status.Poll
+ //
+ // Attached poll information (the statusable will actually
+ // be a Pollable, as a Question is a subset of our Status).
+ if pollable, ok := ap.ToPollable(statusable); ok {
+ // TODO: handle decoding poll data
+ _ = pollable
+ }
// status.Hashtags
//
@@ -341,7 +323,7 @@ func (c *Converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusab
// error if we don't.
attributedTo, err := ap.ExtractAttributedToURI(statusable)
if err != nil {
- return nil, gtserror.Newf("%w", err)
+ return nil, gtserror.Newf("error extracting attributed to uri: %w", err)
}
accountURI := attributedTo.String()
diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go
index d82fe8e04..b920d9a0e 100644
--- a/internal/typeutils/internaltoas.go
+++ b/internal/typeutils/internaltoas.go
@@ -29,6 +29,7 @@ import (
"github.com/superseriousbusiness/activity/pub"
"github.com/superseriousbusiness/activity/streams"
"github.com/superseriousbusiness/activity/streams/vocab"
+ "github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
@@ -403,21 +404,15 @@ func (c *Converter) AccountToASMinimal(ctx context.Context, a *gtsmodel.Account)
return person, nil
}
-// StatusToAS converts a gts model status into an activity streams note, suitable for federation
-func (c *Converter) StatusToAS(ctx context.Context, s *gtsmodel.Status) (vocab.ActivityStreamsNote, error) {
- // ensure prerequisites here before we get stuck in
-
- // check if author account is already attached to status and attach it if not
- // if we can't retrieve this, bail here already because we can't attribute the status to anyone
- if s.Account == nil {
- a, err := c.state.DB.GetAccountByID(ctx, s.AccountID)
- if err != nil {
- return nil, gtserror.Newf("error retrieving author account from db: %w", err)
- }
- s.Account = a
+// StatusToAS converts a gts model status into an ActivityStreams Statusable implementation, suitable for federation
+func (c *Converter) StatusToAS(ctx context.Context, s *gtsmodel.Status) (ap.Statusable, error) {
+ // Ensure the status model is fully populated.
+ // The status and poll models are REQUIRED so nothing to do if this fails.
+ if err := c.state.DB.PopulateStatus(ctx, s); err != nil {
+ return nil, gtserror.Newf("error populating status: %w", err)
}
- // create the Note!
+ // We convert it as an AS Note.
status := streams.NewActivityStreamsNote()
// id
@@ -529,7 +524,6 @@ func (c *Converter) StatusToAS(ctx context.Context, s *gtsmodel.Status) (vocab.A
}
tagProp.AppendTootHashtag(asHashtag)
}
-
status.SetActivityStreamsTag(tagProp)
// parse out some URIs we need here
@@ -1419,7 +1413,7 @@ func (c *Converter) StatusesToASOutboxPage(ctx context.Context, outboxID string,
return nil, err
}
- create, err := c.WrapNoteInCreate(note, true)
+ create, err := c.WrapStatusableInCreate(note, true)
if err != nil {
return nil, err
}
diff --git a/internal/typeutils/wrap.go b/internal/typeutils/wrap.go
index 67d3c2b5c..128c4ef15 100644
--- a/internal/typeutils/wrap.go
+++ b/internal/typeutils/wrap.go
@@ -44,7 +44,6 @@ func (c *Converter) WrapPersonInUpdate(person vocab.ActivityStreamsPerson, origi
update.SetActivityStreamsActor(actorProp)
// set the ID
-
newID, err := id.NewRandomULID()
if err != nil {
return nil, err
@@ -85,26 +84,29 @@ func (c *Converter) WrapPersonInUpdate(person vocab.ActivityStreamsPerson, origi
return update, nil
}
-// WrapNoteInCreate wraps a Note with a Create activity.
+// WrapNoteInCreate wraps a Statusable with a Create activity.
//
// If objectIRIOnly is set to true, then the function won't put the *entire* note in the Object field of the Create,
// but just the AP URI of the note. This is useful in cases where you want to give a remote server something to dereference,
// and still have control over whether or not they're allowed to actually see the contents.
-func (c *Converter) WrapNoteInCreate(note vocab.ActivityStreamsNote, objectIRIOnly bool) (vocab.ActivityStreamsCreate, error) {
+func (c *Converter) WrapStatusableInCreate(status ap.Statusable, objectIRIOnly bool) (vocab.ActivityStreamsCreate, error) {
create := streams.NewActivityStreamsCreate()
// Object property
objectProp := streams.NewActivityStreamsObjectProperty()
if objectIRIOnly {
- objectProp.AppendIRI(note.GetJSONLDId().GetIRI())
+ // Only append the object IRI to objectProp.
+ objectProp.AppendIRI(status.GetJSONLDId().GetIRI())
} else {
- objectProp.AppendActivityStreamsNote(note)
+ // Our statusable's are always note types.
+ asNote := status.(vocab.ActivityStreamsNote)
+ objectProp.AppendActivityStreamsNote(asNote)
}
create.SetActivityStreamsObject(objectProp)
// ID property
idProp := streams.NewJSONLDIdProperty()
- createID := note.GetJSONLDId().GetIRI().String() + "/activity"
+ createID := status.GetJSONLDId().GetIRI().String() + "/activity"
createIDIRI, err := url.Parse(createID)
if err != nil {
return nil, err
@@ -114,7 +116,7 @@ func (c *Converter) WrapNoteInCreate(note vocab.ActivityStreamsNote, objectIRIOn
// Actor Property
actorProp := streams.NewActivityStreamsActorProperty()
- actorIRI, err := ap.ExtractAttributedToURI(note)
+ actorIRI, err := ap.ExtractAttributedToURI(status)
if err != nil {
return nil, gtserror.Newf("couldn't extract AttributedTo: %w", err)
}
@@ -123,7 +125,7 @@ func (c *Converter) WrapNoteInCreate(note vocab.ActivityStreamsNote, objectIRIOn
// Published Property
publishedProp := streams.NewActivityStreamsPublishedProperty()
- published, err := ap.ExtractPublished(note)
+ published, err := ap.ExtractPublished(status)
if err != nil {
return nil, gtserror.Newf("couldn't extract Published: %w", err)
}
@@ -132,7 +134,7 @@ func (c *Converter) WrapNoteInCreate(note vocab.ActivityStreamsNote, objectIRIOn
// To Property
toProp := streams.NewActivityStreamsToProperty()
- if toURIs := ap.ExtractToURIs(note); len(toURIs) != 0 {
+ if toURIs := ap.ExtractToURIs(status); len(toURIs) != 0 {
for _, toURI := range toURIs {
toProp.AppendIRI(toURI)
}
@@ -141,7 +143,7 @@ func (c *Converter) WrapNoteInCreate(note vocab.ActivityStreamsNote, objectIRIOn
// Cc Property
ccProp := streams.NewActivityStreamsCcProperty()
- if ccURIs := ap.ExtractCcURIs(note); len(ccURIs) != 0 {
+ if ccURIs := ap.ExtractCcURIs(status); len(ccURIs) != 0 {
for _, ccURI := range ccURIs {
ccProp.AppendIRI(ccURI)
}
@@ -150,3 +152,64 @@ func (c *Converter) WrapNoteInCreate(note vocab.ActivityStreamsNote, objectIRIOn
return create, nil
}
+
+// WrapStatusableInUpdate wraps a Statusable with an Update activity.
+//
+// If objectIRIOnly is set to true, then the function won't put the *entire* note in the Object field of the Create,
+// but just the AP URI of the note. This is useful in cases where you want to give a remote server something to dereference,
+// and still have control over whether or not they're allowed to actually see the contents.
+func (c *Converter) WrapStatusableInUpdate(status ap.Statusable, objectIRIOnly bool) (vocab.ActivityStreamsUpdate, error) {
+ update := streams.NewActivityStreamsUpdate()
+
+ // Object property
+ objectProp := streams.NewActivityStreamsObjectProperty()
+ if objectIRIOnly {
+ objectProp.AppendIRI(status.GetJSONLDId().GetIRI())
+ } else if _, ok := status.(ap.Pollable); ok {
+ asQuestion := status.(vocab.ActivityStreamsQuestion)
+ objectProp.AppendActivityStreamsQuestion(asQuestion)
+ } else {
+ asNote := status.(vocab.ActivityStreamsNote)
+ objectProp.AppendActivityStreamsNote(asNote)
+ }
+ update.SetActivityStreamsObject(objectProp)
+
+ // ID property
+ idProp := streams.NewJSONLDIdProperty()
+ createID := status.GetJSONLDId().GetIRI().String() + "/activity"
+ createIDIRI, err := url.Parse(createID)
+ if err != nil {
+ return nil, err
+ }
+ idProp.SetIRI(createIDIRI)
+ update.SetJSONLDId(idProp)
+
+ // Actor Property
+ actorProp := streams.NewActivityStreamsActorProperty()
+ actorIRI, err := ap.ExtractAttributedToURI(status)
+ if err != nil {
+ return nil, gtserror.Newf("couldn't extract AttributedTo: %w", err)
+ }
+ actorProp.AppendIRI(actorIRI)
+ update.SetActivityStreamsActor(actorProp)
+
+ // To Property
+ toProp := streams.NewActivityStreamsToProperty()
+ if toURIs := ap.ExtractToURIs(status); len(toURIs) != 0 {
+ for _, toURI := range toURIs {
+ toProp.AppendIRI(toURI)
+ }
+ update.SetActivityStreamsTo(toProp)
+ }
+
+ // Cc Property
+ ccProp := streams.NewActivityStreamsCcProperty()
+ if ccURIs := ap.ExtractCcURIs(status); len(ccURIs) != 0 {
+ for _, ccURI := range ccURIs {
+ ccProp.AppendIRI(ccURI)
+ }
+ update.SetActivityStreamsCc(ccProp)
+ }
+
+ return update, nil
+}
diff --git a/internal/typeutils/wrap_test.go b/internal/typeutils/wrap_test.go
index 46d28f5c4..51f67f455 100644
--- a/internal/typeutils/wrap_test.go
+++ b/internal/typeutils/wrap_test.go
@@ -36,7 +36,7 @@ func (suite *WrapTestSuite) TestWrapNoteInCreateIRIOnly() {
note, err := suite.typeconverter.StatusToAS(context.Background(), testStatus)
suite.NoError(err)
- create, err := suite.typeconverter.WrapNoteInCreate(note, true)
+ create, err := suite.typeconverter.WrapStatusableInCreate(note, true)
suite.NoError(err)
suite.NotNil(create)
@@ -64,7 +64,7 @@ func (suite *WrapTestSuite) TestWrapNoteInCreate() {
note, err := suite.typeconverter.StatusToAS(context.Background(), testStatus)
suite.NoError(err)
- create, err := suite.typeconverter.WrapNoteInCreate(note, false)
+ create, err := suite.typeconverter.WrapStatusableInCreate(note, false)
suite.NoError(err)
suite.NotNil(create)