Skip to content

Commit

Permalink
[feature] tentatively start adding polls support (#2249)
Browse files Browse the repository at this point in the history
  • Loading branch information
NyaaaWhatsUpDoc authored Oct 4, 2023
1 parent 297b6ee commit c6e00af
Show file tree
Hide file tree
Showing 36 changed files with 654 additions and 390 deletions.
1 change: 0 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ run:
linters:
# enable some extra linters, see here for the list: https://golangci-lint.run/usage/linters/
enable:
- forcetypeassert
- goconst
- gocritic
- gofmt
Expand Down
12 changes: 6 additions & 6 deletions internal/ap/collections.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ import (
func ToCollectionPageIterator(t vocab.Type) (CollectionPageIterator, error) {
switch name := t.GetTypeName(); name {
case ObjectCollectionPage:
t := t.(vocab.ActivityStreamsCollectionPage) //nolint:forcetypeassert
t := t.(vocab.ActivityStreamsCollectionPage)
return WrapCollectionPage(t), nil
case ObjectOrderedCollectionPage:
t := t.(vocab.ActivityStreamsOrderedCollectionPage) //nolint:forcetypeassert
t := t.(vocab.ActivityStreamsOrderedCollectionPage)
return WrapOrderedCollectionPage(t), nil
default:
return nil, fmt.Errorf("%T(%s) was not CollectionPage-like", t, name)
Expand Down Expand Up @@ -74,7 +74,7 @@ func (iter *regularCollectionPageIterator) PrevPage() WithIRI {
return iter.GetActivityStreamsPrev()
}

func (iter *regularCollectionPageIterator) NextItem() IteratorItemable {
func (iter *regularCollectionPageIterator) NextItem() TypeOrIRI {
if !iter.initItems() {
return nil
}
Expand All @@ -83,7 +83,7 @@ func (iter *regularCollectionPageIterator) NextItem() IteratorItemable {
return cur
}

func (iter *regularCollectionPageIterator) PrevItem() IteratorItemable {
func (iter *regularCollectionPageIterator) PrevItem() TypeOrIRI {
if !iter.initItems() {
return nil
}
Expand Down Expand Up @@ -130,7 +130,7 @@ func (iter *orderedCollectionPageIterator) PrevPage() WithIRI {
return iter.GetActivityStreamsPrev()
}

func (iter *orderedCollectionPageIterator) NextItem() IteratorItemable {
func (iter *orderedCollectionPageIterator) NextItem() TypeOrIRI {
if !iter.initItems() {
return nil
}
Expand All @@ -139,7 +139,7 @@ func (iter *orderedCollectionPageIterator) NextItem() IteratorItemable {
return cur
}

func (iter *orderedCollectionPageIterator) PrevItem() IteratorItemable {
func (iter *orderedCollectionPageIterator) PrevItem() TypeOrIRI {
if !iter.initItems() {
return nil
}
Expand Down
47 changes: 32 additions & 15 deletions internal/ap/extract.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,39 +35,56 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/util"
)

// ExtractObject will extract an object vocab.Type from given implementing interface.
func ExtractObject(with WithObject) vocab.Type {
// ExtractObjects will extract object vocab.Types from given implementing interface.
func ExtractObjects(with WithObject) []TypeOrIRI {
// Extract the attached object (if any).
obj := with.GetActivityStreamsObject()
if obj == nil {
objProp := with.GetActivityStreamsObject()
if objProp == nil {
return nil
}

// Only support single
// objects (for now...)
if obj.Len() != 1 {
// Check for zero len.
if objProp.Len() == 0 {
return nil
}

// Extract object vocab.Type.
return obj.At(0).GetType()
// Accumulate all of the objects into a slice.
objs := make([]TypeOrIRI, objProp.Len())
for i := 0; i < objProp.Len(); i++ {
objs[i] = objProp.At(i)
}

return objs
}

// ExtractActivityData will extract the usable data type (e.g. Note, Question, etc) and corresponding JSON, from activity.
func ExtractActivityData(activity pub.Activity, rawJSON map[string]any) (vocab.Type, map[string]any, bool) {
func ExtractActivityData(activity pub.Activity, rawJSON map[string]any) ([]TypeOrIRI, []any, bool) {
switch typeName := activity.GetTypeName(); {
// Activity (has "object").
case isActivity(typeName):
objType := ExtractObject(activity)
if objType == nil {
objTypes := ExtractObjects(activity)
if len(objTypes) == 0 {
return nil, nil, false
}
objJSON, _ := rawJSON["object"].(map[string]any)
return objType, objJSON, true

var objJSON []any
switch json := rawJSON["object"].(type) {
case nil:
// do nothing
case map[string]any:
// Wrap map in slice.
objJSON = []any{json}
case []any:
// Use existing slice.
objJSON = json
}

return objTypes, objJSON, true

// IntransitiveAcitivity (no "object").
case isIntransitiveActivity(typeName):
return activity, rawJSON, false
asTypeOrIRI := _TypeOrIRI{activity} // wrap activity.
return []TypeOrIRI{&asTypeOrIRI}, []any{rawJSON}, true

// Unknown.
default:
Expand Down
16 changes: 8 additions & 8 deletions internal/ap/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,14 +247,8 @@ type CollectionPageIterator interface {
NextPage() WithIRI
PrevPage() WithIRI

NextItem() IteratorItemable
PrevItem() IteratorItemable
}

// IteratorItemable represents the minimum interface for an item in an iterator.
type IteratorItemable interface {
WithIRI
WithType
NextItem() TypeOrIRI
PrevItem() TypeOrIRI
}

// Flaggable represents the minimum interface for an activitystreams 'Flag' activity.
Expand All @@ -267,6 +261,12 @@ type Flaggable interface {
WithObject
}

// TypeOrIRI represents the minimum interface for something that may be a vocab.Type OR IRI.
type TypeOrIRI interface {
WithIRI
WithType
}

// WithJSONLDId represents an activity with JSONLDIdProperty.
type WithJSONLDId interface {
GetJSONLDId() vocab.JSONLDIdProperty
Expand Down
76 changes: 32 additions & 44 deletions internal/ap/normalize.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,60 +39,48 @@ import (
// This function is a noop if the type passed in is anything except a Create or Update with a Statusable or Accountable as its Object.
func NormalizeIncomingActivity(activity pub.Activity, rawJSON map[string]interface{}) {
// From the activity extract the data vocab.Type + its "raw" JSON.
dataType, rawData, ok := ExtractActivityData(activity, rawJSON)
if !ok {
dataIfaces, rawData, ok := ExtractActivityData(activity, rawJSON)
if !ok || len(dataIfaces) != len(rawData) {
// non-equal lengths *shouldn't* happen,
// but this is just an integrity check.
return
}

switch dataType.GetTypeName() {
// "Pollable" types.
case ActivityQuestion:
pollable, ok := dataType.(Pollable)
if !ok {
return
// Iterate over the available data.
for i, dataIface := range dataIfaces {
// Try to get as vocab.Type, else
// skip this entry for normalization.
dataType := dataIface.GetType()
if dataType == nil {
continue
}

// Normalize the Pollable specific properties.
NormalizeIncomingPollOptions(pollable, rawData)

// Fallthrough to handle
// the rest as Statusable.
fallthrough

// "Statusable" types.
case ObjectArticle,
ObjectDocument,
ObjectImage,
ObjectVideo,
ObjectNote,
ObjectPage,
ObjectEvent,
ObjectPlace,
ObjectProfile:
statusable, ok := dataType.(Statusable)
// Get the raw data map at index, else skip
// this entry due to impossible normalization.
rawData, ok := rawData[i].(map[string]any)
if !ok {
return
continue
}

// Normalize everything we can on the statusable.
NormalizeIncomingContent(statusable, rawData)
NormalizeIncomingAttachments(statusable, rawData)
NormalizeIncomingSummary(statusable, rawData)
NormalizeIncomingName(statusable, rawData)

// "Accountable" types.
case ActorApplication,
ActorGroup,
ActorOrganization,
ActorPerson,
ActorService:
accountable, ok := dataType.(Accountable)
if !ok {
return
if statusable, ok := ToStatusable(dataType); ok {
if pollable, ok := ToPollable(dataType); ok {
// Normalize the Pollable specific properties.
NormalizeIncomingPollOptions(pollable, rawData)
}

// Normalize everything we can on the statusable.
NormalizeIncomingContent(statusable, rawData)
NormalizeIncomingAttachments(statusable, rawData)
NormalizeIncomingSummary(statusable, rawData)
NormalizeIncomingName(statusable, rawData)
continue
}

// Normalize everything we can on the accountable.
NormalizeIncomingSummary(accountable, rawData)
if accountable, ok := ToAccountable(dataType); ok {
// Normalize everything we can on the accountable.
NormalizeIncomingSummary(accountable, rawData)
continue
}
}
}

Expand Down
43 changes: 43 additions & 0 deletions internal/ap/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// GoToSocial
// Copyright (C) GoToSocial Authors [email protected]
// 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) {}
1 change: 0 additions & 1 deletion internal/api/client/admin/domainpermission.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,6 @@ func (m *Module) createDomainPermissions(
if multiStatus.Metadata.Failure != 0 {
failures := make(map[string]any, multiStatus.Metadata.Failure)
for _, entry := range multiStatus.Data {
// nolint:forcetypeassert
failures[entry.Resource.(string)] = entry.Message
}

Expand Down
2 changes: 1 addition & 1 deletion internal/api/client/statuses/statuscreate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ func (suite *StatusCreateTestSuite) TestReplyToNonexistentStatus() {
defer result.Body.Close()
b, err := ioutil.ReadAll(result.Body)
suite.NoError(err)
suite.Equal(`{"error":"Bad Request: status with id 3759e7ef-8ee1-4c0c-86f6-8b70b9ad3d50 not replyable because it doesn't exist"}`, string(b))
suite.Equal(`{"error":"Bad Request: cannot reply to status that does not exist"}`, string(b))
}

// Post a reply to the status of a local user that allows replies.
Expand Down
17 changes: 10 additions & 7 deletions internal/federation/dereferencing/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,8 @@ func (d *deref) enrichStatus(
return nil, nil, gtserror.Newf("error populating mentions for status %s: %w", uri, err)
}

// Ensure the status' tags are populated.
if err := d.fetchStatusTags(ctx, requestUser, latestStatus); err != nil {
// Ensure the status' tags are populated, (changes are expected / okay).
if err := d.fetchStatusTags(ctx, latestStatus); err != nil {
return nil, nil, gtserror.Newf("error populating tags for status %s: %w", uri, err)
}

Expand All @@ -298,8 +298,8 @@ func (d *deref) enrichStatus(
return nil, nil, gtserror.Newf("error populating attachments for status %s: %w", uri, err)
}

// Ensure the status' emoji attachments are populated, passing in existing to check for changes.
if err := d.fetchStatusEmojis(ctx, requestUser, status, latestStatus); err != nil {
// Ensure the status' emoji attachments are populated, (changes are expected / okay).
if err := d.fetchStatusEmojis(ctx, requestUser, latestStatus); err != nil {
return nil, nil, gtserror.Newf("error populating emojis for status %s: %w", uri, err)
}

Expand Down Expand Up @@ -359,6 +359,8 @@ func (d *deref) fetchStatusMentions(ctx context.Context, requestUser string, exi
}

// Generate new ID according to status creation.
// TODO: update this to use "edited_at" when we add
// support for edited status revision history.
mention.ID, err = id.NewULIDFromTime(status.CreatedAt)
if err != nil {
log.Errorf(ctx, "invalid created at date: %v", err)
Expand Down Expand Up @@ -403,7 +405,7 @@ func (d *deref) fetchStatusMentions(ctx context.Context, requestUser string, exi
return nil
}

func (d *deref) fetchStatusTags(ctx context.Context, requestUser string, status *gtsmodel.Status) error {
func (d *deref) fetchStatusTags(ctx context.Context, status *gtsmodel.Status) error {
// Allocate new slice to take the yet-to-be determined tag IDs.
status.TagIDs = make([]string, len(status.Tags))

Expand All @@ -417,13 +419,14 @@ func (d *deref) fetchStatusTags(ctx context.Context, requestUser string, status
continue
}

// No tag with this name yet, create it.
if tag == nil {
// Create new ID for tag name.
tag = &gtsmodel.Tag{
ID: id.NewULID(),
Name: placeholder.Name,
}

// Insert this tag with new name into the database.
if err := d.state.DB.PutTag(ctx, tag); err != nil {
log.Errorf(ctx, "db error putting tag %s: %v", tag.Name, err)
continue
Expand Down Expand Up @@ -516,7 +519,7 @@ func (d *deref) fetchStatusAttachments(ctx context.Context, tsport transport.Tra
return nil
}

func (d *deref) fetchStatusEmojis(ctx context.Context, requestUser string, existing, status *gtsmodel.Status) error {
func (d *deref) fetchStatusEmojis(ctx context.Context, requestUser string, status *gtsmodel.Status) error {
// Fetch the full-fleshed-out emoji objects for our status.
emojis, err := d.populateEmojis(ctx, status.Emojis, requestUser)
if err != nil {
Expand Down
Loading

0 comments on commit c6e00af

Please sign in to comment.