[feature] tentatively start adding polls support (#2249)
This commit is contained in:
parent
297b6eeaaa
commit
c6e00afc7c
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -39,39 +39,33 @@ 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
|
||||
}
|
||||
|
||||
// Get the raw data map at index, else skip
|
||||
// this entry due to impossible normalization.
|
||||
rawData, ok := rawData[i].(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if statusable, ok := ToStatusable(dataType); ok {
|
||||
if pollable, ok := ToPollable(dataType); ok {
|
||||
// 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)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Normalize everything we can on the statusable.
|
||||
|
@ -79,20 +73,14 @@ func NormalizeIncomingActivity(activity pub.Activity, rawJSON map[string]interfa
|
|||
NormalizeIncomingAttachments(statusable, rawData)
|
||||
NormalizeIncomingSummary(statusable, rawData)
|
||||
NormalizeIncomingName(statusable, rawData)
|
||||
|
||||
// "Accountable" types.
|
||||
case ActorApplication,
|
||||
ActorGroup,
|
||||
ActorOrganization,
|
||||
ActorPerson,
|
||||
ActorService:
|
||||
accountable, ok := dataType.(Accountable)
|
||||
if !ok {
|
||||
return
|
||||
continue
|
||||
}
|
||||
|
||||
if accountable, ok := ToAccountable(dataType); ok {
|
||||
// Normalize everything we can on the accountable.
|
||||
NormalizeIncomingSummary(accountable, rawData)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 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) {}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -46,27 +46,68 @@ 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) {
|
||||
|
||||
// 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: error converting asfollow to gtsfollow: %s", err)
|
||||
}
|
||||
|
||||
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())
|
||||
// make sure the addressee of the original follow is the same as whatever inbox this landed in
|
||||
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, gtsFollow.AccountID, gtsFollow.TargetAccountID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ACCEPT: couldn't get follow request with id %s from the database: %s", acceptedObjectIRI.String(), err)
|
||||
return err
|
||||
}
|
||||
|
||||
f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
|
||||
APObjectType: ap.ActivityFollow,
|
||||
APActivityType: ap.ActivityAccept,
|
||||
GTSModel: follow,
|
||||
ReceivingAccount: receivingAccount,
|
||||
})
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// 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
|
||||
followReq, err := f.state.DB.GetFollowRequestByURI(ctx, iriStr)
|
||||
if err != nil {
|
||||
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 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, followReq.AccountID, followReq.TargetAccountID)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -79,43 +120,9 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA
|
|||
ReceivingAccount: receivingAccount,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// check if iter is an AP object / type
|
||||
if iter.GetType() == nil {
|
||||
continue
|
||||
}
|
||||
if iter.GetType().GetTypeName() == ap.ActivityFollow {
|
||||
// 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)
|
||||
if err != nil {
|
||||
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 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, gtsFollow.AccountID, gtsFollow.TargetAccountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
|
||||
APObjectType: ap.ActivityFollow,
|
||||
APActivityType: ap.ActivityAccept,
|
||||
GTSModel: follow,
|
||||
ReceivingAccount: receivingAccount,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
// If content type wasn't specified, use the author's preferred content-type.
|
||||
contentType := apimodel.StatusContentType(status.Account.StatusContentType)
|
||||
form.ContentType = contentType
|
||||
}
|
||||
|
||||
switch acct.StatusContentType {
|
||||
case "text/plain":
|
||||
form.ContentType = apimodel.StatusContentTypePlain
|
||||
case "text/markdown":
|
||||
form.ContentType = apimodel.StatusContentTypeMarkdown
|
||||
default:
|
||||
form.ContentType = apimodel.StatusContentTypeDefault
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 <p> tags.
|
||||
// Initialize block parser that
|
||||
// doesn't wrap result in <p> 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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in New Issue