mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-10-31 22:40:01 +00:00
[feature] tentatively start adding polls support (#2249)
This commit is contained in:
parent
297b6eeaaa
commit
c6e00afc7c
36 changed files with 657 additions and 393 deletions
|
@ -14,7 +14,6 @@ run:
|
||||||
linters:
|
linters:
|
||||||
# enable some extra linters, see here for the list: https://golangci-lint.run/usage/linters/
|
# enable some extra linters, see here for the list: https://golangci-lint.run/usage/linters/
|
||||||
enable:
|
enable:
|
||||||
- forcetypeassert
|
|
||||||
- goconst
|
- goconst
|
||||||
- gocritic
|
- gocritic
|
||||||
- gofmt
|
- gofmt
|
||||||
|
|
|
@ -32,10 +32,10 @@
|
||||||
func ToCollectionPageIterator(t vocab.Type) (CollectionPageIterator, error) {
|
func ToCollectionPageIterator(t vocab.Type) (CollectionPageIterator, error) {
|
||||||
switch name := t.GetTypeName(); name {
|
switch name := t.GetTypeName(); name {
|
||||||
case ObjectCollectionPage:
|
case ObjectCollectionPage:
|
||||||
t := t.(vocab.ActivityStreamsCollectionPage) //nolint:forcetypeassert
|
t := t.(vocab.ActivityStreamsCollectionPage)
|
||||||
return WrapCollectionPage(t), nil
|
return WrapCollectionPage(t), nil
|
||||||
case ObjectOrderedCollectionPage:
|
case ObjectOrderedCollectionPage:
|
||||||
t := t.(vocab.ActivityStreamsOrderedCollectionPage) //nolint:forcetypeassert
|
t := t.(vocab.ActivityStreamsOrderedCollectionPage)
|
||||||
return WrapOrderedCollectionPage(t), nil
|
return WrapOrderedCollectionPage(t), nil
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("%T(%s) was not CollectionPage-like", t, name)
|
return nil, fmt.Errorf("%T(%s) was not CollectionPage-like", t, name)
|
||||||
|
@ -74,7 +74,7 @@ func (iter *regularCollectionPageIterator) PrevPage() WithIRI {
|
||||||
return iter.GetActivityStreamsPrev()
|
return iter.GetActivityStreamsPrev()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (iter *regularCollectionPageIterator) NextItem() IteratorItemable {
|
func (iter *regularCollectionPageIterator) NextItem() TypeOrIRI {
|
||||||
if !iter.initItems() {
|
if !iter.initItems() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -83,7 +83,7 @@ func (iter *regularCollectionPageIterator) NextItem() IteratorItemable {
|
||||||
return cur
|
return cur
|
||||||
}
|
}
|
||||||
|
|
||||||
func (iter *regularCollectionPageIterator) PrevItem() IteratorItemable {
|
func (iter *regularCollectionPageIterator) PrevItem() TypeOrIRI {
|
||||||
if !iter.initItems() {
|
if !iter.initItems() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -130,7 +130,7 @@ func (iter *orderedCollectionPageIterator) PrevPage() WithIRI {
|
||||||
return iter.GetActivityStreamsPrev()
|
return iter.GetActivityStreamsPrev()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (iter *orderedCollectionPageIterator) NextItem() IteratorItemable {
|
func (iter *orderedCollectionPageIterator) NextItem() TypeOrIRI {
|
||||||
if !iter.initItems() {
|
if !iter.initItems() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -139,7 +139,7 @@ func (iter *orderedCollectionPageIterator) NextItem() IteratorItemable {
|
||||||
return cur
|
return cur
|
||||||
}
|
}
|
||||||
|
|
||||||
func (iter *orderedCollectionPageIterator) PrevItem() IteratorItemable {
|
func (iter *orderedCollectionPageIterator) PrevItem() TypeOrIRI {
|
||||||
if !iter.initItems() {
|
if !iter.initItems() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,39 +35,56 @@
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ExtractObject will extract an object vocab.Type from given implementing interface.
|
// ExtractObjects will extract object vocab.Types from given implementing interface.
|
||||||
func ExtractObject(with WithObject) vocab.Type {
|
func ExtractObjects(with WithObject) []TypeOrIRI {
|
||||||
// Extract the attached object (if any).
|
// Extract the attached object (if any).
|
||||||
obj := with.GetActivityStreamsObject()
|
objProp := with.GetActivityStreamsObject()
|
||||||
if obj == nil {
|
if objProp == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only support single
|
// Check for zero len.
|
||||||
// objects (for now...)
|
if objProp.Len() == 0 {
|
||||||
if obj.Len() != 1 {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract object vocab.Type.
|
// Accumulate all of the objects into a slice.
|
||||||
return obj.At(0).GetType()
|
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.
|
// 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(); {
|
switch typeName := activity.GetTypeName(); {
|
||||||
// Activity (has "object").
|
// Activity (has "object").
|
||||||
case isActivity(typeName):
|
case isActivity(typeName):
|
||||||
objType := ExtractObject(activity)
|
objTypes := ExtractObjects(activity)
|
||||||
if objType == nil {
|
if len(objTypes) == 0 {
|
||||||
return nil, nil, false
|
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").
|
// IntransitiveAcitivity (no "object").
|
||||||
case isIntransitiveActivity(typeName):
|
case isIntransitiveActivity(typeName):
|
||||||
return activity, rawJSON, false
|
asTypeOrIRI := _TypeOrIRI{activity} // wrap activity.
|
||||||
|
return []TypeOrIRI{&asTypeOrIRI}, []any{rawJSON}, true
|
||||||
|
|
||||||
// Unknown.
|
// Unknown.
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -247,14 +247,8 @@ type CollectionPageIterator interface {
|
||||||
NextPage() WithIRI
|
NextPage() WithIRI
|
||||||
PrevPage() WithIRI
|
PrevPage() WithIRI
|
||||||
|
|
||||||
NextItem() IteratorItemable
|
NextItem() TypeOrIRI
|
||||||
PrevItem() IteratorItemable
|
PrevItem() TypeOrIRI
|
||||||
}
|
|
||||||
|
|
||||||
// IteratorItemable represents the minimum interface for an item in an iterator.
|
|
||||||
type IteratorItemable interface {
|
|
||||||
WithIRI
|
|
||||||
WithType
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flaggable represents the minimum interface for an activitystreams 'Flag' activity.
|
// Flaggable represents the minimum interface for an activitystreams 'Flag' activity.
|
||||||
|
@ -267,6 +261,12 @@ type Flaggable interface {
|
||||||
WithObject
|
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.
|
// WithJSONLDId represents an activity with JSONLDIdProperty.
|
||||||
type WithJSONLDId interface {
|
type WithJSONLDId interface {
|
||||||
GetJSONLDId() vocab.JSONLDIdProperty
|
GetJSONLDId() vocab.JSONLDIdProperty
|
||||||
|
|
|
@ -39,60 +39,48 @@
|
||||||
// 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.
|
// 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{}) {
|
func NormalizeIncomingActivity(activity pub.Activity, rawJSON map[string]interface{}) {
|
||||||
// From the activity extract the data vocab.Type + its "raw" JSON.
|
// From the activity extract the data vocab.Type + its "raw" JSON.
|
||||||
dataType, rawData, ok := ExtractActivityData(activity, rawJSON)
|
dataIfaces, rawData, ok := ExtractActivityData(activity, rawJSON)
|
||||||
if !ok {
|
if !ok || len(dataIfaces) != len(rawData) {
|
||||||
|
// non-equal lengths *shouldn't* happen,
|
||||||
|
// but this is just an integrity check.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch dataType.GetTypeName() {
|
// Iterate over the available data.
|
||||||
// "Pollable" types.
|
for i, dataIface := range dataIfaces {
|
||||||
case ActivityQuestion:
|
// Try to get as vocab.Type, else
|
||||||
pollable, ok := dataType.(Pollable)
|
// skip this entry for normalization.
|
||||||
if !ok {
|
dataType := dataIface.GetType()
|
||||||
return
|
if dataType == nil {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize the Pollable specific properties.
|
// Get the raw data map at index, else skip
|
||||||
NormalizeIncomingPollOptions(pollable, rawData)
|
// this entry due to impossible normalization.
|
||||||
|
rawData, ok := rawData[i].(map[string]any)
|
||||||
// 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 {
|
if !ok {
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize everything we can on the statusable.
|
if statusable, ok := ToStatusable(dataType); ok {
|
||||||
NormalizeIncomingContent(statusable, rawData)
|
if pollable, ok := ToPollable(dataType); ok {
|
||||||
NormalizeIncomingAttachments(statusable, rawData)
|
// Normalize the Pollable specific properties.
|
||||||
NormalizeIncomingSummary(statusable, rawData)
|
NormalizeIncomingPollOptions(pollable, rawData)
|
||||||
NormalizeIncomingName(statusable, rawData)
|
}
|
||||||
|
|
||||||
// "Accountable" types.
|
// Normalize everything we can on the statusable.
|
||||||
case ActorApplication,
|
NormalizeIncomingContent(statusable, rawData)
|
||||||
ActorGroup,
|
NormalizeIncomingAttachments(statusable, rawData)
|
||||||
ActorOrganization,
|
NormalizeIncomingSummary(statusable, rawData)
|
||||||
ActorPerson,
|
NormalizeIncomingName(statusable, rawData)
|
||||||
ActorService:
|
continue
|
||||||
accountable, ok := dataType.(Accountable)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize everything we can on the accountable.
|
if accountable, ok := ToAccountable(dataType); ok {
|
||||||
NormalizeIncomingSummary(accountable, rawData)
|
// Normalize everything we can on the accountable.
|
||||||
|
NormalizeIncomingSummary(accountable, rawData)
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
43
internal/ap/util.go
Normal file
43
internal/ap/util.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
// GoToSocial
|
||||||
|
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package 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 {
|
if multiStatus.Metadata.Failure != 0 {
|
||||||
failures := make(map[string]any, multiStatus.Metadata.Failure)
|
failures := make(map[string]any, multiStatus.Metadata.Failure)
|
||||||
for _, entry := range multiStatus.Data {
|
for _, entry := range multiStatus.Data {
|
||||||
// nolint:forcetypeassert
|
|
||||||
failures[entry.Resource.(string)] = entry.Message
|
failures[entry.Resource.(string)] = entry.Message
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -290,7 +290,7 @@ func (suite *StatusCreateTestSuite) TestReplyToNonexistentStatus() {
|
||||||
defer result.Body.Close()
|
defer result.Body.Close()
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
suite.NoError(err)
|
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.
|
// 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)
|
return nil, nil, gtserror.Newf("error populating mentions for status %s: %w", uri, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the status' tags are populated.
|
// Ensure the status' tags are populated, (changes are expected / okay).
|
||||||
if err := d.fetchStatusTags(ctx, requestUser, latestStatus); err != nil {
|
if err := d.fetchStatusTags(ctx, latestStatus); err != nil {
|
||||||
return nil, nil, gtserror.Newf("error populating tags for status %s: %w", uri, err)
|
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)
|
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.
|
// Ensure the status' emoji attachments are populated, (changes are expected / okay).
|
||||||
if err := d.fetchStatusEmojis(ctx, requestUser, status, latestStatus); err != nil {
|
if err := d.fetchStatusEmojis(ctx, requestUser, latestStatus); err != nil {
|
||||||
return nil, nil, gtserror.Newf("error populating emojis for status %s: %w", uri, err)
|
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.
|
// 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)
|
mention.ID, err = id.NewULIDFromTime(status.CreatedAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(ctx, "invalid created at date: %v", err)
|
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
|
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.
|
// Allocate new slice to take the yet-to-be determined tag IDs.
|
||||||
status.TagIDs = make([]string, len(status.Tags))
|
status.TagIDs = make([]string, len(status.Tags))
|
||||||
|
|
||||||
|
@ -417,13 +419,14 @@ func (d *deref) fetchStatusTags(ctx context.Context, requestUser string, status
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// No tag with this name yet, create it.
|
|
||||||
if tag == nil {
|
if tag == nil {
|
||||||
|
// Create new ID for tag name.
|
||||||
tag = >smodel.Tag{
|
tag = >smodel.Tag{
|
||||||
ID: id.NewULID(),
|
ID: id.NewULID(),
|
||||||
Name: placeholder.Name,
|
Name: placeholder.Name,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Insert this tag with new name into the database.
|
||||||
if err := d.state.DB.PutTag(ctx, tag); err != nil {
|
if err := d.state.DB.PutTag(ctx, tag); err != nil {
|
||||||
log.Errorf(ctx, "db error putting tag %s: %v", tag.Name, err)
|
log.Errorf(ctx, "db error putting tag %s: %v", tag.Name, err)
|
||||||
continue
|
continue
|
||||||
|
@ -516,7 +519,7 @@ func (d *deref) fetchStatusAttachments(ctx context.Context, tsport transport.Tra
|
||||||
return nil
|
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.
|
// Fetch the full-fleshed-out emoji objects for our status.
|
||||||
emojis, err := d.populateEmojis(ctx, status.Emojis, requestUser)
|
emojis, err := d.populateEmojis(ctx, status.Emojis, requestUser)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -46,28 +46,29 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA
|
||||||
return nil // Already processed.
|
return nil // Already processed.
|
||||||
}
|
}
|
||||||
|
|
||||||
acceptObject := accept.GetActivityStreamsObject()
|
// Iterate all provided objects in the activity.
|
||||||
if acceptObject == nil {
|
for _, object := range ap.ExtractObjects(accept) {
|
||||||
return errors.New("ACCEPT: no object set on vocab.ActivityStreamsAccept")
|
|
||||||
}
|
|
||||||
|
|
||||||
for iter := acceptObject.Begin(); iter != acceptObject.End(); iter = iter.Next() {
|
// Check and handle any vocab.Type objects.
|
||||||
// check if the object is an IRI
|
if objType := object.GetType(); objType != nil {
|
||||||
if iter.IsIRI() {
|
switch objType.GetTypeName() { //nolint:gocritic
|
||||||
// we have just the URI of whatever is being accepted, so we need to find out what it is
|
|
||||||
acceptedObjectIRI := iter.GetIRI()
|
case ap.ActivityFollow:
|
||||||
if uris.IsFollowPath(acceptedObjectIRI) {
|
// Cast the vocab.Type object to known AS type.
|
||||||
// ACCEPT FOLLOW
|
asFollow := objType.(vocab.ActivityStreamsFollow)
|
||||||
followReq, err := f.state.DB.GetFollowRequestByURI(ctx, acceptedObjectIRI.String())
|
|
||||||
|
// convert the follow to something we can understand
|
||||||
|
gtsFollow, err := f.converter.ASFollowToFollow(ctx, asFollow)
|
||||||
if err != nil {
|
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
|
// 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")
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -78,31 +79,36 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA
|
||||||
GTSModel: follow,
|
GTSModel: follow,
|
||||||
ReceivingAccount: receivingAccount,
|
ReceivingAccount: receivingAccount,
|
||||||
})
|
})
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// check if iter is an AP object / type
|
|
||||||
if iter.GetType() == nil {
|
|
||||||
continue
|
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
|
// ACCEPT FOLLOW
|
||||||
asFollow, ok := iter.GetType().(vocab.ActivityStreamsFollow)
|
followReq, err := f.state.DB.GetFollowRequestByURI(ctx, iriStr)
|
||||||
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 {
|
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
|
// 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")
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -114,8 +120,9 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA
|
||||||
ReceivingAccount: receivingAccount,
|
ReceivingAccount: receivingAccount,
|
||||||
})
|
})
|
||||||
|
|
||||||
return nil
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -81,6 +81,7 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
|
||||||
// FLAG / REPORT SOMETHING
|
// FLAG / REPORT SOMETHING
|
||||||
return f.activityFlag(ctx, asType, receivingAccount, requestingAccount)
|
return f.activityFlag(ctx, asType, receivingAccount, requestingAccount)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,6 +112,7 @@ func (f *federatingDB) activityBlock(ctx context.Context, asType vocab.Type, rec
|
||||||
GTSModel: block,
|
GTSModel: block,
|
||||||
ReceivingAccount: receiving,
|
ReceivingAccount: receiving,
|
||||||
})
|
})
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,37 +134,19 @@ func (f *federatingDB) activityCreate(
|
||||||
return gtserror.Newf("could not convert asType %T to ActivityStreamsCreate", asType)
|
return gtserror.Newf("could not convert asType %T to ActivityStreamsCreate", asType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create must have an Object.
|
for _, object := range ap.ExtractObjects(create) {
|
||||||
objectProp := create.GetActivityStreamsObject()
|
// Try to get object as vocab.Type,
|
||||||
if objectProp == nil {
|
// else skip handling (likely) IRI.
|
||||||
return gtserror.New("create had no Object")
|
objType := object.GetType()
|
||||||
}
|
if objType == nil {
|
||||||
|
|
||||||
// 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)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure given object type is a statusable.
|
if statusable, ok := ap.ToStatusable(objType); ok {
|
||||||
statusable, ok := object.(ap.Statusable)
|
return f.createStatusable(ctx, statusable, receivingAccount, requestingAccount)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle creation of statusable.
|
// TODO: handle CREATE of other types?
|
||||||
return f.createStatusable(ctx,
|
|
||||||
statusable,
|
|
||||||
receivingAccount,
|
|
||||||
requestingAccount,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -34,6 +34,7 @@ type DB interface {
|
||||||
Accept(ctx context.Context, accept vocab.ActivityStreamsAccept) error
|
Accept(ctx context.Context, accept vocab.ActivityStreamsAccept) error
|
||||||
Reject(ctx context.Context, reject vocab.ActivityStreamsReject) error
|
Reject(ctx context.Context, reject vocab.ActivityStreamsReject) error
|
||||||
Announce(ctx context.Context, announce vocab.ActivityStreamsAnnounce) 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.
|
// FederatingDB uses the underlying DB interface to implement the go-fed pub.Database interface.
|
||||||
|
|
32
internal/federation/federatingdb/question.go
Normal file
32
internal/federation/federatingdb/question.go
Normal file
|
@ -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 @@
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
)
|
)
|
||||||
|
@ -48,31 +49,31 @@ func (f *federatingDB) Undo(ctx context.Context, undo vocab.ActivityStreamsUndo)
|
||||||
return nil // Already processed.
|
return nil // Already processed.
|
||||||
}
|
}
|
||||||
|
|
||||||
undoObject := undo.GetActivityStreamsObject()
|
var errs gtserror.MultiError
|
||||||
if undoObject == nil {
|
|
||||||
return errors.New("UNDO: no object set on vocab.ActivityStreamsUndo")
|
|
||||||
}
|
|
||||||
|
|
||||||
for iter := undoObject.Begin(); iter != undoObject.End(); iter = iter.Next() {
|
for _, object := range ap.ExtractObjects(undo) {
|
||||||
t := iter.GetType()
|
// Try to get object as vocab.Type,
|
||||||
if t == nil {
|
// else skip handling (likely) IRI.
|
||||||
|
objType := object.GetType()
|
||||||
|
if objType == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
switch t.GetTypeName() {
|
switch objType.GetTypeName() {
|
||||||
case ap.ActivityFollow:
|
case ap.ActivityFollow:
|
||||||
if err := f.undoFollow(ctx, receivingAccount, undo, t); err != nil {
|
if err := f.undoFollow(ctx, receivingAccount, undo, objType); err != nil {
|
||||||
return err
|
errs.Appendf("error undoing follow: %w", err)
|
||||||
}
|
}
|
||||||
case ap.ActivityLike:
|
case ap.ActivityLike:
|
||||||
if err := f.undoLike(ctx, receivingAccount, undo, t); err != nil {
|
if err := f.undoLike(ctx, receivingAccount, undo, objType); err != nil {
|
||||||
return err
|
errs.Appendf("error undoing like: %w", err)
|
||||||
}
|
}
|
||||||
case ap.ActivityAnnounce:
|
case ap.ActivityAnnounce:
|
||||||
// todo: undo boost / reblog / announce
|
// TODO: actually handle this !
|
||||||
|
log.Warn(ctx, "skipped undo announce")
|
||||||
case ap.ActivityBlock:
|
case ap.ActivityBlock:
|
||||||
if err := f.undoBlock(ctx, receivingAccount, undo, t); err != nil {
|
if err := f.undoBlock(ctx, receivingAccount, undo, objType); err != nil {
|
||||||
return err
|
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.
|
return nil // Already processed.
|
||||||
}
|
}
|
||||||
|
|
||||||
switch asType.GetTypeName() {
|
if accountable, ok := ap.ToAccountable(asType); ok {
|
||||||
case ap.ActorApplication, ap.ActorGroup, ap.ActorOrganization, ap.ActorPerson, ap.ActorService:
|
return f.updateAccountable(ctx, receivingAccount, requestingAccount, accountable)
|
||||||
return f.updateAccountable(ctx, receivingAccount, requestingAccount, asType)
|
}
|
||||||
|
|
||||||
|
if statusable, ok := ap.ToStatusable(asType); ok {
|
||||||
|
return f.updateStatusable(ctx, receivingAccount, requestingAccount, statusable)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *federatingDB) updateAccountable(ctx context.Context, receivingAcct *gtsmodel.Account, requestingAcct *gtsmodel.Account, asType vocab.Type) error {
|
func (f *federatingDB) updateAccountable(ctx context.Context, receivingAcct *gtsmodel.Account, requestingAcct *gtsmodel.Account, accountable ap.Accountable) 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract AP URI of the updated Accountable model.
|
// Extract AP URI of the updated Accountable model.
|
||||||
idProp := accountable.GetJSONLDId()
|
idProp := accountable.GetJSONLDId()
|
||||||
if idProp == nil || !idProp.IsIRI() {
|
if idProp == nil || !idProp.IsIRI() {
|
||||||
|
@ -103,3 +100,43 @@ func (f *federatingDB) updateAccountable(ctx context.Context, receivingAcct *gts
|
||||||
|
|
||||||
return nil
|
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(ctx context.Context, reject vocab.ActivityStreamsReject) error {
|
||||||
func(ctx context.Context, announce vocab.ActivityStreamsAnnounce) error {
|
func(ctx context.Context, announce vocab.ActivityStreamsAnnounce) error {
|
||||||
return f.FederatingDB().Announce(ctx, announce)
|
return f.FederatingDB().Announce(ctx, announce)
|
||||||
},
|
},
|
||||||
|
func(ctx context.Context, question vocab.ActivityStreamsQuestion) error {
|
||||||
|
return f.FederatingDB().Question(ctx, question)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
|
@ -34,70 +34,75 @@
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/text"
|
"github.com/superseriousbusiness/gotosocial/internal/text"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
"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.
|
// 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.
|
// 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) {
|
func (p *Processor) Create(ctx context.Context, requestingAccount *gtsmodel.Account, application *gtsmodel.Application, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, gtserror.WithCode) {
|
||||||
accountURIs := uris.GenerateURIsForAccount(account.Username)
|
// Generate new ID for status.
|
||||||
thisStatusID := id.NewULID()
|
statusID := id.NewULID()
|
||||||
local := true
|
|
||||||
sensitive := form.Sensitive
|
|
||||||
|
|
||||||
newStatus := >smodel.Status{
|
// Generate necessary URIs for username, to build status URIs.
|
||||||
ID: thisStatusID,
|
accountURIs := uris.GenerateURIsForAccount(requestingAccount.Username)
|
||||||
URI: accountURIs.StatusesURI + "/" + thisStatusID,
|
|
||||||
URL: accountURIs.StatusesURL + "/" + thisStatusID,
|
// Get current time.
|
||||||
CreatedAt: time.Now(),
|
now := time.Now()
|
||||||
UpdatedAt: time.Now(),
|
|
||||||
Local: &local,
|
status := >smodel.Status{
|
||||||
AccountID: account.ID,
|
ID: statusID,
|
||||||
AccountURI: account.URI,
|
URI: accountURIs.StatusesURI + "/" + statusID,
|
||||||
ContentWarning: text.SanitizeToPlaintext(form.SpoilerText),
|
URL: accountURIs.StatusesURL + "/" + statusID,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
Local: util.Ptr(true),
|
||||||
|
Account: requestingAccount,
|
||||||
|
AccountID: requestingAccount.ID,
|
||||||
|
AccountURI: requestingAccount.URI,
|
||||||
ActivityStreamsType: ap.ObjectNote,
|
ActivityStreamsType: ap.ObjectNote,
|
||||||
Sensitive: &sensitive,
|
Sensitive: &form.Sensitive,
|
||||||
CreatedWithApplicationID: application.ID,
|
CreatedWithApplicationID: application.ID,
|
||||||
Text: form.Status,
|
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
|
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
|
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)
|
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)
|
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)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// put the new status in the database
|
// Insert this new status in the database.
|
||||||
if err := p.state.DB.PutStatus(ctx, newStatus); err != nil {
|
if err := p.state.DB.PutStatus(ctx, status); err != nil {
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
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{
|
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
|
||||||
APObjectType: ap.ObjectNote,
|
APObjectType: ap.ObjectNote,
|
||||||
APActivityType: ap.ActivityCreate,
|
APActivityType: ap.ActivityCreate,
|
||||||
GTSModel: newStatus,
|
GTSModel: status,
|
||||||
OriginAccount: account,
|
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 == "" {
|
if form.InReplyToID == "" {
|
||||||
return nil
|
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?
|
// 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.
|
// 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 {
|
inReplyTo, err := p.state.DB.GetStatusByID(ctx, form.InReplyToID)
|
||||||
if err == db.ErrNoEntries {
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
err := fmt.Errorf("status with id %s not replyable because it doesn't exist", form.InReplyToID)
|
err := gtserror.Newf("error fetching status %s from db: %w", form.InReplyToID, err)
|
||||||
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)
|
|
||||||
return gtserror.NewErrorInternalError(err)
|
return gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if blocked, err := dbService.IsEitherBlocked(ctx, thisAccountID, repliedAccount.ID); err != nil {
|
if inReplyTo == nil {
|
||||||
err := fmt.Errorf("db error checking block: %s", err)
|
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)
|
return gtserror.NewErrorInternalError(err)
|
||||||
} else if blocked {
|
} else if blocked {
|
||||||
err := fmt.Errorf("status with id %s not replyable", form.InReplyToID)
|
text := fmt.Sprintf("status %s is not replyable", form.InReplyToID)
|
||||||
return gtserror.NewErrorNotFound(err)
|
return gtserror.NewErrorNotFound(errors.New(text), text)
|
||||||
}
|
}
|
||||||
|
|
||||||
status.InReplyToID = repliedStatus.ID
|
// Set status fields from inReplyTo.
|
||||||
status.InReplyToURI = repliedStatus.URI
|
status.InReplyToID = inReplyTo.ID
|
||||||
status.InReplyToAccountID = repliedAccount.ID
|
status.InReplyToURI = inReplyTo.URI
|
||||||
|
status.InReplyToAccountID = inReplyTo.AccountID
|
||||||
|
|
||||||
return nil
|
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 {
|
if form.MediaIDs == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get minimum allowed char descriptions.
|
||||||
|
minChars := config.GetMediaDescriptionMinChars()
|
||||||
|
|
||||||
attachments := []*gtsmodel.MediaAttachment{}
|
attachments := []*gtsmodel.MediaAttachment{}
|
||||||
attachmentIDs := []string{}
|
attachmentIDs := []string{}
|
||||||
for _, mediaID := range form.MediaIDs {
|
for _, mediaID := range form.MediaIDs {
|
||||||
attachment, err := dbService.GetAttachmentByID(ctx, mediaID)
|
attachment, err := p.state.DB.GetAttachmentByID(ctx, mediaID)
|
||||||
if err != nil {
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
if errors.Is(err, db.ErrNoEntries) {
|
err := gtserror.Newf("error fetching media from db: %w", err)
|
||||||
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)
|
|
||||||
return gtserror.NewErrorInternalError(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 {
|
if attachment.AccountID != thisAccountID {
|
||||||
err = fmt.Errorf("ProcessMediaIDs: media with id %s does not belong to account %s", mediaID, thisAccountID)
|
text := fmt.Sprintf("media %s does not belong to account", mediaID)
|
||||||
return gtserror.NewErrorBadRequest(err, err.Error())
|
return gtserror.NewErrorBadRequest(errors.New(text), text)
|
||||||
}
|
}
|
||||||
|
|
||||||
if attachment.StatusID != "" || attachment.ScheduledStatusID != "" {
|
if attachment.StatusID != "" || attachment.ScheduledStatusID != "" {
|
||||||
err = fmt.Errorf("ProcessMediaIDs: media with id %s is already attached to a status", mediaID)
|
text := fmt.Sprintf("media %s already attached to status", mediaID)
|
||||||
return gtserror.NewErrorBadRequest(err, err.Error())
|
return gtserror.NewErrorBadRequest(errors.New(text), text)
|
||||||
}
|
}
|
||||||
|
|
||||||
minDescriptionChars := config.GetMediaDescriptionMinChars()
|
if length := len([]rune(attachment.Description)); length < minChars {
|
||||||
if descriptionLength := len([]rune(attachment.Description)); descriptionLength < minDescriptionChars {
|
text := fmt.Sprintf("media %s description too short, at least %d required", mediaID, minChars)
|
||||||
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(errors.New(text), text)
|
||||||
return gtserror.NewErrorBadRequest(err, err.Error())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
attachments = append(attachments, attachment)
|
attachments = append(attachments, attachment)
|
||||||
|
@ -192,7 +193,7 @@ func processMediaIDs(ctx context.Context, dbService db.DB, form *apimodel.Advanc
|
||||||
return nil
|
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
|
// by default all flags are set to true
|
||||||
federated := true
|
federated := true
|
||||||
boostable := true
|
boostable := true
|
||||||
|
@ -265,7 +266,7 @@ func processVisibility(ctx context.Context, form *apimodel.AdvancedStatusCreateF
|
||||||
return nil
|
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 != "" {
|
if form.Language != "" {
|
||||||
status.Language = form.Language
|
status.Language = form.Language
|
||||||
} else {
|
} else {
|
||||||
|
@ -277,68 +278,80 @@ func processLanguage(ctx context.Context, form *apimodel.AdvancedStatusCreateFor
|
||||||
return nil
|
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 {
|
func (p *Processor) processContent(ctx context.Context, parseMention gtsmodel.ParseMentionFunc, form *apimodel.AdvancedStatusCreateForm, 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
|
|
||||||
if form.ContentType == "" {
|
if form.ContentType == "" {
|
||||||
acct, err := dbService.GetAccountByID(ctx, accountID)
|
// If content type wasn't specified, use the author's preferred content-type.
|
||||||
if err != nil {
|
contentType := apimodel.StatusContentType(status.Account.StatusContentType)
|
||||||
return fmt.Errorf("error processing new content: couldn't retrieve account from db to check post format: %s", err)
|
form.ContentType = contentType
|
||||||
}
|
}
|
||||||
|
|
||||||
switch acct.StatusContentType {
|
// format is the currently set text formatting
|
||||||
case "text/plain":
|
// function, according to the provided content-type.
|
||||||
form.ContentType = apimodel.StatusContentTypePlain
|
var format text.FormatFunc
|
||||||
case "text/markdown":
|
|
||||||
form.ContentType = apimodel.StatusContentTypeMarkdown
|
// formatInput is a shorthand function to format the given input string with the
|
||||||
default:
|
// currently set 'formatFunc', passing in all required args and returning result.
|
||||||
form.ContentType = apimodel.StatusContentTypeDefault
|
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 {
|
switch form.ContentType {
|
||||||
|
// None given / set,
|
||||||
|
// use default (plain).
|
||||||
|
case "":
|
||||||
|
fallthrough
|
||||||
|
|
||||||
|
// Format status according to text/plain.
|
||||||
case apimodel.StatusContentTypePlain:
|
case apimodel.StatusContentTypePlain:
|
||||||
f = formatter.FromPlain
|
format = p.formatter.FromPlain
|
||||||
|
|
||||||
|
// Format status according to text/markdown.
|
||||||
case apimodel.StatusContentTypeMarkdown:
|
case apimodel.StatusContentTypeMarkdown:
|
||||||
f = formatter.FromMarkdown
|
format = p.formatter.FromMarkdown
|
||||||
|
|
||||||
|
// Unknown.
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("format %s not recognised as a valid status format", form.ContentType)
|
return fmt.Errorf("invalid status format: %q", 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
status.Tags = formatted.Tags
|
// Sanitize status text and format.
|
||||||
status.TagIDs = make([]string, 0, len(formatted.Tags))
|
contentRes := formatInput(format, form.Status)
|
||||||
for _, gtstag := range formatted.Tags {
|
|
||||||
status.TagIDs = append(status.TagIDs, gtstag.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
status.Emojis = formatted.Emojis
|
// Collect formatted results.
|
||||||
status.EmojiIDs = make([]string, 0, len(formatted.Emojis))
|
status.Content = contentRes.HTML
|
||||||
for _, gtsemoji := range formatted.Emojis {
|
status.Mentions = append(status.Mentions, contentRes.Mentions...)
|
||||||
status.EmojiIDs = append(status.EmojiIDs, gtsemoji.ID)
|
status.Emojis = append(status.Emojis, contentRes.Emojis...)
|
||||||
}
|
status.Tags = append(status.Tags, contentRes.Tags...)
|
||||||
|
|
||||||
spoilerformatted := formatter.FromPlainEmojiOnly(ctx, parseMention, accountID, status.ID, form.SpoilerText)
|
// From here-on-out just use emoji-only
|
||||||
for _, gtsemoji := range spoilerformatted.Emojis {
|
// plain-text formatting as the FormatFunc.
|
||||||
status.Emojis = append(status.Emojis, gtsemoji)
|
format = p.formatter.FromPlainEmojiOnly
|
||||||
status.EmojiIDs = append(status.EmojiIDs, gtsemoji.ID)
|
|
||||||
}
|
// 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
|
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)
|
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)
|
suite.Nil(apiStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,7 @@ func (p *Processor) toAccount(payload string, event string, streamTypes []string
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil // No entry = nothing to stream.
|
return nil // No entry = nothing to stream.
|
||||||
}
|
}
|
||||||
streamsForAccount := v.(*stream.StreamsForAccount) //nolint:forcetypeassert
|
streamsForAccount := v.(*stream.StreamsForAccount)
|
||||||
|
|
||||||
streamsForAccount.Lock()
|
streamsForAccount.Lock()
|
||||||
defer streamsForAccount.Unlock()
|
defer streamsForAccount.Unlock()
|
||||||
|
|
|
@ -147,27 +147,27 @@ func (f *federate) CreateStatus(ctx context.Context, status *gtsmodel.Status) er
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate model.
|
// Ensure the status model is fully populated.
|
||||||
if err := f.state.DB.PopulateStatus(ctx, status); err != nil {
|
if err := f.state.DB.PopulateStatus(ctx, status); err != nil {
|
||||||
return gtserror.Newf("error populating status: %w", err)
|
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)
|
outboxIRI, err := parseURI(status.Account.OutboxURI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert status to an ActivityStreams
|
// Convert status to ActivityStreams Statusable implementing type.
|
||||||
// Note, wrapped in a Create activity.
|
statusable, err := f.converter.StatusToAS(ctx, status)
|
||||||
asStatus, err := f.converter.StatusToAS(ctx, status)
|
|
||||||
if err != nil {
|
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 {
|
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.
|
// 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate model.
|
// Ensure the status model is fully populated.
|
||||||
if err := f.state.DB.PopulateStatus(ctx, status); err != nil {
|
if err := f.state.DB.PopulateStatus(ctx, status); err != nil {
|
||||||
return gtserror.Newf("error populating status: %w", err)
|
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)
|
outboxIRI, err := parseURI(status.Account.OutboxURI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -226,6 +226,50 @@ func (f *federate) DeleteStatus(ctx context.Context, status *gtsmodel.Status) er
|
||||||
return nil
|
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 {
|
func (f *federate) Follow(ctx context.Context, follow *gtsmodel.Follow) error {
|
||||||
// Populate model.
|
// Populate model.
|
||||||
if err := f.state.DB.PopulateFollow(ctx, follow); err != nil {
|
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:
|
case ap.ActivityUpdate:
|
||||||
switch cMsg.APObjectType {
|
switch cMsg.APObjectType {
|
||||||
|
|
||||||
|
// UPDATE NOTE/STATUS
|
||||||
|
case ap.ObjectNote:
|
||||||
|
return p.clientAPI.UpdateStatus(ctx, cMsg)
|
||||||
|
|
||||||
// UPDATE PROFILE/ACCOUNT
|
// UPDATE PROFILE/ACCOUNT
|
||||||
case ap.ObjectProfile, ap.ActorPerson:
|
case ap.ObjectProfile, ap.ActorPerson:
|
||||||
return p.clientAPI.UpdateAccount(ctx, cMsg)
|
return p.clientAPI.UpdateAccount(ctx, cMsg)
|
||||||
|
@ -332,10 +336,25 @@ func (p *clientAPI) CreateBlock(ctx context.Context, cMsg messages.FromClientAPI
|
||||||
return nil
|
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 {
|
func (p *clientAPI) UpdateAccount(ctx context.Context, cMsg messages.FromClientAPI) error {
|
||||||
account, ok := cMsg.GTSModel.(*gtsmodel.Account)
|
account, ok := cMsg.GTSModel.(*gtsmodel.Account)
|
||||||
if !ok {
|
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 {
|
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:
|
case ap.ActivityUpdate:
|
||||||
switch fMsg.APObjectType { //nolint:gocritic
|
switch fMsg.APObjectType { //nolint:gocritic
|
||||||
|
|
||||||
|
// UPDATE NOTE/STATUS
|
||||||
|
case ap.ObjectNote:
|
||||||
|
return p.fediAPI.UpdateStatus(ctx, fMsg)
|
||||||
|
|
||||||
// UPDATE PROFILE/ACCOUNT
|
// UPDATE PROFILE/ACCOUNT
|
||||||
case ap.ObjectProfile:
|
case ap.ObjectProfile:
|
||||||
return p.fediAPI.UpdateAccount(ctx, fMsg)
|
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.
|
// Parse the old/existing account model.
|
||||||
account, ok := fMsg.GTSModel.(*gtsmodel.Account)
|
account, ok := fMsg.GTSModel.(*gtsmodel.Account)
|
||||||
if !ok {
|
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.
|
// Because this was an Update, the new Accountable should be set on the message.
|
||||||
apubAcc, ok := fMsg.APObjectModel.(ap.Accountable)
|
apubAcc, ok := fMsg.APObjectModel.(ap.Accountable)
|
||||||
if !ok {
|
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.
|
// Fetch up-to-date bio, avatar, header, etc.
|
||||||
|
@ -509,6 +513,34 @@ func (p *fediAPI) UpdateAccount(ctx context.Context, fMsg messages.FromFediAPI)
|
||||||
return nil
|
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 {
|
func (p *fediAPI) DeleteStatus(ctx context.Context, fMsg messages.FromFediAPI) error {
|
||||||
// Delete attachments from this status, since this request
|
// Delete attachments from this status, since this request
|
||||||
// comes from the federating API, and there's no way the
|
// 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,
|
statusToDelete *gtsmodel.Status,
|
||||||
deleteAttachments bool,
|
deleteAttachments bool,
|
||||||
) error {
|
) error {
|
||||||
errs := new(gtserror.MultiError)
|
var errs gtserror.MultiError
|
||||||
|
|
||||||
// Either delete all attachments for this status,
|
// Either delete all attachments for this status,
|
||||||
// or simply unattach + clean them separately later.
|
// 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)
|
// status immediately (in case of delete + redraft)
|
||||||
if deleteAttachments {
|
if deleteAttachments {
|
||||||
// todo:state.DB.DeleteAttachmentsForStatus
|
// todo:state.DB.DeleteAttachmentsForStatus
|
||||||
for _, a := range statusToDelete.AttachmentIDs {
|
for _, id := range statusToDelete.AttachmentIDs {
|
||||||
if err := media.Delete(ctx, a); err != nil {
|
if err := media.Delete(ctx, id); err != nil {
|
||||||
errs.Appendf("error deleting media: %w", err)
|
errs.Appendf("error deleting media: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// todo:state.DB.UnattachAttachmentsForStatus
|
// todo:state.DB.UnattachAttachmentsForStatus
|
||||||
for _, a := range statusToDelete.AttachmentIDs {
|
for _, id := range statusToDelete.AttachmentIDs {
|
||||||
if _, err := media.Unattach(ctx, statusToDelete.Account, a); err != nil {
|
if _, err := media.Unattach(ctx, statusToDelete.Account, id); err != nil {
|
||||||
errs.Appendf("error unattaching media: %w", err)
|
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 {
|
if err != nil {
|
||||||
errs.Appendf("error fetching status boosts: %w", err)
|
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)
|
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)
|
errs.Appendf("error deleting boost: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"codeberg.org/gruf/go-byteutil"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
"github.com/yuin/goldmark"
|
"github.com/yuin/goldmark"
|
||||||
|
@ -103,11 +104,11 @@ func (f *Formatter) FromPlainEmojiOnly(
|
||||||
statusID string,
|
statusID string,
|
||||||
input string,
|
input string,
|
||||||
) *FormatResult {
|
) *FormatResult {
|
||||||
// Initialize standard block parser
|
// Initialize block parser that
|
||||||
// that wraps result in <p> tags.
|
// doesn't wrap result in <p> tags.
|
||||||
plainTextParser := parser.NewParser(
|
plainTextParser := parser.NewParser(
|
||||||
parser.WithBlockParsers(
|
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.
|
// Parse input into HTML.
|
||||||
var htmlBytes bytes.Buffer
|
var htmlBytes bytes.Buffer
|
||||||
if err := md.Convert(
|
if err := md.Convert(
|
||||||
[]byte(input),
|
bInput,
|
||||||
&htmlBytes,
|
&htmlBytes,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
log.Errorf(ctx, "error formatting plaintext input to HTML: %s", err)
|
log.Errorf(ctx, "error formatting plaintext input to HTML: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean and shrink HTML.
|
// Clean and shrink HTML.
|
||||||
result.HTML = htmlBytes.String()
|
result.HTML = byteutil.B2S(htmlBytes.Bytes())
|
||||||
result.HTML = SanitizeToHTML(result.HTML)
|
result.HTML = SanitizeToHTML(result.HTML)
|
||||||
result.HTML = MinifyHTML(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
|
// a point where the items are out of the range
|
||||||
// we're interested in.
|
// we're interested in.
|
||||||
rangeF = func(e *list.Element) (bool, error) {
|
rangeF = func(e *list.Element) (bool, error) {
|
||||||
entry := e.Value.(*indexedItemsEntry) //nolint:forcetypeassert
|
entry := e.Value.(*indexedItemsEntry)
|
||||||
|
|
||||||
if entry.itemID >= behindID {
|
if entry.itemID >= behindID {
|
||||||
// ID of this item is too high,
|
// 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.
|
// Move the mark back one place each loop.
|
||||||
beforeIDMark = e
|
beforeIDMark = e
|
||||||
|
|
||||||
//nolint:forcetypeassert
|
|
||||||
if entry := e.Value.(*indexedItemsEntry); entry.itemID <= beforeID {
|
if entry := e.Value.(*indexedItemsEntry); entry.itemID <= beforeID {
|
||||||
// We've gone as far as we can through
|
// We've gone as far as we can through
|
||||||
// the list and reached entries that are
|
// 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
|
// To preserve ordering, we need to reverse the slice
|
||||||
// when we're finished.
|
// when we're finished.
|
||||||
for e := beforeIDMark; e != nil; e = e.Prev() {
|
for e := beforeIDMark; e != nil; e = e.Prev() {
|
||||||
entry := e.Value.(*indexedItemsEntry) //nolint:forcetypeassert
|
entry := e.Value.(*indexedItemsEntry)
|
||||||
|
|
||||||
if entry.itemID == beforeID {
|
if entry.itemID == beforeID {
|
||||||
// Don't include the 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() {
|
for e := t.items.data.Front(); e != nil; e = e.Next() {
|
||||||
entry := e.Value.(*indexedItemsEntry) //nolint:forcetypeassert
|
entry := e.Value.(*indexedItemsEntry)
|
||||||
|
|
||||||
position++
|
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.
|
// Don't grab more than we need to.
|
||||||
amount-grabbed,
|
amount-grabbed,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Grab function already checks for
|
// Grab function already checks for
|
||||||
// db.ErrNoEntries, so if an error
|
// db.ErrNoEntries, so if an error
|
||||||
|
@ -280,5 +279,5 @@ func (t *timeline) OldestIndexedItemID() string {
|
||||||
return ""
|
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() {
|
for e := i.data.Front(); e != nil; e = e.Next() {
|
||||||
currentPosition++
|
currentPosition++
|
||||||
|
|
||||||
currentEntry := e.Value.(*indexedItemsEntry) //nolint:forcetypeassert
|
currentEntry := e.Value.(*indexedItemsEntry)
|
||||||
|
|
||||||
// Check if we need to skip inserting this item based on
|
// Check if we need to skip inserting this item based on
|
||||||
// the current item.
|
// the current item.
|
||||||
|
|
|
@ -219,7 +219,6 @@ func (m *manager) UnprepareItemFromAllTimelines(ctx context.Context, itemID stri
|
||||||
// Work through all timelines held by this
|
// Work through all timelines held by this
|
||||||
// manager, and call Unprepare for each.
|
// manager, and call Unprepare for each.
|
||||||
m.timelines.Range(func(_ any, v any) bool {
|
m.timelines.Range(func(_ any, v any) bool {
|
||||||
// nolint:forcetypeassert
|
|
||||||
if err := v.(Timeline).Unprepare(ctx, itemID); err != nil {
|
if err := v.(Timeline).Unprepare(ctx, itemID); err != nil {
|
||||||
errs.Append(err)
|
errs.Append(err)
|
||||||
}
|
}
|
||||||
|
@ -248,7 +247,7 @@ func (m *manager) getOrCreateTimeline(ctx context.Context, timelineID string) Ti
|
||||||
i, ok := m.timelines.Load(timelineID)
|
i, ok := m.timelines.Load(timelineID)
|
||||||
if ok {
|
if ok {
|
||||||
// Timeline already existed in sync.Map.
|
// Timeline already existed in sync.Map.
|
||||||
return i.(Timeline) //nolint:forcetypeassert
|
return i.(Timeline)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timeline did not yet exist in sync.Map.
|
// Timeline did not yet exist in sync.Map.
|
||||||
|
|
|
@ -63,7 +63,7 @@ func (t *timeline) prepareXBetweenIDs(ctx context.Context, amount int, behindID
|
||||||
if frontToBack {
|
if frontToBack {
|
||||||
// Paging forwards / down.
|
// Paging forwards / down.
|
||||||
for e := t.items.data.Front(); e != nil; e = e.Next() {
|
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 {
|
if entry.itemID > behindID {
|
||||||
l.Trace("item is too new, continuing")
|
l.Trace("item is too new, continuing")
|
||||||
|
@ -91,7 +91,7 @@ func (t *timeline) prepareXBetweenIDs(ctx context.Context, amount int, behindID
|
||||||
} else {
|
} else {
|
||||||
// Paging backwards / up.
|
// Paging backwards / up.
|
||||||
for e := t.items.data.Back(); e != nil; e = e.Prev() {
|
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 {
|
if entry.itemID < beforeID {
|
||||||
l.Trace("item is too old, continuing")
|
l.Trace("item is too old, continuing")
|
||||||
|
|
|
@ -63,7 +63,7 @@ func (t *timeline) Prune(desiredPreparedItemsLength int, desiredIndexedItemsLeng
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
entry := e.Value.(*indexedItemsEntry) //nolint:forcetypeassert
|
entry := e.Value.(*indexedItemsEntry)
|
||||||
if entry.prepared == nil {
|
if entry.prepared == nil {
|
||||||
// It's already unprepared (mood).
|
// It's already unprepared (mood).
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -42,7 +42,7 @@ func (t *timeline) Remove(ctx context.Context, statusID string) (int, error) {
|
||||||
|
|
||||||
var toRemove []*list.Element
|
var toRemove []*list.Element
|
||||||
for e := t.items.data.Front(); e != nil; e = e.Next() {
|
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 {
|
if entry.itemID != statusID {
|
||||||
// Not relevant.
|
// Not relevant.
|
||||||
|
@ -78,7 +78,7 @@ func (t *timeline) RemoveAllByOrBoosting(ctx context.Context, accountID string)
|
||||||
|
|
||||||
var toRemove []*list.Element
|
var toRemove []*list.Element
|
||||||
for e := t.items.data.Front(); e != nil; e = e.Next() {
|
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 {
|
if entry.accountID != accountID && entry.boostOfAccountID != accountID {
|
||||||
// Not relevant.
|
// 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() {
|
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 {
|
if entry.itemID != itemID && entry.boostOfID != itemID {
|
||||||
// Not relevant.
|
// Not relevant.
|
||||||
|
|
|
@ -216,40 +216,10 @@ func (c *Converter) ASRepresentationToAccount(ctx context.Context, accountable a
|
||||||
return acct, nil
|
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.
|
// 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) {
|
func (c *Converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusable) (*gtsmodel.Status, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
status := new(gtsmodel.Status)
|
status := new(gtsmodel.Status)
|
||||||
|
|
||||||
// status.URI
|
// status.URI
|
||||||
|
@ -281,7 +251,19 @@ func (c *Converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusab
|
||||||
// status.Attachments
|
// status.Attachments
|
||||||
//
|
//
|
||||||
// Media attachments for later dereferencing.
|
// 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
|
// status.Hashtags
|
||||||
//
|
//
|
||||||
|
@ -341,7 +323,7 @@ func (c *Converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusab
|
||||||
// error if we don't.
|
// error if we don't.
|
||||||
attributedTo, err := ap.ExtractAttributedToURI(statusable)
|
attributedTo, err := ap.ExtractAttributedToURI(statusable)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.Newf("%w", err)
|
return nil, gtserror.Newf("error extracting attributed to uri: %w", err)
|
||||||
}
|
}
|
||||||
accountURI := attributedTo.String()
|
accountURI := attributedTo.String()
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
"github.com/superseriousbusiness/activity/pub"
|
"github.com/superseriousbusiness/activity/pub"
|
||||||
"github.com/superseriousbusiness/activity/streams"
|
"github.com/superseriousbusiness/activity/streams"
|
||||||
"github.com/superseriousbusiness/activity/streams/vocab"
|
"github.com/superseriousbusiness/activity/streams/vocab"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
@ -403,21 +404,15 @@ func (c *Converter) AccountToASMinimal(ctx context.Context, a *gtsmodel.Account)
|
||||||
return person, nil
|
return person, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// StatusToAS converts a gts model status into an activity streams note, suitable for federation
|
// StatusToAS converts a gts model status into an ActivityStreams Statusable implementation, suitable for federation
|
||||||
func (c *Converter) StatusToAS(ctx context.Context, s *gtsmodel.Status) (vocab.ActivityStreamsNote, error) {
|
func (c *Converter) StatusToAS(ctx context.Context, s *gtsmodel.Status) (ap.Statusable, error) {
|
||||||
// ensure prerequisites here before we get stuck in
|
// Ensure the status model is fully populated.
|
||||||
|
// The status and poll models are REQUIRED so nothing to do if this fails.
|
||||||
// check if author account is already attached to status and attach it if not
|
if err := c.state.DB.PopulateStatus(ctx, s); err != nil {
|
||||||
// if we can't retrieve this, bail here already because we can't attribute the status to anyone
|
return nil, gtserror.Newf("error populating status: %w", err)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// create the Note!
|
// We convert it as an AS Note.
|
||||||
status := streams.NewActivityStreamsNote()
|
status := streams.NewActivityStreamsNote()
|
||||||
|
|
||||||
// id
|
// id
|
||||||
|
@ -529,7 +524,6 @@ func (c *Converter) StatusToAS(ctx context.Context, s *gtsmodel.Status) (vocab.A
|
||||||
}
|
}
|
||||||
tagProp.AppendTootHashtag(asHashtag)
|
tagProp.AppendTootHashtag(asHashtag)
|
||||||
}
|
}
|
||||||
|
|
||||||
status.SetActivityStreamsTag(tagProp)
|
status.SetActivityStreamsTag(tagProp)
|
||||||
|
|
||||||
// parse out some URIs we need here
|
// parse out some URIs we need here
|
||||||
|
@ -1419,7 +1413,7 @@ func (c *Converter) StatusesToASOutboxPage(ctx context.Context, outboxID string,
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
create, err := c.WrapNoteInCreate(note, true)
|
create, err := c.WrapStatusableInCreate(note, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,6 @@ func (c *Converter) WrapPersonInUpdate(person vocab.ActivityStreamsPerson, origi
|
||||||
update.SetActivityStreamsActor(actorProp)
|
update.SetActivityStreamsActor(actorProp)
|
||||||
|
|
||||||
// set the ID
|
// set the ID
|
||||||
|
|
||||||
newID, err := id.NewRandomULID()
|
newID, err := id.NewRandomULID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -85,26 +84,29 @@ func (c *Converter) WrapPersonInUpdate(person vocab.ActivityStreamsPerson, origi
|
||||||
return update, nil
|
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,
|
// 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,
|
// 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.
|
// 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()
|
create := streams.NewActivityStreamsCreate()
|
||||||
|
|
||||||
// Object property
|
// Object property
|
||||||
objectProp := streams.NewActivityStreamsObjectProperty()
|
objectProp := streams.NewActivityStreamsObjectProperty()
|
||||||
if objectIRIOnly {
|
if objectIRIOnly {
|
||||||
objectProp.AppendIRI(note.GetJSONLDId().GetIRI())
|
// Only append the object IRI to objectProp.
|
||||||
|
objectProp.AppendIRI(status.GetJSONLDId().GetIRI())
|
||||||
} else {
|
} else {
|
||||||
objectProp.AppendActivityStreamsNote(note)
|
// Our statusable's are always note types.
|
||||||
|
asNote := status.(vocab.ActivityStreamsNote)
|
||||||
|
objectProp.AppendActivityStreamsNote(asNote)
|
||||||
}
|
}
|
||||||
create.SetActivityStreamsObject(objectProp)
|
create.SetActivityStreamsObject(objectProp)
|
||||||
|
|
||||||
// ID property
|
// ID property
|
||||||
idProp := streams.NewJSONLDIdProperty()
|
idProp := streams.NewJSONLDIdProperty()
|
||||||
createID := note.GetJSONLDId().GetIRI().String() + "/activity"
|
createID := status.GetJSONLDId().GetIRI().String() + "/activity"
|
||||||
createIDIRI, err := url.Parse(createID)
|
createIDIRI, err := url.Parse(createID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -114,7 +116,7 @@ func (c *Converter) WrapNoteInCreate(note vocab.ActivityStreamsNote, objectIRIOn
|
||||||
|
|
||||||
// Actor Property
|
// Actor Property
|
||||||
actorProp := streams.NewActivityStreamsActorProperty()
|
actorProp := streams.NewActivityStreamsActorProperty()
|
||||||
actorIRI, err := ap.ExtractAttributedToURI(note)
|
actorIRI, err := ap.ExtractAttributedToURI(status)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.Newf("couldn't extract AttributedTo: %w", err)
|
return nil, gtserror.Newf("couldn't extract AttributedTo: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -123,7 +125,7 @@ func (c *Converter) WrapNoteInCreate(note vocab.ActivityStreamsNote, objectIRIOn
|
||||||
|
|
||||||
// Published Property
|
// Published Property
|
||||||
publishedProp := streams.NewActivityStreamsPublishedProperty()
|
publishedProp := streams.NewActivityStreamsPublishedProperty()
|
||||||
published, err := ap.ExtractPublished(note)
|
published, err := ap.ExtractPublished(status)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.Newf("couldn't extract Published: %w", err)
|
return nil, gtserror.Newf("couldn't extract Published: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -132,7 +134,7 @@ func (c *Converter) WrapNoteInCreate(note vocab.ActivityStreamsNote, objectIRIOn
|
||||||
|
|
||||||
// To Property
|
// To Property
|
||||||
toProp := streams.NewActivityStreamsToProperty()
|
toProp := streams.NewActivityStreamsToProperty()
|
||||||
if toURIs := ap.ExtractToURIs(note); len(toURIs) != 0 {
|
if toURIs := ap.ExtractToURIs(status); len(toURIs) != 0 {
|
||||||
for _, toURI := range toURIs {
|
for _, toURI := range toURIs {
|
||||||
toProp.AppendIRI(toURI)
|
toProp.AppendIRI(toURI)
|
||||||
}
|
}
|
||||||
|
@ -141,7 +143,7 @@ func (c *Converter) WrapNoteInCreate(note vocab.ActivityStreamsNote, objectIRIOn
|
||||||
|
|
||||||
// Cc Property
|
// Cc Property
|
||||||
ccProp := streams.NewActivityStreamsCcProperty()
|
ccProp := streams.NewActivityStreamsCcProperty()
|
||||||
if ccURIs := ap.ExtractCcURIs(note); len(ccURIs) != 0 {
|
if ccURIs := ap.ExtractCcURIs(status); len(ccURIs) != 0 {
|
||||||
for _, ccURI := range ccURIs {
|
for _, ccURI := range ccURIs {
|
||||||
ccProp.AppendIRI(ccURI)
|
ccProp.AppendIRI(ccURI)
|
||||||
}
|
}
|
||||||
|
@ -150,3 +152,64 @@ func (c *Converter) WrapNoteInCreate(note vocab.ActivityStreamsNote, objectIRIOn
|
||||||
|
|
||||||
return create, nil
|
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)
|
note, err := suite.typeconverter.StatusToAS(context.Background(), testStatus)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
|
|
||||||
create, err := suite.typeconverter.WrapNoteInCreate(note, true)
|
create, err := suite.typeconverter.WrapStatusableInCreate(note, true)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(create)
|
suite.NotNil(create)
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ func (suite *WrapTestSuite) TestWrapNoteInCreate() {
|
||||||
note, err := suite.typeconverter.StatusToAS(context.Background(), testStatus)
|
note, err := suite.typeconverter.StatusToAS(context.Background(), testStatus)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
|
|
||||||
create, err := suite.typeconverter.WrapNoteInCreate(note, false)
|
create, err := suite.typeconverter.WrapStatusableInCreate(note, false)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(create)
|
suite.NotNil(create)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue